@mokuyoaxis/dsh-iris 0.1.3 → 0.2.0

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 (91) hide show
  1. package/.dsh/skills/iris-compose-media/SKILL.md +23 -49
  2. package/.dsh/skills/iris-compose-media/references/task-v2-recovery.md +30 -0
  3. package/.dsh/skills/iris-compose-media/references/workflows.md +38 -0
  4. package/.dsh/skills/iris-verify-ui/SKILL.md +3 -25
  5. package/.dsh/skills/iris-verify-ui/agents/openai.yaml +4 -0
  6. package/.dsh/skills/iris-verify-ui/references/verdict-and-degradation.md +31 -0
  7. package/CHANGELOG.md +77 -2
  8. package/README.en.md +98 -19
  9. package/README.md +98 -19
  10. package/bin/dsh-iris.js +392 -13
  11. package/docs/AGENT_QUICKSTART.md +35 -0
  12. package/docs/ARCHITECTURE.md +82 -52
  13. package/docs/ARTIFACT_LIBRARY.md +24 -0
  14. package/docs/ARTIFACT_MANIFEST.md +60 -0
  15. package/docs/CORE_RUNTIME_CONTRACT.md +83 -0
  16. package/docs/DSH_CORE_MIGRATION.md +106 -0
  17. package/docs/DSH_RC2_ADAPTATION.md +36 -0
  18. package/docs/HEADLESS_CLI.md +217 -0
  19. package/docs/HOST_ADAPTER_CONTRACT.md +105 -0
  20. package/docs/HOST_DOCTOR.md +49 -0
  21. package/docs/MODEL_PORT_CONTRACT.md +300 -0
  22. package/docs/PROVIDER_ADAPTER_CONTRACT.md +138 -0
  23. package/docs/PROVIDER_HEALTH.md +39 -0
  24. package/docs/PROVIDER_RUNTIME_HARNESS.md +33 -0
  25. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +2 -2
  26. package/docs/ROADMAP.md +42 -76
  27. package/docs/SECURITY.md +10 -5
  28. package/docs/TASK_SEMANTICS.md +3 -2
  29. package/docs/assets/diagrams/iris-architecture.png +0 -0
  30. package/docs/assets/diagrams/iris-architecture.svg +5 -5
  31. package/docs/assets/logo/README.md +55 -0
  32. package/docs/assets/logo/iris-bubble-concept.svg +67 -0
  33. package/docs/assets/logo/iris-bubble.svg +14 -0
  34. package/docs/assets/logo/iris-interim-flower.png +0 -0
  35. package/docs/assets/logo/iris-logo-mono-inverse.svg +78 -0
  36. package/docs/assets/logo/iris-logo-mono.svg +78 -0
  37. package/docs/assets/logo/iris-logo.svg +78 -0
  38. package/docs/assets/logo/iris-mark-mono-inverse.svg +42 -0
  39. package/docs/assets/logo/iris-mark-mono.svg +42 -0
  40. package/docs/assets/logo/iris-mark.svg +42 -0
  41. package/docs/releases/0.1.4.md +59 -0
  42. package/docs/releases/0.2.0.md +48 -0
  43. package/lib/actions.js +673 -525
  44. package/lib/adapters.js +58 -21
  45. package/lib/api.js +182 -16
  46. package/lib/artifacts.js +239 -0
  47. package/lib/bundled-skills.js +5 -5
  48. package/lib/client.js +1116 -86
  49. package/lib/command-service.js +420 -0
  50. package/lib/config.js +245 -49
  51. package/lib/core-artifact-store.js +598 -0
  52. package/lib/core-artifacts.js +4 -0
  53. package/lib/core-attention.js +103 -0
  54. package/lib/core-contract.js +105 -0
  55. package/lib/core-doctor.js +41 -0
  56. package/lib/core-lease-recovery.js +170 -0
  57. package/lib/core-runtime.js +232 -0
  58. package/lib/core-tasks.js +560 -0
  59. package/lib/core-user-projection.js +192 -0
  60. package/lib/doctor.js +260 -30
  61. package/lib/dsh-core-adapter.js +599 -0
  62. package/lib/dsh-host-adapter.js +240 -0
  63. package/lib/generation-input.js +95 -0
  64. package/lib/guard.js +3 -3
  65. package/lib/host-contract.js +131 -0
  66. package/lib/host-runtime.js +156 -0
  67. package/lib/index.js +255 -166
  68. package/lib/locate.js +1 -1
  69. package/lib/media.js +16 -6
  70. package/lib/model-invoker.js +218 -0
  71. package/lib/model-port-contract.js +333 -0
  72. package/lib/models.js +97 -11
  73. package/lib/prompt-optimizer.js +10 -20
  74. package/lib/provider-adapter.js +397 -0
  75. package/lib/provider-adapters.js +342 -0
  76. package/lib/provider-catalog.js +171 -0
  77. package/lib/provider-contract.js +72 -3
  78. package/lib/provider-health.js +59 -0
  79. package/lib/provider-protocol.js +51 -2
  80. package/lib/provider-task-runner.js +328 -0
  81. package/lib/render.js +43 -12
  82. package/lib/tasks.js +45 -9
  83. package/lib/vision.js +51 -56
  84. package/package.json +20 -14
  85. package/user_guide.md +67 -12
  86. package/docs/assets/diagrams/iris-architecture.drawio +0 -1
  87. package/docs/assets/diagrams/iris-task-lifecycle.drawio +0 -1
  88. package/docs/assets/diagrams/iris-workflow-compose.drawio +0 -1
  89. package/docs/releases/0.1.1.md +0 -51
  90. package/docs/releases/0.1.2.md +0 -26
  91. package/docs/releases/0.1.3.md +0 -30
