@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/bin/dsh-iris.js
CHANGED
|
@@ -3,11 +3,13 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createCommandService } from '../lib/command-service.js';
|
|
5
5
|
import { createCoreRuntime } from '../lib/core-runtime.js';
|
|
6
|
+
import { createApplicationRuntime } from '../lib/application-runtime.js';
|
|
6
7
|
import { recoverCoreWriterLease } from '../lib/core-lease-recovery.js';
|
|
7
8
|
import { doctor, formatDoctorReport } from '../lib/doctor.js';
|
|
8
9
|
import { loadProviderCatalog, providerCatalogSnapshot, catalogCapabilitySnapshot, imageCandidatesFromCatalog, musicCandidatesFromCatalog, providerForTaskFromCatalog, providerTaskBinding, transcribeCandidatesFromCatalog, ttsCandidatesFromCatalog, videoCandidatesFromCatalog } from '../lib/provider-catalog.js';
|
|
9
10
|
import { createConfiguredProviderAdapter } from '../lib/provider-adapters.js';
|
|
10
|
-
import { providerForImageModel, selectMediaProtocol, dashscopeTtsInputIssue } from '../lib/provider-protocol.js';
|
|
11
|
+
import { providerForImageModel, selectMediaProtocol, dashscopeTtsInputIssue, supportsImageEdit } from '../lib/provider-protocol.js';
|
|
12
|
+
import { loadProviderRegistry } from '../lib/provider-extensions.js';
|
|
11
13
|
import { prepareProviderInput } from '../lib/provider-adapter.js';
|
|
12
14
|
import { createProviderTaskRunner } from '../lib/provider-task-runner.js';
|
|
13
15
|
import { generateMusicTask } from '../lib/music-generation.js';
|
|
@@ -20,19 +22,24 @@ import { probeVisionModel } from '../lib/vision-model-routing.js';
|
|
|
20
22
|
import { RED_TEST_IMAGE } from '../lib/vision.js';
|
|
21
23
|
import { parseModelRef, modelRef as qualifiedModelRef } from '../lib/models.js';
|
|
22
24
|
import { coreTaskWaitFinished } from '../lib/task-wait.js';
|
|
25
|
+
import { createLocalReadApi, writeLocalApiTokenFile } from '../lib/local-api.js';
|
|
26
|
+
import { manageCoreSkill } from '../lib/skill-installation.js';
|
|
27
|
+
import { createTrackedProviderAdapter } from '../lib/configured-image-runtime.js';
|
|
28
|
+
import { inspectExecutionLease, recoverExecutionLease } from '../lib/operation-runner.js';
|
|
29
|
+
import { createConnectionPair } from '../lib/core-connections.js';
|
|
30
|
+
import { builtinLaunchPlugins, loadPluginLaunch } from '../lib/plugin-launch.js';
|
|
31
|
+
import { checkLocalApiTokenDirectory, writeLocalApiTokenDirectory } from '../lib/local-api-token-directory.js';
|
|
32
|
+
import { resolveWorkbenchAuthDirectory } from '../lib/workbench-auth.js';
|
|
33
|
+
import { manageWorkbenchPassword } from '../lib/workbench-auth-cli.js';
|
|
23
34
|
|
|
24
35
|
function trackedProviderAdapter(provider, options, task) {
|
|
25
|
-
return
|
|
26
|
-
modelId: parseModelRef(task?.modelRef)?.modelId,
|
|
27
|
-
onModelAvailability: event => recordCatalogAvailability(options['provider-config'], event),
|
|
28
|
-
onRateLimit: event => recordCatalogRateLimit(options['provider-config'], event)
|
|
29
|
-
});
|
|
36
|
+
return createTrackedProviderAdapter(provider, options['provider-config'], task, options.providerRegistry);
|
|
30
37
|
}
|
|
31
38
|
|
|
32
|
-
async function generationCatalog(options, capability, modelRef) {
|
|
39
|
+
async function generationCatalog(options, capability, modelRef, signal = cliAbortController.signal) {
|
|
33
40
|
const file = required(options['provider-config'], 'provider-config');
|
|
34
41
|
const catalog = loadProviderCatalog(file);
|
|
35
|
-
if (!modelRef) await refreshCatalogDirectories(file, catalog, capability, { signal:
|
|
42
|
+
if (!modelRef) await refreshCatalogDirectories(file, catalog, capability, { signal, registry: options.providerRegistry });
|
|
36
43
|
return catalog;
|
|
37
44
|
}
|
|
38
45
|
|
|
@@ -48,7 +55,50 @@ function usage() {
|
|
|
48
55
|
return [
|
|
49
56
|
'Usage:',
|
|
50
57
|
' JSON input: --input <json>, --input-file <path>, or --input - (stdin).',
|
|
58
|
+
' --provider-extension <absolute-js-or-mjs-entry> explicitly loads trusted Provider code; repeat for multiple extensions. Experimental v0, no installation or directory discovery.',
|
|
59
|
+
' dsh-iris providers protocols [--provider-extension <absolute-entry>] lists protocol/configuration descriptions without credentials or model calls.',
|
|
60
|
+
' dsh-iris plugins list',
|
|
61
|
+
' dsh-iris plugins inspect --plugin-config <absolute-startup.json>',
|
|
62
|
+
' Explicit trusted plugin entries, versions, contributions and selections; inspect imports/activates trusted code but does not start Core or grant connections.',
|
|
63
|
+
' dsh-iris web serve ... --plugin-config <absolute-startup.json> [--auth-store <absolute-private-directory>]',
|
|
64
|
+
' entries load plugins; operations enables exact business IDs; panel selects pages/settings/renderers. MCP grants remain separate. Do not combine with --provider-extension.',
|
|
65
|
+
' dsh-iris skills <install|status|uninstall> --target kimi [--directory <absolute-skills-directory>]',
|
|
66
|
+
' dsh-iris skills <install|status|uninstall> --directory <absolute-skills-directory>',
|
|
67
|
+
' dsh-iris skills <install|status|uninstall> --bundle <absolute-connection-skill.json> --directory <absolute-skills-directory> [--dry-run]',
|
|
68
|
+
' install/uninstall accept --dry-run. Copy only iris-core; preserve local changes and archive managed updates/removals outside the scan directory. status is read-only and does not verify host discovery.',
|
|
51
69
|
' dsh-iris doctor [--data-root <absolute-path>] [--json]',
|
|
70
|
+
' dsh-iris api init --data-root <empty-existing-flow-root>',
|
|
71
|
+
' dsh-iris execution init --data-root <empty-existing-flow-root> [--image-generation|--audio]',
|
|
72
|
+
' Initialize a NEW v1 request container, v2 with --image-generation, or v3 with --audio (images + speech/transcription). Existing roots are never migrated.',
|
|
73
|
+
' dsh-iris execution inspect --data-root <v1-flow-root>',
|
|
74
|
+
' dsh-iris execution recover-lease --data-root <v1-flow-root> --confirm-stale-pid <pid>',
|
|
75
|
+
' dsh-iris connection create --data-root <execution-flow-root> --directory <new-private-directory> --origin <http://127.0.0.1:port> --label <name> [--harness <generic|kimi|dsh>] [--scopes <read,create,execute,stop>] [--capabilities <image-edit,image-generate,tts,transcribe|none>] [--tools <comma-separated-names|none>]',
|
|
76
|
+
' dsh-iris api serve ... --allow-execution --provider-config <absolute-path> [--connections-file <private-grants-file>]',
|
|
77
|
+
' dsh-iris mcp serve --connection-file <private-client-json> [--scopes <read,create,execute,stop>] [--capabilities <image-edit,image-generate,tts,transcribe|none>] [--tools <comma-separated-names|none>]',
|
|
78
|
+
' Official SDK stdio bridge to an already running Core service. Read-only by default; stdout contains MCP messages only.',
|
|
79
|
+
' dsh-iris mcp capabilities',
|
|
80
|
+
' dsh-iris mcp presets',
|
|
81
|
+
' dsh-iris mcp config --connection-file <private-client-json> [--preset <generic|claude|codex|kimi>-<stdio|http> | --transport <stdio|http> --format <mcpServers|entry|claude-json|codex-toml>] [--output <new-absolute-path>]',
|
|
82
|
+
' dsh-iris mcp check --connection-file <private-client-json> [--scopes <read,create,execute,stop>]',
|
|
83
|
+
' stdio config also accepts scopes/capabilities/tools. HTTP config includes credentials and requires a new file inside the connection private directory.',
|
|
84
|
+
' dsh-iris api serve ... --connections-file <private-grants-file> --allow-mcp',
|
|
85
|
+
' dsh-iris web serve ... --connection-store <private-directory-outside-core> --allow-connection-management [--allow-mcp] [--allow-execution --provider-config <absolute-path>]',
|
|
86
|
+
' dsh-iris web serve ... --provider-config <private-config> --allow-provider-management [--panel <trusted-panel-manifest.json>]',
|
|
87
|
+
' Explicit standard Streamable HTTP endpoint at /mcp; default remains disabled. Uses the existing Core service and user-selected connection tools.',
|
|
88
|
+
' dsh-iris operation list --data-root <execution-root> [--run-id <run-id>] [--limit <1..30>] [--cursor <operation-id>]',
|
|
89
|
+
' dsh-iris operation inspect <operation-id> --data-root <execution-root>',
|
|
90
|
+
' dsh-iris api serve --data-root <existing-flow-root> --token-file <new-absolute-path-outside-root> [--provider-config <absolute-path>] [--port <0..65535>] [--allow-local-writes] [--allow-selection]',
|
|
91
|
+
' Explicit loopback-only API, read-only by default. Local writes import images/audio and create Runs within the root capabilities, without generation. The session token is saved privately; stdout contains no token.',
|
|
92
|
+
' dsh-iris web serve --data-root <existing-flow-root> [--auth-store <absolute-private-directory>] [--provider-config <absolute-path>] [--port <0..65535>] [--allow-local-writes] [--allow-execution] [--allow-selection]',
|
|
93
|
+
' Default: set one access password, then sign in at origin. Remembered browser sessions survive normal restarts. No Token file is required.',
|
|
94
|
+
' Auth state defaults to $IRIS_HOME/web-auth/<root-key> ($IRIS_HOME defaults to ~/.iris). Open the initial loginUrl once to set your password.',
|
|
95
|
+
' dsh-iris web login --data-root <existing-flow-root> [--auth-store <absolute-private-directory>]',
|
|
96
|
+
' dsh-iris web password reset --data-root <existing-flow-root> [--auth-store <absolute-private-directory>]',
|
|
97
|
+
' Reset only the workbench password/browser sessions; Core work and MCP grants remain intact. Prints a setup link while the service is running.',
|
|
98
|
+
' Explicit --token-file or --token-dir keeps the legacy Token/link login mode. Do not combine them with --auth-store. api serve still requires one of these Token options.',
|
|
99
|
+
' dsh-iris web login --origin <http://127.0.0.1:port> --token-file <current-session-token-file>',
|
|
100
|
+
' Get a fresh one-time login link without restarting the workbench.',
|
|
101
|
+
' Workbench media APIs are read-only by default; explicit write flags do not require connection management. --allow-selection permits owner-only, revision-checked image selection. Execution enables supported image/speech/transcription flows; Provider management uses its own owner-only flag.',
|
|
52
102
|
' dsh-iris runtime recover --data-root <absolute-path> --confirm-stale-pid <pid>',
|
|
53
103
|
' dsh-iris providers list --provider-config <absolute-path>',
|
|
54
104
|
' dsh-iris capabilities list --provider-config <absolute-path>',
|
|
@@ -87,16 +137,24 @@ function usage() {
|
|
|
87
137
|
' dsh-iris artifact export-many --data-root <absolute-path> --input <json> --output <absolute-directory>',
|
|
88
138
|
' dsh-iris artifact export <id> --data-root <absolute-path> --output <absolute-path>',
|
|
89
139
|
' dsh-iris artifact rebuild --data-root <absolute-path>',
|
|
140
|
+
' dsh-iris flow init --data-root <new-absolute-path>',
|
|
141
|
+
' dsh-iris flow create --data-root <absolute-path> --provider-config <absolute-path> --input <image-json>',
|
|
142
|
+
' dsh-iris models workflow-check <provider::workflow> --provider-config <absolute-path> [--input <workflow_parameters-json>] (read-only)',
|
|
143
|
+
' Use explicit model_ref and n=1. Edits require an imported source; text-to-image requires a new execution init --image-generation root. create saves private inputs but never submits.',
|
|
144
|
+
' dsh-iris flow <list|inspect> [run-id] --data-root <absolute-path>',
|
|
145
|
+
' dsh-iris flow continue <run-id> --data-root <absolute-path> [--provider-config <absolute-path>]',
|
|
146
|
+
' dsh-iris flow select <run-id> --data-root <absolute-path> --input <json with artifact_id>',
|
|
147
|
+
' dsh-iris flow export <run-id> --data-root <absolute-path> --output <absolute-path>',
|
|
90
148
|
' dsh-iris core <delete|cleanup> --data-root <absolute-path> [--input <json>] [--confirm-delete]',
|
|
91
149
|
' Default is a read-only preview. delete selects task_ids/artifact_ids; cleanup executes only explicitly selected preview paths.',
|
|
92
150
|
' dsh-iris core transactions --data-root <absolute-path>',
|
|
93
151
|
' dsh-iris core restore <transaction-id> --data-root <absolute-path>',
|
|
94
152
|
'',
|
|
95
|
-
'Commands never start DSH. run image and the task observe/redeliver/cancel/retry verbs contact a provider only when explicitly requested; credentials come from a private file. observe polls once, redeliver only re-downloads, and cancel is recorded as canceled only when the provider explicitly confirms; none of them ever submits an existing task again. task retry creates a NEW task that may incur duplicate generation billing and requires --confirm-billing; the prompt must be supplied again because
|
|
153
|
+
'Commands never start DSH. run image and the task observe/redeliver/cancel/retry verbs contact a provider only when explicitly requested; credentials come from a private file. observe polls once, redeliver only re-downloads, and cancel is recorded as canceled only when the provider explicitly confirms; none of them ever submits an existing task again. task retry creates a NEW task that may incur duplicate generation billing and requires --confirm-billing; the prompt must be supplied again because Task records never persist prompts. Flow inputs do retain the prompt privately. flow continue may perform the first submission or observe/redeliver the original task, but never resubmits a task with an Attempt.'
|
|
96
154
|
].join('\n');
|
|
97
155
|
}
|
|
98
156
|
|
|
99
|
-
function
|
|
157
|
+
function parseFlags(args, allowed) {
|
|
100
158
|
const parsed = {};
|
|
101
159
|
for (let index = 0; index < args.length; index += 2) {
|
|
102
160
|
const flag = args[index];
|
|
@@ -154,6 +212,7 @@ function generationInput(capability, input, options) {
|
|
|
154
212
|
/** C-8: catchable signals dispose active runtimes before the CLI exits. */
|
|
155
213
|
const activeRuntimes = new Set();
|
|
156
214
|
const cliAbortController = new AbortController();
|
|
215
|
+
let cliProviderRegistry;
|
|
157
216
|
let shuttingDown = false;
|
|
158
217
|
|
|
159
218
|
function registerRuntime(runtime) {
|
|
@@ -164,6 +223,7 @@ function registerRuntime(runtime) {
|
|
|
164
223
|
async function disposeActiveRuntimes() {
|
|
165
224
|
await Promise.allSettled([...activeRuntimes].map((runtime) => runtime.dispose().catch(() => {})));
|
|
166
225
|
activeRuntimes.clear();
|
|
226
|
+
await cliProviderRegistry?.dispose();
|
|
167
227
|
}
|
|
168
228
|
|
|
169
229
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
@@ -193,12 +253,314 @@ async function withRuntime(mode, dataRoot, callback, ports = {}) {
|
|
|
193
253
|
return withRawRuntime(mode, dataRoot, (runtime) => callback(createCommandService(runtime, ports)));
|
|
194
254
|
}
|
|
195
255
|
|
|
196
|
-
async function
|
|
256
|
+
async function withApplication(dataRoot, callback, resolveImageCandidates, resolveTaskAdapter) {
|
|
257
|
+
const application = createApplicationRuntime({ dataRoot: required(dataRoot, 'data-root'), resolveImageCandidates, resolveTaskAdapter });
|
|
258
|
+
const unregister = registerRuntime(application);
|
|
259
|
+
try { return await callback(application); }
|
|
260
|
+
finally { try { await application.dispose(); } finally { unregister(); } }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Own the service and its plugin registry in order, including cancellation during import/start. */
|
|
264
|
+
async function withCliLaunch(pluginConfig, launchOptions, callback) {
|
|
265
|
+
let loading, launch, api, disposal, startupFailure;
|
|
266
|
+
const detail = error => ({ code: error.code || 'IRIS_CLI_FAILED', message: error.message || String(error),
|
|
267
|
+
...(error instanceof AggregateError ? { errors: error.errors.map(detail) } : {}) });
|
|
268
|
+
const lifetime = { dispose() {
|
|
269
|
+
if (!disposal) disposal = (async () => {
|
|
270
|
+
if (loading) await Promise.allSettled([loading]); // main retains the original startup error.
|
|
271
|
+
const failures = [];
|
|
272
|
+
try { await api?.dispose(); } catch (error) { failures.push({ stage: 'core-stop', error }); }
|
|
273
|
+
try { await launch?.dispose(); } catch (error) { failures.push({ stage: 'plugin-stop', error }); }
|
|
274
|
+
if (failures.length) {
|
|
275
|
+
const error = Object.assign(new AggregateError(failures.map(item => item.error), 'Core 服务或插件释放失败;已尝试两阶段收尾'), { code: 'IRIS_CLI_SERVICE_CLEANUP_FAILED' });
|
|
276
|
+
error.diagnostic = { code: error.code, stage: 'shutdown', message: error.message, ...(pluginConfig ? { file: pluginConfig } : {}),
|
|
277
|
+
errors: failures.map(item => ({ stage: item.stage, ...detail(item.error) })) };
|
|
278
|
+
if (cliAbortController.signal.aborted) console.error(JSON.stringify(error.diagnostic));
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
})();
|
|
282
|
+
return disposal;
|
|
283
|
+
} };
|
|
284
|
+
const unregister = registerRuntime(lifetime);
|
|
285
|
+
try {
|
|
286
|
+
loading = pluginConfig ? loadPluginLaunch(pluginConfig, launchOptions).then(value => (launch = value)) : Promise.resolve();
|
|
287
|
+
await loading; cliAbortController.signal.throwIfAborted();
|
|
288
|
+
return await callback(launch, value => { api = value; });
|
|
289
|
+
} catch (error) { startupFailure = error; throw error; }
|
|
290
|
+
finally {
|
|
291
|
+
try { await lifetime.dispose(); }
|
|
292
|
+
catch (error) { if (startupFailure) error.diagnostic.startup = detail(startupFailure); throw error; }
|
|
293
|
+
finally { unregister(); }
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function main(args, providerRegistry) {
|
|
298
|
+
const flags = (values, allowed) => Object.defineProperty(parseFlags(values, allowed), 'providerRegistry', { value: providerRegistry });
|
|
197
299
|
if (!args.length || args.includes('--help') || args.includes('-h')) {
|
|
198
300
|
console.log(usage());
|
|
199
301
|
return args.length ? 0 : 2;
|
|
200
302
|
}
|
|
201
303
|
|
|
304
|
+
if (args[0] === 'providers' && args[1] === 'protocols') {
|
|
305
|
+
flags(args.slice(2), []);
|
|
306
|
+
console.log(JSON.stringify(providerRegistry.snapshot(), null, 2)); return 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (args[0] === 'plugins') {
|
|
310
|
+
if (args[1] === 'list') {
|
|
311
|
+
flags(args.slice(2), []);
|
|
312
|
+
console.log(JSON.stringify({ contractVersion: 0, builtins: builtinLaunchPlugins(), automaticDiscovery: false })); return 0;
|
|
313
|
+
}
|
|
314
|
+
if (args[1] !== 'inspect') throw new CliUsageError('plugins 仅支持 list/inspect');
|
|
315
|
+
const options = flags(args.slice(2), ['plugin-config']);
|
|
316
|
+
return withCliLaunch(required(options['plugin-config'], 'plugin-config'), {}, async launch => {
|
|
317
|
+
console.log(JSON.stringify(launch.summary, null, 2)); return 0;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (args[0] === 'skills') {
|
|
322
|
+
const rest = args.slice(2), dryRunFlags = rest.filter(value => value === '--dry-run');
|
|
323
|
+
if (dryRunFlags.length > 1) throw new CliUsageError('参数重复:--dry-run');
|
|
324
|
+
const options = flags(rest.filter(value => value !== '--dry-run'), ['target', 'directory', 'bundle']);
|
|
325
|
+
let result;
|
|
326
|
+
try {
|
|
327
|
+
result = manageCoreSkill({ action: args[1], target: options.target, directory: options.directory, bundle: options.bundle, dryRun: dryRunFlags.length === 1 });
|
|
328
|
+
} catch (error) {
|
|
329
|
+
if (error.code === 'IRIS_SKILL_OPTIONS_INVALID') throw new CliUsageError(error.message);
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
console.log(JSON.stringify(result, null, 2));
|
|
333
|
+
return args[1] === 'status' && result.status !== 'current' ? 1 : 0;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (args[0] === 'mcp') {
|
|
337
|
+
if (args[1] === 'presets') {
|
|
338
|
+
flags(args.slice(2), []);
|
|
339
|
+
const { MCP_CONNECTION_PRESETS } = await import('../lib/mcp-delivery.js');
|
|
340
|
+
console.log(JSON.stringify({ contractVersion: 0, presets: MCP_CONNECTION_PRESETS, installationDetected: false }, null, 2)); return 0;
|
|
341
|
+
}
|
|
342
|
+
if (args[1] === 'capabilities') {
|
|
343
|
+
flags(args.slice(2), []);
|
|
344
|
+
const { BRIDGE_CAPABILITIES, BRIDGE_CONTROL_TOOLS } = await import('../lib/core-bridge-selection.js');
|
|
345
|
+
console.log(JSON.stringify({ contractVersion: 0, capabilities: BRIDGE_CAPABILITIES, controlTools: BRIDGE_CONTROL_TOOLS,
|
|
346
|
+
implementedMedia: BRIDGE_CAPABILITIES.map(group => group.recipe), selectionDoesNotGenerate: true }, null, 2)); return 0;
|
|
347
|
+
}
|
|
348
|
+
if (!['serve', 'config', 'check'].includes(args[1])) throw new CliUsageError('mcp 支持 serve/config/check/capabilities/presets');
|
|
349
|
+
const options = flags(args.slice(2), ['connection-file', 'scopes', 'capabilities', 'tools', ...(args[1] === 'config' ? ['transport', 'format', 'preset', 'output'] : [])]);
|
|
350
|
+
const list = value => value === 'none' ? [] : value.split(',');
|
|
351
|
+
const selected = { ...(options.capabilities !== undefined ? { capabilities: list(options.capabilities) } : {}),
|
|
352
|
+
...(options.tools !== undefined ? { tools: list(options.tools) } : {}) };
|
|
353
|
+
if (args[1] === 'check') {
|
|
354
|
+
const { checkMcpConnection } = await import('../lib/mcp-check.js');
|
|
355
|
+
const result = await checkMcpConnection({ connectionFile: required(options['connection-file'], 'connection-file'),
|
|
356
|
+
...(options.scopes !== undefined ? { scopes: options.scopes.split(',') } : {}), ...selected });
|
|
357
|
+
console.log(JSON.stringify(result, null, 2)); return result.ok ? 0 : 1;
|
|
358
|
+
}
|
|
359
|
+
if (args[1] === 'config') {
|
|
360
|
+
const { exportMcpConfiguration } = await import('../lib/mcp-config.js');
|
|
361
|
+
const result = exportMcpConfiguration({ connectionFile: required(options['connection-file'], 'connection-file'), ...selected,
|
|
362
|
+
...(options.scopes !== undefined ? { scopes: options.scopes.split(',') } : {}), transport: options.transport, format: options.format, presetId: options.preset, output: options.output });
|
|
363
|
+
process.stdout.write(typeof result === 'string' ? result : JSON.stringify(result, null, 2) + '\n');
|
|
364
|
+
return 0;
|
|
365
|
+
}
|
|
366
|
+
const { createMcpBridge } = await import('../lib/mcp-bridge.js');
|
|
367
|
+
const manifest = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
368
|
+
const bridge = createMcpBridge({ connectionFile: required(options['connection-file'], 'connection-file'),
|
|
369
|
+
requestedScopes: (options.scopes === undefined ? 'read' : options.scopes).split(','), ...selected, version: manifest.version });
|
|
370
|
+
const unregister = registerRuntime(bridge);
|
|
371
|
+
try { await bridge.start(); await bridge.closed; return 0; }
|
|
372
|
+
finally { try { await bridge.dispose(); } finally { unregister(); } }
|
|
373
|
+
}
|
|
374
|
+
if (args[0] === 'connection') {
|
|
375
|
+
if (args[1] !== 'create') throw new CliUsageError('connection 只支持显式 create');
|
|
376
|
+
const options = flags(args.slice(2), ['data-root', 'directory', 'origin', 'harness', 'label', 'scopes', 'capabilities', 'tools']);
|
|
377
|
+
const result = createConnectionPair({ dataRoot: required(options['data-root'], 'data-root'), directory: required(options.directory, 'directory'), origin: required(options.origin, 'origin'),
|
|
378
|
+
harnessKind: options.harness === undefined ? 'generic' : options.harness, label: required(options.label, 'label'),
|
|
379
|
+
...(options.scopes !== undefined ? { scopes: options.scopes.split(',') } : {}),
|
|
380
|
+
...(options.capabilities !== undefined ? { capabilities: options.capabilities === 'none' ? [] : options.capabilities.split(',') } : {}),
|
|
381
|
+
...(options.tools !== undefined ? { tools: options.tools === 'none' ? [] : options.tools.split(',') } : {}) });
|
|
382
|
+
console.log(JSON.stringify(result, null, 2)); return 0;
|
|
383
|
+
}
|
|
384
|
+
if (args[0] === 'execution' || args[0] === 'operation') {
|
|
385
|
+
if (args[0] === 'execution') {
|
|
386
|
+
if (['inspect', 'recover-lease'].includes(args[1])) {
|
|
387
|
+
const options = flags(args.slice(2), ['data-root', ...(args[1] === 'recover-lease' ? ['confirm-stale-pid'] : [])]);
|
|
388
|
+
const dataRoot = required(options['data-root'], 'data-root');
|
|
389
|
+
if (args[1] === 'recover-lease' && !/^[1-9][0-9]*$/.test(options['confirm-stale-pid'] || '')) throw new CliUsageError('需要明确回显 stale owner PID');
|
|
390
|
+
const result = args[1] === 'inspect' ? inspectExecutionLease(dataRoot)
|
|
391
|
+
: recoverExecutionLease(dataRoot, { confirmStalePid: Number(options['confirm-stale-pid']) });
|
|
392
|
+
console.log(JSON.stringify(result, null, 2)); return 0;
|
|
393
|
+
}
|
|
394
|
+
if (args[1] !== 'init') throw new CliUsageError('不支持的 execution 子命令');
|
|
395
|
+
const initArgs = args.slice(2), generationFlags = initArgs.filter(value => value === '--image-generation'), audioFlags = initArgs.filter(value => value === '--audio');
|
|
396
|
+
if (generationFlags.length > 1 || audioFlags.length > 1 || generationFlags.length && audioFlags.length) throw new CliUsageError('初始化开关重复或冲突;audio已包含文生图');
|
|
397
|
+
const options = flags(initArgs.filter(value => !['--image-generation', '--audio'].includes(value)), ['data-root']);
|
|
398
|
+
const suppliedRoot = required(options['data-root'], 'data-root');
|
|
399
|
+
if (!path.isAbsolute(suppliedRoot)) throw new CliUsageError('data-root 必须是已初始化的空 Flow 根绝对路径');
|
|
400
|
+
let dataRoot;
|
|
401
|
+
try { dataRoot = fs.realpathSync(suppliedRoot); }
|
|
402
|
+
catch (_) { throw new CliUsageError('请先在新的专用数据根执行 flow init'); }
|
|
403
|
+
const result = await withApplication(dataRoot, application =>
|
|
404
|
+
application.execute('execution.init', audioFlags.length ? { audio: true } : generationFlags.length ? { imageGeneration: true } : {}, { signal: cliAbortController.signal }));
|
|
405
|
+
console.log(JSON.stringify(result, null, 2)); return 0;
|
|
406
|
+
}
|
|
407
|
+
if (!['list', 'inspect'].includes(args[1])) throw new CliUsageError('operation 只支持只读 list/inspect');
|
|
408
|
+
const inspecting = args[1] === 'inspect';
|
|
409
|
+
if (inspecting && (!args[2] || args[2].startsWith('--'))) throw new CliUsageError('缺少 Operation ID');
|
|
410
|
+
const options = flags(args.slice(inspecting ? 3 : 2), ['data-root', ...(inspecting ? [] : ['run-id', 'limit', 'cursor'])]);
|
|
411
|
+
if (options.limit !== undefined && !/^(?:[1-9]|[12][0-9]|30)$/.test(options.limit)) throw new CliUsageError('limit 必须是 1–30 的整数');
|
|
412
|
+
const input = inspecting ? { id: args[2] } : { ...(options['run-id'] ? { runId: options['run-id'] } : {}),
|
|
413
|
+
...(options.limit !== undefined ? { limit: Number(options.limit) } : {}), ...(options.cursor ? { cursor: options.cursor } : {}) };
|
|
414
|
+
const result = await withApplication(required(options['data-root'], 'data-root'), application =>
|
|
415
|
+
application.execute('operation.' + args[1], input, { signal: cliAbortController.signal }));
|
|
416
|
+
console.log(JSON.stringify(result, null, 2)); return 0;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (args[0] === 'api' || args[0] === 'web') {
|
|
420
|
+
const serveWorkbench = args[0] === 'web';
|
|
421
|
+
if (serveWorkbench && args[1] === 'password') {
|
|
422
|
+
if (args[2] !== 'reset') throw new CliUsageError('web password 仅支持 reset');
|
|
423
|
+
const options = flags(args.slice(3), ['data-root', 'auth-store']);
|
|
424
|
+
const dataRoot = required(options['data-root'], 'data-root');
|
|
425
|
+
if (!path.isAbsolute(dataRoot)) throw new CliUsageError('data-root 必须是工作台数据根的绝对路径');
|
|
426
|
+
console.log(JSON.stringify(await manageWorkbenchPassword({ dataRoot, directory: options['auth-store'], reset: true, signal: cliAbortController.signal })));
|
|
427
|
+
return 0;
|
|
428
|
+
}
|
|
429
|
+
if (serveWorkbench && args[1] === 'login') {
|
|
430
|
+
const options = flags(args.slice(2), ['origin', 'token-file', 'data-root', 'auth-store']);
|
|
431
|
+
if (options['data-root'] !== undefined) {
|
|
432
|
+
if (options.origin !== undefined || options['token-file'] !== undefined) throw new CliUsageError('口令登录用 data-root,旧 Token 登录用 origin/token-file,不能混用');
|
|
433
|
+
if (!path.isAbsolute(options['data-root'])) throw new CliUsageError('data-root 必须是工作台数据根的绝对路径');
|
|
434
|
+
console.log(JSON.stringify(await manageWorkbenchPassword({ dataRoot: options['data-root'], directory: options['auth-store'], signal: cliAbortController.signal })));
|
|
435
|
+
return 0;
|
|
436
|
+
}
|
|
437
|
+
if (options['auth-store'] !== undefined) throw new CliUsageError('auth-store 需要同时明确 data-root');
|
|
438
|
+
const origin = required(options.origin, 'origin'), tokenFile = required(options['token-file'], 'token-file');
|
|
439
|
+
if (!/^http:\/\/127\.0\.0\.1:[1-9]\d{0,4}$/.test(origin) || Number(origin.split(':').at(-1)) > 65535) throw new CliUsageError('origin 必须是启动时输出的 http://127.0.0.1:端口');
|
|
440
|
+
if (!path.isAbsolute(tokenFile)) throw new CliUsageError('token-file 必须是当前启动文件的绝对路径');
|
|
441
|
+
let token;
|
|
442
|
+
const fd = fs.openSync(tokenFile, 'r');
|
|
443
|
+
try {
|
|
444
|
+
const info = fs.fstatSync(fd);
|
|
445
|
+
if (!info.isFile() || info.size > 256) throw new CliUsageError('请选择当前启动生成的小型 Token 文本文件');
|
|
446
|
+
token = fs.readFileSync(fd, 'utf8').trim();
|
|
447
|
+
} finally { fs.closeSync(fd); }
|
|
448
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(token)) throw new CliUsageError('Token 文件内容无效,请使用当前启动生成的文件');
|
|
449
|
+
const response = await fetch(origin + '/api/v0/workbench/login-link', { method: 'POST',
|
|
450
|
+
headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' }, body: '{}',
|
|
451
|
+
redirect: 'error', signal: AbortSignal.any([cliAbortController.signal, AbortSignal.timeout(15000)]) });
|
|
452
|
+
if (!response.ok) throw new CliUsageError(response.status === 401 ? 'Token 已失效,请使用当前服务启动时生成的 Token 文件'
|
|
453
|
+
: response.status === 404 || response.status === 405 ? '此服务不支持登录链接,请确认地址属于新版 web serve 工作台' : '无法获取登录链接,请检查本机工作台状态');
|
|
454
|
+
const result = await response.json();
|
|
455
|
+
if (result.contractVersion !== 0 || typeof result.loginUrl !== 'string' || !result.loginUrl.startsWith(origin + '/#login=')
|
|
456
|
+
|| !/^[A-Za-z0-9_-]{43}$/.test(result.loginUrl.slice((origin + '/#login=').length)) || !Number.isFinite(Date.parse(result.loginExpiresAt))) {
|
|
457
|
+
throw new CliUsageError('工作台返回的登录链接无效');
|
|
458
|
+
}
|
|
459
|
+
console.log(JSON.stringify({ ...result, message: '打开 loginUrl 即可进入;链接 10 分钟内有效、仅用一次,刷新无需重新登录。' }));
|
|
460
|
+
return 0;
|
|
461
|
+
}
|
|
462
|
+
if (!serveWorkbench && args[1] === 'init') {
|
|
463
|
+
const options = flags(args.slice(2), ['data-root']);
|
|
464
|
+
const suppliedRoot = required(options['data-root'], 'data-root');
|
|
465
|
+
if (!path.isAbsolute(suppliedRoot)) throw new CliUsageError('data-root 必须是已初始化的空 Flow 根绝对路径');
|
|
466
|
+
let dataRoot;
|
|
467
|
+
try { dataRoot = fs.realpathSync(suppliedRoot); }
|
|
468
|
+
catch (_) { throw new CliUsageError('请先在新的专用数据根执行 flow init'); }
|
|
469
|
+
const initialized = await withApplication(dataRoot, application => application.execute('creation.init', {}, { signal: cliAbortController.signal }));
|
|
470
|
+
console.log(JSON.stringify(initialized));
|
|
471
|
+
return 0;
|
|
472
|
+
}
|
|
473
|
+
if (args[1] !== 'serve') throw new CliUsageError(`不支持的 ${args[0]} 子命令`);
|
|
474
|
+
const rest = args.slice(2), localWriteFlags = rest.filter(value => value === '--allow-local-writes'), executionFlags = rest.filter(value => value === '--allow-execution'), selectionFlags = rest.filter(value => value === '--allow-selection'), mcpFlags = rest.filter(value => value === '--allow-mcp'), managementFlags = rest.filter(value => value === '--allow-connection-management'), providerFlags = rest.filter(value => value === '--allow-provider-management');
|
|
475
|
+
if (localWriteFlags.length > 1 || executionFlags.length > 1 || selectionFlags.length > 1 || mcpFlags.length > 1 || managementFlags.length > 1 || providerFlags.length > 1) throw new CliUsageError('写入/执行/选择/MCP/管理参数重复');
|
|
476
|
+
const allowExecution = executionFlags.length === 1, allowLocalWrites = localWriteFlags.length === 1 || allowExecution;
|
|
477
|
+
const allowSelection = selectionFlags.length === 1;
|
|
478
|
+
const allowConnectionManagement = managementFlags.length === 1;
|
|
479
|
+
const options = flags(rest.filter(value => !['--allow-local-writes', '--allow-execution', '--allow-selection', '--allow-mcp', '--allow-connection-management', '--allow-provider-management'].includes(value)), ['data-root', 'token-file', 'token-dir', 'auth-store', 'provider-config', 'port', 'connections-file', 'connection-store', 'panel', 'plugin-config']);
|
|
480
|
+
const suppliedRoot = required(options['data-root'], 'data-root');
|
|
481
|
+
const passwordMode = serveWorkbench && options['token-file'] === undefined && options['token-dir'] === undefined;
|
|
482
|
+
if (!passwordMode && Boolean(options['token-file']) === Boolean(options['token-dir'])) throw new CliUsageError('请选择且只选择 --token-file 或 --token-dir');
|
|
483
|
+
if (options['auth-store'] !== undefined && !passwordMode) throw new CliUsageError('auth-store 仅用于默认口令工作台;不能与 Token 参数或 api serve 混用');
|
|
484
|
+
if (!path.isAbsolute(suppliedRoot) || !passwordMode && !path.isAbsolute(options['token-file'] || options['token-dir'])) throw new CliUsageError('data-root 与 token-file/token-dir 必须是绝对路径');
|
|
485
|
+
let tokenFile = options['token-file'];
|
|
486
|
+
if (options.port !== undefined && (!/^\d+$/.test(options.port) || Number(options.port) > 65535)) throw new CliUsageError('port 必须是 0–65535 的整数');
|
|
487
|
+
// Pin the physical root for both the server and the outside-root credential check.
|
|
488
|
+
let dataRoot;
|
|
489
|
+
try { dataRoot = fs.realpathSync(suppliedRoot); }
|
|
490
|
+
catch (_) { throw new CliUsageError('data-root 必须是已初始化且可读取的 Flow 根'); }
|
|
491
|
+
const authDirectory = passwordMode ? resolveWorkbenchAuthDirectory(dataRoot, options['auth-store']) : undefined;
|
|
492
|
+
if (options['token-dir']) checkLocalApiTokenDirectory(options['token-dir'], dataRoot);
|
|
493
|
+
return withCliLaunch(options['plugin-config'], { serveWorkbench, panelManifestPath: options.panel }, async (launch, ownApi) => {
|
|
494
|
+
const api = createLocalReadApi({ dataRoot, providerConfigPath: options['provider-config'], allowLocalWrites, serveWorkbench,
|
|
495
|
+
...(passwordMode ? { workbenchAuth: { directory: authDirectory } } : {}),
|
|
496
|
+
allowExecution, allowSelection, allowMcp: mcpFlags.length === 1, connectionsFile: options['connections-file'], allowConnectionManagement, connectionStore: options['connection-store'], providerRegistry,
|
|
497
|
+
...(launch ? { pluginRegistry: launch.registry, providerRegistry: launch.registry.providers, allowedOperations: launch.allowedOperations,
|
|
498
|
+
panelContributions: launch.panelContributions } : {}),
|
|
499
|
+
allowProviderManagement: providerFlags.length === 1, panelManifestPath: options.panel });
|
|
500
|
+
ownApi(api);
|
|
501
|
+
const started = await api.start({ port: options.port === undefined ? 0 : Number(options.port) });
|
|
502
|
+
if (!passwordMode) {
|
|
503
|
+
if (options['token-dir']) tokenFile = writeLocalApiTokenDirectory(options['token-dir'], started.accessToken, dataRoot);
|
|
504
|
+
else writeLocalApiTokenFile(tokenFile, started.accessToken, dataRoot);
|
|
505
|
+
}
|
|
506
|
+
console.log(JSON.stringify({ contractVersion: 0, readOnly: !allowLocalWrites && !allowSelection, origin: started.origin,
|
|
507
|
+
...(passwordMode ? { authentication: started.authentication, authStore: authDirectory } : { tokenFile }),
|
|
508
|
+
...(launch ? { pluginLaunch: launch.summary } : {}),
|
|
509
|
+
...(serveWorkbench ? { loginUrl: started.loginUrl, loginExpiresAt: started.loginExpiresAt } : {}),
|
|
510
|
+
...(allowConnectionManagement ? { connectionManagement: true } : {}),
|
|
511
|
+
...(providerFlags.length ? { providerManagement: true } : {}),
|
|
512
|
+
...(started.mcpEndpoint ? { mcpEndpoint: started.mcpEndpoint } : {}),
|
|
513
|
+
message: passwordMode ? (started.authentication.configured ? '工作台已启动,打开 origin 用访问口令登录;记住设备后可跨正常重启继续使用。'
|
|
514
|
+
: '首次使用请打开 loginUrl 设置访问口令;之后打开普通地址即可登录,无需 Token 文件。')
|
|
515
|
+
: serveWorkbench ? (providerFlags.length ? 'Core 工作台与供应商管理已启动;打开 loginUrl 即可进入。配置保存不生成媒体。'
|
|
516
|
+
: allowConnectionManagement ? 'Core 工作台与连接管理已启动;打开 loginUrl 即可进入。配置连接不执行媒体。'
|
|
517
|
+
: allowLocalWrites || allowSelection ? 'Core 工作台已启动;打开 loginUrl 即可进入。媒体 API 按显式开关开放。'
|
|
518
|
+
: launch?.allowedOperations !== undefined ? 'Core 工作台已启动;打开 loginUrl 即可进入。插件业务按启动选择开放,媒体 API 保持只读。'
|
|
519
|
+
: '只读工作台已启动;打开 loginUrl 即可进入。')
|
|
520
|
+
: allowExecution ? '有限执行 API 已启动;显式继续才执行,停止只中止本地等待,重启不自动重提。'
|
|
521
|
+
: allowLocalWrites ? '本地创建 API 已启动;只上传/创建,不执行生成。会话 Token 在指定私有文件中,退出后失效。'
|
|
522
|
+
: allowSelection ? '本地选择 API 已启动;只更新已完成流程的选择,不执行生成。会话 Token 在指定私有文件中,退出后失效。'
|
|
523
|
+
: launch?.allowedOperations !== undefined ? '插件业务 API 已启动;按启动选择执行,媒体 API 保持只读。会话 Token 在指定私有文件中,退出后失效。'
|
|
524
|
+
: '只读本地 API 已启动;会话 Token 在指定私有文件中,退出后失效。' }));
|
|
525
|
+
await api.closed;
|
|
526
|
+
return 0;
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (args[0] === 'flow') {
|
|
531
|
+
const verb = args[1];
|
|
532
|
+
if (!['init', 'create', 'list', 'inspect', 'continue', 'select', 'export'].includes(verb)) throw new CliUsageError('不支持的 flow 子命令');
|
|
533
|
+
const needsId = ['inspect', 'continue', 'select', 'export'].includes(verb);
|
|
534
|
+
if (needsId && (!args[2] || args[2].startsWith('--'))) throw new CliUsageError('缺少 Run ID');
|
|
535
|
+
const allowed = ['data-root', ...(['create', 'continue'].includes(verb) ? ['provider-config'] : []),
|
|
536
|
+
...(['create', 'select'].includes(verb) ? ['input'] : []), ...(verb === 'export' ? ['output'] : [])];
|
|
537
|
+
const options = flags(args.slice(needsId ? 3 : 2), allowed);
|
|
538
|
+
const input = ['create', 'select'].includes(verb) ? jsonInput(options.input) : {};
|
|
539
|
+
if (verb === 'select' && Object.keys(input).some(key => key !== 'artifact_id')) throw new CliUsageError('flow select 只接受 artifact_id');
|
|
540
|
+
if (needsId) input.run_id = args[2];
|
|
541
|
+
if (verb === 'export') input.output_path = required(options.output, 'output');
|
|
542
|
+
const resolveImageCandidates = options['provider-config'] ? async ({ modelRef, signal }) => {
|
|
543
|
+
const catalog = await generationCatalog(options, 'image-gen', modelRef, signal);
|
|
544
|
+
return imageCandidatesFromCatalog(catalog, modelRef, providerRegistry).map(route => ({ adapter: trackedProviderAdapter(route.provider, options),
|
|
545
|
+
model: route.modelRef, supportsImageEdit: supportsImageEdit(route.provider.mediaProtocol, route.model, providerRegistry),
|
|
546
|
+
selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider, providerRegistry) }));
|
|
547
|
+
} : undefined;
|
|
548
|
+
const resolveTaskAdapter = options['provider-config'] ? task => {
|
|
549
|
+
const catalog = loadProviderCatalog(options['provider-config']);
|
|
550
|
+
return trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task);
|
|
551
|
+
} : undefined;
|
|
552
|
+
let result;
|
|
553
|
+
try {
|
|
554
|
+
result = await withApplication(options['data-root'], application =>
|
|
555
|
+
application.execute('flow.' + verb, input, { signal: cliAbortController.signal }), resolveImageCandidates, resolveTaskAdapter);
|
|
556
|
+
} catch (error) {
|
|
557
|
+
if (error instanceof GenerationInputError) throw new CliUsageError(error.message);
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
560
|
+
console.log(JSON.stringify(result, null, 2));
|
|
561
|
+
return 0;
|
|
562
|
+
}
|
|
563
|
+
|
|
202
564
|
if (args[0] === 'doctor') {
|
|
203
565
|
const rest = args.slice(1);
|
|
204
566
|
if (rest.filter((arg) => arg === '--json').length > 1) throw new CliUsageError('参数重复:--json');
|
|
@@ -235,14 +597,14 @@ async function main(args) {
|
|
|
235
597
|
if (['providers', 'capabilities'].includes(args[0]) && args[1] === 'list') {
|
|
236
598
|
const options = flags(args.slice(2), ['provider-config']);
|
|
237
599
|
const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
|
|
238
|
-
const report = args[0] === 'providers' ? providerCatalogSnapshot(catalog) : catalogCapabilitySnapshot(catalog);
|
|
600
|
+
const report = args[0] === 'providers' ? providerCatalogSnapshot(catalog) : catalogCapabilitySnapshot(catalog, providerRegistry);
|
|
239
601
|
console.log(JSON.stringify(report, null, 2));
|
|
240
602
|
return 0;
|
|
241
603
|
}
|
|
242
604
|
|
|
243
605
|
if (['config', 'providers', 'models', 'assignments'].includes(args[0])) {
|
|
244
606
|
const [group, verb] = args;
|
|
245
|
-
const verbs = { config: ['init', 'show', 'check'], providers: ['add', 'set', 'remove'], models: ['list', 'add', 'remove', 'caps', 'protocol', 'music-protocol', 'vision-input', 'discover', 'test'], assignments: ['list', 'set', 'clear'] };
|
|
607
|
+
const verbs = { config: ['init', 'show', 'check'], providers: ['add', 'set', 'remove'], models: ['list', 'add', 'remove', 'caps', 'protocol', 'music-protocol', 'vision-input', 'discover', 'test', 'workflow-check'], assignments: ['list', 'set', 'clear'] };
|
|
246
608
|
if (!verbs[group].includes(verb)) throw new CliUsageError('未知配置命令');
|
|
247
609
|
const positional = (group === 'providers' && verb === 'remove') || (group === 'models' && verb !== 'list');
|
|
248
610
|
const id = positional ? required(args[2], group === 'providers' || verb === 'discover' ? 'provider-id' : 'model-ref') : undefined;
|
|
@@ -259,7 +621,7 @@ async function main(args) {
|
|
|
259
621
|
}
|
|
260
622
|
}
|
|
261
623
|
const result = await executeConfigCommand(required(options['provider-config'], 'provider-config'), group === 'config' ? verb : group + '.' + verb, input, {
|
|
262
|
-
signal: cliAbortController.signal,
|
|
624
|
+
signal: cliAbortController.signal, registry: providerRegistry,
|
|
263
625
|
probe: async ({ provider, modelId, capability, signal }) => {
|
|
264
626
|
if (capability === 'music-gen') return { skipped: true, ok: false, reason: 'Music 不发起小样本探测;请使用 run music 提交创作要求' };
|
|
265
627
|
if (capability === 'vision') return probeVisionModel(provider, modelId, { bytes: Buffer.from(RED_TEST_IMAGE.split(',')[1], 'base64'), mediaType: 'image/png' }, { signal });
|
|
@@ -272,14 +634,14 @@ async function main(args) {
|
|
|
272
634
|
}
|
|
273
635
|
return withRawRuntime('writer', options['data-root'], async runtime => {
|
|
274
636
|
const mediaCapability = capability === 'image-gen' ? 'image' : capability === 'video-gen' ? 'video' : capability;
|
|
275
|
-
if (mediaCapability === 'image') provider = providerForImageModel(provider, modelId);
|
|
276
|
-
const adapter = createConfiguredProviderAdapter(provider, { allowRateLimited: true, modelId });
|
|
637
|
+
if (mediaCapability === 'image') provider = providerForImageModel(provider, modelId, providerRegistry);
|
|
638
|
+
const adapter = createConfiguredProviderAdapter(provider, { allowRateLimited: true, modelId, registry: providerRegistry });
|
|
277
639
|
let providerInput = mediaCapability === 'image'
|
|
278
|
-
? { prompt: 'red circle', n: 1, ...(['openai-chat-images', 'openai-responses-images'].includes(adapter.protocol) ? {} : {
|
|
640
|
+
? { prompt: 'red circle', n: 1, ...(['openai-chat-images', 'openai-responses-images', 'comfyui'].includes(adapter.protocol) ? {} : {
|
|
279
641
|
size: adapter.protocol === 'openai-images' ? '256x256' : '512*512'
|
|
280
642
|
}) }
|
|
281
643
|
: { text: '你好', voice: probeInput.voice || provider.ttsVoice || 'Cherry' };
|
|
282
|
-
let candidates = [{ adapter, model: qualifiedModelRef(provider.id, modelId), selectionReason: 'explicit', providerBinding: providerTaskBinding(provider) }];
|
|
644
|
+
let candidates = [{ adapter, model: qualifiedModelRef(provider.id, modelId), selectionReason: 'explicit', providerBinding: providerTaskBinding(provider, providerRegistry) }];
|
|
283
645
|
if (['video', 'transcribe'].includes(mediaCapability)) {
|
|
284
646
|
const normalized = generationInput(mediaCapability, { ...probeInput, model_ref: qualifiedModelRef(provider.id, modelId) },
|
|
285
647
|
{ allowVideoPaths: true, allowAudioPath: true });
|
|
@@ -320,8 +682,8 @@ async function main(args) {
|
|
|
320
682
|
} catch (_) { throw new CliUsageError('输出文件已存在或父目录不可写'); }
|
|
321
683
|
}
|
|
322
684
|
const catalog = await generationCatalog(options, 'vision', options['model-ref'] || input.model_ref);
|
|
323
|
-
if (input.transcribe && !input.transcribe_model_ref) await refreshCatalogDirectories(options['provider-config'], catalog, 'transcribe', { signal: cliAbortController.signal });
|
|
324
|
-
const execute = runtime => executeVisionCommand({ command, input, catalog, runtime,
|
|
685
|
+
if (input.transcribe && !input.transcribe_model_ref) await refreshCatalogDirectories(options['provider-config'], catalog, 'transcribe', { signal: cliAbortController.signal, registry: providerRegistry });
|
|
686
|
+
const execute = runtime => executeVisionCommand({ command, input, catalog, runtime, registry: providerRegistry,
|
|
325
687
|
onRateLimit: event => recordCatalogRateLimit(options['provider-config'], event),
|
|
326
688
|
onModelAvailability: event => recordCatalogAvailability(options['provider-config'], event),
|
|
327
689
|
modelRef: options['model-ref'] || '', signal: cliAbortController.signal,
|
|
@@ -358,8 +720,9 @@ async function main(args) {
|
|
|
358
720
|
const options = flags(args.slice(2), ['data-root', 'input']);
|
|
359
721
|
const input = jsonInput(options.input);
|
|
360
722
|
const command = args[1] === 'diff' ? 'media.diff' : 'media.frames';
|
|
361
|
-
const result =
|
|
362
|
-
|
|
723
|
+
const result = args[1] === 'diff'
|
|
724
|
+
? await withApplication(options['data-root'], application => application.execute(command, input, { signal: cliAbortController.signal }))
|
|
725
|
+
: await withRuntime('writer', options['data-root'], commands => commands.execute(command, input));
|
|
363
726
|
console.log(JSON.stringify(result, null, 2));
|
|
364
727
|
return 0;
|
|
365
728
|
}
|
|
@@ -375,21 +738,25 @@ async function main(args) {
|
|
|
375
738
|
|
|
376
739
|
if (args[0] === 'run' && args[1] === 'image') {
|
|
377
740
|
const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const candidates = routes.map((route) => ({
|
|
741
|
+
const input = jsonInput(options.input);
|
|
742
|
+
const resolveImageCandidates = async ({ modelRef, signal }) => {
|
|
743
|
+
const catalog = await generationCatalog(options, 'image-gen', modelRef, signal);
|
|
744
|
+
return imageCandidatesFromCatalog(catalog, modelRef, providerRegistry).map(route => ({
|
|
383
745
|
adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
|
|
746
|
+
supportsImageEdit: supportsImageEdit(route.provider.mediaProtocol, route.model, providerRegistry),
|
|
384
747
|
selectionReason: route.selectionReason,
|
|
385
|
-
providerBinding: providerTaskBinding(route.provider)
|
|
748
|
+
providerBinding: providerTaskBinding(route.provider, providerRegistry)
|
|
386
749
|
}));
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
})
|
|
750
|
+
};
|
|
751
|
+
let result;
|
|
752
|
+
try {
|
|
753
|
+
result = await withApplication(options['data-root'], application =>
|
|
754
|
+
application.generateImage(input, { signal: cliAbortController.signal }), resolveImageCandidates);
|
|
755
|
+
} catch (error) {
|
|
756
|
+
if (error instanceof GenerationInputError) throw new CliUsageError(error.message);
|
|
757
|
+
throw error;
|
|
758
|
+
}
|
|
759
|
+
console.log(JSON.stringify(result, null, 2));
|
|
393
760
|
return 0;
|
|
394
761
|
}
|
|
395
762
|
|
|
@@ -399,12 +766,12 @@ async function main(args) {
|
|
|
399
766
|
const normalized = generationInput('video', jsonInput(options.input), { allowVideoPaths: true });
|
|
400
767
|
const { providerInput, modelRef } = normalized;
|
|
401
768
|
const catalog = await generationCatalog(options, 'video-gen', modelRef);
|
|
402
|
-
const routes = videoCandidatesFromCatalog(catalog, modelRef);
|
|
769
|
+
const routes = videoCandidatesFromCatalog(catalog, modelRef, providerRegistry);
|
|
403
770
|
await withRawRuntime('writer', options['data-root'], async (runtime) => {
|
|
404
771
|
const candidates = videoTaskCandidates(routes.map((route) => ({
|
|
405
772
|
adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
|
|
406
773
|
selectionReason: route.selectionReason,
|
|
407
|
-
providerBinding: providerTaskBinding(route.provider)
|
|
774
|
+
providerBinding: providerTaskBinding(route.provider, providerRegistry)
|
|
408
775
|
})), normalized);
|
|
409
776
|
const result = await createProviderTaskRunner(runtime).submit({
|
|
410
777
|
capability: 'video', candidates,
|
|
@@ -420,12 +787,12 @@ async function main(args) {
|
|
|
420
787
|
const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
|
|
421
788
|
const normalized = generationInput('music', jsonInput(options.input));
|
|
422
789
|
const catalog = await generationCatalog(options, 'music-gen', normalized.modelRef);
|
|
423
|
-
const routes = musicCandidatesFromCatalog(catalog, normalized.modelRef);
|
|
790
|
+
const routes = musicCandidatesFromCatalog(catalog, normalized.modelRef, providerRegistry);
|
|
424
791
|
let result;
|
|
425
792
|
await withRawRuntime('writer', options['data-root'], async runtime => {
|
|
426
793
|
result = await generateMusicTask({ runner: createProviderTaskRunner(runtime),
|
|
427
794
|
candidates: routes.map(route => ({ adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
|
|
428
|
-
selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider) })),
|
|
795
|
+
selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider, providerRegistry) })),
|
|
429
796
|
providerInput: normalized.providerInput, options: normalized.musicOptions, signal: cliAbortController.signal });
|
|
430
797
|
console.log(JSON.stringify(result, null, 2));
|
|
431
798
|
});
|
|
@@ -436,12 +803,12 @@ async function main(args) {
|
|
|
436
803
|
const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
|
|
437
804
|
const { providerInput, modelRef } = generationInput('tts', jsonInput(options.input));
|
|
438
805
|
const catalog = await generationCatalog(options, 'tts', modelRef);
|
|
439
|
-
const routes = ttsCandidatesFromCatalog(catalog, modelRef);
|
|
806
|
+
const routes = ttsCandidatesFromCatalog(catalog, modelRef, providerRegistry);
|
|
440
807
|
await withRawRuntime('writer', options['data-root'], async (runtime) => {
|
|
441
808
|
const candidates = routes.map((route) => ({
|
|
442
809
|
adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
|
|
443
810
|
selectionReason: route.selectionReason,
|
|
444
|
-
providerBinding: providerTaskBinding(route.provider)
|
|
811
|
+
providerBinding: providerTaskBinding(route.provider, providerRegistry)
|
|
445
812
|
}));
|
|
446
813
|
const result = await createProviderTaskRunner(runtime).submit({
|
|
447
814
|
capability: 'tts', candidates,
|
|
@@ -458,7 +825,7 @@ async function main(args) {
|
|
|
458
825
|
const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
|
|
459
826
|
const normalized = generationInput('transcribe', jsonInput(options.input), { allowAudioPath: true });
|
|
460
827
|
const catalog = await generationCatalog(options, 'transcribe', normalized.modelRef);
|
|
461
|
-
const routes = transcribeCandidatesFromCatalog(catalog, normalized.modelRef);
|
|
828
|
+
const routes = transcribeCandidatesFromCatalog(catalog, normalized.modelRef, providerRegistry);
|
|
462
829
|
let providerInput = normalized.providerInput;
|
|
463
830
|
if (normalized.audioPath) {
|
|
464
831
|
if (!fs.existsSync(normalized.audioPath)) throw new CliUsageError('音频文件不存在');
|
|
@@ -471,7 +838,7 @@ async function main(args) {
|
|
|
471
838
|
const candidates = routes.map((route) => ({
|
|
472
839
|
adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
|
|
473
840
|
selectionReason: route.selectionReason,
|
|
474
|
-
providerBinding: providerTaskBinding(route.provider)
|
|
841
|
+
providerBinding: providerTaskBinding(route.provider, providerRegistry)
|
|
475
842
|
}));
|
|
476
843
|
const result = await createProviderTaskRunner(runtime).submit({
|
|
477
844
|
capability: 'transcribe', candidates,
|
|
@@ -489,7 +856,7 @@ async function main(args) {
|
|
|
489
856
|
const inspected = await withRuntime('reader', options['data-root'], commands => commands.execute('task.inspect', { task_id: args[2] }));
|
|
490
857
|
const catalog = coreTaskWaitFinished(inspected.task) ? null : loadProviderCatalog(required(options['provider-config'], 'provider-config'));
|
|
491
858
|
const result = await withRuntime(catalog ? 'writer' : 'reader', options['data-root'], commands => commands.execute('task.wait', input), {
|
|
492
|
-
...(catalog ? { resolveTaskAdapter: task => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task) } : {}) });
|
|
859
|
+
...(catalog ? { resolveTaskAdapter: task => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task) } : {}) });
|
|
493
860
|
console.log(JSON.stringify(result, null, 2));
|
|
494
861
|
return result.timedOut ? 3 : result.ready ? 0 : 1;
|
|
495
862
|
}
|
|
@@ -499,7 +866,7 @@ async function main(args) {
|
|
|
499
866
|
const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
|
|
500
867
|
await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
|
|
501
868
|
const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.observe', { task_id: args[2] }), {
|
|
502
|
-
resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task)
|
|
869
|
+
resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task)
|
|
503
870
|
});
|
|
504
871
|
console.log(JSON.stringify(result, null, 2));
|
|
505
872
|
return 0;
|
|
@@ -510,7 +877,7 @@ async function main(args) {
|
|
|
510
877
|
const catalog = options['provider-config'] ? loadProviderCatalog(options['provider-config']) : null;
|
|
511
878
|
await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
|
|
512
879
|
const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.redeliver', { task_id: args[2] }), {
|
|
513
|
-
...(catalog ? { resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task) } : {})
|
|
880
|
+
...(catalog ? { resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task) } : {})
|
|
514
881
|
});
|
|
515
882
|
console.log(JSON.stringify(result, null, 2));
|
|
516
883
|
return 0;
|
|
@@ -521,7 +888,7 @@ async function main(args) {
|
|
|
521
888
|
const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
|
|
522
889
|
await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
|
|
523
890
|
const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.cancel', { task_id: args[2] }), {
|
|
524
|
-
resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task)
|
|
891
|
+
resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task)
|
|
525
892
|
});
|
|
526
893
|
console.log(JSON.stringify(result, null, 2));
|
|
527
894
|
return 0;
|
|
@@ -545,19 +912,19 @@ async function main(args) {
|
|
|
545
912
|
resolveTaskCandidates: async ({ capability, modelRef }) => {
|
|
546
913
|
const catalog = await generationCatalog(options, { image: 'image-gen', video: 'video-gen', music: 'music-gen' }[capability] || capability, modelRef);
|
|
547
914
|
return (capability === 'video'
|
|
548
|
-
? videoCandidatesFromCatalog(catalog, modelRef || undefined)
|
|
915
|
+
? videoCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
|
|
549
916
|
: capability === 'tts'
|
|
550
|
-
? ttsCandidatesFromCatalog(catalog, modelRef || undefined)
|
|
917
|
+
? ttsCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
|
|
551
918
|
: capability === 'music'
|
|
552
|
-
? musicCandidatesFromCatalog(catalog, modelRef || undefined)
|
|
919
|
+
? musicCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
|
|
553
920
|
: capability === 'transcribe'
|
|
554
|
-
? transcribeCandidatesFromCatalog(catalog, modelRef || undefined)
|
|
555
|
-
: imageCandidatesFromCatalog(catalog, modelRef || undefined)
|
|
921
|
+
? transcribeCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
|
|
922
|
+
: imageCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
|
|
556
923
|
).map((route) => ({
|
|
557
924
|
adapter: trackedProviderAdapter(route.provider, options),
|
|
558
925
|
model: route.modelRef,
|
|
559
926
|
selectionReason: route.selectionReason,
|
|
560
|
-
providerBinding: providerTaskBinding(route.provider)
|
|
927
|
+
providerBinding: providerTaskBinding(route.provider, providerRegistry)
|
|
561
928
|
}));
|
|
562
929
|
}
|
|
563
930
|
});
|
|
@@ -567,8 +934,8 @@ async function main(args) {
|
|
|
567
934
|
|
|
568
935
|
if (args[0] === 'task' && args[1] === 'inspect' && args[2]) {
|
|
569
936
|
const options = flags(args.slice(3), ['data-root']);
|
|
570
|
-
const result = await
|
|
571
|
-
|
|
937
|
+
const result = await withApplication(options['data-root'], application =>
|
|
938
|
+
application.execute('task.inspect', { task_id: args[2] }, { signal: cliAbortController.signal }));
|
|
572
939
|
console.log(JSON.stringify(result, null, 2));
|
|
573
940
|
return 0;
|
|
574
941
|
}
|
|
@@ -584,15 +951,16 @@ async function main(args) {
|
|
|
584
951
|
|
|
585
952
|
if (args[0] === 'artifact' && args[1] === 'import') {
|
|
586
953
|
const options = flags(args.slice(2), ['data-root', 'input']);
|
|
587
|
-
const result = await
|
|
954
|
+
const result = await withApplication(options['data-root'], application =>
|
|
955
|
+
application.execute('artifact.import', jsonInput(options.input), { signal: cliAbortController.signal }));
|
|
588
956
|
console.log(JSON.stringify(result, null, 2));
|
|
589
957
|
return 0;
|
|
590
958
|
}
|
|
591
959
|
|
|
592
960
|
if (args[0] === 'artifact' && args[1] === 'inspect' && args[2]) {
|
|
593
961
|
const options = flags(args.slice(3), ['data-root']);
|
|
594
|
-
const result = await
|
|
595
|
-
|
|
962
|
+
const result = await withApplication(options['data-root'], application =>
|
|
963
|
+
application.execute('artifact.inspect', { artifact_id: args[2] }, { signal: cliAbortController.signal }));
|
|
596
964
|
console.log(JSON.stringify(result, null, 2));
|
|
597
965
|
return 0;
|
|
598
966
|
}
|
|
@@ -600,8 +968,8 @@ async function main(args) {
|
|
|
600
968
|
if (args[0] === 'artifact' && args[1] === 'list') {
|
|
601
969
|
const options = flags(args.slice(2), ['data-root', 'offset', 'limit', 'kind', 'media-type', 'task-id']);
|
|
602
970
|
const input = Object.fromEntries(Object.entries(options).filter(([key]) => key !== 'data-root').map(([key, value]) => [key.replaceAll('-', '_'), ['offset', 'limit'].includes(key) ? Number(value) : value]));
|
|
603
|
-
const result = await
|
|
604
|
-
|
|
971
|
+
const result = await withApplication(options['data-root'], application =>
|
|
972
|
+
application.execute('artifact.list', input, { signal: cliAbortController.signal }));
|
|
605
973
|
console.log(JSON.stringify(result, null, 2));
|
|
606
974
|
return 0;
|
|
607
975
|
}
|
|
@@ -626,11 +994,11 @@ async function main(args) {
|
|
|
626
994
|
|
|
627
995
|
if (args[0] === 'artifact' && args[1] === 'export' && args[2]) {
|
|
628
996
|
const options = flags(args.slice(3), ['data-root', 'output']);
|
|
629
|
-
const result = await
|
|
630
|
-
|
|
997
|
+
const result = await withApplication(options['data-root'], application =>
|
|
998
|
+
application.execute('artifact.export', {
|
|
631
999
|
artifact_id: args[2],
|
|
632
1000
|
output_path: required(options.output, 'output')
|
|
633
|
-
}));
|
|
1001
|
+
}, { signal: cliAbortController.signal }));
|
|
634
1002
|
console.log(JSON.stringify(result, null, 2));
|
|
635
1003
|
return 0;
|
|
636
1004
|
}
|
|
@@ -639,11 +1007,25 @@ async function main(args) {
|
|
|
639
1007
|
}
|
|
640
1008
|
|
|
641
1009
|
try {
|
|
642
|
-
|
|
1010
|
+
const args = [], entries = [], raw = process.argv.slice(2);
|
|
1011
|
+
for (let index = 0; index < raw.length; index++) {
|
|
1012
|
+
if (raw[index] !== '--provider-extension') args.push(raw[index]);
|
|
1013
|
+
else {
|
|
1014
|
+
const entry = raw[++index];
|
|
1015
|
+
if (!entry || entry.startsWith('--')) throw new CliUsageError('--provider-extension 缺少入口路径');
|
|
1016
|
+
entries.push(entry);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
try {
|
|
1020
|
+
if (entries.length && args.includes('--plugin-config')) throw new CliUsageError('--plugin-config 与 --provider-extension 不能混用;Provider 插件应通过同一插件装配实例加载');
|
|
1021
|
+
if (args.length && !args.includes('--help') && !args.includes('-h')) cliProviderRegistry = await loadProviderRegistry(entries);
|
|
1022
|
+
process.exitCode = await main(args, cliProviderRegistry);
|
|
1023
|
+
} finally { await cliProviderRegistry?.dispose(); }
|
|
643
1024
|
} catch (error) {
|
|
644
1025
|
const code = error && error.code || 'IRIS_CLI_FAILED';
|
|
645
1026
|
const message = error && error.message || '命令执行失败';
|
|
646
|
-
console.error(
|
|
1027
|
+
if (error.diagnostic) console.error(JSON.stringify(error.diagnostic));
|
|
1028
|
+
else console.error(code + ': ' + message);
|
|
647
1029
|
if (error instanceof CliUsageError) console.error('\n' + usage());
|
|
648
1030
|
process.exitCode = error instanceof CliUsageError ? 2 : 1;
|
|
649
1031
|
}
|