@mokuyoaxis/dsh-iris 0.2.0 → 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.
Files changed (88) hide show
  1. package/.dsh/skills/iris-compose-media/SKILL.md +10 -5
  2. package/.dsh/skills/iris-compose-media/references/workflows.md +7 -1
  3. package/.dsh/skills/iris-verify-ui/SKILL.md +2 -1
  4. package/CHANGELOG.md +47 -2
  5. package/README.en.md +65 -29
  6. package/README.md +65 -29
  7. package/bin/dsh-iris.js +222 -21
  8. package/docs/AGENT_QUICKSTART.md +10 -6
  9. package/docs/ARCHITECTURE.md +25 -18
  10. package/docs/ARTIFACT_LIBRARY.md +5 -5
  11. package/docs/ARTIFACT_MANIFEST.md +1 -1
  12. package/docs/CLI_MANAGEMENT.md +181 -0
  13. package/docs/COMPOSITE_VISION.md +35 -0
  14. package/docs/CORE_RUNTIME_CONTRACT.md +8 -7
  15. package/docs/DSH_CORE_MIGRATION.md +48 -19
  16. package/docs/DSH_RC2_ADAPTATION.md +4 -2
  17. package/docs/FAULT_INJECTION.md +12 -2
  18. package/docs/HEADLESS_CLI.md +13 -7
  19. package/docs/HOST_ADAPTER_CONTRACT.md +10 -10
  20. package/docs/HOST_DOCTOR.md +3 -3
  21. package/docs/MODEL_PORT_CONTRACT.md +19 -14
  22. package/docs/OCR_MODEL.md +39 -0
  23. package/docs/PROMPT_OPTIMIZER.md +47 -0
  24. package/docs/PROVIDER_ADAPTER_CONTRACT.md +6 -4
  25. package/docs/PROVIDER_HEALTH.md +22 -5
  26. package/docs/PROVIDER_RUNTIME_HARNESS.md +1 -1
  27. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +2 -2
  28. package/docs/ROADMAP.md +29 -19
  29. package/docs/SECURITY.md +5 -5
  30. package/docs/TASK_SEMANTICS.md +2 -2
  31. package/docs/VISION_CLI.md +132 -0
  32. package/docs/VISION_MODEL.md +51 -0
  33. package/docs/WORKBENCH_ARTIFACTS.md +67 -0
  34. package/docs/file-access-across-environments.md +8 -5
  35. package/docs/releases/0.2.1.md +56 -0
  36. package/lib/actions.js +187 -270
  37. package/lib/adapters.js +133 -22
  38. package/lib/api.js +49 -7
  39. package/lib/chromium-browser.js +117 -0
  40. package/lib/client.js +671 -101
  41. package/lib/command-service.js +121 -7
  42. package/lib/composite-vision-routing.js +42 -0
  43. package/lib/config.js +89 -10
  44. package/lib/core-artifact-store.js +45 -13
  45. package/lib/core-maintenance.js +213 -0
  46. package/lib/core-tasks.js +12 -5
  47. package/lib/core-user-projection.js +1 -0
  48. package/lib/dsh-core-adapter.js +63 -9
  49. package/lib/dsh-text-model-adapter.js +222 -0
  50. package/lib/dsh-vision-model-adapter.js +75 -0
  51. package/lib/generation-input.js +36 -5
  52. package/lib/headless-vision.js +181 -0
  53. package/lib/host-runtime.js +1 -0
  54. package/lib/http-vision-model-adapter.js +187 -0
  55. package/lib/image-edit-input.js +34 -0
  56. package/lib/index.js +205 -175
  57. package/lib/locate.js +86 -28
  58. package/lib/media-probe.js +2 -1
  59. package/lib/model-call-runtime.js +45 -0
  60. package/lib/model-invoker.js +4 -2
  61. package/lib/model-port-contract.js +13 -5
  62. package/lib/models.js +10 -5
  63. package/lib/ocr-model-routing.js +26 -0
  64. package/lib/ocr.js +110 -64
  65. package/lib/prompt-optimizer-config.js +2 -1
  66. package/lib/prompt-optimizer-core.js +121 -0
  67. package/lib/prompt-optimizer.js +37 -193
  68. package/lib/provider-adapters.js +85 -9
  69. package/lib/provider-catalog.js +19 -7
  70. package/lib/provider-config-service.js +236 -0
  71. package/lib/provider-contract.js +3 -2
  72. package/lib/provider-health.js +80 -1
  73. package/lib/provider-protocol.js +23 -0
  74. package/lib/provider-task-runner.js +58 -16
  75. package/lib/summarize.js +31 -20
  76. package/lib/summary-input.js +52 -0
  77. package/lib/task-wait.js +40 -0
  78. package/lib/video-input.js +30 -0
  79. package/lib/vision-core.js +42 -0
  80. package/lib/vision-image-input.js +83 -0
  81. package/lib/vision-input.js +31 -0
  82. package/lib/vision-model-routing.js +180 -0
  83. package/lib/vision.js +3 -2
  84. package/lib/workbench-artifacts.js +52 -0
  85. package/lib/workbench-management.js +151 -0
  86. package/lib/workbench-zip.js +39 -0
  87. package/package.json +1 -1
  88. package/user_guide.md +52 -22