package/bin/dsh-iris.js CHANGED
@@ -1,23 +1,402 @@
1
1
  #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import { createCommandService } from '../lib/command-service.js';
4
+ import { createCoreRuntime } from '../lib/core-runtime.js';
5
+ import { recoverCoreWriterLease } from '../lib/core-lease-recovery.js';
2
6
  import { doctor, formatDoctorReport } from '../lib/doctor.js';
7
+ import { loadProviderCatalog, providerCatalogSnapshot, catalogCapabilitySnapshot, imageCandidatesFromCatalog, providerForTaskFromCatalog, providerTaskBinding, transcribeCandidatesFromCatalog, ttsCandidatesFromCatalog, videoCandidatesFromCatalog } from '../lib/provider-catalog.js';
8
+ import { createConfiguredProviderAdapter } from '../lib/provider-adapters.js';
9
+ import { prepareProviderInput } from '../lib/provider-adapter.js';
10
+ import { createProviderTaskRunner } from '../lib/provider-task-runner.js';
11
+ import { GenerationInputError, normalizeGenerationInput } from '../lib/generation-input.js';
12
+
13
+ class CliUsageError extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = 'CliUsageError';
17
+ this.code = 'IRIS_CLI_USAGE';
18
+ }
19
+ }
3
20
 
4
21
  function usage() {
5
22
  return [
6
- 'Usage: dsh-iris doctor [--json]',
23
+ 'Usage:',
24
+ ' dsh-iris doctor [--data-root <absolute-path>] [--json]',
25
+ ' dsh-iris runtime recover --data-root <absolute-path> --confirm-stale-pid <pid>',
26
+ ' dsh-iris providers list --provider-config <absolute-path>',
27
+ ' dsh-iris capabilities list --provider-config <absolute-path>',
28
+ ' dsh-iris run crop --data-root <absolute-path> --input <json>',
29
+ ' dsh-iris media diff --data-root <absolute-path> --input <json>',
30
+ ' dsh-iris media frames --data-root <absolute-path> --input <json>',
31
+ ' dsh-iris run image --data-root <absolute-path> --provider-config <absolute-path> --input <json>',
32
+ ' dsh-iris run video --data-root <absolute-path> --provider-config <absolute-path> --input <json>',
33
+ ' dsh-iris run tts --data-root <absolute-path> --provider-config <absolute-path> --input <json>',
34
+ ' dsh-iris run transcribe --data-root <absolute-path> --provider-config <absolute-path> --input <json>',
35
+ ' dsh-iris task inspect <id> --data-root <absolute-path>',
36
+ ' dsh-iris task list --data-root <absolute-path>',
37
+ ' dsh-iris task observe <id> --data-root <absolute-path> --provider-config <absolute-path>',
38
+ ' dsh-iris task redeliver <id> --data-root <absolute-path> --provider-config <absolute-path>',
39
+ ' dsh-iris task cancel <id> --data-root <absolute-path> --provider-config <absolute-path>',
40
+ ' dsh-iris task retry <id> --data-root <absolute-path> --provider-config <absolute-path> --input <json> --confirm-billing [--model-ref <providerId::modelId>]',
41
+ ' dsh-iris artifact inspect <id> --data-root <absolute-path>',
42
+ ' dsh-iris artifact list --data-root <absolute-path>',
43
+ ' dsh-iris artifact export <id> --data-root <absolute-path> --output <absolute-path>',
44
+ ' dsh-iris artifact rebuild --data-root <absolute-path>',
7
45
  '',
8
- 'Runs offline diagnostics only. It does not start DSH or send provider requests.'
46
+ '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
47
  ].join('\n');
10
48
  }
11
49
 
