@mokuyoaxis/dsh-iris 0.2.6 → 0.2.7

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 (58) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.en.md +28 -19
  3. package/README.md +27 -18
  4. package/bin/dsh-iris.js +140 -26
  5. package/docs/AGENT_QUICKSTART.md +5 -5
  6. package/docs/ARCHITECTURE.md +9 -3
  7. package/docs/CLI_MANAGEMENT.md +1 -1
  8. package/docs/CORE_RUNTIME_CONTRACT.md +36 -5
  9. package/docs/CORE_SKILLS.md +62 -0
  10. package/docs/DSH_COMPATIBILITY.md +4 -4
  11. package/docs/DSH_CORE_MIGRATION.md +4 -2
  12. package/docs/DSH_PROVIDER_CONNECTIONS.md +1 -1
  13. package/docs/DSH_RUNTIME.md +36 -0
  14. package/docs/FUNCTION_MODEL_POOLS.md +1 -1
  15. package/docs/HEADLESS_CLI.md +11 -3
  16. package/docs/HOST_DOCTOR.md +13 -1
  17. package/docs/IMAGE_FLOW.md +71 -0
  18. package/docs/LOCAL_API.md +60 -0
  19. package/docs/LOCAL_API_CONTRACT.md +115 -0
  20. package/docs/LOCAL_CREATION.md +50 -0
  21. package/docs/PROVIDER_HEALTH.md +1 -1
  22. package/docs/ROADMAP.md +14 -2
  23. package/docs/SECURITY.md +9 -1
  24. package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
  25. package/docs/VISION_CLI.md +1 -1
  26. package/docs/WEB_PROTOTYPE.md +47 -0
  27. package/docs/releases/0.2.7.md +62 -0
  28. package/lib/actions.js +79 -68
  29. package/lib/api.js +38 -38
  30. package/lib/application-runtime.js +186 -0
  31. package/lib/client.js +89 -3
  32. package/lib/core-artifact-store.js +5 -0
  33. package/lib/core-maintenance.js +25 -2
  34. package/lib/core-runtime.js +19 -0
  35. package/lib/core-tasks.js +62 -8
  36. package/lib/creation-request-store.js +159 -0
  37. package/lib/doctor.js +12 -1
  38. package/lib/dsh-core-adapter.js +666 -572
  39. package/lib/dsh-host-adapter.js +4 -1
  40. package/lib/flow-store.js +195 -0
  41. package/lib/image-flow.js +188 -0
  42. package/lib/index.js +59 -53
  43. package/lib/local-api-projection.js +27 -0
  44. package/lib/local-api.js +381 -0
  45. package/lib/local-creation.js +85 -0
  46. package/lib/private-storage.js +12 -0
  47. package/lib/provider-task-runner.js +22 -2
  48. package/lib/render-address.js +35 -0
  49. package/lib/render.js +52 -13
  50. package/lib/skill-installation.js +199 -0
  51. package/lib/workbench-artifacts.js +1 -2
  52. package/lib/workbench-management.js +8 -9
  53. package/package.json +3 -2
  54. package/skills/iris-core/SKILL.md +40 -0
  55. package/skills/iris-core/agents/openai.yaml +4 -0
  56. package/skills/iris-core/references/cli.md +95 -0
  57. package/skills/iris-core/references/tasks.md +58 -0
  58. package/user_guide.md +14 -8
package/lib/api.js CHANGED
@@ -26,7 +26,6 @@ import { selectMediaProtocol } from './provider-protocol.js';
26
26
  import { deriveUserState } from './task-semantics.js';
27
27
  import { hostDoctor } from './doctor.js';
28
28
  import { hostRuntimeEvidence, recordHostClient } from './host-runtime.js';
29
- import { cancelProviderTaskForDsh, coreSnapshotForDsh, disposeCoreTaskAttention, readCoreArtifactMediaForDsh, redeliverProviderTaskForDsh, reobserveProviderTaskForDsh, retryProviderTaskForDsh } from './dsh-core-adapter.js';
30
29
  import { optimizePrompt } from './prompt-optimizer.js';
31
30
  import { PROMPT_OPTIMIZER_CAPABILITIES } from './prompt-optimizer-core.js';
