@mokuyoaxis/dsh-iris 0.1.4 → 0.2.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/.dsh/skills/iris-compose-media/SKILL.md +10 -5
- package/.dsh/skills/iris-compose-media/references/workflows.md +7 -1
- package/.dsh/skills/iris-verify-ui/SKILL.md +2 -1
- package/CHANGELOG.md +99 -1
- package/README.en.md +124 -31
- package/README.md +124 -31
- package/bin/dsh-iris.js +593 -13
- package/docs/AGENT_QUICKSTART.md +39 -0
- package/docs/ARCHITECTURE.md +87 -64
- package/docs/ARTIFACT_LIBRARY.md +5 -5
- package/docs/ARTIFACT_MANIFEST.md +60 -0
- package/docs/CLI_MANAGEMENT.md +181 -0
- package/docs/COMPOSITE_VISION.md +35 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +84 -0
- package/docs/DSH_CORE_MIGRATION.md +135 -0
- package/docs/DSH_RC2_ADAPTATION.md +38 -0
- package/docs/FAULT_INJECTION.md +12 -2
- package/docs/HEADLESS_CLI.md +223 -0
- package/docs/HOST_ADAPTER_CONTRACT.md +36 -17
- package/docs/HOST_DOCTOR.md +4 -2
- package/docs/MODEL_PORT_CONTRACT.md +305 -0
- package/docs/OCR_MODEL.md +39 -0
- package/docs/PROMPT_OPTIMIZER.md +47 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +53 -10
- package/docs/PROVIDER_HEALTH.md +22 -5
- package/docs/PROVIDER_RUNTIME_HARNESS.md +33 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +2 -2
- package/docs/ROADMAP.md +49 -83
- package/docs/SECURITY.md +8 -7
- package/docs/TASK_SEMANTICS.md +2 -2
- package/docs/VISION_CLI.md +132 -0
- package/docs/VISION_MODEL.md +51 -0
- package/docs/WORKBENCH_ARTIFACTS.md +67 -0
- package/docs/assets/logo/README.md +55 -0
- package/docs/assets/logo/iris-bubble-concept.svg +67 -0
- package/docs/assets/logo/iris-bubble.svg +14 -0
- package/docs/assets/logo/iris-interim-flower.png +0 -0
- package/docs/assets/logo/iris-logo-mono-inverse.svg +78 -0
- package/docs/assets/logo/iris-logo-mono.svg +78 -0
- package/docs/assets/logo/iris-logo.svg +78 -0
- package/docs/assets/logo/iris-mark-mono-inverse.svg +42 -0
- package/docs/assets/logo/iris-mark-mono.svg +42 -0
- package/docs/assets/logo/iris-mark.svg +42 -0
- package/docs/file-access-across-environments.md +8 -5
- package/docs/releases/0.2.0.md +48 -0
- package/docs/releases/0.2.1.md +56 -0
- package/lib/actions.js +520 -591
- package/lib/adapters.js +184 -36
- package/lib/api.js +200 -11
- package/lib/chromium-browser.js +117 -0
- package/lib/client.js +1390 -105
- package/lib/command-service.js +534 -0
- package/lib/composite-vision-routing.js +42 -0
- package/lib/config.js +143 -51
- package/lib/core-artifact-store.js +630 -0
- package/lib/core-artifacts.js +4 -0
- package/lib/core-attention.js +103 -0
- package/lib/core-contract.js +105 -0
- package/lib/core-doctor.js +41 -0
- package/lib/core-lease-recovery.js +170 -0
- package/lib/core-maintenance.js +213 -0
- package/lib/core-runtime.js +232 -0
- package/lib/core-tasks.js +567 -0
- package/lib/core-user-projection.js +193 -0
- package/lib/doctor.js +92 -33
- package/lib/dsh-core-adapter.js +653 -0
- package/lib/dsh-host-adapter.js +58 -7
- package/lib/dsh-text-model-adapter.js +222 -0
- package/lib/dsh-vision-model-adapter.js +75 -0
- package/lib/generation-input.js +126 -0
- package/lib/guard.js +3 -3
- package/lib/headless-vision.js +181 -0
- package/lib/host-runtime.js +1 -0
- package/lib/http-vision-model-adapter.js +187 -0
- package/lib/image-edit-input.js +34 -0
- package/lib/index.js +323 -188
- package/lib/locate.js +86 -28
- package/lib/media-probe.js +2 -1
- package/lib/model-call-runtime.js +45 -0
- package/lib/model-invoker.js +220 -0
- package/lib/model-port-contract.js +341 -0
- package/lib/models.js +105 -14
- package/lib/ocr-model-routing.js +26 -0
- package/lib/ocr.js +110 -64
- package/lib/prompt-optimizer-config.js +2 -1
- package/lib/prompt-optimizer-core.js +121 -0
- package/lib/prompt-optimizer.js +37 -193
- package/lib/provider-adapter.js +49 -23
- package/lib/provider-adapters.js +165 -26
- package/lib/provider-catalog.js +183 -0
- package/lib/provider-config-service.js +236 -0
- package/lib/provider-contract.js +74 -4
- package/lib/provider-health.js +80 -1
- package/lib/provider-protocol.js +74 -2
- package/lib/provider-task-runner.js +370 -0
- package/lib/summarize.js +31 -20
- package/lib/summary-input.js +52 -0
- package/lib/task-wait.js +40 -0
- package/lib/tasks.js +16 -4
- package/lib/video-input.js +30 -0
- package/lib/vision-core.js +42 -0
- package/lib/vision-image-input.js +83 -0
- package/lib/vision-input.js +31 -0
- package/lib/vision-model-routing.js +180 -0
- package/lib/vision.js +31 -9
- package/lib/workbench-artifacts.js +52 -0
- package/lib/workbench-management.js +151 -0
- package/lib/workbench-zip.js +39 -0
- package/package.json +8 -8
- package/user_guide.md +71 -26
package/bin/dsh-iris.js
CHANGED
|
@@ -1,23 +1,603 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createCommandService } from '../lib/command-service.js';
|
|
5
|
+
import { createCoreRuntime } from '../lib/core-runtime.js';
|
|
6
|
+
import { recoverCoreWriterLease } from '../lib/core-lease-recovery.js';
|
|
2
7
|
import { doctor, formatDoctorReport } from '../lib/doctor.js';
|
|
8
|
+
import { loadProviderCatalog, providerCatalogSnapshot, catalogCapabilitySnapshot, imageCandidatesFromCatalog, providerForTaskFromCatalog, providerTaskBinding, transcribeCandidatesFromCatalog, ttsCandidatesFromCatalog, videoCandidatesFromCatalog } from '../lib/provider-catalog.js';
|
|
9
|
+
import { createConfiguredProviderAdapter } from '../lib/provider-adapters.js';
|
|
10
|
+
import { providerForImageModel } from '../lib/provider-protocol.js';
|
|
11
|
+
import { prepareProviderInput } from '../lib/provider-adapter.js';
|
|
12
|
+
import { createProviderTaskRunner } from '../lib/provider-task-runner.js';
|
|
13
|
+
import { GenerationInputError, normalizeGenerationInput } from '../lib/generation-input.js';
|
|
14
|
+
import { executeVisionCommand, normalizeVisionInput } from '../lib/headless-vision.js';
|
|
15
|
+
import { videoTaskCandidates } from '../lib/video-input.js';
|
|
16
|
+
import { createChromiumBrowser } from '../lib/chromium-browser.js';
|
|
17
|
+
import { executeConfigCommand, recordCatalogRateLimit } from '../lib/provider-config-service.js';
|
|
18
|
+
import { probeVisionModel } from '../lib/vision-model-routing.js';
|
|
19
|
+
import { RED_TEST_IMAGE } from '../lib/vision.js';
|
|
20
|
+
import { parseModelRef, modelRef as qualifiedModelRef } from '../lib/models.js';
|
|
21
|
+
import { coreTaskWaitFinished } from '../lib/task-wait.js';
|
|
22
|
+
|
|
23
|
+
function trackedProviderAdapter(provider, options, task) {
|
|
24
|
+
return createConfiguredProviderAdapter(provider, {
|
|
25
|
+
modelId: parseModelRef(task?.modelRef)?.modelId,
|
|
26
|
+
onRateLimit: event => recordCatalogRateLimit(options['provider-config'], event)
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class CliUsageError extends Error {
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'CliUsageError';
|
|
34
|
+
this.code = 'IRIS_CLI_USAGE';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
3
37
|
|
|
4
38
|
function usage() {
|
|
5
39
|
return [
|
|
6
|
-
'Usage:
|
|
40
|
+
'Usage:',
|
|
41
|
+
' JSON input: --input <json>, --input-file <path>, or --input - (stdin).',
|
|
42
|
+
' dsh-iris doctor [--data-root <absolute-path>] [--json]',
|
|
43
|
+
' dsh-iris runtime recover --data-root <absolute-path> --confirm-stale-pid <pid>',
|
|
44
|
+
' dsh-iris providers list --provider-config <absolute-path>',
|
|
45
|
+
' dsh-iris capabilities list --provider-config <absolute-path>',
|
|
46
|
+
' dsh-iris config <init|show|check> --provider-config <absolute-path>',
|
|
47
|
+
' dsh-iris providers <add|set> --provider-config <absolute-path> --input <provider-json>',
|
|
48
|
+
' dsh-iris providers remove <provider-id> --provider-config <absolute-path>',
|
|
49
|
+
' dsh-iris models list --provider-config <absolute-path>',
|
|
50
|
+
' dsh-iris models <add|remove|caps|protocol|vision-input> <providerId::modelId> --provider-config <absolute-path> [--input <json>]',
|
|
51
|
+
' dsh-iris models discover <provider-id> --provider-config <absolute-path> [--apply <true|false>]',
|
|
52
|
+
' dsh-iris models test <providerId::modelId> --capability <vision|tts|image-gen|video-gen|transcribe> --provider-config <absolute-path> [--data-root <absolute-path>]',
|
|
53
|
+
' dsh-iris assignments <list|set|clear> --provider-config <absolute-path> [--input <json>]',
|
|
54
|
+
' dsh-iris vision <look|locate|ocr|summarize> --provider-config <absolute-path> --input <json> [--model-ref <providerId::modelId>] [--format <json|text>] [--output <path>] [--timeout-ms <1..120000>]',
|
|
55
|
+
' Core artifact_id / frame_artifact_ids inputs or transcribe=true require --data-root <absolute-path>; summarize supports --sheet-output <path>.',
|
|
56
|
+
' dsh-iris run crop --data-root <absolute-path> --input <json>',
|
|
57
|
+
' dsh-iris media diff --data-root <absolute-path> --input <json>',
|
|
58
|
+
' dsh-iris media frames --data-root <absolute-path> --input <json>',
|
|
59
|
+
' dsh-iris media html --data-root <absolute-path> --input <json> --browser-executable <absolute-path> [--browser-no-sandbox <true|false>] [--output <absolute-path>]',
|
|
60
|
+
' dsh-iris run image --data-root <absolute-path> --provider-config <absolute-path> --input <json>',
|
|
61
|
+
' dsh-iris run video --data-root <absolute-path> --provider-config <absolute-path> --input <json>',
|
|
62
|
+
' dsh-iris run tts --data-root <absolute-path> --provider-config <absolute-path> --input <json>',
|
|
63
|
+
' dsh-iris run transcribe --data-root <absolute-path> --provider-config <absolute-path> --input <json>',
|
|
64
|
+
' dsh-iris task inspect <id> --data-root <absolute-path>',
|
|
65
|
+
' dsh-iris task list --data-root <absolute-path>',
|
|
66
|
+
' list supports --offset, --limit, --status, --capability, --provider-id, --model-ref, --outcome, --delivery-state.',
|
|
67
|
+
' dsh-iris <task|artifact> inspect-many --data-root <absolute-path> --input <json>',
|
|
68
|
+
' dsh-iris task observe <id> --data-root <absolute-path> --provider-config <absolute-path>',
|
|
69
|
+
' dsh-iris task wait <id> --data-root <absolute-path> [--provider-config <absolute-path>] [--timeout-ms <1..1200000>] [--poll-interval-ms <50..60000>]',
|
|
70
|
+
' dsh-iris task redeliver <id> --data-root <absolute-path> --provider-config <absolute-path>',
|
|
71
|
+
' dsh-iris task cancel <id> --data-root <absolute-path> --provider-config <absolute-path>',
|
|
72
|
+
' dsh-iris task retry <id> --data-root <absolute-path> --provider-config <absolute-path> --input <json> --confirm-billing [--model-ref <providerId::modelId>]',
|
|
73
|
+
' dsh-iris artifact inspect <id> --data-root <absolute-path>',
|
|
74
|
+
' dsh-iris artifact list --data-root <absolute-path>',
|
|
75
|
+
' artifact list supports --offset, --limit, --kind, --media-type, --task-id.',
|
|
76
|
+
' dsh-iris artifact export-many --data-root <absolute-path> --input <json> --output <absolute-directory>',
|
|
77
|
+
' dsh-iris artifact export <id> --data-root <absolute-path> --output <absolute-path>',
|
|
78
|
+
' dsh-iris artifact rebuild --data-root <absolute-path>',
|
|
79
|
+
' dsh-iris core <delete|cleanup> --data-root <absolute-path> [--input <json>] [--confirm-delete]',
|
|
80
|
+
' Default is a read-only preview. delete selects task_ids/artifact_ids; cleanup executes only explicitly selected preview paths.',
|
|
81
|
+
' dsh-iris core transactions --data-root <absolute-path>',
|
|
82
|
+
' dsh-iris core restore <transaction-id> --data-root <absolute-path>',
|
|
7
83
|
'',
|
|
8
|
-
'
|
|
84
|
+
'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 Core never persists prompts.'
|
|
9
85
|
].join('\n');
|
|
10
86
|
}
|
|
11
87
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
88
|
+
function flags(args, allowed) {
|
|
89
|
+
const parsed = {};
|
|
90
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
91
|
+
const flag = args[index];
|
|
92
|
+
const name = typeof flag === 'string' && flag.startsWith('--') ? flag.slice(2) : '';
|
|
93
|
+
if (!name || !(allowed.includes(name) || name === 'input-file' && allowed.includes('input')) || index + 1 >= args.length || String(args[index + 1]).startsWith('--')) {
|
|
94
|
+
throw new CliUsageError('参数无效:' + String(flag || ''));
|
|
95
|
+
}
|
|
96
|
+
if (Object.prototype.hasOwnProperty.call(parsed, name)) throw new CliUsageError('参数重复:--' + name);
|
|
97
|
+
parsed[name] = args[index + 1];
|
|
98
|
+
}
|
|
99
|
+
if (parsed['input-file']) {
|
|
100
|
+
if (parsed.input !== undefined) throw new CliUsageError('--input 与 --input-file 只能选一个');
|
|
101
|
+
parsed.input = readJsonSource(parsed['input-file']);
|
|
102
|
+
} else if (parsed.input === '-') parsed.input = readJsonSource(0);
|
|
103
|
+
return parsed;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readJsonSource(file) {
|
|
107
|
+
let fd;
|
|
108
|
+
try {
|
|
109
|
+
fd = file === 0 ? 0 : fs.openSync(file, 'r');
|
|
110
|
+
const bytes = Buffer.alloc(2 * 1024 * 1024 + 1);
|
|
111
|
+
let offset = 0, count;
|
|
112
|
+
while (offset < bytes.length && (count = fs.readSync(fd, bytes, offset, bytes.length - offset, null))) offset += count;
|
|
113
|
+
if (offset === bytes.length) throw new Error('too large');
|
|
114
|
+
return bytes.subarray(0, offset).toString('utf8');
|
|
115
|
+
} catch (_) { throw new CliUsageError('无法读取 JSON 输入,或大小超过 2 MiB'); }
|
|
116
|
+
finally { if (fd !== undefined && fd !== 0) fs.closeSync(fd); }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function required(value, flag) {
|
|
120
|
+
if (!String(value || '').trim()) throw new CliUsageError('缺少参数:--' + flag);
|
|
121
|
+
return String(value);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function jsonInput(value) {
|
|
125
|
+
try {
|
|
126
|
+
const parsed = JSON.parse(required(value, 'input'));
|
|
127
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('not object');
|
|
128
|
+
return parsed;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (error instanceof CliUsageError) throw error;
|
|
131
|
+
throw new CliUsageError('--input 必须是 JSON 对象');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function generationInput(capability, input, options) {
|
|
136
|
+
try { return normalizeGenerationInput(capability, input, options); }
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error instanceof GenerationInputError) throw new CliUsageError(error.message);
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** C-8: catchable signals dispose active runtimes before the CLI exits. */
|
|
144
|
+
const activeRuntimes = new Set();
|
|
145
|
+
const cliAbortController = new AbortController();
|
|
146
|
+
let shuttingDown = false;
|
|
147
|
+
|
|
148
|
+
function registerRuntime(runtime) {
|
|
149
|
+
activeRuntimes.add(runtime);
|
|
150
|
+
return () => activeRuntimes.delete(runtime);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function disposeActiveRuntimes() {
|
|
154
|
+
await Promise.allSettled([...activeRuntimes].map((runtime) => runtime.dispose().catch(() => {})));
|
|
155
|
+
activeRuntimes.clear();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
159
|
+
process.on(signal, () => {
|
|
160
|
+
const exitCode = signal === 'SIGINT' ? 130 : 143;
|
|
161
|
+
if (shuttingDown) process.exit(exitCode);
|
|
162
|
+
shuttingDown = true;
|
|
163
|
+
cliAbortController.abort();
|
|
164
|
+
disposeActiveRuntimes()
|
|
165
|
+
.catch(() => {})
|
|
166
|
+
.finally(() => process.exit(exitCode));
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function withRawRuntime(mode, dataRoot, callback) {
|
|
171
|
+
const runtime = createCoreRuntime({ dataRoot: required(dataRoot, 'data-root'), mode });
|
|
172
|
+
const unregister = registerRuntime(runtime);
|
|
173
|
+
try {
|
|
174
|
+
runtime.start();
|
|
175
|
+
return await callback(runtime);
|
|
176
|
+
} finally {
|
|
177
|
+
try { await runtime.dispose(); } finally { unregister(); }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function withRuntime(mode, dataRoot, callback, ports = {}) {
|
|
182
|
+
return withRawRuntime(mode, dataRoot, (runtime) => callback(createCommandService(runtime, ports)));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function main(args) {
|
|
186
|
+
if (!args.length || args.includes('--help') || args.includes('-h')) {
|
|
187
|
+
console.log(usage());
|
|
188
|
+
return args.length ? 0 : 2;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (args[0] === 'doctor') {
|
|
192
|
+
const rest = args.slice(1);
|
|
193
|
+
if (rest.filter((arg) => arg === '--json').length > 1) throw new CliUsageError('参数重复:--json');
|
|
194
|
+
const options = flags(rest.filter((arg) => arg !== '--json'), ['data-root']);
|
|
195
|
+
const report = await doctor({ ...(options['data-root'] ? { dataRoot: options['data-root'] } : {}) });
|
|
196
|
+
console.log(rest.includes('--json') ? JSON.stringify(report, null, 2) : formatDoctorReport(report));
|
|
197
|
+
return report.exitCode;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (args[0] === 'runtime' && args[1] === 'recover') {
|
|
201
|
+
const options = flags(args.slice(2), ['data-root', 'confirm-stale-pid']);
|
|
202
|
+
const confirmStalePid = Number(required(options['confirm-stale-pid'], 'confirm-stale-pid'));
|
|
203
|
+
if (!Number.isSafeInteger(confirmStalePid) || confirmStalePid <= 0) {
|
|
204
|
+
throw new CliUsageError('--confirm-stale-pid 必须是 Doctor 报告中的正整数 PID');
|
|
205
|
+
}
|
|
206
|
+
const result = recoverCoreWriterLease(required(options['data-root'], 'data-root'), { confirmStalePid });
|
|
207
|
+
console.log(JSON.stringify(result, null, 2));
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (args[0] === 'core' && ['delete', 'cleanup', 'transactions', 'restore'].includes(args[1])) {
|
|
212
|
+
const verb = args[1], restoring = verb === 'restore', selecting = ['delete', 'cleanup'].includes(verb);
|
|
213
|
+
const rest = args.slice(restoring ? 3 : 2);
|
|
214
|
+
const confirmation = rest.filter(arg => arg === '--confirm-delete').length;
|
|
215
|
+
if (confirmation > 1 || (confirmation && !selecting)) throw new CliUsageError('--confirm-delete 参数无效或重复');
|
|
216
|
+
const options = flags(rest.filter(arg => arg !== '--confirm-delete'), ['data-root', ...(selecting ? ['input'] : [])]);
|
|
217
|
+
const input = restoring ? { transaction_id: required(args[2], 'transaction-id') } : selecting
|
|
218
|
+
? { ...(options.input ? jsonInput(options.input) : {}), confirm_delete: confirmation === 1 } : {};
|
|
219
|
+
const result = await withRuntime(restoring || confirmation ? 'writer' : 'reader', options['data-root'], commands => commands.execute('core.' + verb, input));
|
|
220
|
+
console.log(JSON.stringify(result, null, 2));
|
|
221
|
+
return result.allowed === false ? 1 : 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (['providers', 'capabilities'].includes(args[0]) && args[1] === 'list') {
|
|
225
|
+
const options = flags(args.slice(2), ['provider-config']);
|
|
226
|
+
const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
|
|
227
|
+
const report = args[0] === 'providers' ? providerCatalogSnapshot(catalog) : catalogCapabilitySnapshot(catalog);
|
|
228
|
+
console.log(JSON.stringify(report, null, 2));
|
|
229
|
+
return 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (['config', 'providers', 'models', 'assignments'].includes(args[0])) {
|
|
233
|
+
const [group, verb] = args;
|
|
234
|
+
const verbs = { config: ['init', 'show', 'check'], providers: ['add', 'set', 'remove'], models: ['list', 'add', 'remove', 'caps', 'protocol', 'vision-input', 'discover', 'test'], assignments: ['list', 'set', 'clear'] };
|
|
235
|
+
if (!verbs[group].includes(verb)) throw new CliUsageError('未知配置命令');
|
|
236
|
+
const positional = (group === 'providers' && verb === 'remove') || (group === 'models' && verb !== 'list');
|
|
237
|
+
const id = positional ? required(args[2], group === 'providers' || verb === 'discover' ? 'provider-id' : 'model-ref') : undefined;
|
|
238
|
+
const options = flags(args.slice(positional ? 3 : 2), ['provider-config', 'input', 'apply', 'capability', 'data-root']);
|
|
239
|
+
if (options.apply !== undefined && !['true', 'false'].includes(options.apply)) throw new CliUsageError('--apply 必须是 true 或 false');
|
|
240
|
+
let input = options.input ? jsonInput(options.input) : {};
|
|
241
|
+
const probeInput = { ...input };
|
|
242
|
+
if (group === 'providers') input = ['add', 'set'].includes(verb) ? { provider: jsonInput(options.input) } : { provider_id: id };
|
|
243
|
+
if (group === 'models' && positional) {
|
|
244
|
+
if (verb === 'discover') input = { provider_id: id, apply: options.apply === 'true' };
|
|
245
|
+
else {
|
|
246
|
+
if (!parseModelRef(id)) throw new CliUsageError('模型必须用 providerId::modelId 复合引用');
|
|
247
|
+
input = { ...input, model_ref: id, ...(verb === 'test' ? { capability: required(options.capability, 'capability') } : {}) };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const result = await executeConfigCommand(required(options['provider-config'], 'provider-config'), group === 'config' ? verb : group + '.' + verb, input, {
|
|
251
|
+
signal: cliAbortController.signal,
|
|
252
|
+
probe: async ({ provider, modelId, capability, signal }) => {
|
|
253
|
+
if (capability === 'vision') return probeVisionModel(provider, modelId, { bytes: Buffer.from(RED_TEST_IMAGE.split(',')[1], 'base64'), mediaType: 'image/png' }, { signal });
|
|
254
|
+
if (['video-gen', 'transcribe'].includes(capability) && !options.input) return {
|
|
255
|
+
skipped: true, ok: false, reason: '请用 models test --input 提供与 run video/transcribe 相同的真实素材;未发起请求'
|
|
256
|
+
};
|
|
257
|
+
return withRawRuntime('writer', options['data-root'], async runtime => {
|
|
258
|
+
const mediaCapability = capability === 'image-gen' ? 'image' : capability === 'video-gen' ? 'video' : capability;
|
|
259
|
+
if (mediaCapability === 'image') provider = providerForImageModel(provider, modelId);
|
|
260
|
+
const adapter = createConfiguredProviderAdapter(provider, { allowRateLimited: true, modelId });
|
|
261
|
+
let providerInput = mediaCapability === 'image'
|
|
262
|
+
? { prompt: 'red circle', n: 1, ...(['openai-chat-images', 'openai-responses-images'].includes(adapter.protocol) ? {} : {
|
|
263
|
+
size: adapter.protocol === 'openai-images' ? '256x256' : '512*512'
|
|
264
|
+
}) }
|
|
265
|
+
: { text: '你好', voice: provider.ttsVoice || 'Cherry' };
|
|
266
|
+
let candidates = [{ adapter, model: qualifiedModelRef(provider.id, modelId), selectionReason: 'explicit', providerBinding: providerTaskBinding(provider) }];
|
|
267
|
+
if (['video', 'transcribe'].includes(mediaCapability)) {
|
|
268
|
+
const normalized = generationInput(mediaCapability, { ...probeInput, model_ref: qualifiedModelRef(provider.id, modelId) },
|
|
269
|
+
{ allowVideoPaths: true, allowAudioPath: true });
|
|
270
|
+
providerInput = normalized.providerInput;
|
|
271
|
+
if (mediaCapability === 'video') candidates = videoTaskCandidates(candidates, normalized);
|
|
272
|
+
else if (normalized.audioPath) {
|
|
273
|
+
const prepared = await prepareProviderInput(adapter, { model: modelId, filePath: normalized.audioPath, signal });
|
|
274
|
+
providerInput = generationInput('transcribe', { audio_url: prepared.url }).providerInput;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const submitted = await createProviderTaskRunner(runtime).submit({ capability: mediaCapability,
|
|
278
|
+
candidates, providerInput });
|
|
279
|
+
const waited = await createCommandService(runtime, { resolveTaskAdapter: () => adapter }).execute('task.wait', {
|
|
280
|
+
task_id: submitted.task.id, timeout_ms: 60000, poll_interval_ms: 1000 });
|
|
281
|
+
const task = waited.task;
|
|
282
|
+
return { ok: task.outcome === 'succeeded' && task.deliveryState === 'ready', taskId: task.id,
|
|
283
|
+
category: task.lastError?.category, httpStatus: task.lastError?.httpStatus };
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
console.log(JSON.stringify(result, null, 2));
|
|
288
|
+
return result.valid === false || result.passed === false ? 1 : 0;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (args[0] === 'vision' && ['look', 'locate', 'ocr', 'summarize'].includes(args[1])) {
|
|
292
|
+
const command = args[1];
|
|
293
|
+
const options = flags(args.slice(2), ['provider-config', 'input', 'model-ref', 'format', 'output', 'sheet-output', 'data-root', 'timeout-ms']);
|
|
294
|
+
const input = normalizeVisionInput(command, jsonInput(options.input));
|
|
295
|
+
const format = options.format || 'json';
|
|
296
|
+
if (!['json', 'text'].includes(format)) throw new CliUsageError('--format 只支持 json 或 text');
|
|
297
|
+
if (options['sheet-output'] && command !== 'summarize') throw new CliUsageError('--sheet-output 仅适用于 summarize');
|
|
298
|
+
const outputs = [options.output, options['sheet-output']].filter(Boolean).map(file => path.resolve(file));
|
|
299
|
+
if (new Set(outputs).size !== outputs.length) throw new CliUsageError('结果文件与拼图文件不能是同一路径');
|
|
300
|
+
for (const file of outputs) {
|
|
301
|
+
try {
|
|
302
|
+
if (fs.existsSync(file) || !fs.statSync(path.dirname(file)).isDirectory()) throw new Error();
|
|
303
|
+
fs.accessSync(path.dirname(file), fs.constants.W_OK);
|
|
304
|
+
} catch (_) { throw new CliUsageError('输出文件已存在或父目录不可写'); }
|
|
305
|
+
}
|
|
306
|
+
const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
|
|
307
|
+
const execute = runtime => executeVisionCommand({ command, input, catalog, runtime,
|
|
308
|
+
onRateLimit: event => recordCatalogRateLimit(options['provider-config'], event),
|
|
309
|
+
modelRef: options['model-ref'] || '', signal: cliAbortController.signal,
|
|
310
|
+
...(options['timeout-ms'] !== undefined ? { timeoutMs: Number(options['timeout-ms']) } : {}) });
|
|
311
|
+
const completed = input.transcribe
|
|
312
|
+
? await withRawRuntime('writer', options['data-root'], execute)
|
|
313
|
+
: input.artifact_id || input.frame_artifact_ids ? await withRawRuntime('reader', options['data-root'], execute) : await execute();
|
|
314
|
+
if (cliAbortController.signal.aborted) return 130;
|
|
315
|
+
const rendered = (format === 'text' ? completed.text : JSON.stringify(completed.result, null, 2)) + '\n';
|
|
316
|
+
try {
|
|
317
|
+
if (options.output) fs.writeFileSync(path.resolve(options.output), rendered, { flag: 'wx', mode: 0o600 });
|
|
318
|
+
if (options['sheet-output']) fs.writeFileSync(path.resolve(options['sheet-output']), completed.sheetBuffer, { flag: 'wx', mode: 0o600 });
|
|
319
|
+
} catch (_) { throw new CliUsageError('无法保存输出文件(不会覆盖已有文件)'); }
|
|
320
|
+
process.stdout.write(rendered);
|
|
321
|
+
return completed.exitCode;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (args[0] === 'media' && args[1] === 'html') {
|
|
325
|
+
const options = flags(args.slice(2), ['data-root', 'input', 'browser-executable', 'browser-no-sandbox', 'output']);
|
|
326
|
+
if (options['browser-no-sandbox'] !== undefined && !['true', 'false'].includes(options['browser-no-sandbox'])) throw new CliUsageError('--browser-no-sandbox 必须是 true 或 false');
|
|
327
|
+
const browser = createChromiumBrowser({ executable: options['browser-executable'] || process.env.IRIS_BROWSER_EXECUTABLE,
|
|
328
|
+
noSandbox: options['browser-no-sandbox'] === 'true' });
|
|
329
|
+
const input = jsonInput(options.input);
|
|
330
|
+
if (options.output && (!path.isAbsolute(options.output) || fs.existsSync(options.output))) throw new CliUsageError('--output 必须是未存在的绝对路径');
|
|
331
|
+
const result = await withRuntime('writer', options['data-root'], async commands => {
|
|
332
|
+
const rendered = await commands.execute('media.html', input);
|
|
333
|
+
return options.output ? { ...rendered, export: await commands.execute('artifact.export', { artifact_id: rendered.artifact.id, output_path: options.output }) } : rendered;
|
|
334
|
+
}, { browser });
|
|
335
|
+
console.log(JSON.stringify(result, null, 2));
|
|
336
|
+
return 0;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (args[0] === 'media' && ['diff', 'frames'].includes(args[1])) {
|
|
340
|
+
const options = flags(args.slice(2), ['data-root', 'input']);
|
|
341
|
+
const input = jsonInput(options.input);
|
|
342
|
+
const command = args[1] === 'diff' ? 'media.diff' : 'media.frames';
|
|
343
|
+
const result = await withRuntime('writer', options['data-root'], (commands) =>
|
|
344
|
+
commands.execute(command, input));
|
|
345
|
+
console.log(JSON.stringify(result, null, 2));
|
|
346
|
+
return 0;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (args[0] === 'run' && args[1] === 'crop') {
|
|
350
|
+
const options = flags(args.slice(2), ['data-root', 'input']);
|
|
351
|
+
const input = jsonInput(options.input);
|
|
352
|
+
const result = await withRuntime('writer', options['data-root'], (commands) =>
|
|
353
|
+
commands.execute('crop', input));
|
|
354
|
+
console.log(JSON.stringify(result, null, 2));
|
|
355
|
+
return 0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (args[0] === 'run' && args[1] === 'image') {
|
|
359
|
+
const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
|
|
360
|
+
const { providerInput, modelRef, sourceArtifactId } = generationInput('image', jsonInput(options.input));
|
|
361
|
+
const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
|
|
362
|
+
const routes = imageCandidatesFromCatalog(catalog, modelRef);
|
|
363
|
+
await withRawRuntime('writer', options['data-root'], async (runtime) => {
|
|
364
|
+
const candidates = routes.map((route) => ({
|
|
365
|
+
adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
|
|
366
|
+
selectionReason: route.selectionReason,
|
|
367
|
+
providerBinding: providerTaskBinding(route.provider)
|
|
368
|
+
}));
|
|
369
|
+
const result = await createProviderTaskRunner(runtime).submit({
|
|
370
|
+
capability: 'image', candidates,
|
|
371
|
+
providerInput, sourceArtifactId
|
|
372
|
+
});
|
|
373
|
+
console.log(JSON.stringify(result, null, 2));
|
|
374
|
+
});
|
|
375
|
+
return 0;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 视频共用 Core;本地首帧/音频按实际候选在 Attempt 内准备。
|
|
379
|
+
if (args[0] === 'run' && args[1] === 'video') {
|
|
380
|
+
const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
|
|
381
|
+
const normalized = generationInput('video', jsonInput(options.input), { allowVideoPaths: true });
|
|
382
|
+
const { providerInput, modelRef } = normalized;
|
|
383
|
+
const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
|
|
384
|
+
const routes = videoCandidatesFromCatalog(catalog, modelRef);
|
|
385
|
+
await withRawRuntime('writer', options['data-root'], async (runtime) => {
|
|
386
|
+
const candidates = videoTaskCandidates(routes.map((route) => ({
|
|
387
|
+
adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
|
|
388
|
+
selectionReason: route.selectionReason,
|
|
389
|
+
providerBinding: providerTaskBinding(route.provider)
|
|
390
|
+
})), normalized);
|
|
391
|
+
const result = await createProviderTaskRunner(runtime).submit({
|
|
392
|
+
capability: 'video', candidates,
|
|
393
|
+
providerInput
|
|
394
|
+
});
|
|
395
|
+
console.log(JSON.stringify(result, null, 2));
|
|
396
|
+
});
|
|
397
|
+
return 0;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// 语音合成输入冻结(E2 Profile):同步完成型,一次命令内完成 Task → Artifact。
|
|
401
|
+
if (args[0] === 'run' && args[1] === 'tts') {
|
|
402
|
+
const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
|
|
403
|
+
const { providerInput, modelRef } = generationInput('tts', jsonInput(options.input));
|
|
404
|
+
const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
|
|
405
|
+
const routes = ttsCandidatesFromCatalog(catalog, modelRef);
|
|
406
|
+
await withRawRuntime('writer', options['data-root'], async (runtime) => {
|
|
407
|
+
const candidates = routes.map((route) => ({
|
|
408
|
+
adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
|
|
409
|
+
selectionReason: route.selectionReason,
|
|
410
|
+
providerBinding: providerTaskBinding(route.provider)
|
|
411
|
+
}));
|
|
412
|
+
const result = await createProviderTaskRunner(runtime).submit({
|
|
413
|
+
capability: 'tts', candidates,
|
|
414
|
+
providerInput
|
|
415
|
+
});
|
|
416
|
+
console.log(JSON.stringify(result, null, 2));
|
|
417
|
+
});
|
|
418
|
+
return 0;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 转写输入冻结(E3 Profile):上传型异步——audio_url(公网/oss://)或
|
|
422
|
+
// audio_path(本地文件,经首选候选 Provider 的临时存储上传,签名 URL 不落 Core)。
|
|
423
|
+
if (args[0] === 'run' && args[1] === 'transcribe') {
|
|
424
|
+
const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
|
|
425
|
+
const normalized = generationInput('transcribe', jsonInput(options.input), { allowAudioPath: true });
|
|
426
|
+
const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
|
|
427
|
+
const routes = transcribeCandidatesFromCatalog(catalog, normalized.modelRef);
|
|
428
|
+
let providerInput = normalized.providerInput;
|
|
429
|
+
if (normalized.audioPath) {
|
|
430
|
+
if (!fs.existsSync(normalized.audioPath)) throw new CliUsageError('音频文件不存在');
|
|
431
|
+
const prepared = await prepareProviderInput(trackedProviderAdapter(routes[0].provider, options), {
|
|
432
|
+
model: routes[0].model, filePath: normalized.audioPath
|
|
433
|
+
});
|
|
434
|
+
providerInput = generationInput('transcribe', { audio_url: prepared.url }).providerInput;
|
|
435
|
+
}
|
|
436
|
+
await withRawRuntime('writer', options['data-root'], async (runtime) => {
|
|
437
|
+
const candidates = routes.map((route) => ({
|
|
438
|
+
adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
|
|
439
|
+
selectionReason: route.selectionReason,
|
|
440
|
+
providerBinding: providerTaskBinding(route.provider)
|
|
441
|
+
}));
|
|
442
|
+
const result = await createProviderTaskRunner(runtime).submit({
|
|
443
|
+
capability: 'transcribe', candidates,
|
|
444
|
+
providerInput
|
|
445
|
+
});
|
|
446
|
+
console.log(JSON.stringify(result, null, 2));
|
|
447
|
+
});
|
|
448
|
+
return 0;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (args[0] === 'task' && args[1] === 'wait' && args[2]) {
|
|
452
|
+
const options = flags(args.slice(3), ['data-root', 'provider-config', 'timeout-ms', 'poll-interval-ms']);
|
|
453
|
+
const input = { task_id: args[2], ...(options['timeout-ms'] !== undefined ? { timeout_ms: Number(options['timeout-ms']) } : {}),
|
|
454
|
+
...(options['poll-interval-ms'] !== undefined ? { poll_interval_ms: Number(options['poll-interval-ms']) } : {}) };
|
|
455
|
+
const inspected = await withRuntime('reader', options['data-root'], commands => commands.execute('task.inspect', { task_id: args[2] }));
|
|
456
|
+
const catalog = coreTaskWaitFinished(inspected.task) ? null : loadProviderCatalog(required(options['provider-config'], 'provider-config'));
|
|
457
|
+
const result = await withRuntime(catalog ? 'writer' : 'reader', options['data-root'], commands => commands.execute('task.wait', input), {
|
|
458
|
+
...(catalog ? { resolveTaskAdapter: task => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task) } : {}) });
|
|
459
|
+
console.log(JSON.stringify(result, null, 2));
|
|
460
|
+
return result.timedOut ? 3 : result.ready ? 0 : 1;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (args[0] === 'task' && args[1] === 'observe' && args[2]) {
|
|
464
|
+
const options = flags(args.slice(3), ['data-root', 'provider-config']);
|
|
465
|
+
const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
|
|
466
|
+
await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
|
|
467
|
+
const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.observe', { task_id: args[2] }), {
|
|
468
|
+
resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task)
|
|
469
|
+
});
|
|
470
|
+
console.log(JSON.stringify(result, null, 2));
|
|
471
|
+
return 0;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (args[0] === 'task' && args[1] === 'redeliver' && args[2]) {
|
|
475
|
+
const options = flags(args.slice(3), ['data-root', 'provider-config']);
|
|
476
|
+
const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
|
|
477
|
+
await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
|
|
478
|
+
const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.redeliver', { task_id: args[2] }), {
|
|
479
|
+
resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task)
|
|
480
|
+
});
|
|
481
|
+
console.log(JSON.stringify(result, null, 2));
|
|
482
|
+
return 0;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (args[0] === 'task' && args[1] === 'cancel' && args[2]) {
|
|
486
|
+
const options = flags(args.slice(3), ['data-root', 'provider-config']);
|
|
487
|
+
const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
|
|
488
|
+
await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
|
|
489
|
+
const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.cancel', { task_id: args[2] }), {
|
|
490
|
+
resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task)
|
|
491
|
+
});
|
|
492
|
+
console.log(JSON.stringify(result, null, 2));
|
|
493
|
+
return 0;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (args[0] === 'task' && args[1] === 'retry' && args[2]) {
|
|
497
|
+
const rest = args.slice(3);
|
|
498
|
+
const confirmBilling = rest.includes('--confirm-billing');
|
|
499
|
+
const options = flags(rest.filter((arg) => arg !== '--confirm-billing'),
|
|
500
|
+
['data-root', 'provider-config', 'input', 'model-ref']);
|
|
501
|
+
const input = jsonInput(options.input);
|
|
502
|
+
const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
|
|
503
|
+
const dataRoot = required(options['data-root'], 'data-root');
|
|
504
|
+
await withRuntime('reader', dataRoot, (commands) => commands.execute('task.inspect', { task_id: args[2] }));
|
|
505
|
+
const result = await withRuntime('writer', dataRoot, (commands) => commands.execute('task.retry', {
|
|
506
|
+
task_id: args[2],
|
|
507
|
+
provider_input: input,
|
|
508
|
+
model_ref: options['model-ref'],
|
|
509
|
+
confirm_billing: confirmBilling
|
|
510
|
+
}), {
|
|
511
|
+
resolveTaskCandidates: ({ capability, modelRef }) => (capability === 'video'
|
|
512
|
+
? videoCandidatesFromCatalog(catalog, modelRef || undefined)
|
|
513
|
+
: capability === 'tts'
|
|
514
|
+
? ttsCandidatesFromCatalog(catalog, modelRef || undefined)
|
|
515
|
+
: capability === 'transcribe'
|
|
516
|
+
? transcribeCandidatesFromCatalog(catalog, modelRef || undefined)
|
|
517
|
+
: imageCandidatesFromCatalog(catalog, modelRef || undefined)
|
|
518
|
+
).map((route) => ({
|
|
519
|
+
adapter: trackedProviderAdapter(route.provider, options),
|
|
520
|
+
model: route.modelRef,
|
|
521
|
+
selectionReason: route.selectionReason,
|
|
522
|
+
providerBinding: providerTaskBinding(route.provider)
|
|
523
|
+
}))
|
|
524
|
+
});
|
|
525
|
+
console.log(JSON.stringify(result, null, 2));
|
|
526
|
+
return 0;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (args[0] === 'task' && args[1] === 'inspect' && args[2]) {
|
|
530
|
+
const options = flags(args.slice(3), ['data-root']);
|
|
531
|
+
const result = await withRuntime('reader', options['data-root'], (commands) =>
|
|
532
|
+
commands.execute('task.inspect', { task_id: args[2] }));
|
|
533
|
+
console.log(JSON.stringify(result, null, 2));
|
|
534
|
+
return 0;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (args[0] === 'task' && args[1] === 'list') {
|
|
538
|
+
const options = flags(args.slice(2), ['data-root', 'offset', 'limit', 'status', 'capability', 'provider-id', 'model-ref', 'outcome', 'delivery-state']);
|
|
539
|
+
const input = Object.fromEntries(Object.entries(options).filter(([key]) => key !== 'data-root').map(([key, value]) => [key.replaceAll('-', '_'), ['offset', 'limit'].includes(key) ? Number(value) : value]));
|
|
540
|
+
const result = await withRuntime('reader', options['data-root'], (commands) =>
|
|
541
|
+
commands.execute('task.list', input));
|
|
542
|
+
console.log(JSON.stringify(result, null, 2));
|
|
543
|
+
return 0;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (args[0] === 'artifact' && args[1] === 'inspect' && args[2]) {
|
|
547
|
+
const options = flags(args.slice(3), ['data-root']);
|
|
548
|
+
const result = await withRuntime('reader', options['data-root'], (commands) =>
|
|
549
|
+
commands.execute('artifact.inspect', { artifact_id: args[2] }));
|
|
550
|
+
console.log(JSON.stringify(result, null, 2));
|
|
551
|
+
return 0;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (args[0] === 'artifact' && args[1] === 'list') {
|
|
555
|
+
const options = flags(args.slice(2), ['data-root', 'offset', 'limit', 'kind', 'media-type', 'task-id']);
|
|
556
|
+
const input = Object.fromEntries(Object.entries(options).filter(([key]) => key !== 'data-root').map(([key, value]) => [key.replaceAll('-', '_'), ['offset', 'limit'].includes(key) ? Number(value) : value]));
|
|
557
|
+
const result = await withRuntime('reader', options['data-root'], (commands) =>
|
|
558
|
+
commands.execute('artifact.list', input));
|
|
559
|
+
console.log(JSON.stringify(result, null, 2));
|
|
560
|
+
return 0;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (['task', 'artifact'].includes(args[0]) && ['inspect-many', 'export-many'].includes(args[1])) {
|
|
564
|
+
if (args[0] === 'task' && args[1] === 'export-many') throw new CliUsageError('仅 Artifact 支持 export-many');
|
|
565
|
+
const exporting = args[1] === 'export-many';
|
|
566
|
+
const options = flags(args.slice(2), ['data-root', 'input', ...(exporting ? ['output'] : [])]);
|
|
567
|
+
const input = { ...jsonInput(options.input), ...(exporting ? { output_directory: required(options.output, 'output') } : {}) };
|
|
568
|
+
const result = await withRuntime(exporting ? 'writer' : 'reader', options['data-root'], commands => commands.execute(args[0] + '.' + args[1], input));
|
|
569
|
+
console.log(JSON.stringify(result, null, 2));
|
|
570
|
+
return result.results.some(entry => entry.error) ? 1 : 0;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (args[0] === 'artifact' && args[1] === 'rebuild') {
|
|
574
|
+
const options = flags(args.slice(2), ['data-root']);
|
|
575
|
+
const result = await withRuntime('writer', options['data-root'], (commands) =>
|
|
576
|
+
commands.execute('artifact.rebuild', {}));
|
|
577
|
+
console.log(JSON.stringify(result, null, 2));
|
|
578
|
+
return 0;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (args[0] === 'artifact' && args[1] === 'export' && args[2]) {
|
|
582
|
+
const options = flags(args.slice(3), ['data-root', 'output']);
|
|
583
|
+
const result = await withRuntime('writer', options['data-root'], (commands) =>
|
|
584
|
+
commands.execute('artifact.export', {
|
|
585
|
+
artifact_id: args[2],
|
|
586
|
+
output_path: required(options.output, 'output')
|
|
587
|
+
}));
|
|
588
|
+
console.log(JSON.stringify(result, null, 2));
|
|
589
|
+
return 0;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
throw new CliUsageError('未知命令');
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
597
|
+
} catch (error) {
|
|
598
|
+
const code = error && error.code || 'IRIS_CLI_FAILED';
|
|
599
|
+
const message = error && error.message || '命令执行失败';
|
|
600
|
+
console.error(code + ': ' + message);
|
|
601
|
+
if (error instanceof CliUsageError) console.error('\n' + usage());
|
|
602
|
+
process.exitCode = error instanceof CliUsageError ? 2 : 1;
|
|
23
603
|
}
|