12
- const args = process.argv.slice(2);
13
- if (args.includes('--help') || args.includes('-h')) {
14
- console.log(usage());
15
- process.exitCode = 0;
16
- } else if (args[0] !== 'doctor' || args.some((arg, index) => index > 0 && arg !== '--json')) {
17
- console.error(usage());
18
- process.exitCode = 2;
19
- } else {
20
- const report = await doctor();
21
- console.log(args.includes('--json') ? JSON.stringify(report, null, 2) : formatDoctorReport(report));
22
- process.exitCode = report.exitCode;
50
+ function flags(args, allowed) {
51
+ const parsed = {};
52
+ for (let index = 0; index < args.length; index += 2) {
53
+ const flag = args[index];
54
+ 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('--')) {
56
+ throw new CliUsageError('参数无效:' + String(flag || ''));
57
+ }
58
+ if (Object.prototype.hasOwnProperty.call(parsed, name)) throw new CliUsageError('参数重复:--' + name);
59
+ parsed[name] = args[index + 1];
60
+ }
61
+ return parsed;
62
+ }
63
+
64
+ function required(value, flag) {
65
+ if (!String(value || '').trim()) throw new CliUsageError('缺少参数:--' + flag);
66
+ return String(value);
67
+ }
68
+
69
+ function jsonInput(value) {
70
+ try {
71
+ const parsed = JSON.parse(required(value, 'input'));
72
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('not object');
73
+ return parsed;
74
+ } catch (error) {
75
+ if (error instanceof CliUsageError) throw error;
76
+ throw new CliUsageError('--input 必须是 JSON 对象');
77
+ }
78
+ }
79
+
80
+ function generationInput(capability, input, options) {
81
+ try { return normalizeGenerationInput(capability, input, options); }
82
+ catch (error) {
83
+ if (error instanceof GenerationInputError) throw new CliUsageError(error.message);
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ /** C-8: catchable signals dispose active runtimes before the CLI exits. */
89
+ const activeRuntimes = new Set();
90
+ let shuttingDown = false;
91
+
92
+ function registerRuntime(runtime) {
93
+ activeRuntimes.add(runtime);
94
+ return () => activeRuntimes.delete(runtime);
95
+ }
96
+
97
+ async function disposeActiveRuntimes() {
98
+ await Promise.allSettled([...activeRuntimes].map((runtime) => runtime.dispose().catch(() => {})));
99
+ activeRuntimes.clear();
100
+ }
101
+
102
+ for (const signal of ['SIGINT', 'SIGTERM']) {
103
+ process.on(signal, () => {
104
+ const exitCode = signal === 'SIGINT' ? 130 : 143;
105
+ if (shuttingDown) process.exit(exitCode);
106
+ shuttingDown = true;
107
+ disposeActiveRuntimes()
108
+ .catch(() => {})
109
+ .finally(() => process.exit(exitCode));
110
+ });
111
+ }
112
+
113
+ async function withRawRuntime(mode, dataRoot, callback) {
114
+ const runtime = createCoreRuntime({ dataRoot: required(dataRoot, 'data-root'), mode });
115
+ const unregister = registerRuntime(runtime);
116
+ try {
117
+ runtime.start();
118
+ return await callback(runtime);
119
+ } finally {
120
+ try { await runtime.dispose(); } finally { unregister(); }
121
+ }
122
+ }
123
+
124
+ async function withRuntime(mode, dataRoot, callback, ports = {}) {
125
+ return withRawRuntime(mode, dataRoot, (runtime) => callback(createCommandService(runtime, ports)));
126
+ }
127
+
128
+ async function main(args) {
129
+ if (!args.length || args.includes('--help') || args.includes('-h')) {
130
+ console.log(usage());
131
+ return args.length ? 0 : 2;
132
+ }
133
+
134
+ if (args[0] === 'doctor') {
135
+ const rest = args.slice(1);
136
+ if (rest.filter((arg) => arg === '--json').length > 1) throw new CliUsageError('参数重复:--json');
137
+ const options = flags(rest.filter((arg) => arg !== '--json'), ['data-root']);
138
+ const report = await doctor({ ...(options['data-root'] ? { dataRoot: options['data-root'] } : {}) });
139
+ console.log(rest.includes('--json') ? JSON.stringify(report, null, 2) : formatDoctorReport(report));
140
+ return report.exitCode;
141
+ }
142
+
143
+ if (args[0] === 'runtime' && args[1] === 'recover') {
144
+ const options = flags(args.slice(2), ['data-root', 'confirm-stale-pid']);
145
+ const confirmStalePid = Number(required(options['confirm-stale-pid'], 'confirm-stale-pid'));
146
+ if (!Number.isSafeInteger(confirmStalePid) || confirmStalePid <= 0) {
147
+ throw new CliUsageError('--confirm-stale-pid 必须是 Doctor 报告中的正整数 PID');
148
+ }
149
+ const result = recoverCoreWriterLease(required(options['data-root'], 'data-root'), { confirmStalePid });
150
+ console.log(JSON.stringify(result, null, 2));
151
+ return 0;
152
+ }
153
+
154
+ if (['providers', 'capabilities'].includes(args[0]) && args[1] === 'list') {
155
+ const options = flags(args.slice(2), ['provider-config']);
156
+ const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
157
+ const report = args[0] === 'providers' ? providerCatalogSnapshot(catalog) : catalogCapabilitySnapshot(catalog);
158
+ console.log(JSON.stringify(report, null, 2));
159
+ return 0;
160
+ }
161
+
162
+ if (args[0] === 'media' && ['diff', 'frames'].includes(args[1])) {
163
+ const options = flags(args.slice(2), ['data-root', 'input']);
164
+ const input = jsonInput(options.input);
165
+ const command = args[1] === 'diff' ? 'media.diff' : 'media.frames';
166
+ const result = await withRuntime('writer', options['data-root'], (commands) =>
167
+ commands.execute(command, input));
168
+ console.log(JSON.stringify(result, null, 2));
169
+ return 0;
170
+ }
171
+
172
+ if (args[0] === 'run' && args[1] === 'crop') {
173
+ const options = flags(args.slice(2), ['data-root', 'input']);
174
+ const input = jsonInput(options.input);
175
+ const result = await withRuntime('writer', options['data-root'], (commands) =>
176
+ commands.execute('crop', input));
177
+ console.log(JSON.stringify(result, null, 2));
178
+ return 0;
179
+ }
180
+
181
+ if (args[0] === 'run' && args[1] === 'image') {
182
+ const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
183
+ const { providerInput, modelRef } = generationInput('image', jsonInput(options.input));
184
+ const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
185
+ const routes = imageCandidatesFromCatalog(catalog, modelRef);
186
+ await withRawRuntime('writer', options['data-root'], async (runtime) => {
187
+ const candidates = routes.map((route) => ({
188
+ adapter: createConfiguredProviderAdapter(route.provider), model: route.modelRef,
189
+ selectionReason: route.selectionReason,
190
+ providerBinding: providerTaskBinding(route.provider)
191
+ }));
192
+ const result = await createProviderTaskRunner(runtime).submit({
193
+ capability: 'image', candidates,
194
+ providerInput
195
+ });
196
+ console.log(JSON.stringify(result, null, 2));
197
+ });
198
+ return 0;
199
+ }
200
+
201
+ // 视频输入冻结(E 阶段 Profile):t2v/i2v 的 prompt/img_data_url/size/duration;
202
+ // s2v 上传流程不在 headless 面开放。
203
+ if (args[0] === 'run' && args[1] === 'video') {
204
+ const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
205
+ const { providerInput, modelRef } = generationInput('video', jsonInput(options.input));
206
+ const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
207
+ const routes = videoCandidatesFromCatalog(catalog, modelRef);
208
+ await withRawRuntime('writer', options['data-root'], async (runtime) => {
209
+ const candidates = routes.map((route) => ({
210
+ adapter: createConfiguredProviderAdapter(route.provider), model: route.modelRef,
211
+ selectionReason: route.selectionReason,
212
+ providerBinding: providerTaskBinding(route.provider)
213
+ }));
214
+ const result = await createProviderTaskRunner(runtime).submit({
215
+ capability: 'video', candidates,
216
+ providerInput
217
+ });
218
+ console.log(JSON.stringify(result, null, 2));
219
+ });
220
+ return 0;
221
+ }
222
+
223
+ // 语音合成输入冻结(E2 Profile):同步完成型,一次命令内完成 Task → Artifact。
224
+ if (args[0] === 'run' && args[1] === 'tts') {
225
+ const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
226
+ const { providerInput, modelRef } = generationInput('tts', jsonInput(options.input));
227
+ const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
228
+ const routes = ttsCandidatesFromCatalog(catalog, modelRef);
229
+ await withRawRuntime('writer', options['data-root'], async (runtime) => {
230
+ const candidates = routes.map((route) => ({
231
+ adapter: createConfiguredProviderAdapter(route.provider), model: route.modelRef,
232
+ selectionReason: route.selectionReason,
233
+ providerBinding: providerTaskBinding(route.provider)
234
+ }));
235
+ const result = await createProviderTaskRunner(runtime).submit({
236
+ capability: 'tts', candidates,
237
+ providerInput
238
+ });
239
+ console.log(JSON.stringify(result, null, 2));
240
+ });
241
+ return 0;
242
+ }
243
+
244
+ // 转写输入冻结(E3 Profile):上传型异步——audio_url(公网/oss://)或
245
+ // audio_path(本地文件,经首选候选 Provider 的临时存储上传,签名 URL 不落 Core)。
246
+ if (args[0] === 'run' && args[1] === 'transcribe') {
247
+ const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
248
+ const normalized = generationInput('transcribe', jsonInput(options.input), { allowAudioPath: true });
249
+ const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
250
+ const routes = transcribeCandidatesFromCatalog(catalog, normalized.modelRef);
251
+ let providerInput = normalized.providerInput;
252
+ if (normalized.audioPath) {
253
+ if (!fs.existsSync(normalized.audioPath)) throw new CliUsageError('音频文件不存在');
254
+ const prepared = await prepareProviderInput(createConfiguredProviderAdapter(routes[0].provider), {
255
+ model: routes[0].model, filePath: normalized.audioPath
256
+ });
257
+ providerInput = generationInput('transcribe', { audio_url: prepared.url }).providerInput;
258
+ }
259
+ await withRawRuntime('writer', options['data-root'], async (runtime) => {
260
+ const candidates = routes.map((route) => ({
261
+ adapter: createConfiguredProviderAdapter(route.provider), model: route.modelRef,
262
+ selectionReason: route.selectionReason,
263
+ providerBinding: providerTaskBinding(route.provider)
264
+ }));
265
+ const result = await createProviderTaskRunner(runtime).submit({
266
+ capability: 'transcribe', candidates,
267
+ providerInput
268
+ });
269
+ console.log(JSON.stringify(result, null, 2));
270
+ });
271
+ return 0;
272
+ }
273
+
274
+ if (args[0] === 'task' && args[1] === 'observe' && args[2]) {
275
+ const options = flags(args.slice(3), ['data-root', 'provider-config']);
276
+ const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
277
+ await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
278
+ const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.observe', { task_id: args[2] }), {
279
+ resolveTaskAdapter: (task) => createConfiguredProviderAdapter(providerForTaskFromCatalog(catalog, task))
280
+ });
281
+ console.log(JSON.stringify(result, null, 2));
282
+ return 0;
283
+ }
284
+
285
+ if (args[0] === 'task' && args[1] === 'redeliver' && args[2]) {
286
+ const options = flags(args.slice(3), ['data-root', 'provider-config']);
287
+ const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
288
+ await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
289
+ const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.redeliver', { task_id: args[2] }), {
290
+ resolveTaskAdapter: (task) => createConfiguredProviderAdapter(providerForTaskFromCatalog(catalog, task))
291
+ });
292
+ console.log(JSON.stringify(result, null, 2));
293
+ return 0;
294
+ }
295
+
296
+ if (args[0] === 'task' && args[1] === 'cancel' && args[2]) {
297
+ const options = flags(args.slice(3), ['data-root', 'provider-config']);
298
+ const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
299
+ await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
300
+ const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.cancel', { task_id: args[2] }), {
301
+ resolveTaskAdapter: (task) => createConfiguredProviderAdapter(providerForTaskFromCatalog(catalog, task))
302
+ });
303
+ console.log(JSON.stringify(result, null, 2));
304
+ return 0;
305
+ }
306
+
307
+ if (args[0] === 'task' && args[1] === 'retry' && args[2]) {
308
+ const rest = args.slice(3);
309
+ const confirmBilling = rest.includes('--confirm-billing');
310
+ const options = flags(rest.filter((arg) => arg !== '--confirm-billing'),
311
+ ['data-root', 'provider-config', 'input', 'model-ref']);
312
+ const input = jsonInput(options.input);
313
+ const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
314
+ const dataRoot = required(options['data-root'], 'data-root');
315
+ await withRuntime('reader', dataRoot, (commands) => commands.execute('task.inspect', { task_id: args[2] }));
316
+ const result = await withRuntime('writer', dataRoot, (commands) => commands.execute('task.retry', {
317
+ task_id: args[2],
318
+ provider_input: input,
319
+ model_ref: options['model-ref'],
320
+ confirm_billing: confirmBilling
321
+ }), {
322
+ resolveTaskCandidates: ({ capability, modelRef }) => (capability === 'video'
323
+ ? videoCandidatesFromCatalog(catalog, modelRef || undefined)
324
+ : capability === 'tts'
325
+ ? ttsCandidatesFromCatalog(catalog, modelRef || undefined)
326
+ : capability === 'transcribe'
327
+ ? transcribeCandidatesFromCatalog(catalog, modelRef || undefined)
328
+ : imageCandidatesFromCatalog(catalog, modelRef || undefined)
329
+ ).map((route) => ({
330
+ adapter: createConfiguredProviderAdapter(route.provider),
331
+ model: route.modelRef,
332
+ selectionReason: route.selectionReason,
333
+ providerBinding: providerTaskBinding(route.provider)
334
+ }))
335
+ });
336
+ console.log(JSON.stringify(result, null, 2));
337
+ return 0;
338
+ }
339
+
340
+ if (args[0] === 'task' && args[1] === 'inspect' && args[2]) {
341
+ const options = flags(args.slice(3), ['data-root']);
342
+ const result = await withRuntime('reader', options['data-root'], (commands) =>
343
+ commands.execute('task.inspect', { task_id: args[2] }));
344
+ console.log(JSON.stringify(result, null, 2));
345
+ return 0;
346
+ }
347
+
348
+ if (args[0] === 'task' && args[1] === 'list') {
349
+ const options = flags(args.slice(2), ['data-root']);
350
+ const result = await withRuntime('reader', options['data-root'], (commands) =>
351
+ commands.execute('task.list', {}));
352
+ console.log(JSON.stringify(result, null, 2));
353
+ return 0;
354
+ }
355
+
356
+ if (args[0] === 'artifact' && args[1] === 'inspect' && args[2]) {
357
+ const options = flags(args.slice(3), ['data-root']);
358
+ const result = await withRuntime('reader', options['data-root'], (commands) =>
359
+ commands.execute('artifact.inspect', { artifact_id: args[2] }));
360
+ console.log(JSON.stringify(result, null, 2));
361
+ return 0;
362
+ }
363
+
364
+ if (args[0] === 'artifact' && args[1] === 'list') {
365
+ const options = flags(args.slice(2), ['data-root']);
366
+ const result = await withRuntime('reader', options['data-root'], (commands) =>
367
+ commands.execute('artifact.list', {}));
368
+ console.log(JSON.stringify(result, null, 2));
369
+ return 0;
370
+ }
371
+
372
+ if (args[0] === 'artifact' && args[1] === 'rebuild') {
373
+ const options = flags(args.slice(2), ['data-root']);
374
+ const result = await withRuntime('writer', options['data-root'], (commands) =>
375
+ commands.execute('artifact.rebuild', {}));
376
+ console.log(JSON.stringify(result, null, 2));
377
+ return 0;
378
+ }
379
+
380
+ if (args[0] === 'artifact' && args[1] === 'export' && args[2]) {
381
+ const options = flags(args.slice(3), ['data-root', 'output']);
382
+ const result = await withRuntime('writer', options['data-root'], (commands) =>
383
+ commands.execute('artifact.export', {
384
+ artifact_id: args[2],
385
+ output_path: required(options.output, 'output')
386
+ }));
387
+ console.log(JSON.stringify(result, null, 2));
388
+ return 0;
389
+ }
390
+
391
+ throw new CliUsageError('未知命令');
392
+ }
393
+
394
+ try {
395
+ process.exitCode = await main(process.argv.slice(2));
396
+ } catch (error) {
397
+ const code = error && error.code || 'IRIS_CLI_FAILED';
398
+ const message = error && error.message || '命令执行失败';
399
+ console.error(code + ': ' + message);
400
+ if (error instanceof CliUsageError) console.error('\n' + usage());
401
+ process.exitCode = error instanceof CliUsageError ? 2 : 1;
23
402
  }