package/bin/dsh-iris.js CHANGED
@@ -1,14 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
+ import path from 'node:path';
3
4
  import { createCommandService } from '../lib/command-service.js';
4
5
  import { createCoreRuntime } from '../lib/core-runtime.js';
5
6
  import { recoverCoreWriterLease } from '../lib/core-lease-recovery.js';
6
7
  import { doctor, formatDoctorReport } from '../lib/doctor.js';
7
8
  import { loadProviderCatalog, providerCatalogSnapshot, catalogCapabilitySnapshot, imageCandidatesFromCatalog, providerForTaskFromCatalog, providerTaskBinding, transcribeCandidatesFromCatalog, ttsCandidatesFromCatalog, videoCandidatesFromCatalog } from '../lib/provider-catalog.js';
8
9
  import { createConfiguredProviderAdapter } from '../lib/provider-adapters.js';
10
+ import { providerForImageModel } from '../lib/provider-protocol.js';
9
11
  import { prepareProviderInput } from '../lib/provider-adapter.js';
10
12
  import { createProviderTaskRunner } from '../lib/provider-task-runner.js';
11
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
+ }
12
29
 
13
30
  class CliUsageError extends Error {
14
31
  constructor(message) {
@@ -21,27 +38,48 @@ class CliUsageError extends Error {
21
38
  function usage() {
22
39
  return [
23
40
  'Usage:',
41
+ ' JSON input: --input <json>, --input-file <path>, or --input - (stdin).',
24
42
  ' dsh-iris doctor [--data-root <absolute-path>] [--json]',
25
43
  ' dsh-iris runtime recover --data-root <absolute-path> --confirm-stale-pid <pid>',
26
44
  ' dsh-iris providers list --provider-config <absolute-path>',
27
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>.',
28
56
  ' dsh-iris run crop --data-root <absolute-path> --input <json>',
29
57
  ' dsh-iris media diff --data-root <absolute-path> --input <json>',
30
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>]',
31
60
  ' dsh-iris run image --data-root <absolute-path> --provider-config <absolute-path> --input <json>',
32
61
  ' dsh-iris run video --data-root <absolute-path> --provider-config <absolute-path> --input <json>',
33
62
  ' dsh-iris run tts --data-root <absolute-path> --provider-config <absolute-path> --input <json>',
34
63
  ' dsh-iris run transcribe --data-root <absolute-path> --provider-config <absolute-path> --input <json>',
35
64
  ' dsh-iris task inspect <id> --data-root <absolute-path>',
36
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>',
37
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>]',
38
70
  ' dsh-iris task redeliver <id> --data-root <absolute-path> --provider-config <absolute-path>',
39
71
  ' dsh-iris task cancel <id> --data-root <absolute-path> --provider-config <absolute-path>',
40
72
  ' dsh-iris task retry <id> --data-root <absolute-path> --provider-config <absolute-path> --input <json> --confirm-billing [--model-ref <providerId::modelId>]',
41
73
  ' dsh-iris artifact inspect <id> --data-root <absolute-path>',
42
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>',
43
77
  ' dsh-iris artifact export <id> --data-root <absolute-path> --output <absolute-path>',
44
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>',
45
83
  '',
46
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.'
47
85
  ].join('\n');
@@ -52,15 +90,32 @@ function flags(args, allowed) {
52
90
  for (let index = 0; index < args.length; index += 2) {
53
91
  const flag = args[index];
54
92
  const name = typeof flag === 'string' && flag.startsWith('--') ? flag.slice(2) : '';
55
- if (!name || !allowed.includes(name) || index + 1 >= args.length || String(args[index + 1]).startsWith('--')) {
93
+ if (!name || !(allowed.includes(name) || name === 'input-file' && allowed.includes('input')) || index + 1 >= args.length || String(args[index + 1]).startsWith('--')) {
56
94
  throw new CliUsageError('参数无效:' + String(flag || ''));
57
95
  }
58
96
  if (Object.prototype.hasOwnProperty.call(parsed, name)) throw new CliUsageError('参数重复:--' + name);
59
97
  parsed[name] = args[index + 1];
60
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);
61
103
  return parsed;
62
104
  }
63
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
+
64
119
  function required(value, flag) {
65
120
  if (!String(value || '').trim()) throw new CliUsageError('缺少参数:--' + flag);
66
121
  return String(value);
@@ -87,6 +142,7 @@ function generationInput(capability, input, options) {
87
142
 
88
143
  /** C-8: catchable signals dispose active runtimes before the CLI exits. */
89
144
  const activeRuntimes = new Set();
145
+ const cliAbortController = new AbortController();
90
146
  let shuttingDown = false;
91
147
 
92
148
  function registerRuntime(runtime) {
@@ -104,6 +160,7 @@ for (const signal of ['SIGINT', 'SIGTERM']) {
104
160
  const exitCode = signal === 'SIGINT' ? 130 : 143;
105
161
  if (shuttingDown) process.exit(exitCode);
106
162
  shuttingDown = true;
163
+ cliAbortController.abort();
107
164
  disposeActiveRuntimes()
108
165
  .catch(() => {})
109
166
  .finally(() => process.exit(exitCode));
@@ -151,6 +208,19 @@ async function main(args) {
151
208
  return 0;
152
209
  }
153
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
+
154
224
  if (['providers', 'capabilities'].includes(args[0]) && args[1] === 'list') {
155
225
  const options = flags(args.slice(2), ['provider-config']);
156
226
  const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
@@ -159,6 +229,113 @@ async function main(args) {
159
229
  return 0;
160
230
  }
161
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
+
162
339
  if (args[0] === 'media' && ['diff', 'frames'].includes(args[1])) {
163
340
  const options = flags(args.slice(2), ['data-root', 'input']);
164
341
  const input = jsonInput(options.input);
@@ -180,37 +357,37 @@ async function main(args) {
180
357
 
181
358
  if (args[0] === 'run' && args[1] === 'image') {
182
359
  const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
183
- const { providerInput, modelRef } = generationInput('image', jsonInput(options.input));
360
+ const { providerInput, modelRef, sourceArtifactId } = generationInput('image', jsonInput(options.input));
184
361
  const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
185
362
  const routes = imageCandidatesFromCatalog(catalog, modelRef);
186
363
  await withRawRuntime('writer', options['data-root'], async (runtime) => {
187
364
  const candidates = routes.map((route) => ({
188
- adapter: createConfiguredProviderAdapter(route.provider), model: route.modelRef,
365
+ adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
189
366
  selectionReason: route.selectionReason,
190
367
  providerBinding: providerTaskBinding(route.provider)
191
368
  }));
192
369
  const result = await createProviderTaskRunner(runtime).submit({
193
370
  capability: 'image', candidates,
194
- providerInput
371
+ providerInput, sourceArtifactId
195
372
  });
196
373
  console.log(JSON.stringify(result, null, 2));
197
374
  });
198
375
  return 0;
199
376
  }
200
377
 
201
- // 视频输入冻结(E 阶段 Profile):t2v/i2v 的 prompt/img_data_url/size/duration;
202
- // s2v 上传流程不在 headless 面开放。
378
+ // 视频共用 Core;本地首帧/音频按实际候选在 Attempt 内准备。
203
379
  if (args[0] === 'run' && args[1] === 'video') {
204
380
  const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
205
- const { providerInput, modelRef } = generationInput('video', jsonInput(options.input));
381
+ const normalized = generationInput('video', jsonInput(options.input), { allowVideoPaths: true });
382
+ const { providerInput, modelRef } = normalized;
206
383
  const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
207
384
  const routes = videoCandidatesFromCatalog(catalog, modelRef);
208
385
  await withRawRuntime('writer', options['data-root'], async (runtime) => {
209
- const candidates = routes.map((route) => ({
210
- adapter: createConfiguredProviderAdapter(route.provider), model: route.modelRef,
386
+ const candidates = videoTaskCandidates(routes.map((route) => ({
387
+ adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
211
388
  selectionReason: route.selectionReason,
212
389
  providerBinding: providerTaskBinding(route.provider)
213
- }));
390
+ })), normalized);
214
391
  const result = await createProviderTaskRunner(runtime).submit({
215
392
  capability: 'video', candidates,
216
393
  providerInput
@@ -228,7 +405,7 @@ async function main(args) {
228
405
  const routes = ttsCandidatesFromCatalog(catalog, modelRef);
229
406
  await withRawRuntime('writer', options['data-root'], async (runtime) => {
230
407
  const candidates = routes.map((route) => ({
231
- adapter: createConfiguredProviderAdapter(route.provider), model: route.modelRef,
408
+ adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
232
409
  selectionReason: route.selectionReason,
233
410
  providerBinding: providerTaskBinding(route.provider)
234
411
  }));
@@ -251,14 +428,14 @@ async function main(args) {
251
428
  let providerInput = normalized.providerInput;
252
429
  if (normalized.audioPath) {
253
430
  if (!fs.existsSync(normalized.audioPath)) throw new CliUsageError('音频文件不存在');
254
- const prepared = await prepareProviderInput(createConfiguredProviderAdapter(routes[0].provider), {
431
+ const prepared = await prepareProviderInput(trackedProviderAdapter(routes[0].provider, options), {
255
432
  model: routes[0].model, filePath: normalized.audioPath
256
433
  });
257
434
  providerInput = generationInput('transcribe', { audio_url: prepared.url }).providerInput;
258
435
  }
259
436
  await withRawRuntime('writer', options['data-root'], async (runtime) => {
260
437
  const candidates = routes.map((route) => ({
261
- adapter: createConfiguredProviderAdapter(route.provider), model: route.modelRef,
438
+ adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
262
439
  selectionReason: route.selectionReason,
263
440
  providerBinding: providerTaskBinding(route.provider)
264
441
  }));
@@ -271,12 +448,24 @@ async function main(args) {
271
448
  return 0;
272
449
  }
273
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
+
274
463
  if (args[0] === 'task' && args[1] === 'observe' && args[2]) {
275
464
  const options = flags(args.slice(3), ['data-root', 'provider-config']);
276
465
  const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
277
466
  await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
278
467
  const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.observe', { task_id: args[2] }), {
279
- resolveTaskAdapter: (task) => createConfiguredProviderAdapter(providerForTaskFromCatalog(catalog, task))
468
+ resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task)
280
469
  });
281
470
  console.log(JSON.stringify(result, null, 2));
282
471
  return 0;
@@ -287,7 +476,7 @@ async function main(args) {
287
476
  const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
288
477
  await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
289
478
  const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.redeliver', { task_id: args[2] }), {
290
- resolveTaskAdapter: (task) => createConfiguredProviderAdapter(providerForTaskFromCatalog(catalog, task))
479
+ resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task)
291
480
  });
292
481
  console.log(JSON.stringify(result, null, 2));
293
482
  return 0;
@@ -298,7 +487,7 @@ async function main(args) {
298
487
  const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
299
488
  await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
300
489
  const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.cancel', { task_id: args[2] }), {
301
- resolveTaskAdapter: (task) => createConfiguredProviderAdapter(providerForTaskFromCatalog(catalog, task))
490
+ resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task)
302
491
  });
303
492
  console.log(JSON.stringify(result, null, 2));
304
493
  return 0;
@@ -327,7 +516,7 @@ async function main(args) {
327
516
  ? transcribeCandidatesFromCatalog(catalog, modelRef || undefined)
328
517
  : imageCandidatesFromCatalog(catalog, modelRef || undefined)
329
518
  ).map((route) => ({
330
- adapter: createConfiguredProviderAdapter(route.provider),
519
+ adapter: trackedProviderAdapter(route.provider, options),
331
520
  model: route.modelRef,
332
521
  selectionReason: route.selectionReason,
333
522
  providerBinding: providerTaskBinding(route.provider)
@@ -346,9 +535,10 @@ async function main(args) {
346
535
  }
347
536
 
348
537
  if (args[0] === 'task' && args[1] === 'list') {
349
- const options = flags(args.slice(2), ['data-root']);
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]));
350
540
  const result = await withRuntime('reader', options['data-root'], (commands) =>
351
- commands.execute('task.list', {}));
541
+ commands.execute('task.list', input));
352
542
  console.log(JSON.stringify(result, null, 2));
353
543
  return 0;
354
544
  }
@@ -362,13 +552,24 @@ async function main(args) {
362
552
  }
363
553
 
364
554
  if (args[0] === 'artifact' && args[1] === 'list') {
365
- const options = flags(args.slice(2), ['data-root']);
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]));
366
557
  const result = await withRuntime('reader', options['data-root'], (commands) =>
367
- commands.execute('artifact.list', {}));
558
+ commands.execute('artifact.list', input));
368
559
  console.log(JSON.stringify(result, null, 2));
369
560
  return 0;
370
561
  }
371
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
+
372
573
  if (args[0] === 'artifact' && args[1] === 'rebuild') {
373
574
  const options = flags(args.slice(2), ['data-root']);
374
575
  const result = await withRuntime('writer', options['data-root'], (commands) =>
@@ -2,15 +2,18 @@
2
2
 
3
3
  把下面一段交给负责安装或使用 Iris 的 Agent。版本与发布状态以当前 [README](../README.md) 为准;Iris 和 DSH 的版本号独立。
4
4
 
5
+ 当前安装版本为 Iris 0.2.1。先核对宿主与实际安装版本,再使用新视觉命令或 Artifact 改图;旧宿主仍固定安装 0.1.4。
6
+
5
7
  ## 中文
6
8
 
7
9
  ```text
8
10
  请根据本仓库 README,帮我安装并使用 Iris(@mokuyoaxis/dsh-iris)。
9
- 先确认运行环境、目标 DSH profile,并用 dsh --version 核对宿主;Iris 0.2.0 要求 Node.js >=22.0.0(旧 npm 0.1.4 的历史要求为 >=20.10.0):
10
- - DSH 0.2.0-rc.2:固定安装 dsh plugin --profile web add @mokuyoaxis/dsh-iris@0.2.0。
11
+ 先确认运行环境、目标 DSH profile,并用 dsh --version 核对宿主;Iris 0.2.x 要求 Node.js >=22.0.0(旧 npm 0.1.4 的历史要求为 >=20.10.0):
12
+ - DSH 0.2.0-rc.2:固定安装 dsh plugin --profile web add @mokuyoaxis/dsh-iris@0.2.1。
11
13
  - DSH >=0.1.2-rc.1 <0.1.3-0 或 0.1.5-rc.1:固定安装 dsh plugin --profile web add @mokuyoaxis/dsh-iris@0.1.4;只下载用 npm pack @mokuyoaxis/dsh-iris@0.1.4。
12
14
  - 其他 DSH 版本:先报告没有支持声明,不自行升级宿主或跳过版本检查。
13
- 没有 DSH、希望用 CLI 时,按 docs/HEADLESS_CLI.md 使用 npm 0.2.0;npm 0.1.4 不含新的 Core 媒体 CLI。
15
+ 没有 DSH、希望用 CLI 时,按 docs/HEADLESS_CLI.md 使用 npm 0.2.1;npm 0.1.4 不含新的 Core 媒体 CLI。
16
+ 先用 --help 和 Doctor 核对实际版本与命令。Iris 0.2.1 支持 OpenAI 兼容图片与视觉工作流;其他协议与 music 是后续计划。
14
17
  从 dsh-market 安装也要核对实际包名、版本和兼容范围;市场缓存不代表支持旧宿主,不对旧 DSH 安装 latest。
15
18
  若使用其他 profile,把示例中的 web 换成已确认的目标。保留完整 scoped 包名,不把 dsh-iris 当作同一 npm 包。
16
19
  安装后检查 Doctor 和工具可达性,再执行我指定的媒体任务;不要输出 API Key,重启宿主前说明影响。
@@ -22,11 +25,12 @@
22
25
 
23
26
  ```text
24
27
  Help me install and use Iris (@mokuyoaxis/dsh-iris) according to this repository's README.
25
- Check the environment, target DSH profile, and dsh --version first; Iris 0.2.0 requires Node.js >=22.0.0 (the published npm 0.1.4 historically requires >=20.10.0):
26
- - DSH 0.2.0-rc.2: pin dsh plugin --profile web add @mokuyoaxis/dsh-iris@0.2.0.
28
+ Check the environment, target DSH profile, and dsh --version first; Iris 0.2.x requires Node.js >=22.0.0 (the published npm 0.1.4 historically requires >=20.10.0):
29
+ - DSH 0.2.0-rc.2: pin dsh plugin --profile web add @mokuyoaxis/dsh-iris@0.2.1.
27
30
  - DSH >=0.1.2-rc.1 <0.1.3-0 or 0.1.5-rc.1: pin dsh plugin --profile web add @mokuyoaxis/dsh-iris@0.1.4; download only with npm pack @mokuyoaxis/dsh-iris@0.1.4.
28
31
  - Other DSH versions: report the missing support claim; do not upgrade the host or bypass its version checks automatically.
29
- For CLI use without DSH, follow docs/HEADLESS_CLI.md using npm 0.2.0; npm 0.1.4 does not include the new Core media CLI.
32
+ For CLI use without DSH, follow docs/HEADLESS_CLI.md using npm 0.2.1; npm 0.1.4 does not include the new Core media CLI.
33
+ Check --help, Doctor and the actual installed version before using commands. Iris 0.2.1 supports the OpenAI-compatible image and vision workflow; additional protocols and music remain future work.
30
34
  For dsh-market, check the actual package name, version, and compatibility range too. Cached listings do not establish older-host support; do not install latest on an old DSH host.
31
35
  Replace web with the confirmed profile when needed. Keep the scoped package name; unscoped dsh-iris is a different npm package.
32
36
  Check Doctor and tool availability after installation, then carry out my requested media task. Do not expose API keys; explain the impact before restarting the host.
@@ -1,6 +1,6 @@
1
1
  # Iris 架构与边界
2
2
 
3
- 状态:Iris `0.2.0` 提供可在无 DSH 进程中运行的实例化 Core Runtime、Headless CLI、共享 Provider 生命周期、Core Task/Attempt 与 Artifact Manifest,并让 DSH 逐项消费同一套事实。DSH 插件仅支持 `0.2.0-rc.2`,Node.js 最低为 22.0.0;旧宿主固定使用 npm `0.1.4`。Core 内部 API 尚未冻结为公共 SDK。
3
+ 状态:本文面向 **Iris 0.2.1**。0.2.0 建立实例化 Core Runtime、Headless CLI、共享 Provider 生命周期与 Task/Artifact 事实;0.2.1 补共享视觉、模型级图片协议、Artifact 编辑及维护入口。DSH 插件仅支持 `0.2.0-rc.2`,Node.js 最低为 22.0.0;旧宿主固定使用 npm `0.1.4`。Core 内部 API 尚未冻结为公共 SDK。
4
4
 
5
5
  ## 当前形态
6
6
 
@@ -22,7 +22,7 @@ DSH Host / Cordis Headless CLI
22
22
  ┌────────────┴────────────┐
23
23
  ▼ ▼
24
24
  Command Service v0 Provider Task Runner
25
- crop / diff / frames submit / observe /
25
+ crop / diff / frames / html submit / observe /
26
26
  inspect / control cancel / delivery
27
27
  └────────────┬────────────┘
28
28
  ▼
@@ -35,20 +35,21 @@ DSH Host / Cordis Headless CLI
35
35
 
36
36
  `lib/index.js` 是 DSH 插件装配入口,`lib/dsh-host-adapter.js` 是原始 DSH/Cordis 对象进入 Iris 的服务端边界;`lib/dsh-core-adapter.js` 只负责选择 DSH profile 的 Core 数据根、恢复 Provider 配置和投影 Host DTO。`bin/dsh-iris.js` 不启动 DSH,也不推断 profile。`lib/api.js` 与 `lib/client.js` 是 Host 投影,不是真相来源。
37
37
 
38
- 未迁移能力仍走 0.1.4 legacy 链。当前主要包括 s2v 数字人视频、视觉理解、长图 OCR、媒体摘要、HTML 渲染和提示词优化;它们不会被伪装成 Core 能力。
38
+ 0.2.1的 S2V 使用 Core Task/Attempt,HTML 使用 `media.html` 与显式 Browser Port;视觉和提示词预览通过共享 Model Ports 执行。纯视觉不创建 Task/Artifact,摘要主动转写仍使用 Core 任务。聊天改图属于媒体生成链,按来源 Artifact ID 创建新 Task 和关联产物。
39
39
 
40
- ## 0.2.0 核心
40
+ ## 共享核心
41
41
 
42
42
  | 模块 | 职责 | DSH 运行时依赖 |
43
43
  |---|---|---:|
44
44
  | `core-contract.js` / `core-runtime.js` | 显式数据根、reader/writer 权限、单写者租约、取消与释放 | 无 |
45
45
  | `command-service.js` | 本地媒体命令、Task 控制面和 Artifact 查询/导出 | 无 |
46
46
  | `generation-input.js` | CLI、DSH 与重试共用的生成参数校验和 Provider 字段转换 | 无 |
47
- | `model-port-contract.js` / `model-invoker.js` | Text/Vision 内部契约、有限调用预算与取消;真实消费者尚未接入 | 无 |
47
+ | `model-port-contract.js` / `model-invoker.js` | Text/Vision 内部契约、有限调用预算与取消 | 无 |
48
48
  | `core-tasks.js` | Core Task/Attempt 持久事实、受理与交付状态 | 无 |
49
49
  | `core-artifacts.js` / `core-artifact-store.js` | 对象、Manifest、SHA-256、关系边与可重建 Index | 无 |
50
50
  | `provider-contract.js` / `provider-adapter.js` | canonical 结果、错误脱敏、受理边界与 Provider v0 操作 | 无 |
51
- | `provider-adapters.js` | DashScope / OpenAI Images 协议实现和可选输入准备 | 无 |
51
+ | `provider-adapters.js` | DashScope / Images / 聊天 / Responses 图片协议和可选输入准备 | 无 |
52
+ | `vision-image-input.js` / `image-edit-input.js` | 视觉发送副本与聊天改图来源预检;Core 原图保持不变 | 无 |
52
53
  | `provider-task-runner.js` | 写前 Attempt、submit/poll/cancel/download 与 Profile 化交付 | 无 |
53
54
  | `core-doctor.js` / `core-lease-recovery.js` | 只读完整性诊断与需显式确认的陈旧租约恢复 | 无 |
54
55
  | `provider-catalog.js` | 显式 Provider 配置、能力候选和原 Task Adapter 恢复 | 无 |
@@ -56,7 +57,7 @@ DSH Host / Cordis Headless CLI
56
57
  | `dsh-host-adapter.js` / `index.js` | Host 能力映射、工具/路由/Skill 与生命周期装配 | 有 |
57
58
  | `client.js` | DSH Web Slot、统一任务/作品区、泡泡和提示词入口 | 有 |
58
59
 
59
- 完整规则见 [Core Runtime v0](CORE_RUNTIME_CONTRACT.md)、[Artifact Manifest v0](ARTIFACT_MANIFEST.md)、[Provider Adapter v0](PROVIDER_ADAPTER_CONTRACT.md) 与 [DSH → Core 渐进迁移](DSH_CORE_MIGRATION.md)。这些是 rc 候选内部契约,不是第三方 SDK 兼容承诺。
60
+ 完整规则见 [Core Runtime v0](CORE_RUNTIME_CONTRACT.md)、[Artifact Manifest v0](ARTIFACT_MANIFEST.md)、[Provider Adapter v0](PROVIDER_ADAPTER_CONTRACT.md) 与 [DSH → Core 渐进迁移](DSH_CORE_MIGRATION.md)。这些仍是内部契约,不是第三方 SDK 兼容承诺。
60
61
 
61
62
  ## Task、Attempt 与 Artifact
62
63
 
@@ -65,23 +66,29 @@ DSH Host / Cordis Headless CLI
65
66
  - 生成结果与本地交付是两类事实。远端成功后下载失败仍保持 `outcome=succeeded / deliveryState=failed`,可显式重新交付而不重新生成。
66
67
  - 新 Attempt 可选保存阶段时间戳与模型选择来源;旧记录缺少这些字段仍可读取。
67
68
  - Artifact Manifest v0 保存内容哈希、媒体类型、中性元数据和关系边;不保存 Prompt、API Key、签名 URL、Provider 端点或宿主绝对路径。
68
- - Core Task/Artifact 的删除与清理能力在 0.2.0 中保持封闭:DSH 与 CLI 都不提供 delete、clear 或 orphan purge。Doctor 只报告,不自动修复。
69
+ - Core/CLI 已提供默认只读预览、明确选择与确认、文件隔离和恢复的删除/清理 Command,保护活跃 Task 和引用;索引更新只使用已提交记录,不升级或接回孤儿。DSH 作品区共用删除预览、隔离与事务恢复,永久 purge 未开放。Doctor 只报告,不自动修复。见 [CLI 管理与补齐](CLI_MANAGEMENT.md)。
69
70
 
70
- 0.1.4 的 `tasks.json`、`artifacts.json`、`outputs/` 与上传目录继续作为 legacy 数据存在;Core 使用隔离的 `core-v0`。已迁移的新图片、视频、TTS 与转写任务不双写 legacy,回退到 0.1.4 时也不删除或改写 Core 数据。
71
+ 0.1.4 的 `tasks.json`、`artifacts.json`、`outputs/` 与上传目录继续作为 legacy 数据存在;Core 使用隔离的 `core-v0`。已迁移的新图片、视频、S2V、TTS 与转写任务不双写 legacy,回退到 0.1.4 时也不删除或改写 Core 数据。
71
72
 
72
73
  ## Provider 边界
73
74
 
74
75
  Provider Adapter v0 固定 discovery、submit、poll、cancel、download 与 error mapping 六类生命周期操作;提交前的临时输入上传是可选准备能力,不自动成为 Core 事实。协议实现只接收显式凭据、模型和媒体端点,Core 保存复合模型身份与非敏感 binding,不保存端点原文。
75
76
 
76
- Core Runner 不拥有后台 timer。Headless `task observe` 每次最多 poll 一次;DSH 的有界自动观察属于 Host,插件释放时停止 timer 并中止在途观察,但不会把“停止本地观察”写成“远端已取消”。端点或协议 binding 漂移时恢复在联网前拒绝。
77
+ Core Runner 不拥有后台 timer。Headless `task observe` 每次最多 poll 一次,`task wait` 仅在本次命令内有界观察;DSH 的有界自动观察属于 Host,插件释放时停止 timer 并中止在途观察,但不会把“停止本地观察”写成“远端已取消”。端点或协议 binding 漂移时恢复在联网前拒绝。
77
78
 
78
79
  每个操作必须明确声明 supported 或 unsupported。当前 DashScope 与 OpenAI Images 媒体协议没有经过验证的远端取消实现,因此取消请求如实返回 not-supported,而不是伪造 canceled。完整受理与错误规则见 [Provider 提交契约 v0](PROVIDER_SUBMISSION_CONTRACT.md)。
79
80
 
80
- ## Text/Vision 共享边界(M1 已实现)
81
+ ## Text/Vision 共享边界
81
82
 
82
- [Text/Vision Model Port v0](MODEL_PORT_CONTRACT.md) 的纯契约、调用控制与 test-only Fake Port 已实现,尚未接入真实消费者。它以 `describe()` 和单轮 `complete()` 统一文本/视觉输入、正常终态、预算、取消与安全错误;图片以字节和 MIME 进入共享层,模型选择、会话与附件桥接保留在入口/适配器。
83
+ [Text/Vision Model Port v0](MODEL_PORT_CONTRACT.md) 的纯契约、调用控制与 test-only Fake Port 已发布于 0.2.0。它以 `describe()` 和单轮 `complete()` 统一文本/视觉输入、正常终态、预算、取消与安全错误;图片以字节和 MIME 进入共享层,模型选择、会话与附件桥接保留在入口/适配器。
83
84
 
84
- 提示词优化与视觉理解不创建媒体生成 Task,不进入 Provider Task Runner。提示词优化的功能定位与输入隔离正在讨论,其接入待后续确定;look/relook、定位、OCR 与摘要仍按独立切片迁移。现有 Host 方法、配置与 CLI 均未因 M1 改变;自持文本目录和独立入口需要另行审查。
85
+ 0.2.1通过 `prompt-optimizer-core.js` → `model-invoker.js` → `dsh-text-model-adapter.js` 接入首个提示词消费者;`prompt-optimizer.js` 继续拥有 v1 配置、模型选择、reasoning 策略和 Host 投影。共享核心不读取 Host/配置/Store,不取得 writer,也不创建媒体 Task/Attempt/Artifact。规则与预览见 [提示词优化系统](PROMPT_OPTIMIZER.md)。
86
+
87
+ 单图视觉将 Agent/工作台 look/relook 和显式视觉实测接入 `vision-core.js` 与 HTTP/DSH Vision Port:共享 120 秒整体预算、完整终态与候选策略,图片桥接核验原图与实际附件,接受合法宿主规范化。DSH 附件保存属于宿主副作用,不是 Core Artifact;rc.2 保存不可取消的限制如实保留。见 [单图视觉调用](VISION_MODEL.md)。视觉 CLI 复用这些业务;可选 `visionInput` 在入口层控制发送副本。未新增公共 SDK export,自持文本目录和独立提示词入口另行安排。
88
+
89
+ OCR 切片通过 `ocr-model-routing.js` 在入口解引用前建立整体 operation,`ocr.js` 仅接收图片字节和模型端口,使用现有 Sharp 做有限分块;全部块复用 `completeVision`,没有每块重置计时或调用额度。普通块失败可以产生明确部分结果,取消/超时终止整次操作;不取得 Core writer 或写入 Task/Artifact。见 [长图 OCR](OCR_MODEL.md)。
90
+
91
+ 定位、拼图摘要与生成后自述复用 `runVisionOperation`,`locate.js` / `summarize.js` 消费共享端口;入口处理附件、抽帧和可选 Core 转写,摘要读取已成功转写的文本 Artifact。纯视觉业务不写 Task/Artifact。legacy 视觉兼容实现保留,现有入口不再调用。见 [复合视觉调用](COMPOSITE_VISION.md)。
85
92
 
86
93
  ## Host Port 与 Headless 边界
87
94
 
@@ -90,7 +97,7 @@ Core Runner 不拥有后台 timer。Headless `task observe` 每次最多 poll
90
97
  | 工具注册与 Fiber 生命周期 | `ctx.tools` / `ctx.effect` | 不注册 DSH 工具;CLI/Core 不受影响 |
91
98
  | Web 路由 | `webServer` 或 `httpServer` | 无工作台 API 和同源媒体路由;CLI 直接导出 Artifact |
92
99
  | UI Slot | DSH Web Client | 不影响 Core Task、Artifact 与离线 Doctor |
93
- | Browser | DSH browser service | HTML 渲染不可用;本地媒体命令不受影响 |
100
+ | Browser | DSH browser service | CLI 可显式提供独立 Chromium Browser Port;未提供时 HTML 不可用 |
94
101
  | 会话附件与输入草稿 | session/input ports | CLI 使用显式文件或 JSON 输入,不创建会话对象 |
95
102
  | Skills registry | DSH skills service | 随包 Skill 不自动注册;CLI/Core 仍可用 |
96
103
 
@@ -98,14 +105,14 @@ Command、Core Store、Provider Runner 和 Headless CLI 不接收原始 `ctx`、
98
105
 
99
106
  ## 事件、UI 与媒体读取
100
107
 
101
- DSH 工作台把 Core 与 legacy Task 合并到一个用户任务区,把 Core 图片与旧 `outputs/` 合并到一个作品区。普通投影只有五类用户状态;原始 Attempt 事实留在高级诊断。刷新、展开、关闭、读取作品和查询状态都不得触发 submit、poll 或隐式修复。
108
+ DSH 工作台把 Core 与 legacy Task 合并到一个用户任务区,把 Core 与旧 `outputs/` 的图片、视频、音频和文本合并到一个作品区。普通投影只有五类用户状态;原始 Attempt 事实留在高级诊断。刷新、展开、关闭、读取作品和查询状态都不得触发 submit、poll 或隐式修复。
102
109
 
103
110
  Core 媒体路由按随机 Artifact ID 读取并逐次验证 SHA-256;它没有独立 token。浏览器请求还受可信 Host、`Origin`/`Sec-Fetch-Site` 和 `Referrer-Policy: no-referrer` 约束。Artifact ID 是持有者标识,不等于多租户认证;Iris 与 DSH 仍处于同一本机信任域。
104
111
 
105
112
  ## 非目标与发布边界
106
113
 
107
- - `0.2.0` 不启动独立常驻服务,也不复制 DSH 聊天界面。
114
+ - `0.2.1` 不启动独立常驻服务,也不复制 DSH 聊天界面。
108
115
  - 不迁移到 SQLite/JSONL,不原地改写 0.1.4 数据,不自动接管不确定的写者租约。
109
- - 不在本候选新增 Gemini、Fal、Replicate 或本地模型协议;注册表只是后续接入点。
116
+ - 0.2.1 未新增原生 Gemini、Fal、Replicate 或本地模型协议;注册表只是后续接入点。
110
117
  - 不把离线测试、一次 canary 或 Headless 可运行等同于公开 Core API 已冻结。
111
- - rc.1 仍须通过最终包审计、仓库外安装、跨平台 CI、Android 目视、回退和机器可读验证摘要后才能发布。
118
+ - 发布检查覆盖版本元数据、实际包审计、仓库外安装与跨平台 CI;浏览器/移动端和真实异步重启范围单独记录,不由程序化检查推定通过。
@@ -1,12 +1,12 @@
1
- # Iris 作品库 v0
1
+ # Iris legacy 作品库 v0
2
2
 
3
- 状态:v0.1.4 候选实现。
3
+ 状态:自 v0.1.4 实现,Iris 0.2.1 保留兼容。本文只描述旧 `outputs/` 与最小索引;当前工作台同时展示 Core Artifact,分页、过滤、批量下载与可恢复隔离见 [工作台作品管理](WORKBENCH_ARTIFACTS.md)。
4
4
 
5
5
  作品库解决“清除任务历史后仍能看到本地作品”的问题。任务记录描述执行过程;作品记录只负责本地媒体的发现、展示和访问。两者生命周期彼此独立。
6
6
 
7
7
  ## 用户语义
8
8
 
9
- - 新生成并成功落盘的图片、视频和音频会自动进入作品库。
9
+ - 旧链路生成并成功落盘的图片、视频和音频会进入此索引;0.2.1 的 Core 媒体链路使用独立 Manifest,并由统一作品库展示。
10
10
  - 首次升级到带作品库的版本时,Iris 会扫描现有 `outputs/` 并建立最小索引。
11
11
  - “清空已完成”或“清理 7 天前”只删除终态任务记录,作品仍可从工作台和泡泡打开。
12
12
  - “找回本地作品”会重新扫描 `outputs/`,补录受支持的普通媒体文件,并移除文件已经不存在的失效索引。
@@ -19,6 +19,6 @@
19
19
 
20
20
  作品链接继续复用 `/iris/media`,使用 128-bit 随机能力令牌并只从索引解析文件。错误 ID、令牌、文件名或路径穿越输入统一返回 404。作品库不是备份:删除作品文件、删除整个 Iris 数据目录或存储损坏仍可能造成数据丢失,重要作品应另行备份。
21
21
 
22
- ## 与未来 Artifact Manifest 的边界
22
+ ## 与 Core Artifact Manifest 的边界
23
23
 
24
- v0 不是完整 Artifact Manifest:没有内容哈希、去重、旁车数据、来源/派生关系、引用图或跨宿主导出契约。完整 Manifest 完成崩溃一致性和安全评审后,再承载收藏、标签、搜索、筛选和批量导出等能力。
24
+ legacy v0 没有内容哈希、去重、旁车数据、来源/派生关系、引用图或跨宿主导出契约。[Artifact Manifest v0](ARTIFACT_MANIFEST.md) 已在 0.2.0 实现,Core 保存 SHA-256、关系边和可重建索引,不原地改写旧作品。0.2.1 的统一工作台已提供分页、过滤、显式选择与批量 ZIP 下载;收藏、标签和搜索仍属后续计划。旧文件的永久删除与 Core 的可恢复隔离具有不同语义。