@mokuyoaxis/dsh-iris 0.2.7 → 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 +81 -1
- package/README.en.md +103 -312
- package/README.md +100 -310
- package/bin/dsh-iris.js +333 -65
- package/docs/AGENT_CORE_QUICKSTART.md +64 -0
- package/docs/AGENT_QUICKSTART.md +2 -0
- package/docs/ARCHITECTURE.md +13 -3
- package/docs/AUDIO_FLOWS.md +71 -0
- 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 +7 -2
- package/docs/CORE_SERVICE.md +97 -0
- package/docs/CORE_SKILLS.md +35 -3
- package/docs/DSH_COMPATIBILITY.md +10 -0
- package/docs/DSH_RC2_ADAPTATION.md +1 -1
- package/docs/DSH_RUNTIME.md +6 -2
- package/docs/HEADLESS_CLI.md +2 -2
- 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 +39 -14
- 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 +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 +3 -1
- package/docs/PROVIDER_SETTINGS.md +85 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +6 -0
- package/docs/ROADMAP.md +17 -7
- package/docs/VISION_CLI.md +1 -1
- package/docs/WEB_PROTOTYPE.md +21 -6
- 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.3.0-alpha.1.md +51 -0
- package/docs/screenshots.md +41 -1
- package/lib/actions.js +53 -2
- package/lib/adapters.js +20 -11
- package/lib/application-runtime.js +34 -7
- package/lib/audio-flow-input.js +37 -0
- package/lib/client.js +346 -53
- 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 +16 -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 +9 -3
- package/lib/core-runtime.js +2 -1
- package/lib/core-service-client.js +170 -0
- package/lib/core-tasks.js +29 -12
- package/lib/core-writer-lease.js +16 -0
- package/lib/creation-request-store.js +38 -64
- package/lib/dsh-host-adapter.js +4 -1
- package/lib/dsh-service-bridge.js +66 -0
- package/lib/execution-ownership.js +43 -0
- package/lib/flow-store.js +105 -10
- 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 +73 -29
- package/lib/index.js +37 -17
- package/lib/local-api-projection.js +10 -5
- package/lib/local-api-token-directory.js +31 -0
- package/lib/local-api.js +769 -67
- package/lib/local-creation.js +89 -24
- 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/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 +22 -9
- package/lib/skill-installation.js +23 -11
- 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-auth-cli.js +47 -0
- package/lib/workbench-auth.js +200 -0
- package/package.json +20 -5
- 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/user_guide.md +5 -1
package/bin/dsh-iris.js
CHANGED
|
@@ -8,7 +8,8 @@ import { recoverCoreWriterLease } from '../lib/core-lease-recovery.js';
|
|
|
8
8
|
import { doctor, formatDoctorReport } from '../lib/doctor.js';
|
|
9
9
|
import { loadProviderCatalog, providerCatalogSnapshot, catalogCapabilitySnapshot, imageCandidatesFromCatalog, musicCandidatesFromCatalog, providerForTaskFromCatalog, providerTaskBinding, transcribeCandidatesFromCatalog, ttsCandidatesFromCatalog, videoCandidatesFromCatalog } from '../lib/provider-catalog.js';
|
|
10
10
|
import { createConfiguredProviderAdapter } from '../lib/provider-adapters.js';
|
|
11
|
-
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';
|
|
12
13
|
import { prepareProviderInput } from '../lib/provider-adapter.js';
|
|
13
14
|
import { createProviderTaskRunner } from '../lib/provider-task-runner.js';
|
|
14
15
|
import { generateMusicTask } from '../lib/music-generation.js';
|
|
@@ -23,19 +24,22 @@ import { parseModelRef, modelRef as qualifiedModelRef } from '../lib/models.js';
|
|
|
23
24
|
import { coreTaskWaitFinished } from '../lib/task-wait.js';
|
|
24
25
|
import { createLocalReadApi, writeLocalApiTokenFile } from '../lib/local-api.js';
|
|
25
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';
|
|
26
34
|
|
|
27
35
|
function trackedProviderAdapter(provider, options, task) {
|
|
28
|
-
return
|
|
29
|
-
modelId: parseModelRef(task?.modelRef)?.modelId,
|
|
30
|
-
onModelAvailability: event => recordCatalogAvailability(options['provider-config'], event),
|
|
31
|
-
onRateLimit: event => recordCatalogRateLimit(options['provider-config'], event)
|
|
32
|
-
});
|
|
36
|
+
return createTrackedProviderAdapter(provider, options['provider-config'], task, options.providerRegistry);
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
async function generationCatalog(options, capability, modelRef, signal = cliAbortController.signal) {
|
|
36
40
|
const file = required(options['provider-config'], 'provider-config');
|
|
37
41
|
const catalog = loadProviderCatalog(file);
|
|
38
|
-
if (!modelRef) await refreshCatalogDirectories(file, catalog, capability, { signal });
|
|
42
|
+
if (!modelRef) await refreshCatalogDirectories(file, catalog, capability, { signal, registry: options.providerRegistry });
|
|
39
43
|
return catalog;
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -51,13 +55,50 @@ function usage() {
|
|
|
51
55
|
return [
|
|
52
56
|
'Usage:',
|
|
53
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.',
|
|
54
65
|
' dsh-iris skills <install|status|uninstall> --target kimi [--directory <absolute-skills-directory>]',
|
|
55
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]',
|
|
56
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.',
|
|
57
69
|
' dsh-iris doctor [--data-root <absolute-path>] [--json]',
|
|
58
70
|
' dsh-iris api init --data-root <empty-existing-flow-root>',
|
|
59
|
-
' dsh-iris
|
|
60
|
-
'
|
|
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.',
|
|
61
102
|
' dsh-iris runtime recover --data-root <absolute-path> --confirm-stale-pid <pid>',
|
|
62
103
|
' dsh-iris providers list --provider-config <absolute-path>',
|
|
63
104
|
' dsh-iris capabilities list --provider-config <absolute-path>',
|
|
@@ -97,8 +138,9 @@ function usage() {
|
|
|
97
138
|
' dsh-iris artifact export <id> --data-root <absolute-path> --output <absolute-path>',
|
|
98
139
|
' dsh-iris artifact rebuild --data-root <absolute-path>',
|
|
99
140
|
' dsh-iris flow init --data-root <new-absolute-path>',
|
|
100
|
-
' dsh-iris flow create --data-root <absolute-path> --provider-config <absolute-path> --input <image-
|
|
101
|
-
'
|
|
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.',
|
|
102
144
|
' dsh-iris flow <list|inspect> [run-id] --data-root <absolute-path>',
|
|
103
145
|
' dsh-iris flow continue <run-id> --data-root <absolute-path> [--provider-config <absolute-path>]',
|
|
104
146
|
' dsh-iris flow select <run-id> --data-root <absolute-path> --input <json with artifact_id>',
|
|
@@ -112,7 +154,7 @@ function usage() {
|
|
|
112
154
|
].join('\n');
|
|
113
155
|
}
|
|
114
156
|
|
|
115
|
-
function
|
|
157
|
+
function parseFlags(args, allowed) {
|
|
116
158
|
const parsed = {};
|
|
117
159
|
for (let index = 0; index < args.length; index += 2) {
|
|
118
160
|
const flag = args[index];
|
|
@@ -170,6 +212,7 @@ function generationInput(capability, input, options) {
|
|
|
170
212
|
/** C-8: catchable signals dispose active runtimes before the CLI exits. */
|
|
171
213
|
const activeRuntimes = new Set();
|
|
172
214
|
const cliAbortController = new AbortController();
|
|
215
|
+
let cliProviderRegistry;
|
|
173
216
|
let shuttingDown = false;
|
|
174
217
|
|
|
175
218
|
function registerRuntime(runtime) {
|
|
@@ -180,6 +223,7 @@ function registerRuntime(runtime) {
|
|
|
180
223
|
async function disposeActiveRuntimes() {
|
|
181
224
|
await Promise.allSettled([...activeRuntimes].map((runtime) => runtime.dispose().catch(() => {})));
|
|
182
225
|
activeRuntimes.clear();
|
|
226
|
+
await cliProviderRegistry?.dispose();
|
|
183
227
|
}
|
|
184
228
|
|
|
185
229
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
@@ -216,19 +260,71 @@ async function withApplication(dataRoot, callback, resolveImageCandidates, resol
|
|
|
216
260
|
finally { try { await application.dispose(); } finally { unregister(); } }
|
|
217
261
|
}
|
|
218
262
|
|
|
219
|
-
|
|
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 });
|
|
220
299
|
if (!args.length || args.includes('--help') || args.includes('-h')) {
|
|
221
300
|
console.log(usage());
|
|
222
301
|
return args.length ? 0 : 2;
|
|
223
302
|
}
|
|
224
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
|
+
|
|
225
321
|
if (args[0] === 'skills') {
|
|
226
322
|
const rest = args.slice(2), dryRunFlags = rest.filter(value => value === '--dry-run');
|
|
227
323
|
if (dryRunFlags.length > 1) throw new CliUsageError('参数重复:--dry-run');
|
|
228
|
-
const options = flags(rest.filter(value => value !== '--dry-run'), ['target', 'directory']);
|
|
324
|
+
const options = flags(rest.filter(value => value !== '--dry-run'), ['target', 'directory', 'bundle']);
|
|
229
325
|
let result;
|
|
230
326
|
try {
|
|
231
|
-
result = manageCoreSkill({ action: args[1], target: options.target, directory: options.directory, dryRun: dryRunFlags.length === 1 });
|
|
327
|
+
result = manageCoreSkill({ action: args[1], target: options.target, directory: options.directory, bundle: options.bundle, dryRun: dryRunFlags.length === 1 });
|
|
232
328
|
} catch (error) {
|
|
233
329
|
if (error.code === 'IRIS_SKILL_OPTIONS_INVALID') throw new CliUsageError(error.message);
|
|
234
330
|
throw error;
|
|
@@ -237,8 +333,133 @@ async function main(args) {
|
|
|
237
333
|
return args[1] === 'status' && result.status !== 'current' ? 1 : 0;
|
|
238
334
|
}
|
|
239
335
|
|
|
240
|
-
if (args[0] === '
|
|
241
|
-
if (args[1] === '
|
|
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') {
|
|
242
463
|
const options = flags(args.slice(2), ['data-root']);
|
|
243
464
|
const suppliedRoot = required(options['data-root'], 'data-root');
|
|
244
465
|
if (!path.isAbsolute(suppliedRoot)) throw new CliUsageError('data-root 必须是已初始化的空 Flow 根绝对路径');
|
|
@@ -249,30 +470,61 @@ async function main(args) {
|
|
|
249
470
|
console.log(JSON.stringify(initialized));
|
|
250
471
|
return 0;
|
|
251
472
|
}
|
|
252
|
-
if (args[1] !== 'serve') throw new CliUsageError(
|
|
253
|
-
const rest = args.slice(2), localWriteFlags = rest.filter(value => value === '--allow-local-writes');
|
|
254
|
-
if (localWriteFlags.length > 1) throw new CliUsageError('
|
|
255
|
-
const allowLocalWrites = localWriteFlags.length === 1;
|
|
256
|
-
const
|
|
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']);
|
|
257
480
|
const suppliedRoot = required(options['data-root'], 'data-root');
|
|
258
|
-
const
|
|
259
|
-
if (!
|
|
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'];
|
|
260
486
|
if (options.port !== undefined && (!/^\d+$/.test(options.port) || Number(options.port) > 65535)) throw new CliUsageError('port 必须是 0–65535 的整数');
|
|
261
487
|
// Pin the physical root for both the server and the outside-root credential check.
|
|
262
488
|
let dataRoot;
|
|
263
489
|
try { dataRoot = fs.realpathSync(suppliedRoot); }
|
|
264
490
|
catch (_) { throw new CliUsageError('data-root 必须是已初始化且可读取的 Flow 根'); }
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
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);
|
|
268
501
|
const started = await api.start({ port: options.port === undefined ? 0 : Number(options.port) });
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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 在指定私有文件中,退出后失效。'
|
|
272
524
|
: '只读本地 API 已启动;会话 Token 在指定私有文件中,退出后失效。' }));
|
|
273
525
|
await api.closed;
|
|
274
526
|
return 0;
|
|
275
|
-
}
|
|
527
|
+
});
|
|
276
528
|
}
|
|
277
529
|
|
|
278
530
|
if (args[0] === 'flow') {
|
|
@@ -289,12 +541,13 @@ async function main(args) {
|
|
|
289
541
|
if (verb === 'export') input.output_path = required(options.output, 'output');
|
|
290
542
|
const resolveImageCandidates = options['provider-config'] ? async ({ modelRef, signal }) => {
|
|
291
543
|
const catalog = await generationCatalog(options, 'image-gen', modelRef, signal);
|
|
292
|
-
return imageCandidatesFromCatalog(catalog, modelRef).map(route => ({ adapter: trackedProviderAdapter(route.provider, options),
|
|
293
|
-
model: route.modelRef,
|
|
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) }));
|
|
294
547
|
} : undefined;
|
|
295
548
|
const resolveTaskAdapter = options['provider-config'] ? task => {
|
|
296
549
|
const catalog = loadProviderCatalog(options['provider-config']);
|
|
297
|
-
return trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task);
|
|
550
|
+
return trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task);
|
|
298
551
|
} : undefined;
|
|
299
552
|
let result;
|
|
300
553
|
try {
|
|
@@ -344,14 +597,14 @@ async function main(args) {
|
|
|
344
597
|
if (['providers', 'capabilities'].includes(args[0]) && args[1] === 'list') {
|
|
345
598
|
const options = flags(args.slice(2), ['provider-config']);
|
|
346
599
|
const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
|
|
347
|
-
const report = args[0] === 'providers' ? providerCatalogSnapshot(catalog) : catalogCapabilitySnapshot(catalog);
|
|
600
|
+
const report = args[0] === 'providers' ? providerCatalogSnapshot(catalog) : catalogCapabilitySnapshot(catalog, providerRegistry);
|
|
348
601
|
console.log(JSON.stringify(report, null, 2));
|
|
349
602
|
return 0;
|
|
350
603
|
}
|
|
351
604
|
|
|
352
605
|
if (['config', 'providers', 'models', 'assignments'].includes(args[0])) {
|
|
353
606
|
const [group, verb] = args;
|
|
354
|
-
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'] };
|
|
355
608
|
if (!verbs[group].includes(verb)) throw new CliUsageError('未知配置命令');
|
|
356
609
|
const positional = (group === 'providers' && verb === 'remove') || (group === 'models' && verb !== 'list');
|
|
357
610
|
const id = positional ? required(args[2], group === 'providers' || verb === 'discover' ? 'provider-id' : 'model-ref') : undefined;
|
|
@@ -368,7 +621,7 @@ async function main(args) {
|
|
|
368
621
|
}
|
|
369
622
|
}
|
|
370
623
|
const result = await executeConfigCommand(required(options['provider-config'], 'provider-config'), group === 'config' ? verb : group + '.' + verb, input, {
|
|
371
|
-
signal: cliAbortController.signal,
|
|
624
|
+
signal: cliAbortController.signal, registry: providerRegistry,
|
|
372
625
|
probe: async ({ provider, modelId, capability, signal }) => {
|
|
373
626
|
if (capability === 'music-gen') return { skipped: true, ok: false, reason: 'Music 不发起小样本探测;请使用 run music 提交创作要求' };
|
|
374
627
|
if (capability === 'vision') return probeVisionModel(provider, modelId, { bytes: Buffer.from(RED_TEST_IMAGE.split(',')[1], 'base64'), mediaType: 'image/png' }, { signal });
|
|
@@ -381,14 +634,14 @@ async function main(args) {
|
|
|
381
634
|
}
|
|
382
635
|
return withRawRuntime('writer', options['data-root'], async runtime => {
|
|
383
636
|
const mediaCapability = capability === 'image-gen' ? 'image' : capability === 'video-gen' ? 'video' : capability;
|
|
384
|
-
if (mediaCapability === 'image') provider = providerForImageModel(provider, modelId);
|
|
385
|
-
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 });
|
|
386
639
|
let providerInput = mediaCapability === 'image'
|
|
387
|
-
? { 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) ? {} : {
|
|
388
641
|
size: adapter.protocol === 'openai-images' ? '256x256' : '512*512'
|
|
389
642
|
}) }
|
|
390
643
|
: { text: '你好', voice: probeInput.voice || provider.ttsVoice || 'Cherry' };
|
|
391
|
-
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) }];
|
|
392
645
|
if (['video', 'transcribe'].includes(mediaCapability)) {
|
|
393
646
|
const normalized = generationInput(mediaCapability, { ...probeInput, model_ref: qualifiedModelRef(provider.id, modelId) },
|
|
394
647
|
{ allowVideoPaths: true, allowAudioPath: true });
|
|
@@ -429,8 +682,8 @@ async function main(args) {
|
|
|
429
682
|
} catch (_) { throw new CliUsageError('输出文件已存在或父目录不可写'); }
|
|
430
683
|
}
|
|
431
684
|
const catalog = await generationCatalog(options, 'vision', options['model-ref'] || input.model_ref);
|
|
432
|
-
if (input.transcribe && !input.transcribe_model_ref) await refreshCatalogDirectories(options['provider-config'], catalog, 'transcribe', { signal: cliAbortController.signal });
|
|
433
|
-
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,
|
|
434
687
|
onRateLimit: event => recordCatalogRateLimit(options['provider-config'], event),
|
|
435
688
|
onModelAvailability: event => recordCatalogAvailability(options['provider-config'], event),
|
|
436
689
|
modelRef: options['model-ref'] || '', signal: cliAbortController.signal,
|
|
@@ -488,10 +741,11 @@ async function main(args) {
|
|
|
488
741
|
const input = jsonInput(options.input);
|
|
489
742
|
const resolveImageCandidates = async ({ modelRef, signal }) => {
|
|
490
743
|
const catalog = await generationCatalog(options, 'image-gen', modelRef, signal);
|
|
491
|
-
return imageCandidatesFromCatalog(catalog, modelRef).map(route => ({
|
|
744
|
+
return imageCandidatesFromCatalog(catalog, modelRef, providerRegistry).map(route => ({
|
|
492
745
|
adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
|
|
746
|
+
supportsImageEdit: supportsImageEdit(route.provider.mediaProtocol, route.model, providerRegistry),
|
|
493
747
|
selectionReason: route.selectionReason,
|
|
494
|
-
providerBinding: providerTaskBinding(route.provider)
|
|
748
|
+
providerBinding: providerTaskBinding(route.provider, providerRegistry)
|
|
495
749
|
}));
|
|
496
750
|
};
|
|
497
751
|
let result;
|
|
@@ -512,12 +766,12 @@ async function main(args) {
|
|
|
512
766
|
const normalized = generationInput('video', jsonInput(options.input), { allowVideoPaths: true });
|
|
513
767
|
const { providerInput, modelRef } = normalized;
|
|
514
768
|
const catalog = await generationCatalog(options, 'video-gen', modelRef);
|
|
515
|
-
const routes = videoCandidatesFromCatalog(catalog, modelRef);
|
|
769
|
+
const routes = videoCandidatesFromCatalog(catalog, modelRef, providerRegistry);
|
|
516
770
|
await withRawRuntime('writer', options['data-root'], async (runtime) => {
|
|
517
771
|
const candidates = videoTaskCandidates(routes.map((route) => ({
|
|
518
772
|
adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
|
|
519
773
|
selectionReason: route.selectionReason,
|
|
520
|
-
providerBinding: providerTaskBinding(route.provider)
|
|
774
|
+
providerBinding: providerTaskBinding(route.provider, providerRegistry)
|
|
521
775
|
})), normalized);
|
|
522
776
|
const result = await createProviderTaskRunner(runtime).submit({
|
|
523
777
|
capability: 'video', candidates,
|
|
@@ -533,12 +787,12 @@ async function main(args) {
|
|
|
533
787
|
const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
|
|
534
788
|
const normalized = generationInput('music', jsonInput(options.input));
|
|
535
789
|
const catalog = await generationCatalog(options, 'music-gen', normalized.modelRef);
|
|
536
|
-
const routes = musicCandidatesFromCatalog(catalog, normalized.modelRef);
|
|
790
|
+
const routes = musicCandidatesFromCatalog(catalog, normalized.modelRef, providerRegistry);
|
|
537
791
|
let result;
|
|
538
792
|
await withRawRuntime('writer', options['data-root'], async runtime => {
|
|
539
793
|
result = await generateMusicTask({ runner: createProviderTaskRunner(runtime),
|
|
540
794
|
candidates: routes.map(route => ({ adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
|
|
541
|
-
selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider) })),
|
|
795
|
+
selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider, providerRegistry) })),
|
|
542
796
|
providerInput: normalized.providerInput, options: normalized.musicOptions, signal: cliAbortController.signal });
|
|
543
797
|
console.log(JSON.stringify(result, null, 2));
|
|
544
798
|
});
|
|
@@ -549,12 +803,12 @@ async function main(args) {
|
|
|
549
803
|
const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
|
|
550
804
|
const { providerInput, modelRef } = generationInput('tts', jsonInput(options.input));
|
|
551
805
|
const catalog = await generationCatalog(options, 'tts', modelRef);
|
|
552
|
-
const routes = ttsCandidatesFromCatalog(catalog, modelRef);
|
|
806
|
+
const routes = ttsCandidatesFromCatalog(catalog, modelRef, providerRegistry);
|
|
553
807
|
await withRawRuntime('writer', options['data-root'], async (runtime) => {
|
|
554
808
|
const candidates = routes.map((route) => ({
|
|
555
809
|
adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
|
|
556
810
|
selectionReason: route.selectionReason,
|
|
557
|
-
providerBinding: providerTaskBinding(route.provider)
|
|
811
|
+
providerBinding: providerTaskBinding(route.provider, providerRegistry)
|
|
558
812
|
}));
|
|
559
813
|
const result = await createProviderTaskRunner(runtime).submit({
|
|
560
814
|
capability: 'tts', candidates,
|
|
@@ -571,7 +825,7 @@ async function main(args) {
|
|
|
571
825
|
const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
|
|
572
826
|
const normalized = generationInput('transcribe', jsonInput(options.input), { allowAudioPath: true });
|
|
573
827
|
const catalog = await generationCatalog(options, 'transcribe', normalized.modelRef);
|
|
574
|
-
const routes = transcribeCandidatesFromCatalog(catalog, normalized.modelRef);
|
|
828
|
+
const routes = transcribeCandidatesFromCatalog(catalog, normalized.modelRef, providerRegistry);
|
|
575
829
|
let providerInput = normalized.providerInput;
|
|
576
830
|
if (normalized.audioPath) {
|
|
577
831
|
if (!fs.existsSync(normalized.audioPath)) throw new CliUsageError('音频文件不存在');
|
|
@@ -584,7 +838,7 @@ async function main(args) {
|
|
|
584
838
|
const candidates = routes.map((route) => ({
|
|
585
839
|
adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
|
|
586
840
|
selectionReason: route.selectionReason,
|
|
587
|
-
providerBinding: providerTaskBinding(route.provider)
|
|
841
|
+
providerBinding: providerTaskBinding(route.provider, providerRegistry)
|
|
588
842
|
}));
|
|
589
843
|
const result = await createProviderTaskRunner(runtime).submit({
|
|
590
844
|
capability: 'transcribe', candidates,
|
|
@@ -602,7 +856,7 @@ async function main(args) {
|
|
|
602
856
|
const inspected = await withRuntime('reader', options['data-root'], commands => commands.execute('task.inspect', { task_id: args[2] }));
|
|
603
857
|
const catalog = coreTaskWaitFinished(inspected.task) ? null : loadProviderCatalog(required(options['provider-config'], 'provider-config'));
|
|
604
858
|
const result = await withRuntime(catalog ? 'writer' : 'reader', options['data-root'], commands => commands.execute('task.wait', input), {
|
|
605
|
-
...(catalog ? { resolveTaskAdapter: task => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task) } : {}) });
|
|
859
|
+
...(catalog ? { resolveTaskAdapter: task => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task) } : {}) });
|
|
606
860
|
console.log(JSON.stringify(result, null, 2));
|
|
607
861
|
return result.timedOut ? 3 : result.ready ? 0 : 1;
|
|
608
862
|
}
|
|
@@ -612,7 +866,7 @@ async function main(args) {
|
|
|
612
866
|
const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
|
|
613
867
|
await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
|
|
614
868
|
const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.observe', { task_id: args[2] }), {
|
|
615
|
-
resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task)
|
|
869
|
+
resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task)
|
|
616
870
|
});
|
|
617
871
|
console.log(JSON.stringify(result, null, 2));
|
|
618
872
|
return 0;
|
|
@@ -623,7 +877,7 @@ async function main(args) {
|
|
|
623
877
|
const catalog = options['provider-config'] ? loadProviderCatalog(options['provider-config']) : null;
|
|
624
878
|
await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
|
|
625
879
|
const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.redeliver', { task_id: args[2] }), {
|
|
626
|
-
...(catalog ? { resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task) } : {})
|
|
880
|
+
...(catalog ? { resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task) } : {})
|
|
627
881
|
});
|
|
628
882
|
console.log(JSON.stringify(result, null, 2));
|
|
629
883
|
return 0;
|
|
@@ -634,7 +888,7 @@ async function main(args) {
|
|
|
634
888
|
const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
|
|
635
889
|
await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
|
|
636
890
|
const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.cancel', { task_id: args[2] }), {
|
|
637
|
-
resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task)
|
|
891
|
+
resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task)
|
|
638
892
|
});
|
|
639
893
|
console.log(JSON.stringify(result, null, 2));
|
|
640
894
|
return 0;
|
|
@@ -658,19 +912,19 @@ async function main(args) {
|
|
|
658
912
|
resolveTaskCandidates: async ({ capability, modelRef }) => {
|
|
659
913
|
const catalog = await generationCatalog(options, { image: 'image-gen', video: 'video-gen', music: 'music-gen' }[capability] || capability, modelRef);
|
|
660
914
|
return (capability === 'video'
|
|
661
|
-
? videoCandidatesFromCatalog(catalog, modelRef || undefined)
|
|
915
|
+
? videoCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
|
|
662
916
|
: capability === 'tts'
|
|
663
|
-
? ttsCandidatesFromCatalog(catalog, modelRef || undefined)
|
|
917
|
+
? ttsCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
|
|
664
918
|
: capability === 'music'
|
|
665
|
-
? musicCandidatesFromCatalog(catalog, modelRef || undefined)
|
|
919
|
+
? musicCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
|
|
666
920
|
: capability === 'transcribe'
|
|
667
|
-
? transcribeCandidatesFromCatalog(catalog, modelRef || undefined)
|
|
668
|
-
: imageCandidatesFromCatalog(catalog, modelRef || undefined)
|
|
921
|
+
? transcribeCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
|
|
922
|
+
: imageCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
|
|
669
923
|
).map((route) => ({
|
|
670
924
|
adapter: trackedProviderAdapter(route.provider, options),
|
|
671
925
|
model: route.modelRef,
|
|
672
926
|
selectionReason: route.selectionReason,
|
|
673
|
-
providerBinding: providerTaskBinding(route.provider)
|
|
927
|
+
providerBinding: providerTaskBinding(route.provider, providerRegistry)
|
|
674
928
|
}));
|
|
675
929
|
}
|
|
676
930
|
});
|
|
@@ -753,11 +1007,25 @@ async function main(args) {
|
|
|
753
1007
|
}
|
|
754
1008
|
|
|
755
1009
|
try {
|
|
756
|
-
|
|
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(); }
|
|
757
1024
|
} catch (error) {
|
|
758
1025
|
const code = error && error.code || 'IRIS_CLI_FAILED';
|
|
759
1026
|
const message = error && error.message || '命令执行失败';
|
|
760
|
-
console.error(
|
|
1027
|
+
if (error.diagnostic) console.error(JSON.stringify(error.diagnostic));
|
|
1028
|
+
else console.error(code + ': ' + message);
|
|
761
1029
|
if (error instanceof CliUsageError) console.error('\n' + usage());
|
|
762
1030
|
process.exitCode = error instanceof CliUsageError ? 2 : 1;
|
|
763
1031
|
}
|