@@ -0,0 +1,35 @@
1
+ # 给 AI Agent 的简要 Prompt
2
+
3
+ 把下面一段交给负责安装或使用 Iris 的 Agent。版本与发布状态以当前 [README](../README.md) 为准;Iris 和 DSH 的版本号独立。
4
+
5
+ ## 中文
6
+
7
+ ```text
8
+ 请根据本仓库 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 >=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
+ - 其他 DSH 版本:先报告没有支持声明,不自行升级宿主或跳过版本检查。
13
+ 没有 DSH、希望用 CLI 时,按 docs/HEADLESS_CLI.md 使用 npm 0.2.0;npm 0.1.4 不含新的 Core 媒体 CLI。
14
+ 从 dsh-market 安装也要核对实际包名、版本和兼容范围;市场缓存不代表支持旧宿主,不对旧 DSH 安装 latest。
15
+ 若使用其他 profile,把示例中的 web 换成已确认的目标。保留完整 scoped 包名,不把 dsh-iris 当作同一 npm 包。
16
+ 安装后检查 Doctor 和工具可达性,再执行我指定的媒体任务;不要输出 API Key,重启宿主前说明影响。
17
+ 查询、观察和重新取回不等于重新生成;任务已受理或受理未知时,不自动重新提交。
18
+ 最后报告所用 Iris/DSH 版本、安装来源和实际验证结果。
19
+ ```
20
+
21
+ ## English
22
+
23
+ ```text
24
+ 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.
27
+ - 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
+ - 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.
30
+ 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
+ Replace web with the confirmed profile when needed. Keep the scoped package name; unscoped dsh-iris is a different npm package.
32
+ 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.
33
+ Inspect, observe, and redeliver do not create a new generation. Never resubmit automatically after accepted or unknown acceptance.
34
+ Report the Iris/DSH versions, installation source, and actual validation results.
35
+ ```
@@ -1,81 +1,111 @@
1
1
  # Iris 架构与边界