32
31
  import {
@@ -332,16 +331,17 @@ function serveSse(res) {
332
331
  }
333
332
 
334
333
  /** webServer 前缀路由 handler:状态、任务、动作与提示词优化器 API。 */
335
- export function serveApi(req, res, host) {
334
+ export function serveApi(req, res, host, core) {
335
+ if (!core || core.snapshot().state !== 'open') return sendJson(res, 503, { error: 'Iris DSH 实例正在关闭或不可用', code: 'IRIS_DSH_RUNTIME_CLOSED' });
336
336
  try {
337
337
  const method = String(req.method || 'GET').toUpperCase();
338
338
  const urlPath = String(req.url || '').split('?')[0];
339
339
 
340
340
  // POST /iris/api/actions/:name —— GUI 直连触发工具执行
341
341
  if (method === 'POST') {
342
- if (urlPath === '/iris/api/works/download') return handleWorkbenchPost(req, res, downloadWorkbenchSelection, true);
343
- if (urlPath === '/iris/api/works/delete') return handleWorkbenchPost(req, res, deleteWorkbenchArtifacts);
344
- if (urlPath === '/iris/api/works/restore') return handleWorkbenchPost(req, res, restoreWorkbenchTransaction);
342
+ if (urlPath === '/iris/api/works/download') return handleWorkbenchPost(req, res, downloadWorkbenchSelection, true, core);
343
+ if (urlPath === '/iris/api/works/delete') return handleWorkbenchPost(req, res, deleteWorkbenchArtifacts, false, core);
344
+ if (urlPath === '/iris/api/works/restore') return handleWorkbenchPost(req, res, restoreWorkbenchTransaction, false, core);
345
345
  if (urlPath === '/iris/api/host-client') {
346
346
  if (!host || typeof host !== 'object') return sendJson(res, 503, { error: 'host unavailable' });
347
347
  return handleJsonPost(req, res, (body) => recordHostClient(body));
@@ -352,16 +352,16 @@ export function serveApi(req, res, host) {
352
352
  }
353
353
  // D4 人工 retry as new task:产生新的真实计费,请求体必须显式 confirmBilling:true
354
354
  const coreTaskRetry = urlPath.match(/^\/iris\/api\/core\/task\/([^/]+)\/retry\/?$/);
355
- if (coreTaskRetry) return handleCoreTaskRetry(coreTaskRetry[1], req, res);
355
+ if (coreTaskRetry) return handleCoreTaskRetry(coreTaskRetry[1], req, res, core);
356
356
  // D1/D2/D3 人工动作:reobserve 显式单步 poll 一次;redeliver 只对 succeeded+failed
357
357
  // 重新取回产物;cancel 只有 Provider 明确确认才写已取消。三者绝不 submit。
358
358
  const coreTaskAction = urlPath.match(/^\/iris\/api\/core\/task\/([^/]+)\/(reobserve|redeliver|cancel|acknowledge|restore|hide|unhide)\/?$/);
359
- if (coreTaskAction) return handleCoreTaskManual(coreTaskAction[2], coreTaskAction[1], req, res);
359
+ if (coreTaskAction) return handleCoreTaskManual(coreTaskAction[2], coreTaskAction[1], req, res, core);
360
360
  const promptMatch = typeof host === 'object' && urlPath.match(/^\/iris\/api\/prompt-optimizer\/(optimize|import|reset|enabled)\/?$/);
361
- if (promptMatch) return handlePromptOptimizer(host, promptMatch[1], req, res);
361
+ if (promptMatch) return handlePromptOptimizer(host, promptMatch[1], req, res, core);
362
362
  const actionMatch = typeof host === 'object' && urlPath.match(/^\/iris\/api\/actions\/([A-Za-z0-9_-]+)\/?$/);
363
363
  if (!actionMatch) { return sendJson(res, 405, { error: 'method not allowed' }); }
364
- return handleAction(host, actionMatch[1], req, res);
364
+ return handleAction(host, actionMatch[1], req, res, core);
365
365
  }
366
366
  // GET/HEAD 原逻辑
367
367
  if (method !== 'GET' && method !== 'HEAD') {
@@ -382,16 +382,16 @@ export function serveApi(req, res, host) {
382
382
  if (urlPath === '/iris/api/core/snapshot') {
383
383
  if (method === 'HEAD') return sendJson(res, 200, {});
384
384
  const query = new URL(req.url || '', 'http://iris.local').searchParams;
385
- return serveCoreSnapshot(res, { limit: Number(query.get('limit') || 12) });
385
+ return serveCoreSnapshot(res, { limit: Number(query.get('limit') || 12) }, core);
386
386
  }
387
387
  if (urlPath === '/iris/api/works') {
388
- return serveWorkbenchArtifacts(req, method, res);
388
+ return serveWorkbenchArtifacts(req, method, res, core);
389
389
  }
390
- if (urlPath === '/iris/api/works/transactions') return serveWorkbenchRead(res, method, () => workbenchTransactions());
390
+ if (urlPath === '/iris/api/works/transactions') return serveWorkbenchRead(res, method, () => workbenchTransactions({ core }));
391
391
  const workDetailMatch = urlPath.match(/^\/iris\/api\/works\/(core|legacy)\/([^/]+)$/);
392
- if (workDetailMatch) return serveWorkbenchRead(res, method, () => workbenchDetail(workDetailMatch[1], workDetailMatch[2]));
392
+ if (workDetailMatch) return serveWorkbenchRead(res, method, () => workbenchDetail(workDetailMatch[1], workDetailMatch[2], { core }));
393
393
  const coreMediaMatch = urlPath.match(/^\/iris\/api\/core\/artifact\/(artifact_[a-f0-9]{24})\/media$/);
394
- if (coreMediaMatch) return serveCoreArtifactMedia(coreMediaMatch[1], method, res);
394
+ if (coreMediaMatch) return serveCoreArtifactMedia(coreMediaMatch[1], method, res, core);
395
395
  if (urlPath === '/iris/api/artifacts') {
396
396
  const query = new URL(req.url || '', 'http://iris.local').searchParams;
397
397
  const page = artifacts.page({ offset: Number(query.get('offset') || 0), limit: Number(query.get('limit') || 24) });
@@ -417,11 +417,11 @@ export function serveApi(req, res, host) {
417
417
  }
418
418
  }
419
419
 
420
- async function serveWorkbenchArtifacts(req, method, res) {
420
+ async function serveWorkbenchArtifacts(req, method, res, core) {
421
421
  const params = new URL(req.url || '', 'http://iris.local').searchParams;
422
422
  const input = Object.fromEntries(params);
423
423
  for (const key of ['offset', 'limit']) if (input[key] !== undefined) input[key] = Number(input[key]);
424
- try { return sendJson(res, 200, method === 'HEAD' ? {} : await workbenchArtifacts(input)); }
424
+ try { return sendJson(res, 200, method === 'HEAD' ? {} : await workbenchArtifacts(input, { core })); }
425
425
  catch (error) { return sendJson(res, error.statusCode || 500, { error: '作品读取失败', code: error.code || 'IRIS_GALLERY_FAILED' }); }
426
426
  }
427
427
 
@@ -430,9 +430,9 @@ async function serveWorkbenchRead(res, method, execute) {
430
430
  catch (error) { const safe = workbenchError(error); return sendJson(res, safe.statusCode || 400, { error: safe.message, code: safe.code }); }
431
431
  }
432
432
 
433
- function handleWorkbenchPost(req, res, execute, download = false) {
433
+ function handleWorkbenchPost(req, res, execute, download = false, core) {
434
434
  return handleJsonPost(req, res, async (body, signal) => {
435
- try { return await execute(body, { signal }); }
435
+ try { return await core.runHostOperation(operationSignal => execute(body, { signal: operationSignal, core }), { signal }); }
436
436
  catch (error) { throw workbenchError(error); }
437
437
  }, download ? (response, _status, bytes) => {
438
438
  if (response.writableEnded || response.destroyed) return;
@@ -443,9 +443,9 @@ function handleWorkbenchPost(req, res, execute, download = false) {
443
443
  } : sendJson);
444
444
  }
445
445
 
446
- async function serveCoreArtifactMedia(artifactId, method, res) {
446
+ async function serveCoreArtifactMedia(artifactId, method, res, core) {
447
447
  try {
448
- const result = await readCoreArtifactMediaForDsh(artifactId);
448
+ const result = await core.readCoreArtifactMediaForDsh(artifactId);
449
449
  if (res.writableEnded || res.destroyed) return;
450
450
  res.writeHead(200, {
451
451
  'Content-Type': result.artifact.mediaType,
@@ -462,9 +462,9 @@ async function serveCoreArtifactMedia(artifactId, method, res) {
462
462
  }
463
463
  }
464
464
 
465
- async function serveCoreSnapshot(res, options = {}) {
465
+ async function serveCoreSnapshot(res, options = {}, core) {
466
466
  try {
467
- return sendJson(res, 200, await coreSnapshotForDsh({ limit: options.limit || 12 }));
467
+ return sendJson(res, 200, await core.coreSnapshotForDsh({ limit: options.limit || 12 }));
468
468
  } catch (error) {
469
469
  console.error('[iris] Core 只读快照失败:', error && error.code || 'unknown');
470
470
  return sendJson(res, 409, {
@@ -527,8 +527,8 @@ function handleJsonPost(req, res, execute, respond = sendJson) {
527
527
  req.on('error', () => finish(400, { error: 'bad request' }));
528
528
  }
529
529
 
530
- function handleAction(host, name, req, res) {
531
- return handleJsonPost(req, res, (args, signal) => runAction(host, name, args, { signal }));
530
+ function handleAction(host, name, req, res, core) {
531
+ return handleJsonPost(req, res, (args, signal) => runAction(host, name, args, { signal, core }));
532
532
  }
533
533
 
534
534
  /**
@@ -539,14 +539,14 @@ function handleAction(host, name, req, res) {
539
539
  * 经 AbortSignal 传入在途操作,与 GUI actions 同一约定。
540
540
  */
541
541
  const CORE_TASK_MANUAL_ACTIONS = Object.freeze({
542
- reobserve: { command: 'task.reobserve', run: reobserveProviderTaskForDsh },
543
- redeliver: { command: 'task.redeliver', run: redeliverProviderTaskForDsh },
544
- cancel: { command: 'task.cancel', run: cancelProviderTaskForDsh },
542
+ reobserve: { command: 'task.reobserve', run: (core, input) => core.reobserveProviderTaskForDsh(input) },
543
+ redeliver: { command: 'task.redeliver', run: (core, input) => core.redeliverProviderTaskForDsh(input) },
544
+ cancel: { command: 'task.cancel', run: (core, input) => core.cancelProviderTaskForDsh(input) },
545
545
  // 注意力处置是 Host 偏好(不写 Core 事实):acknowledge/restore/hide/unhide。
546
- acknowledge: { command: 'attention.acknowledge', run: (input) => disposeCoreTaskAttention({ ...input, action: 'acknowledge' }) },
547
- restore: { command: 'attention.restore', run: (input) => disposeCoreTaskAttention({ ...input, action: 'restore' }) },
548
- hide: { command: 'attention.hide', run: (input) => disposeCoreTaskAttention({ ...input, action: 'hide' }) },
549
- unhide: { command: 'attention.unhide', run: (input) => disposeCoreTaskAttention({ ...input, action: 'unhide' }) }
546
+ acknowledge: { command: 'attention.acknowledge', run: (core, input) => core.disposeCoreTaskAttention({ ...input, action: 'acknowledge' }) },
547
+ restore: { command: 'attention.restore', run: (core, input) => core.disposeCoreTaskAttention({ ...input, action: 'restore' }) },
548
+ hide: { command: 'attention.hide', run: (core, input) => core.disposeCoreTaskAttention({ ...input, action: 'hide' }) },
549
+ unhide: { command: 'attention.unhide', run: (core, input) => core.disposeCoreTaskAttention({ ...input, action: 'unhide' }) }
550
550
  });
551
551
 
552
552
  /**
@@ -555,7 +555,7 @@ const CORE_TASK_MANUAL_ACTIONS = Object.freeze({
555
555
  * Prompt,只能由调用方重新提供;缺显式计费确认在读取旧 Task 之前拒绝。
556
556
  * 成功返回 200 + 新任务的五类用户投影行与 retriedFrom 关系。
557
557
  */
558
- function handleCoreTaskRetry(taskId, req, res) {
558
+ function handleCoreTaskRetry(taskId, req, res, core) {
559
559
  if (!/^task_[a-f0-9]{24}$/.test(taskId)) {
560
560
  return sendJson(res, 400, { error: { code: 'IRIS_DSH_TASK_INVALID', message: 'Core Task ID 无效' } });
561
561
  }
@@ -568,7 +568,7 @@ function handleCoreTaskRetry(taskId, req, res) {
568
568
  throw error;
569
569
  }
570
570
  const prompt = String(body.prompt || '');
571
- const result = await retryProviderTaskForDsh({
571
+ const result = await core.retryProviderTaskForDsh({
572
572
  taskId,
573
573
  prompt,
574
574
  ...(body.source_artifact_id !== undefined ? { sourceArtifactId: body.source_artifact_id } : {}),
@@ -597,7 +597,7 @@ function handleCoreTaskRetry(taskId, req, res) {
597
597
  });
598
598
  }
599
599
 
600
- function handleCoreTaskManual(action, taskId, req, res) {
600
+ function handleCoreTaskManual(action, taskId, req, res, core) {
601
601
  if (!/^task_[a-f0-9]{24}$/.test(taskId)) {
602
602
  return sendJson(res, 400, { error: { code: 'IRIS_DSH_TASK_INVALID', message: 'Core Task ID 无效' } });
603
603
  }
@@ -614,7 +614,7 @@ function handleCoreTaskManual(action, taskId, req, res) {
614
614
  if (typeof req.on === 'function') req.on('aborted', disconnect);
615
615
  if (typeof res.on === 'function') res.on('close', disconnect);
616
616
  if (typeof req.resume === 'function') req.resume(); // 无 body 入参;避免小请求体滞留
617
- entry.run({ taskId, signal: controller.signal })
617
+ entry.run(core, { taskId, signal: controller.signal })
618
618
  .then((result) => finish(200, {
619
619
  ok: true,
620
620
  contractVersion: 0,
@@ -631,8 +631,8 @@ function handleCoreTaskManual(action, taskId, req, res) {
631
631
  });
632
632
  }
633
633
 
634
- function handlePromptOptimizer(host, operation, req, res) {
635
- return handleJsonPost(req, res, async (body, signal) => {
634
+ function handlePromptOptimizer(host, operation, req, res, core) {
635
+ return handleJsonPost(req, res, (body, requestSignal) => core.runHostOperation(async signal => {
636
636
  if (operation === 'optimize') return optimizePrompt(host, body, { signal });
637
637
  if (operation === 'import') {
638
638
  const imported = body && body.config !== undefined ? body.config : body;
@@ -645,7 +645,7 @@ function handlePromptOptimizer(host, operation, req, res) {
645
645
  }
646
646
  if (operation === 'enabled') return { ok: true, ...setPromptOptimizerEnabled(body && body.enabled), capabilities: PROMPT_CONFIG_CAPABILITIES };
647
647
  throw new Error('unknown prompt optimizer operation');
648
- });
648
+ }, { signal: requestSignal }));
649
649
  }
650
650
 
651
651
  /* ---------- 文件选择器 L2:本地文件上传(阶段 10) ---------- */
@@ -0,0 +1,186 @@
1
+ 'use strict';
2
+
3
+ import { normalizeCoreOptions } from './core-contract.js';
4
+ import { createCoreRuntime } from './core-runtime.js';
5
+ import { createCommandService } from './command-service.js';
6
+ import { normalizeGenerationInput } from './generation-input.js';
7
+ import { createProviderTaskRunner } from './provider-task-runner.js';
8
+ import { createImageFlowService } from './image-flow.js';
9
+ import { createLocalCreationService } from './local-creation.js';
10
+
11
+ // Internal first-slice execution surface, not a public API or an HTTP allowlist.
12
+ const COMMAND_MODES = Object.freeze({
13
+ 'artifact.import': 'writer', 'artifact.export': 'writer', 'media.diff': 'writer',
14
+ 'task.observe': 'writer', 'task.reobserve': 'writer', 'task.redeliver': 'writer', 'task.cancel': 'writer',
15
+ 'artifact.inspect': 'reader', 'artifact.list': 'reader', 'task.inspect': 'reader', 'task.list': 'reader',
16
+ 'flow.init': 'writer', 'flow.create': 'writer', 'flow.continue': 'writer', 'flow.select': 'writer', 'flow.export': 'writer',
17
+ 'flow.inspect': 'reader', 'flow.list': 'reader',
18
+ 'creation.init': 'writer', 'creation.image': 'writer', 'creation.flow': 'writer'
19
+ });
20
+
21
+ export class ApplicationRuntimeError extends Error {
22
+ constructor(code, message) { super(message); this.name = 'ApplicationRuntimeError'; this.code = code; }
23
+ }
24
+
25
+ function aborted() {
26
+ return Object.assign(new ApplicationRuntimeError('IRIS_APP_ABORTED', 'Iris 操作已取消'), { name: 'AbortError' });
27
+ }
28
+
29
+ function closed() {
30
+ return new ApplicationRuntimeError('IRIS_APP_CLOSED', 'Iris 应用运行时正在关闭或已关闭');
31
+ }
32
+
33
+ /** Instance-owned FIFO and operation-scoped Core leases. No I/O until an explicit operation. */
34
+ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, resolveTaskAdapter } = {}) {
35
+ const root = normalizeCoreOptions({ dataRoot }).dataRoot;
36
+ if (resolveImageCandidates !== undefined && typeof resolveImageCandidates !== 'function') {
37
+ throw new TypeError('resolveImageCandidates 必须是显式候选解析函数');
38
+ }
39
+ if (resolveTaskAdapter !== undefined && typeof resolveTaskAdapter !== 'function') {
40
+ throw new TypeError('resolveTaskAdapter 必须是显式 Task Provider 解析函数');
41
+ }
42
+ let state = 'open';
43
+ const queue = [];
44
+ const active = new Set();
45
+ let writer = null;
46
+ let disposePromise;
47
+ let cleanupFailure;
48
+
49
+ function snapshot() {
50
+ return Object.freeze({ state, queuedWrites: queue.length, activeWrites: Number(Boolean(writer)),
51
+ activeReaders: active.size - Number(Boolean(writer)), cleanupFailed: Boolean(cleanupFailure) });
52
+ }
53
+
54
+ function detach(job) { job.signal?.removeEventListener('abort', job.onAbort); }
55
+
56
+ function rejectQueued(job, error) {
57
+ detach(job);
58
+ job.reject(error);
59
+ }
60
+
61
+ function stop(job) {
62
+ job.aborted = true;
63
+ if (job.runtime && !job.disposal) {
64
+ job.disposal = job.runtime.dispose();
65
+ // The operation awaits this same promise below and reports any cleanup error.
66
+ job.disposal.catch(() => {});
67
+ }
68
+ }
69
+
70
+ function stopAll() {
71
+ for (const job of queue.splice(0)) rejectQueued(job, closed());
72
+ for (const job of active) stop(job);
73
+ }
74
+
75
+ async function perform(job) {
76
+ if (job.aborted) throw aborted();
77
+ const runtime = createCoreRuntime({ dataRoot: root, mode: job.mode });
78
+ job.runtime = runtime;
79
+ let result, operationError, failed = false;
80
+ try {
81
+ runtime.start();
82
+ result = await job.action(runtime);
83
+ } catch (error) { failed = true; operationError = error; }
84
+ try { await (job.disposal || runtime.dispose()); }
85
+ catch (error) {
86
+ cleanupFailure ||= error;
87
+ state = 'closing';
88
+ stopAll();
89
+ if (failed) {
90
+ throw Object.assign(new AggregateError([operationError, error], 'Iris 操作失败且运行时未能安全释放'), { code: error.code });
91
+ }
92
+ throw error;
93
+ }
94
+ if (failed) throw operationError;
95
+ return result;
96
+ }
97
+
98
+ function start(job) {
99
+ active.add(job);
100
+ job.done = Promise.resolve().then(async () => {
101
+ let result, error, failed = false;
102
+ try { result = await perform(job); }
103
+ catch (caught) { failed = true; error = caught; }
104
+ detach(job);
105
+ active.delete(job);
106
+ if (writer === job) writer = null;
107
+ if (failed) job.reject(error); else job.resolve(result);
108
+ dispatch();
109
+ });
110
+ }
111
+
112
+ function dispatch() {
113
+ if (state !== 'open' || writer || !queue.length) return;
114
+ writer = queue.shift();
115
+ start(writer);
116
+ }
117
+
118
+ function schedule(mode, action, { signal } = {}) {
119
+ if (signal !== undefined && !(signal instanceof AbortSignal)) throw new TypeError('signal 必须是 AbortSignal');
120
+ if (state !== 'open') return Promise.reject(closed());
121
+ if (signal?.aborted) return Promise.reject(aborted());
122
+ let job;
123
+ const result = new Promise((resolve, reject) => { job = { mode, action, signal, resolve, reject, aborted: false }; });
124
+ job.onAbort = () => {
125
+ const index = queue.indexOf(job);
126
+ if (index >= 0) { queue.splice(index, 1); rejectQueued(job, aborted()); }
127
+ else stop(job);
128
+ };
129
+ signal?.addEventListener('abort', job.onAbort, { once: true });
130
+ if (mode === 'writer') { queue.push(job); dispatch(); }
131
+ else start(job);
132
+ return result;
133
+ }
134
+
135
+ /** Trusted in-process Host composition; never an external command/HTTP dispatch surface. */
136
+ function run(mode, action, options) {
137
+ if (!['reader', 'writer'].includes(mode)) throw new TypeError('Host 操作需要明确 reader/writer 模式');
138
+ if (typeof action !== 'function') throw new TypeError('Host 操作必须是函数');
139
+ return schedule(mode, action, options);
140
+ }
141
+
142
+ async function execute(name, input, options) {
143
+ if (!Object.hasOwn(COMMAND_MODES, name)) {
144
+ throw new ApplicationRuntimeError('IRIS_APP_COMMAND_UNSUPPORTED', '应用运行时尚未装配该 Command');
145
+ }
146
+ const ownedInput = structuredClone(input);
147
+ return schedule(COMMAND_MODES[name], runtime => {
148
+ const ports = resolveTaskAdapter ? { resolveTaskAdapter: async task => {
149
+ const adapter = await resolveTaskAdapter(task, { signal: runtime.signal });
150
+ if (runtime.signal.aborted) throw aborted();
151
+ return adapter;
152
+ } } : {};
153
+ if (name.startsWith('creation.')) return createLocalCreationService(runtime, { resolveImageCandidates,
154
+ onPrepared: options?.onPrepared }).execute(name, ownedInput);
155
+ if (name.startsWith('flow.')) return createImageFlowService(runtime, { ...ports, resolveImageCandidates }).execute(name, ownedInput);
156
+ return createCommandService(runtime, ports).execute(name, ownedInput);
157
+ }, options);
158
+ }
159
+
160
+ async function generateImage(input, options) {
161
+ const normalized = normalizeGenerationInput('image', input);
162
+ if (!resolveImageCandidates) {
163
+ throw new ApplicationRuntimeError('IRIS_APP_IMAGE_RESOLVER_MISSING', '图片生成需要显式提供候选解析函数');
164
+ }
165
+ return schedule('writer', async runtime => {
166
+ const candidates = await resolveImageCandidates({ modelRef: normalized.modelRef, signal: runtime.signal });
167
+ if (runtime.signal.aborted) throw aborted();
168
+ return createProviderTaskRunner(runtime).submit({ capability: 'image', candidates,
169
+ providerInput: normalized.providerInput, sourceArtifactId: normalized.sourceArtifactId });
170
+ }, options);
171
+ }
172
+
173
+ function dispose() {
174
+ if (disposePromise) return disposePromise;
175
+ state = 'closing';
176
+ stopAll();
177
+ disposePromise = Promise.all([...active].map(job => job.done)).then(() => {
178
+ if (cleanupFailure) throw cleanupFailure;
179
+ state = 'closed';
180
+ return snapshot();
181
+ });
182
+ return disposePromise;
183
+ }
184
+
185
+ return Object.freeze({ execute, generateImage, run, dispose, snapshot });
186
+ }
package/lib/client.js CHANGED
@@ -21,7 +21,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
21
21
  var exports = module.exports;
22
22
 
23
23
  var React = require('react');
24
- var IRIS_CLIENT_VERSION = '0.2.6';
24
+ var IRIS_CLIENT_VERSION = '0.2.7';
25
25
  var HOST_CLIENT_PROTOCOL_VERSION = 0;
26
26
  var reportedSeats = [];
27
27
 
@@ -140,11 +140,17 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
140
140
  '.iris-doctor-score { font-weight: 600; flex: 1; }',
141
141
  '.iris-doctor-list { display: grid; gap: 4px; margin-top: 7px; }',
142
142
  '.iris-doctor-row { display: grid; grid-template-columns: 14px 1fr; gap: 6px; font-size: 11px; }',
143
+ '.iris-doctor-row > span:last-child { min-width: 0; overflow-wrap: anywhere; }',
143
144
  '.iris-doctor-row.ok { color: var(--dsw-alias-label-secondary, #9a9a9a); }',
144
145
  '.iris-doctor-row.warn { color: var(--dsw-alias-state-warn-primary, #d9a941); }',
145
146
  '.iris-doctor-row.error { color: var(--dsw-alias-state-error-primary, #e06c6c); }',
146
147
  '.iris-doctor-details { margin-top: 7px; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
147
148
  '.iris-doctor-details summary { cursor: pointer; font-size: 11px; }',
149
+ '.iris-browser-check { border-top: 1px solid var(--dsw-alias-border-l1, #ffffff12); margin-top: 10px; padding-top: 9px; min-width: 0; }',
150
+ '.iris-browser-check .iris-doctor-top { margin-bottom: 5px; }',
151
+ '.iris-browser-check img { display: block; width: 100%; max-height: 200px; object-fit: contain; margin-top: 7px; border-radius: 7px; background: var(--dsw-alias-bg-layer-2, #ffffff05); }',
152
+ '.iris-browser-check-note { font-size: 12px; line-height: 1.6; overflow-wrap: anywhere; }',
153
+ '.iris-browser-check-note.error { color: var(--dsw-alias-state-error-primary, #e06c6c); }',
148
154
  '.iris-core-runtime { cursor: default; padding: 0; overflow: hidden; }',
149
155
  '.iris-core-runtime-head { display: flex; align-items: center; gap: 8px; padding: 9px 10px; }',
150
156
  '.iris-core-runtime-title { min-width: 0; flex: 1; display: grid; gap: 1px; }',
@@ -3136,6 +3142,80 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
3136
3142
  note ? React.createElement('div', { className: 'iris-clean-note' }, note) : null);
3137
3143
  }
3138
3144
 
3145
+ function BrowserCheckPanel(props) {
3146
+ var statePair = React.useState({ status: 'idle', message: '尚未实测' });
3147
+ var state = statePair[0], setState = statePair[1];
3148
+ var request = React.useRef(null);
3149
+ React.useEffect(function () { return function () {
3150
+ var operation = request.current;
3151
+ request.current = null;
3152
+ if (operation) { clearTimeout(operation.timer); operation.controller.abort(); }
3153
+ }; }, []);
3154
+
3155
+ function stop(timedOut) {
3156
+ var operation = request.current;
3157
+ if (!operation) return;
3158
+ request.current = null;
3159
+ clearTimeout(operation.timer);
3160
+ operation.controller.abort();
3161
+ setState({ status: 'canceled', message: timedOut
3162
+ ? '截图自检超过 60 秒,已停止等待。若恰好完成保存,可在作品库查看。'
3163
+ : '已取消本次自检等待。若恰好完成保存,可在作品库查看。' });
3164
+ }
3165
+ function start() {
3166
+ if (request.current || !props.available) return;
3167
+ var operation = { controller: new AbortController(), started: Date.now() };
3168
+ request.current = operation;
3169
+ setState({ status: 'running', message: '正在打开浏览器并生成测试截图…' });
3170
+ operation.timer = setTimeout(function () { if (request.current === operation) stop(true); }, 60000);
3171
+ fetch('/iris/api/actions/html', {
3172
+ method: 'POST', headers: { 'content-type': 'application/json' }, credentials: 'same-origin',
3173
+ signal: operation.controller.signal,
3174
+ body: JSON.stringify({ fullPage: false, width: 480, height: 240,
3175
+ html: '<style>body{margin:0;background:#edf3f1;color:#183e34;font-family:system-ui,sans-serif}.iris-test{padding:32px}h1{font-size:26px;margin:0 0 12px}p{font-size:16px;margin:8px 0}</style><div class="iris-test"><h1>Iris · 截图自检</h1><p>中文与 HTML 渲染测试</p><p>本页只使用内联样式,不调用模型。</p></div>' })
3176
+ }).then(function (res) { return res.json().then(function (data) {
3177
+ if (!res.ok || data.ok !== true) throw Object.assign(new Error(data.error || '截图自检失败'), { code: data.code });
3178
+ var id = data.artifactIds && data.artifactIds[0];
3179
+ if (data.storage !== 'core' || !/^artifact_[a-f0-9]{24}$/.test(id || '')) throw new Error('服务端未返回有效截图作品,请核对前后端版本。');
3180
+ return id;
3181
+ }); }).then(function (id) {
3182
+ if (request.current !== operation || operation.controller.signal.aborted) return;
3183
+ setState({ status: 'passed', artifactId: id, message: '本次截图已保存 · ' + ((Date.now() - operation.started) / 1000).toFixed(1) + ' 秒 · ' + new Date().toLocaleTimeString() });
3184
+ window.dispatchEvent(new CustomEvent('iris-core-artifacts-changed'));
3185
+ }).catch(function (error) {
3186
+ if (request.current !== operation || operation.controller.signal.aborted) return;
3187
+ var code = String(error && error.code || '');
3188
+ var detail = String(error && error.message || error);
3189
+ var message = '截图自检未通过。请查看错误详情。';
3190
+ if (code === 'IRIS_RENDER_ADDRESS_INVALID') message = '截图地址配置有误,请核对 DSH_WEB_BASE。';
3191
+ else if (code === 'IRIS_CORE_DATA_ROOT_BUSY') message = '作品库正在被其他操作写入,请稍后重试。';
3192
+ else if (/^IRIS_HOST_CAPABILITY_/.test(code)) message = '宿主浏览器接口不可用,请检查浏览器插件是否启用。';
3193
+ else if (/Missing X server|DISPLAY|BROWSER_HOST_/.test(detail + code)) message = '浏览器进程未能启动,请检查运行环境与显示服务。';
3194
+ else if (/BROWSER_NAVIGATION|ERR_CONNECTION_REFUSED|ERR_NAME_NOT_RESOLVED/.test(detail + code)) message = '浏览器未能打开渲染页面,请核对上方截图地址与宿主监听状态。';
3195
+ else if (/BROWSER_SCREENSHOT/.test(code)) message = '浏览器未能生成截图,请查看错误详情。';
3196
+ setState({ status: 'failed', message: message, detail: detail, code: code });
3197
+ }).finally(function () {
3198
+ clearTimeout(operation.timer);
3199
+ if (request.current === operation) request.current = null;
3200
+ });
3201
+ }
3202
+ var busy = state.status === 'running';
3203
+ return React.createElement('div', { className: 'iris-browser-check', 'data-iris-browser-check': state.status },
3204
+ React.createElement('div', { className: 'iris-doctor-top' },
3205
+ React.createElement('strong', { className: 'iris-doctor-score' }, 'HTML 截图'),
3206
+ React.createElement('button', { className: 'iris-pm-btn', disabled: busy || !props.available, onClick: start }, busy ? '检测中…' : '截图自检'),
3207
+ busy ? React.createElement('button', { className: 'iris-pm-btn', onClick: function () { stop(false); } }, '取消') : null),
3208
+ React.createElement('div', { className: 'iris-browser-check-note' + (state.status === 'failed' ? ' error' : ''), role: 'status', 'aria-live': 'polite' }, state.message),
3209
+ props.address ? React.createElement('div', { className: 'iris-browser-check-note iris-wb-muted' }, props.address) : null,
3210
+ React.createElement('div', { className: 'iris-wb-muted' }, '点击后打开浏览器,生成一张本地测试作品;不调用模型。'),
3211
+ state.artifactId ? React.createElement('details', { className: 'iris-doctor-details' },
3212
+ React.createElement('summary', {}, '查看测试截图'),
3213
+ React.createElement('img', { src: '/iris/api/core/artifact/' + state.artifactId + '/media', alt: 'Iris HTML 截图自检结果', loading: 'lazy' })) : null,
3214
+ state.detail ? React.createElement('details', { className: 'iris-doctor-details' },
3215
+ React.createElement('summary', {}, '错误详情'),
3216
+ React.createElement('div', { className: 'iris-browser-check-note' }, (state.code ? state.code + ' · ' : '') + state.detail)) : null);
3217
+ }
3218
+
3139
3219
  function HostDoctorPanel() {
3140
3220
  var pair = React.useState(null);
3141
3221
  var report = pair[0];
@@ -3160,12 +3240,14 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
3160
3240
 
3161
3241
  var checks = report && Array.isArray(report.checks) ? report.checks : [];
3162
3242
  var issues = checks.filter(function (item) { return item.status !== 'ok'; });
3243
+ var renderAddress = checks.find(function (item) { return item.id === 'render-address'; });
3163
3244
  var mark = { ok: '✓', warn: '!', error: '×' };
3164
3245
  function rows(list) {
3165
3246
  return React.createElement('div', { className: 'iris-doctor-list' }, list.map(function (item) {
3166
3247
  return React.createElement('div', { key: item.id, className: 'iris-doctor-row ' + item.status },
3167
3248
  React.createElement('span', {}, mark[item.status] || '·'),
3168
- React.createElement('span', {}, item.summary));
3249
+ React.createElement('span', {}, item.summary,
3250
+ item.status !== 'ok' && item.suggestion ? React.createElement('div', { className: 'iris-wb-muted' }, item.suggestion) : null));
3169
3251
  }));
3170
3252
  }
3171
3253
  var score = report
@@ -3181,7 +3263,11 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
3181
3263
  checks.length ? React.createElement('details', { className: 'iris-doctor-details' },
3182
3264
  React.createElement('summary', {}, '查看全部 ' + checks.length + ' 项'),
3183
3265
  rows(checks)) : null,
3184
- React.createElement('div', { className: 'iris-wb-muted' }, '只读本机注册证据;不调用 Browser、模型或供应商。'));
3266
+ React.createElement('div', { className: 'iris-wb-muted' }, '刷新只读装载证据,不启动浏览器;接口已提供不代表截图已通过。'),
3267
+ React.createElement(BrowserCheckPanel, {
3268
+ available: Boolean(report && report.capabilities && report.capabilities.browser && report.capabilities.browser.status === 'available'),
3269
+ address: renderAddress && renderAddress.status === 'ok' ? renderAddress.summary : ''
3270
+ }));
3185
3271
  }
3186
3272
 
3187
3273
  function revealWorkbenchItem(node) {
@@ -422,6 +422,11 @@ export function inspectCoreArtifact(dataRoot, id) {
422
422
  return publicArtifact(readArtifact(dataRoot, id).manifest);
423
423
  }
424
424
 
425
+ /** 先检查清单/普通文件大小,用于媒体边界限额;读取或提供字节时仍必须校验内容哈希。 */
426
+ export function inspectCoreArtifactMetadata(dataRoot, id) {
427
+ return publicArtifact(readArtifact(dataRoot, id, { verifyContent: false }).manifest);
428
+ }
429
+
425
430
  /** 供宿主投影层读取已经校验过内容哈希的字节;不暴露 Core 内部路径。 */
426
431
  export function readCoreArtifactBytes(dataRoot, id) {
427
432
  const hit = readArtifact(dataRoot, id);
@@ -2,9 +2,11 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import crypto from 'node:crypto';
4
4
  import { inspectCoreArtifact, refreshCoreArtifactInventory } from './core-artifacts.js';
5
- import { inspectCoreTask, listCoreTasks } from './core-tasks.js';
5
+ import { assertFlowTaskMatches, inspectCoreTask, listCoreTasks } from './core-tasks.js';
6
6
  import { atomicWritePrivate } from './private-storage.js';
7
7
  import { readDeliveryCache } from './core-delivery-cache.js';
8
+ import { flowReferences } from './flow-store.js';
9
+ import { creationReferences } from './creation-request-store.js';
8
10
 
9
11
  const TASK = /^task_[a-f0-9]{24}$/;
10
12
  const ARTIFACT = /^artifact_[a-f0-9]{24}$/;
@@ -51,7 +53,18 @@ function inventory(root) {
51
53
  }
52
54
  const artifacts = names(root, 'artifact-store/v0/records').filter(name => ARTIFACT.test(name.slice(0, -5)) && name.endsWith('.json'))
53
55
  .map(name => inspectCoreArtifact(root, name.slice(0, -5)));
54
- return { tasks, artifacts };
56
+ const flows = flowReferences(root);
57
+ for (const reference of flows) {
58
+ if (reference.run.edit.claimed && !tasks.some(task => task.id === reference.run.edit.taskId)) {
59
+ fail('IRIS_FLOW_TASK_CONFLICT', '流程已建立的 Task 丢失,停止管理');
60
+ }
61
+ }
62
+ for (const task of tasks.filter(task => task.flowBinding)) {
63
+ const reference = flows.find(item => item.run.id === task.flowBinding.runId);
64
+ if (!reference) fail('IRIS_FLOW_TASK_CONFLICT', '流程 Task 的 Run 记录丢失,停止管理');
65
+ assertFlowTaskMatches(task, reference.run);
66
+ }
67
+ return { tasks, artifacts, flows, creations: creationReferences(root) };
55
68
  }
56
69
 
57
70
  function ids(value, pattern, label) {
@@ -65,6 +78,14 @@ export function planCoreDeletion(root, input) {
65
78
  if (!taskIds.length && !artifactIds.length) fail('IRIS_COMMAND_INPUT_INVALID', '请明确指定 Task 或 Artifact ID');
66
79
  const existing = inventory(root), selectedTasks = new Set(taskIds), selectedArtifacts = new Set(artifactIds);
67
80
  const blockers = [], files = [];
81
+ for (const reference of existing.creations) {
82
+ for (const id of reference.taskIds) if (selectedTasks.has(id)) blockers.push({ id, referencedBy: reference.requestId, reason: 'creation_task_reference' });
83
+ for (const id of reference.artifactIds) if (selectedArtifacts.has(id)) blockers.push({ id, referencedBy: reference.requestId, reason: 'creation_artifact_reference' });
84
+ }
85
+ for (const reference of existing.flows) {
86
+ for (const id of reference.taskIds) if (selectedTasks.has(id)) blockers.push({ id, referencedBy: reference.run.id, reason: 'flow_task_reference' });
87
+ for (const id of reference.artifactIds) if (selectedArtifacts.has(id)) blockers.push({ id, referencedBy: reference.run.id, reason: 'flow_artifact_reference' });
88
+ }
68
89
  for (const id of taskIds) {
69
90
  const task = inspectCoreTask(root, id);
70
91
  if (task.phase !== 'terminal' || task.acceptance === 'unknown' || task.outcome === 'unknown') blockers.push({ id, reason: 'task_not_settled' });
@@ -115,6 +136,8 @@ export function planCoreCleanup(root, { older_than_ms = 3600000 } = {}) {
115
136
  if (!Number.isSafeInteger(older_than_ms) || older_than_ms < 60000) fail('IRIS_COMMAND_INPUT_INVALID', 'older_than_ms 至少为 60000');
116
137
  const existing = inventory(root);
117
138
  const referenced = new Set(existing.artifacts.map(artifact => artifact.id));
139
+ for (const reference of existing.creations) for (const id of reference.artifactIds) referenced.add(id);
140
+ for (const reference of existing.flows) for (const id of reference.artifactIds) referenced.add(id);
118
141
  for (const artifact of existing.artifacts) for (const edge of artifact.relations) referenced.add(edge.artifactId);
119
142
  for (const task of existing.tasks) for (const id of task.artifactIds) referenced.add(id);
120
143
  for (const task of existing.tasks) if (task.deliveryCache && task.deliveryState !== 'ready') {
@@ -13,6 +13,7 @@ import {
13
13
  transitionCoreLifecycle
14
14
  } from './core-contract.js';
15
15
  import { ensurePrivateDir } from './private-storage.js';
16
+ import { listCreationRequests } from './creation-request-store.js';
16
17
  import { WRITER_LEASE_DIR, WRITER_INTENT_FILE, publishLeaseRecord, readWriterLease,
17
18
  removeWriterIntent, retireLeaseDirectory, writerOwnerStatus } from './core-writer-lease.js';
18
19
 
@@ -162,6 +163,24 @@ export function createCoreRuntime(input) {
162
163
  }
163
164
  const candidateRoot = physicalDataRoot(options);
164
165
  const candidateLease = options.mode === 'writer' ? acquireWriterLease(candidateRoot, instanceId) : null;
166
+ if (candidateLease) {
167
+ try { listCreationRequests(candidateRoot); }
168
+ catch (error) {
169
+ try { releaseWriterLease(candidateLease); }
170
+ catch (cleanupError) {
171
+ // Retained ownership must also make dispose fail, so the application stops its writer queue.
172
+ root = candidateRoot; lease = candidateLease;
173
+ lifecycle = transitionCoreLifecycle(lifecycle, 'dispose');
174
+ abortController.abort();
175
+ const failure = Object.assign(new AggregateError([error, cleanupError], '请求区检查失败且租约未能安全释放'),
176
+ { code: 'IRIS_CORE_LEASE_INVALID' });
177
+ disposePromise = Promise.reject(failure);
178
+ void disposePromise.catch(() => {});
179
+ throw failure;
180
+ }
181
+ throw error;
182
+ }
183
+ }
165
184
  root = candidateRoot;
166
185
  lease = candidateLease;
167
186
  lifecycle = transitionCoreLifecycle(lifecycle, 'start');