2
2
 
3
- 状态:v0.1.3 当前实现。本文描述已经存在的代码边界,不把 0.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。
4
4
 
5
5
  ## 当前形态
6
6
 
7
- Iris 目前以 DeepSeek Harness 插件运行。DSH 提供 Agent 工具注册、会话附件、浏览器和 Web UI 插槽;Iris 自己负责供应商配置、媒体动作、可信任务、产物落盘、媒体授权路由和工作台投影。
7
+ Iris 仍以一个 npm 包交付,但已有两个执行入口:DSH 插件负责 Agent 工具、会话附件、浏览器、Web UI 和后台观察;Headless CLI 在没有 DSH/Cordis 的进程中显式接收数据根与私有 Provider 配置。两者共用 Core Runtime、Provider Task Runner、Task/Artifact Store 和本地 Command,不能在同一数据根上同时充当写者。
8
8
 
9
9
  ```text
10
- DSH Host / Cordis
11
- ├─ Agent tools ───────────────┐
12
- ├─ Web slots / browser │
13
- └─ attachment/session ports │
14
- ▼
15
- actions.runAction
16
- ├─ provider adapters
17
- ├─ Task / Attempt v2
18
- ├─ media delivery
19
- └─ vision / local media tools
20
- │
21
- ▼
22
- $DSH_HOME/iris/v1
23
- providers.json · tasks.json
24
- outputs/ · uploads/
10
+ DSH Host / Cordis Headless CLI
11
+ ├─ Agent tools / Web UI ├─ explicit --data-root
12
+ ├─ attachment / browser ports └─ private provider config
13
+ └─ bounded observation timers │
14
+ │ │
15
+ dsh-host-adapter.js bin/dsh-iris.js
16
+ │ │
17
+ dsh-core-adapter.js ────────────────┬───────────┘
18
+ ▼
19
+ Core Runtime v0
20
+ explicit root · reader/writer
21
+ lease · AbortSignal · disposal
22
+ ┌────────────┴────────────┐
23
+ ▼ ▼
24
+ Command Service v0 Provider Task Runner
25
+ crop / diff / frames submit / observe /
26
+ inspect / control cancel / delivery
27
+ └────────────┬────────────┘
28
+ ▼
29
+ Core Task Store + Artifact Manifest
30
+ records · objects · SHA-256 · index
31
+ │
32
+ DSH: $DSH_HOME/iris/v1/core-v0
33
+ CLI: explicit absolute data root
25
34
  ```
26
35
 
27
- `lib/index.js` 是 DSH Host Adapter 的当前集中入口;`lib/client.js` 只消费同源 API 与 DSH UI Slot。`lib/api.js` 是工作台 HTTP 投影,不是真相来源。Task 的持久事实由 `lib/tasks.js` 管理。
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 投影,不是真相来源。
28
37
 
29
- ## 已稳定的核心候选
38
+ 未迁移能力仍走 0.1.4 legacy 链。当前主要包括 s2v 数字人视频、视觉理解、长图 OCR、媒体摘要、HTML 渲染和提示词优化;它们不会被伪装成 Core 能力。
39
+
40
+ ## 0.2.0 核心
30
41
 
31
42
  | 模块 | 职责 | DSH 运行时依赖 |
32
43
  |---|---|---:|
33
- | `task-semantics.js` | Task v2 枚举、旧状态与用户状态派生、自洽校验 | 无 |
34
- | `provider-contract.js` | 提交结果、受理边界、错误脱敏、候选调度 | 无 |
35
- | `tasks.js` | Task/Attempt 持久化、观察、恢复与交付状态 | 无;数据根仍沿用 `DSH_HOME` |
36
- | `doctor.js` | 零网络离线环境与数据诊断 | 无 |
37
- | `actions.js` | GUI 与 Agent 共用动作 | 有宿主上下文参数,部分动作依赖 Browser/会话能力 |
38
- | `index.js` | DSH 工具、路由、Skill 与生命周期注册 | 有 |
39
- | `client.js` | DSH Web Slot、工作台、泡泡和提示词入口 | 有 |
40
-
41
- 0.1.3 不声称已有完整独立 Core 或 Local Host Adapter。完整 Command Service、Host Port 接口和非 DSH 运行入口属于后续版本;本版先保证任务语义与离线 Doctor 不依赖 DSH 包即可执行。
44
+ | `core-contract.js` / `core-runtime.js` | 显式数据根、reader/writer 权限、单写者租约、取消与释放 | 无 |
45
+ | `command-service.js` | 本地媒体命令、Task 控制面和 Artifact 查询/导出 | 无 |
46
+ | `generation-input.js` | CLI、DSH 与重试共用的生成参数校验和 Provider 字段转换 | 无 |
47
+ | `model-port-contract.js` / `model-invoker.js` | Text/Vision 内部契约、有限调用预算与取消;真实消费者尚未接入 | 无 |
48
+ | `core-tasks.js` | Core Task/Attempt 持久事实、受理与交付状态 | 无 |
49
+ | `core-artifacts.js` / `core-artifact-store.js` | 对象、Manifest、SHA-256、关系边与可重建 Index | 无 |
50
+ | `provider-contract.js` / `provider-adapter.js` | canonical 结果、错误脱敏、受理边界与 Provider v0 操作 | 无 |
51
+ | `provider-adapters.js` | DashScope / OpenAI Images 协议实现和可选输入准备 | 无 |
52
+ | `provider-task-runner.js` | 写前 Attempt、submit/poll/cancel/download 与 Profile 化交付 | 无 |
53
+ | `core-doctor.js` / `core-lease-recovery.js` | 只读完整性诊断与需显式确认的陈旧租约恢复 | 无 |
54
+ | `provider-catalog.js` | 显式 Provider 配置、能力候选和原 Task Adapter 恢复 | 无 |
55
+ | `dsh-core-adapter.js` | DSH 数据根、后台观察与 Core 到 Host 的投影 | 有;DSH 专属边界 |
56
+ | `dsh-host-adapter.js` / `index.js` | Host 能力映射、工具/路由/Skill 与生命周期装配 | 有 |
57
+ | `client.js` | DSH Web Slot、统一任务/作品区、泡泡和提示词入口 | 有 |
58
+
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 兼容承诺。
42
60
 
43
61
  ## Task、Attempt 与 Artifact
44
62
 
45
- - 一个用户意图对应一个 Task;每个候选供应商调用对应一个稳定 Attempt。
46
- - Attempt 必须在可能发送计费请求前落盘。只有明确 `not_accepted` 才允许自动 failover。
47
- - 生成结果与本地交付是两类事实。生成成功后下载失败仍保持 `outcome=succeeded`。
48
- - 当前 Artifact 仍由任务中的文件、媒体令牌和附件记录表示;正式 Artifact Manifest、内容哈希和谱系属于 0.2.0。
49
- - 每次 v2 任务变更递增 Task `revision`;完整状态快照另带进程 `stateEpoch/stateRevision`,客户端拒绝同一 epoch 中迟到的旧快照。
63
+ - 一个用户意图对应一个 Core Task;每次候选供应商调用对应一个稳定 Attempt。Attempt 必须在可能发送计费请求前落盘。
64
+ - 只有明确 `not_accepted` 才允许自动切换候选;`accepted` 或 `unknown` 后禁止自动重提。重试会创建新 Task,并要求用户再次提供未持久化的输入和明确确认潜在费用。
65
+ - 生成结果与本地交付是两类事实。远端成功后下载失败仍保持 `outcome=succeeded / deliveryState=failed`,可显式重新交付而不重新生成。
66
+ - 新 Attempt 可选保存阶段时间戳与模型选择来源;旧记录缺少这些字段仍可读取。
67
+ - Artifact Manifest v0 保存内容哈希、媒体类型、中性元数据和关系边;不保存 Prompt、API Key、签名 URL、Provider 端点或宿主绝对路径。
68
+ - Core Task/Artifact 的删除与清理能力在 0.2.0 中保持封闭:DSH 与 CLI 都不提供 delete、clear 或 orphan purge。Doctor 只报告,不自动修复。
50
69
 
51
- 详细状态机见 [Task/Attempt v2 语义契约](TASK_SEMANTICS.md),故障证据见 [故障注入矩阵](FAULT_INJECTION.md)。
70
+ 0.1.4 的 `tasks.json`、`artifacts.json`、`outputs/` 与上传目录继续作为 legacy 数据存在;Core 使用隔离的 `core-v0`。已迁移的新图片、视频、TTS 与转写任务不双写 legacy,回退到 0.1.4 时也不删除或改写 Core 数据。
52
71
 
53
72
  ## Provider 边界
54
73
 
55
- 现有 DashScope 与 OpenAI Images 兼容分支仍位于 `adapters.js`,尚未宣称完整 Provider SDK。0.1.3 已冻结供应商提交所需的最小契约:复合模型身份、写前 Hook、四类提交结果、受理证据和安全错误。新增 Provider 不应绕过该契约直接在失败时切换候选。
74
+ Provider Adapter v0 固定 discovery、submit、poll、cancel、download 与 error mapping 六类生命周期操作;提交前的临时输入上传是可选准备能力,不自动成为 Core 事实。协议实现只接收显式凭据、模型和媒体端点,Core 保存复合模型身份与非敏感 binding,不保存端点原文。
75
+
76
+ Core Runner 不拥有后台 timer。Headless `task observe` 每次最多 poll 一次;DSH 的有界自动观察属于 Host,插件释放时停止 timer 并中止在途观察,但不会把“停止本地观察”写成“远端已取消”。端点或协议 binding 漂移时恢复在联网前拒绝。
77
+
78
+ 每个操作必须明确声明 supported 或 unsupported。当前 DashScope 与 OpenAI Images 媒体协议没有经过验证的远端取消实现,因此取消请求如实返回 not-supported,而不是伪造 canceled。完整受理与错误规则见 [Provider 提交契约 v0](PROVIDER_SUBMISSION_CONTRACT.md)。
56
79
 
57
- 完整接口与 conformance runner 延后到 0.1.4+,见 [Provider 提交契约 v0](PROVIDER_SUBMISSION_CONTRACT.md)。
80
+ ## Text/Vision 共享边界(M1 已实现)
58
81
 
59
- ## Host Port 清单
82
+ [Text/Vision Model Port v0](MODEL_PORT_CONTRACT.md) 的纯契约、调用控制与 test-only Fake Port 已实现,尚未接入真实消费者。它以 `describe()` 和单轮 `complete()` 统一文本/视觉输入、正常终态、预算、取消与安全错误;图片以字节和 MIME 进入共享层,模型选择、会话与附件桥接保留在入口/适配器。
60
83
 
61
- | 能力 | 当前来源 | 缺失时行为 |
84
+ 提示词优化与视觉理解不创建媒体生成 Task,不进入 Provider Task Runner。提示词优化的功能定位与输入隔离正在讨论,其接入待后续确定;look/relook、定位、OCR 与摘要仍按独立切片迁移。现有 Host 方法、配置与 CLI 均未因 M1 改变;自持文本目录和独立入口需要另行审查。
85
+
86
+ ## Host Port 与 Headless 边界
87
+
88
+ | 能力 | DSH 来源 | 缺失或 Headless 时行为 |
62
89
  |---|---|---|
63
- | 工具注册与 Fiber 生命周期 | DSH `ctx.tools` / `ctx.effect` | 对应工具不注册;其余能力继续装载 |
64
- | Web 路由 | DSH `webServer` 或 `httpServer` | 工作台 API、媒体链接和 HTML 渲染不可用 |
65
- | UI Slot | DSH Web Client | 不影响 Host 侧任务与离线 Doctor |
66
- | Browser | DSH browser service | 仅 HTML 截图不可用 |
67
- | 会话附件与输入草稿 | DSH session/input ports | 退化为 Iris 自有产物或浏览器上传 |
68
- | Skills registry | DSH skills service | 随包 Skill 不自动注册;核心动作仍可用 |
90
+ | 工具注册与 Fiber 生命周期 | `ctx.tools` / `ctx.effect` | 不注册 DSH 工具;CLI/Core 不受影响 |
91
+ | Web 路由 | `webServer` 或 `httpServer` | 无工作台 API 和同源媒体路由;CLI 直接导出 Artifact |
92
+ | UI Slot | DSH Web Client | 不影响 Core Task、Artifact 与离线 Doctor |
93
+ | Browser | DSH browser service | HTML 渲染不可用;本地媒体命令不受影响 |
94
+ | 会话附件与输入草稿 | session/input ports | CLI 使用显式文件或 JSON 输入,不创建会话对象 |
95
+ | Skills registry | DSH skills service | 随包 Skill 不自动注册;CLI/Core 仍可用 |
96
+
97
+ Command、Core Store、Provider Runner 和 Headless CLI 不接收原始 `ctx`、会话对象或 Browser 对象。Host URL、attachment 与 UI 文案只在 DSH 投影层产生。
69
98
 
70
- 这些依赖目前由 `index.js` 的可选注入和守卫集中处理。0.1.4 将把它们转成正式 Host Adapter 接口并增加 Local Host fixture。
99
+ ## 事件、UI 与媒体读取
71
100
 
72
- ## 事件与 UI
101
+ DSH 工作台把 Core 与 legacy Task 合并到一个用户任务区,把 Core 图片与旧 `outputs/` 合并到一个作品区。普通投影只有五类用户状态;原始 Attempt 事实留在高级诊断。刷新、展开、关闭、读取作品和查询状态都不得触发 submit、poll 或隐式修复。
73
102
 
74
- 任务和配置写入会触发节流 SSE 快照。每份快照都有进程 epoch 与单调序号;慢消费者进入背压后只保留最新快照,重连仍通过持久化全量状态收敛。工作台只展示稳定人类状态,内部事实轴保留在 API 详情中供诊断和人工动作判断。
103
+ Core 媒体路由按随机 Artifact ID 读取并逐次验证 SHA-256;它没有独立 token。浏览器请求还受可信 Host、`Origin`/`Sec-Fetch-Site` 和 `Referrer-Policy: no-referrer` 约束。Artifact ID 是持有者标识,不等于多租户认证;Iris 与 DSH 仍处于同一本机信任域。
75
104
 
76
- ## 非目标
105
+ ## 非目标与发布边界
77
106
 
78
- - 不在 0.1.3 启动独立常驻服务或复制 DSH 聊天界面。
79
- - 不迁移到 SQLite/JSONL,不破坏旧任务读取。
80
- - 不新增 Gemini、Fal 或 Replicate 分支。
81
- - 不把离线 Doctor 的成功等同于 DSH Host、浏览器或真实供应商已可用。
107
+ - `0.2.0` 不启动独立常驻服务,也不复制 DSH 聊天界面。
108
+ - 不迁移到 SQLite/JSONL,不原地改写 0.1.4 数据,不自动接管不确定的写者租约。
109
+ - 不在本候选新增 Gemini、Fal、Replicate 或本地模型协议;注册表只是后续接入点。
110
+ - 不把离线测试、一次 canary 或 Headless 可运行等同于公开 Core API 已冻结。
111
+ - rc.1 仍须通过最终包审计、仓库外安装、跨平台 CI、Android 目视、回退和机器可读验证摘要后才能发布。