@mokuyoaxis/dsh-iris 0.2.7 → 0.3.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +81 -1
  2. package/README.en.md +103 -312
  3. package/README.md +100 -310
  4. package/bin/dsh-iris.js +333 -65
  5. package/docs/AGENT_CORE_QUICKSTART.md +64 -0
  6. package/docs/AGENT_QUICKSTART.md +2 -0
  7. package/docs/ARCHITECTURE.md +13 -3
  8. package/docs/AUDIO_FLOWS.md +71 -0
  9. package/docs/CONTACT_SHEET.md +62 -0
  10. package/docs/CORE_CONNECTIONS.md +99 -0
  11. package/docs/CORE_QUICKSTART.md +108 -0
  12. package/docs/CORE_RUNTIME_CONTRACT.md +7 -2
  13. package/docs/CORE_SERVICE.md +97 -0
  14. package/docs/CORE_SKILLS.md +35 -3
  15. package/docs/DSH_COMPATIBILITY.md +10 -0
  16. package/docs/DSH_RC2_ADAPTATION.md +1 -1
  17. package/docs/DSH_RUNTIME.md +6 -2
  18. package/docs/HEADLESS_CLI.md +2 -2
  19. package/docs/IMAGE_FLOW.md +4 -0
  20. package/docs/IMAGE_GENERATION.md +127 -0
  21. package/docs/LOCAL_API.md +4 -2
  22. package/docs/LOCAL_API_CONTRACT.md +39 -14
  23. package/docs/LOCAL_CREATION.md +7 -1
  24. package/docs/LOCAL_OPERATIONS.md +46 -0
  25. package/docs/LOCAL_SELECTION.md +57 -0
  26. package/docs/LOCAL_WEB.md +201 -0
  27. package/docs/MCP.md +191 -0
  28. package/docs/MUSIC.md +2 -0
  29. package/docs/PANEL_EXTENSIONS.md +64 -0
  30. package/docs/PLUGINS.md +189 -0
  31. package/docs/PLUGIN_STARTUP.md +100 -0
  32. package/docs/PROVIDER_ADAPTER_CONTRACT.md +1 -1
  33. package/docs/PROVIDER_EXTENSIONS.md +78 -0
  34. package/docs/PROVIDER_HEALTH.md +3 -1
  35. package/docs/PROVIDER_SETTINGS.md +85 -0
  36. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +6 -0
  37. package/docs/ROADMAP.md +17 -7
  38. package/docs/VISION_CLI.md +1 -1
  39. package/docs/WEB_PROTOTYPE.md +21 -6
  40. package/docs/WEB_RECOVERY.md +46 -0
  41. package/docs/WORKBENCH_LOGIN.md +63 -0
  42. package/docs/assets/screenshots/core-connections-preview.webp +0 -0
  43. package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
  44. package/docs/assets/screenshots/core-overview-preview.webp +0 -0
  45. package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
  46. package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
  47. package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
  48. package/docs/assets/screenshots/core-studio-preview.webp +0 -0
  49. package/docs/releases/0.3.0-alpha.1.md +51 -0
  50. package/docs/screenshots.md +41 -1
  51. package/lib/actions.js +53 -2
  52. package/lib/adapters.js +20 -11
  53. package/lib/application-runtime.js +34 -7
  54. package/lib/audio-flow-input.js +37 -0
  55. package/lib/client.js +346 -53
  56. package/lib/comfyui-adapter.js +94 -0
  57. package/lib/comfyui-client.js +51 -0
  58. package/lib/comfyui-inspection.js +73 -0
  59. package/lib/comfyui-workflow.js +94 -0
  60. package/lib/config.js +36 -1
  61. package/lib/configured-image-runtime.js +50 -0
  62. package/lib/connection-skill.js +89 -0
  63. package/lib/connection-template.js +23 -0
  64. package/lib/core-artifact-store.js +16 -2
  65. package/lib/core-bridge-selection.js +31 -0
  66. package/lib/core-bridge-tools.js +83 -0
  67. package/lib/core-capability-catalog.js +182 -0
  68. package/lib/core-connection-diagnostics.js +49 -0
  69. package/lib/core-connection-store.js +277 -0
  70. package/lib/core-connections.js +225 -0
  71. package/lib/core-maintenance.js +9 -3
  72. package/lib/core-runtime.js +2 -1
  73. package/lib/core-service-client.js +170 -0
  74. package/lib/core-tasks.js +29 -12
  75. package/lib/core-writer-lease.js +16 -0
  76. package/lib/creation-request-store.js +38 -64
  77. package/lib/dsh-host-adapter.js +4 -1
  78. package/lib/dsh-service-bridge.js +66 -0
  79. package/lib/execution-ownership.js +43 -0
  80. package/lib/flow-store.js +105 -10
  81. package/lib/generation-input.js +10 -1
  82. package/lib/headless-vision.js +7 -7
  83. package/lib/image-edit-input.js +1 -1
  84. package/lib/image-flow.js +73 -29
  85. package/lib/index.js +37 -17
  86. package/lib/local-api-projection.js +10 -5
  87. package/lib/local-api-token-directory.js +31 -0
  88. package/lib/local-api.js +769 -67
  89. package/lib/local-creation.js +89 -24
  90. package/lib/local-operations.js +36 -0
  91. package/lib/local-request-root.js +86 -0
  92. package/lib/local-web-assets.js +37 -0
  93. package/lib/mcp-bridge.js +58 -0
  94. package/lib/mcp-check.js +26 -0
  95. package/lib/mcp-config.js +42 -0
  96. package/lib/mcp-delivery.js +46 -0
  97. package/lib/mcp-http.js +101 -0
  98. package/lib/mcp-server.js +47 -0
  99. package/lib/model-directory-service.js +16 -6
  100. package/lib/model-directory.js +3 -3
  101. package/lib/models.js +2 -2
  102. package/lib/music-transport.js +2 -1
  103. package/lib/operation-runner.js +236 -0
  104. package/lib/operation-store.js +216 -0
  105. package/lib/panel-extension.js +36 -0
  106. package/lib/plugin-contract.js +194 -0
  107. package/lib/plugin-execution.js +51 -0
  108. package/lib/plugin-launch.js +86 -0
  109. package/lib/plugin-operation-store.js +101 -0
  110. package/lib/plugin-operations.js +28 -0
  111. package/lib/plugin-registry.js +109 -0
  112. package/lib/plugin-selection.js +15 -0
  113. package/lib/plugin-source-image.js +5 -0
  114. package/lib/plugin-web-assets.js +54 -0
  115. package/lib/plugins.d.ts +288 -0
  116. package/lib/plugins.js +58 -0
  117. package/lib/provider-adapter.js +5 -1
  118. package/lib/provider-adapters.js +56 -18
  119. package/lib/provider-catalog.js +40 -29
  120. package/lib/provider-config-service.js +105 -23
  121. package/lib/provider-diagnostics.js +56 -0
  122. package/lib/provider-extensions.js +29 -0
  123. package/lib/provider-health.js +8 -1
  124. package/lib/provider-monitor.js +79 -0
  125. package/lib/provider-protocol.js +70 -20
  126. package/lib/provider-registry.js +118 -0
  127. package/lib/provider-response.js +28 -0
  128. package/lib/provider-settings.js +134 -0
  129. package/lib/provider-task-runner.js +22 -9
  130. package/lib/skill-installation.js +23 -11
  131. package/lib/vision-model-routing.js +3 -0
  132. package/lib/web/app.js +642 -0
  133. package/lib/web/appearance.js +111 -0
  134. package/lib/web/comfyui.js +146 -0
  135. package/lib/web/connections.js +377 -0
  136. package/lib/web/creation.js +355 -0
  137. package/lib/web/flow-view.js +33 -0
  138. package/lib/web/gallery.js +132 -0
  139. package/lib/web/index.html +291 -0
  140. package/lib/web/media-client.js +204 -0
  141. package/lib/web/plugin-panel.js +221 -0
  142. package/lib/web/plugin-surface.css +17 -0
  143. package/lib/web/providers.js +492 -0
  144. package/lib/web/recovery.js +105 -0
  145. package/lib/web/startup.js +18 -0
  146. package/lib/web/styles.css +518 -0
  147. package/lib/web/transport.js +96 -0
  148. package/lib/web/workbench-login.js +165 -0
  149. package/lib/workbench-auth-cli.js +47 -0
  150. package/lib/workbench-auth.js +200 -0
  151. package/package.json +20 -5
  152. package/plugins/contact-sheet/index.d.mts +4 -0
  153. package/plugins/contact-sheet/index.mjs +33 -0
  154. package/plugins/contact-sheet/web/board.css +14 -0
  155. package/plugins/contact-sheet/web/board.mjs +93 -0
  156. package/skills/iris-core/SKILL.md +8 -4
  157. package/skills/iris-core/references/cli.md +2 -2
  158. package/skills/iris-core/references/comfyui.md +69 -0
  159. package/skills/iris-core/references/tasks.md +2 -0
  160. package/user_guide.md +5 -1
@@ -4,45 +4,115 @@ import { createCoreArtifact, readCoreArtifactBytes } from './core-artifacts.js';
4
4
  import { createImageFlowService } from './image-flow.js';
5
5
  import { readImportImage } from './image-import.js';
6
6
  import { normalizeGenerationInput } from './generation-input.js';
7
+ import { readFlowRun, requireAudioRoot } from './flow-store.js';
8
+ import { readImportAudio, normalizeAudioFlowInput } from './audio-flow-input.js';
7
9
  import { initializeCreationRoot, inspectCreationRoot, creationRequestId, creationInputDigest,
8
- readCreationRequest, publishCreationRequest, completeCreationRequest, creationFail } from './creation-request-store.js';
10
+ readCreationRequest, publishCreationRequest, completeCreationRequest, creationFail, CREATION_KINDS, isImportCreation } from './creation-request-store.js';
9
11
 
10
12
  const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
11
- function flowInput(input) {
13
+ function verifyImported(artifact, intent) {
14
+ if (artifact.digest.value !== intent.sha256 || artifact.size !== intent.size || artifact.mediaType !== intent.mediaType
15
+ || artifact.kind !== (intent.mediaType.startsWith('audio/') ? 'imported-audio' : 'imported-image') || artifact.createdAt !== intent.createdAt
16
+ || JSON.stringify(artifact.metadata) !== JSON.stringify(intent.metadata) || artifact.relations.length) {
17
+ creationFail('IRIS_CREATION_RESULT_CONFLICT', '原作品与请求意图不一致');
18
+ }
19
+ return artifact;
20
+ }
21
+ function flowInput(input, generate = false) {
22
+ const required = generate ? ['modelRef', 'prompt'] : ['sourceArtifactId', 'modelRef', 'prompt'];
12
23
  if (!input || typeof input !== 'object' || Array.isArray(input)
13
- || Object.keys(input).some(key => !['sourceArtifactId', 'modelRef', 'prompt', 'size'].includes(key))
14
- || !['sourceArtifactId', 'modelRef', 'prompt'].every(key => typeof input[key] === 'string')
24
+ || Object.keys(input).some(key => ![...required, 'size', ...(generate ? ['workflowParameters'] : [])].includes(key))
25
+ || !required.every(key => typeof input[key] === 'string')
15
26
  || input.size !== undefined && typeof input.size !== 'string') {
16
27
  creationFail('IRIS_CREATION_INPUT_INVALID', '流程创建只接受来源作品、明确模型、Prompt 和可选 size');
17
28
  }
18
- const normalized = normalizeGenerationInput('image', { source_artifact_id: input.sourceArtifactId,
19
- model_ref: input.modelRef, prompt: input.prompt, ...(input.size !== undefined ? { size: input.size } : {}) });
20
- return { source_artifact_id: normalized.sourceArtifactId, model_ref: normalized.modelRef,
29
+ const normalized = normalizeGenerationInput('image', { ...(!generate ? { source_artifact_id: input.sourceArtifactId } : {}),
30
+ model_ref: input.modelRef, prompt: input.prompt, ...(input.size !== undefined ? { size: input.size } : {}),
31
+ ...(input.workflowParameters !== undefined ? { workflow_parameters: input.workflowParameters } : {}) });
32
+ return { ...(normalized.sourceArtifactId ? { source_artifact_id: normalized.sourceArtifactId } : {}), model_ref: normalized.modelRef,
21
33
  ...normalized.providerInput };
22
34
  }
23
- export function createLocalCreationService(runtime, { resolveImageCandidates, onPrepared } = {}) {
24
- const flows = createImageFlowService(runtime, { resolveImageCandidates });
35
+ export function createLocalCreationService(runtime, { resolveImageCandidates, resolveAudioCandidates, onPrepared } = {}) {
36
+ const flows = createImageFlowService(runtime, { resolveImageCandidates, resolveAudioCandidates });
25
37
  function execute(name, input) {
38
+ if (name === 'creation.root' || name === 'creation.lookup') {
39
+ return runtime.run('inspect', ({ dataRoot, signal }) => {
40
+ signal.throwIfAborted();
41
+ const fields = name === 'creation.root' ? [] : ['kind', 'key'];
42
+ if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).length !== fields.length
43
+ || fields.some(key => !Object.hasOwn(input, key))
44
+ || name === 'creation.lookup' && (!CREATION_KINDS.includes(input.kind) || typeof input.key !== 'string')) {
45
+ creationFail('IRIS_CREATION_INPUT_INVALID', '需要明确的创建类型和原 Key');
46
+ }
47
+ const marker = inspectCreationRoot(dataRoot, { allowAbsent: true });
48
+ if (name === 'creation.root') return { contractVersion: 0, rootId: marker?.rootId ?? null,
49
+ ...([2, 3].includes(marker?.schemaVersion) ? { imageGeneration: true } : {}), ...(marker?.schemaVersion === 3 ? { audio: true } : {}) };
50
+ if (!marker) creationFail('IRIS_CREATION_NOT_FOUND', '此数据根尚无创建请求区');
51
+ const id = creationRequestId(marker, input.kind, input.key);
52
+ const request = readCreationRequest(dataRoot, id, marker);
53
+ if (!request) creationFail('IRIS_CREATION_NOT_FOUND', '此数据根没有对应的创建请求');
54
+ if (request.kind !== input.kind) creationFail('IRIS_CREATION_RESULT_CONFLICT', '原请求类型与查询身份不一致');
55
+ const intent = request.intent;
56
+ let result = null;
57
+ if (isImportCreation(request.kind)) {
58
+ let artifact;
59
+ try { artifact = readCoreArtifactBytes(dataRoot, intent.artifactId).artifact; }
60
+ catch (error) {
61
+ if (error.code !== 'IRIS_ARTIFACT_NOT_FOUND') throw error;
62
+ if (request.state === 'succeeded') creationFail('IRIS_CREATION_RESULT_UNAVAILABLE', '原创建结果已不可用,不会重新创建');
63
+ }
64
+ if (artifact) result = { artifactId: verifyImported(artifact, intent).id };
65
+ } else {
66
+ let run;
67
+ try { run = readFlowRun(dataRoot, intent.runId); }
68
+ catch (error) {
69
+ if (error.code !== 'IRIS_FLOW_NOT_FOUND') throw error;
70
+ if (request.state === 'succeeded') creationFail('IRIS_CREATION_RESULT_UNAVAILABLE', '原创建流程已不可用,不会重新创建');
71
+ }
72
+ if (run) {
73
+ try { flows.inspectCreation(dataRoot, intent, { allowCreate: false }); }
74
+ catch (error) {
75
+ if (['IRIS_FLOW_NOT_FOUND', 'IRIS_ARTIFACT_NOT_FOUND'].includes(error.code)) {
76
+ creationFail('IRIS_CREATION_RESULT_UNAVAILABLE', '原创建流程的作品已不可用,不会恢复数据');
77
+ }
78
+ throw error;
79
+ }
80
+ result = { runId: run.id };
81
+ }
82
+ }
83
+ return { contractVersion: 0, kind: request.kind, state: request.state, result };
84
+ });
85
+ }
26
86
  return runtime.run('execute', async ({ dataRoot, signal }) => {
27
87
  if (name === 'creation.init') return { contractVersion: 0, root: initializeCreationRoot(dataRoot) };
28
- if (!['creation.image', 'creation.flow'].includes(name)) throw new TypeError('未知本地创建操作');
29
- const kind = name === 'creation.image' ? 'image-import' : 'flow-create';
88
+ const kinds = { 'creation.image': 'image-import', 'creation.flow': 'flow-create', 'creation.generate': 'generation-create',
89
+ 'creation.audio': 'audio-import', 'creation.speech': 'speech-create', 'creation.transcribe': 'transcription-create' };
90
+ const kind = kinds[name];
91
+ if (!kind) throw new TypeError('未知本地创建操作');
92
+ if (['audio-import', 'speech-create', 'transcription-create'].includes(kind)) requireAudioRoot(dataRoot);
30
93
  const marker = inspectCreationRoot(dataRoot), id = creationRequestId(marker, kind, input.key);
31
94
  let source, owned;
32
- if (kind === 'image-import') {
33
- source = await readImportImage({ imageBytes: input.bytes }, signal);
95
+ if (isImportCreation(kind)) {
96
+ source = kind === 'audio-import' ? readImportAudio(input.bytes, input.mediaType) : await readImportImage({ imageBytes: input.bytes }, signal);
34
97
  if (source.mediaType !== input.mediaType) creationFail('IRIS_CREATION_MEDIA_MISMATCH', '图片真实格式与 Content-Type 不一致');
35
98
  owned = { mediaType: source.mediaType, size: source.bytes.length, sha256: sha(source.bytes) };
36
- } else owned = flowInput(input.value);
99
+ } else if (['speech-create', 'transcription-create'].includes(kind)) {
100
+ const value = input.value, speech = kind === 'speech-create';
101
+ const allowed = speech ? ['modelRef', 'text', 'voice'] : ['modelRef', 'sourceArtifactId'];
102
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !allowed.includes(key))) creationFail('IRIS_CREATION_INPUT_INVALID', '音频创建输入无效');
103
+ owned = normalizeAudioFlowInput(speech ? 'speech/v0' : 'transcribe/v0', { model_ref: value.modelRef,
104
+ ...(speech ? { text: value.text, ...(value.voice !== undefined ? { voice: value.voice } : {}) } : { source_artifact_id: value.sourceArtifactId }) });
105
+ } else owned = flowInput(input.value, kind === 'generation-create');
37
106
  const inputDigest = creationInputDigest(kind, owned);
38
107
  let request = readCreationRequest(dataRoot, id, marker);
39
108
  const replay = Boolean(request);
40
109
  if (request && request.inputDigest !== inputDigest) creationFail('IRIS_CREATION_KEY_CONFLICT', '该 Key 已绑定其他输入,请保留原请求或为新动作使用新 Key');
41
110
  if (!request) {
42
- const intent = kind === 'image-import'
111
+ const intent = isImportCreation(kind)
43
112
  ? { artifactId: 'artifact_' + crypto.randomBytes(12).toString('hex'), ...owned,
44
113
  metadata: source.metadata, createdAt: new Date().toISOString() }
45
- : await flows.prepareCreation(dataRoot, signal, owned);
114
+ : await flows.prepareCreation(dataRoot, signal, { ...owned, ...(kind === 'speech-create' ? { recipe: 'speech/v0' }
115
+ : kind === 'transcription-create' ? { recipe: 'transcribe/v0' } : {}) });
46
116
  signal.throwIfAborted();
47
117
  request = publishCreationRequest(dataRoot, { schemaVersion: 0, id, rootId: marker.rootId, kind,
48
118
  inputDigest, createdAt: intent.createdAt, intent, state: 'prepared' });
@@ -51,10 +121,10 @@ export function createLocalCreationService(runtime, { resolveImageCandidates, on
51
121
  signal.throwIfAborted();
52
122
  let result;
53
123
  const intent = request.intent;
54
- if (kind === 'image-import') {
124
+ if (isImportCreation(kind)) {
55
125
  if (request.state === 'prepared') {
56
126
  result = createCoreArtifact(dataRoot, { artifactId: intent.artifactId, bytes: source.bytes,
57
- mediaType: intent.mediaType, kind: 'imported-image', metadata: intent.metadata,
127
+ mediaType: intent.mediaType, kind: kind === 'audio-import' ? 'imported-audio' : 'imported-image', metadata: intent.metadata,
58
128
  expectedDigest: intent.sha256, expectedSize: intent.size, createdAt: intent.createdAt });
59
129
  } else {
60
130
  try { result = readCoreArtifactBytes(dataRoot, intent.artifactId).artifact; }
@@ -63,12 +133,7 @@ export function createLocalCreationService(runtime, { resolveImageCandidates, on
63
133
  creationFail('IRIS_CREATION_RESULT_UNAVAILABLE', '该请求的原结果已不可用;不会重新创建或恢复已删除作品');
64
134
  }
65
135
  }
66
- if (result.digest.value !== intent.sha256 || result.size !== intent.size || result.mediaType !== intent.mediaType
67
- || result.kind !== 'imported-image' || result.createdAt !== intent.createdAt
68
- || JSON.stringify(result.metadata) !== JSON.stringify(intent.metadata) || result.relations.length) {
69
- creationFail('IRIS_CREATION_RESULT_CONFLICT', '原作品与请求意图不一致');
70
- }
71
- result = { artifact: result };
136
+ result = { artifact: verifyImported(result, intent) };
72
137
  } else {
73
138
  try { result = { flow: flows.inspectCreation(dataRoot, intent, { allowCreate: request.state === 'prepared' }) }; }
74
139
  catch (error) {
@@ -0,0 +1,36 @@
1
+ // Explicit, store-only application surface; no Provider, scheduling loop or HTTP ownership.
2
+ import { initializeExecutionRoot, inspectExecutionRoot, acceptOperation, transitionOperation,
3
+ readOperation, operationId, listOperations, projectOperation, operationFail } from './operation-store.js';
4
+
5
+ export function createLocalOperationService(runtime) {
6
+ function execute(name, input) {
7
+ const readOnly = ['operation.inspect', 'operation.lookup', 'operation.list'].includes(name);
8
+ return runtime.run(readOnly ? 'inspect' : 'execute', ({ dataRoot, signal }) => {
9
+ signal.throwIfAborted();
10
+ if (name === 'execution.init') {
11
+ if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some(key => !['imageGeneration', 'audio'].includes(key))
12
+ || ['imageGeneration', 'audio'].some(key => input[key] !== undefined && typeof input[key] !== 'boolean')) operationFail('IRIS_OPERATION_INPUT_INVALID', 'execution.init 只接受可选 imageGeneration/audio 开关');
13
+ return { contractVersion: 0, root: initializeExecutionRoot(dataRoot, input), executionEnabled: false };
14
+ }
15
+ if (name === 'operation.accept') {
16
+ const result = acceptOperation(dataRoot, input);
17
+ return { contractVersion: 0, operation: projectOperation(result.operation), idempotentReplay: result.idempotentReplay };
18
+ }
19
+ if (name === 'operation.transition') return { contractVersion: 0, operation: projectOperation(transitionOperation(dataRoot, input)) };
20
+ if (name === 'operation.list') {
21
+ const result = listOperations(dataRoot, input);
22
+ return { contractVersion: 0, ...result, operations: result.operations.map(projectOperation) };
23
+ }
24
+ if (name === 'operation.inspect' || name === 'operation.lookup') {
25
+ const key = name === 'operation.inspect' ? 'id' : 'key';
26
+ if (!input || Object.keys(input).length !== 1 || typeof input[key] !== 'string') operationFail('IRIS_OPERATION_INPUT_INVALID', '需要单个 Operation ID 或请求 Key');
27
+ const marker = inspectExecutionRoot(dataRoot), id = key === 'id' ? input.id : operationId(marker, input.key);
28
+ const record = readOperation(dataRoot, id, marker);
29
+ if (!record) operationFail('IRIS_OPERATION_NOT_FOUND', 'Operation 不存在');
30
+ return { contractVersion: 0, operation: projectOperation(record) };
31
+ }
32
+ throw new TypeError('未知 Operation 存储操作');
33
+ });
34
+ }
35
+ return Object.freeze({ execute });
36
+ }
@@ -0,0 +1,86 @@
1
+ // Private request containers. Version selection is explicit; readers never initialize or migrate.
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { atomicCreatePrivate } from './private-storage.js';
6
+ import { inspectFlowRoot } from './flow-store.js';
7
+ import { WRITER_LEASE_DIR, WRITER_INTENT_FILE, isAbandonedWriterPreparation } from './core-writer-lease.js';
8
+ import { inspectRunnerArea } from './execution-ownership.js';
9
+
10
+ const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
11
+ export function requestRootFail(code, message) { throw Object.assign(new Error(message), { code }); }
12
+ const invalid = () => requestRootFail('IRIS_CREATION_STORE_INVALID', '创建请求区未知、损坏或尚未完整初始化');
13
+ function directory(root, segments, create = false) {
14
+ let current = root;
15
+ for (const segment of segments) {
16
+ current = path.join(current, segment);
17
+ if (create && !fs.existsSync(current)) fs.mkdirSync(current, { mode: 0o700 });
18
+ const stat = fs.lstatSync(current);
19
+ if (!stat.isDirectory() || stat.isSymbolicLink()) invalid();
20
+ }
21
+ return current;
22
+ }
23
+ export function requestDirectory(root, marker, leaf) {
24
+ return directory(root, ['creation-requests', 'v' + marker.schemaVersion, ...(leaf ? [leaf] : [])]);
25
+ }
26
+ export function inspectLocalRequestRoot(root, { allowAbsent = false } = {}) {
27
+ let base;
28
+ try { base = directory(root, ['creation-requests']); }
29
+ catch (error) {
30
+ if (error.code === 'ENOENT' && !fs.existsSync(path.join(root, 'creation-requests'))) {
31
+ if (allowAbsent) return null;
32
+ requestRootFail('IRIS_CREATION_ROOT_REQUIRED', '请先在空的专用 Flow 根显式执行 api init 或 execution init');
33
+ }
34
+ if (error.code === 'ENOENT') invalid();
35
+ throw error;
36
+ }
37
+ try {
38
+ const versions = fs.readdirSync(base);
39
+ if (versions.length !== 1 || !['v0', 'v1', 'v2', 'v3'].includes(versions[0])) invalid();
40
+ const schemaVersion = Number(versions[0].slice(1)), version = directory(root, ['creation-requests', versions[0]]);
41
+ const allowed = schemaVersion === 0 ? ['root.json', 'requests'] : ['root.json', 'requests', 'operations', 'runner'];
42
+ if (fs.readdirSync(version).some(name => !allowed.includes(name) && !/^root\.json\.tmp-[1-9][0-9]*-[a-f0-9]{12}$/.test(name))) invalid();
43
+ const file = path.join(version, 'root.json'), stat = fs.lstatSync(file);
44
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16384) invalid();
45
+ let marker;
46
+ try { marker = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) { invalid(); }
47
+ if (!marker || typeof marker !== 'object' || Array.isArray(marker)
48
+ || Object.keys(marker).length !== 4 || !['schemaVersion', 'kind', 'rootId', 'createdAt'].every(key => Object.hasOwn(marker, key))
49
+ || marker.schemaVersion !== schemaVersion || marker.kind !== (schemaVersion === 0 ? 'iris-creation-requests' : 'iris-execution-requests')
50
+ || !UUID.test(marker.rootId) || typeof marker.createdAt !== 'string'
51
+ || !Number.isFinite(Date.parse(marker.createdAt)) || new Date(marker.createdAt).toISOString() !== marker.createdAt) invalid();
52
+ directory(root, ['creation-requests', versions[0], 'requests']);
53
+ if (schemaVersion >= 1) {
54
+ inspectFlowRoot(root);
55
+ directory(root, ['creation-requests', versions[0], 'operations']);
56
+ // A separate, fully checked Core lease owns execution; it does not occupy the business writer.
57
+ try { inspectRunnerArea(directory(root, ['creation-requests', versions[0], 'runner'])); }
58
+ catch (error) { if (error.code === 'IRIS_EXECUTION_LEASE_INVALID') invalid(); throw error; }
59
+ }
60
+ return structuredClone(marker);
61
+ } catch (error) { if (error.code === 'ENOENT') invalid(); throw error; }
62
+ }
63
+
64
+ export function initializeLocalRequestRoot(root, schemaVersion) {
65
+ if (![0, 1, 2, 3].includes(schemaVersion)) throw new TypeError('未知请求容器版本');
66
+ const existing = inspectLocalRequestRoot(root, { allowAbsent: true });
67
+ if (existing) {
68
+ if (existing.schemaVersion !== schemaVersion) requestRootFail('IRIS_CREATION_ROOT_VERSION_CONFLICT', '请求根版本不同;不自动升级、降级或迁移,请使用新的专用根');
69
+ return existing;
70
+ }
71
+ inspectFlowRoot(root);
72
+ if (fs.readdirSync(root).some(name => !['flow-store', WRITER_LEASE_DIR, WRITER_INTENT_FILE].includes(name) && !isAbandonedWriterPreparation(root, name))
73
+ || fs.readdirSync(path.join(root, 'flow-store')).some(name => name !== 'v0')
74
+ || fs.readdirSync(path.join(root, 'flow-store/v0')).some(name => !['root.json', 'runs'].includes(name))
75
+ || fs.readdirSync(path.join(root, 'flow-store/v0/runs')).length) {
76
+ requestRootFail('IRIS_CREATION_ROOT_NOT_EMPTY', '请求区仅在新的空 Flow 根初始化;不转换已有创作数据');
77
+ }
78
+ const version = 'v' + schemaVersion;
79
+ for (const leaf of schemaVersion === 0 ? ['requests'] : ['requests', 'operations', 'runner']) {
80
+ directory(root, ['creation-requests', version, leaf], true);
81
+ }
82
+ const marker = { schemaVersion, kind: schemaVersion === 0 ? 'iris-creation-requests' : 'iris-execution-requests',
83
+ rootId: crypto.randomUUID(), createdAt: new Date().toISOString() };
84
+ atomicCreatePrivate(path.join(root, 'creation-requests', version, 'root.json'), JSON.stringify(marker, null, 2) + '\n');
85
+ return structuredClone(marker);
86
+ }
@@ -0,0 +1,37 @@
1
+ import fs from 'node:fs/promises';
2
+
3
+ // Fixed package resources only. Never resolve a request path against a filesystem directory.
4
+ const ASSETS = new Map([
5
+ ['/', ['index.html', 'text/html; charset=utf-8']],
6
+ ['/startup.js', ['startup.js', 'text/javascript; charset=utf-8']],
7
+ ['/app.js', ['app.js', 'text/javascript; charset=utf-8']],
8
+ ['/transport.js', ['transport.js', 'text/javascript; charset=utf-8']],
9
+ ['/workbench-login.js', ['workbench-login.js', 'text/javascript; charset=utf-8']],
10
+ ['/media-client.js', ['media-client.js', 'text/javascript; charset=utf-8']],
11
+ ['/recovery.js', ['recovery.js', 'text/javascript; charset=utf-8']],
12
+ ['/creation.js', ['creation.js', 'text/javascript; charset=utf-8']],
13
+ ['/gallery.js', ['gallery.js', 'text/javascript; charset=utf-8']],
14
+ ['/plugin-panel.js', ['plugin-panel.js', 'text/javascript; charset=utf-8']],
15
+ ['/plugin-surface.css', ['plugin-surface.css', 'text/css; charset=utf-8']],
16
+ ['/flow-view.js', ['flow-view.js', 'text/javascript; charset=utf-8']],
17
+ ['/connections.js', ['connections.js', 'text/javascript; charset=utf-8']],
18
+ ['/providers.js', ['providers.js', 'text/javascript; charset=utf-8']],
19
+ ['/comfyui.js', ['comfyui.js', 'text/javascript; charset=utf-8']],
20
+ ['/appearance.js', ['appearance.js', 'text/javascript; charset=utf-8']],
21
+ ['/styles.css', ['styles.css', 'text/css; charset=utf-8']],
22
+ ['/iris-mark.svg', ['../../docs/assets/logo/iris-mark.svg', 'image/svg+xml']]
23
+ ]);
24
+ export const LOCAL_WEB_CSP = "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' blob:; media-src blob:; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
25
+
26
+ export async function readLocalWebAsset(pathname) {
27
+ const entry = ASSETS.get(pathname);
28
+ if (!entry) return null;
29
+ return { bytes: await fs.readFile(new URL('./web/' + entry[0], import.meta.url)), mediaType: entry[1] };
30
+ }
31
+
32
+ export const isLocalWebAsset = pathname => ASSETS.has(pathname);
33
+
34
+ export const DEFAULT_PANEL = Object.freeze({
35
+ identity: Object.freeze({ contractVersion: 0, id: 'iris.default-panel', version: '0.1.0' }),
36
+ has: isLocalWebAsset, read: readLocalWebAsset
37
+ });
@@ -0,0 +1,58 @@
1
+ import { createCoreServiceClient } from './core-service-client.js';
2
+ import { createMcpServerFactory } from './mcp-server.js';
3
+ import { annotateCoreConnectionError, coreConnectionDiagnostic } from './core-connection-diagnostics.js';
4
+
5
+ /** Official SDK stdio server. All business calls use the same explicitly authorized Core service. */
6
+ export function createMcpBridge({ connectionFile, requestedScopes = ['read'], capabilities, tools, version, stdin = process.stdin, stdout = process.stdout } = {}) {
7
+ const client = createCoreServiceClient({ connectionFile, requestedScopes, requestedCapabilities: capabilities, requestedTools: tools, harnessVersion: 'Iris MCP bridge ' + version });
8
+ let state = 'new', handle, disposal, starting, closeRequested = false, resolveClosed, rejectClosed;
9
+ const closed = new Promise((resolve, reject) => { resolveClosed = resolve; rejectClosed = reject; });
10
+ void closed.catch(() => {});
11
+ function requireStarting() {
12
+ if (closeRequested) throw Object.assign(new Error('MCP 桥启动已停止'), { code: 'IRIS_MCP_CLOSED' });
13
+ }
14
+ function start() {
15
+ if (state !== 'new') return Promise.reject(Object.assign(new Error('MCP 桥只能显式启动一次'), { code: 'IRIS_MCP_CLOSED' }));
16
+ state = 'starting';
17
+ starting = (async () => { try {
18
+ await client.connect();
19
+ requireStarting();
20
+ if (client.snapshot().adapterId !== 'iris.mcp') throw Object.assign(new Error('连接文件不是 MCP 授权'), { code: 'IRIS_CONNECTION_INPUT_INVALID' });
21
+ const { serveStdio, StdioServerTransport } = await import('@modelcontextprotocol/server/stdio');
22
+ requireStarting();
23
+ const built = await createMcpServerFactory({ client, version });
24
+ requireStarting();
25
+ const transport = new StdioServerTransport(stdin, stdout, { maxBufferSize: 9 * 1024 * 1024 });
26
+ handle = serveStdio(built.factory, { transport, legacy: 'serve', maxSubscriptions: 4,
27
+ onerror: () => console.error('[iris] MCP transport error; no private request data logged') });
28
+ const sdkClose = transport.onclose;
29
+ transport.onclose = () => { sdkClose?.(); void dispose().catch(error => console.error('[iris] ' + JSON.stringify(coreConnectionDiagnostic(error, { stage: 'shutdown' })))); };
30
+ state = 'open';
31
+ return { contractVersion: 0, tools: built.tools, connection: client.snapshot() };
32
+ } catch (error) {
33
+ if (!closeRequested) state = 'failed';
34
+ try { await client.dispose(); } catch (_) { /* Original initialization failure remains the caller's error. */ }
35
+ if (!closeRequested) rejectClosed(error);
36
+ throw annotateCoreConnectionError(error, 'startup');
37
+ } })();
38
+ return starting;
39
+ }
40
+ function dispose() {
41
+ if (disposal) return disposal;
42
+ closeRequested = true; state = 'closing';
43
+ const disconnecting = client.dispose();
44
+ void disconnecting.catch(() => {});
45
+ disposal = Promise.resolve().then(async () => {
46
+ try {
47
+ if (starting) await Promise.allSettled([starting]);
48
+ const results = await Promise.allSettled([handle?.close(), disconnecting]);
49
+ const failed = results.filter(result => result.status === 'rejected');
50
+ if (failed.length) throw new AggregateError(failed.map(result => result.reason), 'MCP 桥释放失败');
51
+ state = 'closed'; resolveClosed({ state, remoteDisconnect: results[1].value.remoteDisconnect });
52
+ }
53
+ catch (error) { state = 'failed'; rejectClosed(error); throw error; }
54
+ });
55
+ return disposal;
56
+ }
57
+ return Object.freeze({ start, dispose, closed, snapshot: () => ({ state, connection: client.snapshot() }) });
58
+ }
@@ -0,0 +1,26 @@
1
+ import { createCoreServiceClient } from './core-service-client.js';
2
+ import { checkedClientConnection, readPrivateConnectionJson, connectionFail } from './core-connections.js';
3
+ import { bridgeToolsForScopes } from './core-bridge-tools.js';
4
+ import { coreConnectionDiagnostic } from './core-connection-diagnostics.js';
5
+
6
+ /** Explicit configuration + one temporary handshake. Never opens a writer, calls media or starts Core. */
7
+ export async function checkMcpConnection({ connectionFile, scopes = ['read'], capabilities, tools } = {}) {
8
+ const result = { contractVersion: 0, ok: false, checks: [], lifecycle: { core: 'externally-started', bridge: 'client-owned' }, mediaTested: false };
9
+ let client, stage = 'configuration';
10
+ try {
11
+ const connection = checkedClientConnection(readPrivateConnectionJson(connectionFile));
12
+ if (connection.adapterId !== 'iris.mcp') connectionFail('IRIS_CONNECTION_INPUT_INVALID', '请选择 MCP client.json');
13
+ result.checks.push('configuration'); result.origin = connection.origin;
14
+ client = createCoreServiceClient({ connection, requestedScopes: scopes, requestedCapabilities: capabilities, requestedTools: tools,
15
+ harnessVersion: 'Iris MCP connection check' });
16
+ stage = 'handshake'; await client.connect();
17
+ result.checks.push('transport', 'handshake'); result.connection = client.snapshot();
18
+ result.tools = bridgeToolsForScopes(result.connection.scopes, result.connection.capabilities, result.connection.tools, result.connection.pluginOperations).map(tool => tool.name);
19
+ result.checks.push('tool-selection'); result.ok = true;
20
+ } catch (error) { result.error = coreConnectionDiagnostic(error, { stage }); }
21
+ finally {
22
+ if (client) try { result.remoteDisconnect = (await client.dispose()).remoteDisconnect; }
23
+ catch (error) { result.ok = false; result.error = coreConnectionDiagnostic(error, { stage: 'disconnect' }); }
24
+ }
25
+ return result;
26
+ }
@@ -0,0 +1,42 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { checkedClientConnection, readPrivateConnectionJson, checkedConnectionScopes, connectionFail } from './core-connections.js';
5
+ import { checkedBridgeCapabilities, checkedBridgeTools, selectedBridgeTools } from './core-bridge-selection.js';
6
+ import { resolveMcpConfigurationChoice, formatMcpConfiguration } from './mcp-delivery.js';
7
+
8
+ /** Export configuration only. Never starts Core, contacts a Provider, or edits a harness configuration. */
9
+ export function exportMcpConfiguration({ connectionFile, scopes, capabilities, tools, transport, format, presetId, output } = {}) {
10
+ const connection = checkedClientConnection(readPrivateConnectionJson(connectionFile));
11
+ if (connection.adapterId !== 'iris.mcp') connectionFail('IRIS_CONNECTION_INPUT_INVALID', '请选择 MCP 连接文件');
12
+ ({ transport, format } = resolveMcpConfigurationChoice({ transport, format, presetId }));
13
+ let entry;
14
+ if (transport === 'stdio') {
15
+ if (scopes === undefined) scopes = ['read']; checkedConnectionScopes(scopes);
16
+ if (capabilities === undefined) capabilities = connection.capabilities; checkedBridgeCapabilities(capabilities);
17
+ if (tools === undefined) tools = selectedBridgeTools(capabilities, connection.tools, connection.pluginOperations); checkedBridgeTools(tools);
18
+ if (capabilities.some(id => !connection.capabilities.includes(id)) || tools.some(name => !connection.tools.includes(name) || !selectedBridgeTools(capabilities, undefined, connection.pluginOperations).includes(name))) {
19
+ connectionFail('IRIS_CONNECTION_CAPABILITY_DENIED', '导出配置不能扩大连接的功能或工具范围');
20
+ }
21
+ entry = { command: process.execPath, args: [fileURLToPath(new URL('../bin/dsh-iris.js', import.meta.url)), 'mcp', 'serve',
22
+ '--connection-file', path.resolve(connectionFile), '--scopes', scopes.join(','),
23
+ '--capabilities', capabilities.length ? capabilities.join(',') : 'none', '--tools', tools.length ? tools.join(',') : 'none'] };
24
+ } else {
25
+ if (scopes !== undefined || capabilities !== undefined || tools !== undefined) connectionFail('IRIS_CONNECTION_INPUT_INVALID', 'HTTP 的功能/工具/权限在创建连接授权时选择;请导出该专用连接');
26
+ if (!output) connectionFail('IRIS_CONNECTION_INPUT_INVALID', 'HTTP 配置含认证信息,需要明确的新私有输出文件,不写入 stdout');
27
+ entry = { url: connection.origin + '/mcp', headers: { Authorization: 'Bearer ' + connection.accessToken } };
28
+ }
29
+ const rendered = formatMcpConfiguration(entry, { transport, format });
30
+ if (!output) return format === 'codex-toml' ? rendered.content : rendered.configuration;
31
+ if (!path.isAbsolute(output)) connectionFail('IRIS_CONNECTION_INPUT_INVALID', '配置输出必须是新的绝对路径');
32
+ try { fs.realpathSync(path.dirname(output)); }
33
+ catch (error) { if (!['ENOENT', 'ENOTDIR'].includes(error.code)) throw error; connectionFail('IRIS_CONNECTION_INPUT_INVALID', '请先创建配置输出的父目录'); }
34
+ if (transport === 'http') {
35
+ const parent = fs.realpathSync(path.dirname(output)), privateParent = fs.realpathSync(path.dirname(connectionFile));
36
+ if (parent !== privateParent || process.platform !== 'win32' && fs.statSync(parent).mode & 0o077) {
37
+ connectionFail('IRIS_CONNECTION_INPUT_INVALID', '含认证信息的配置只导出到原连接的私有目录');
38
+ }
39
+ }
40
+ fs.writeFileSync(output, rendered.content, { flag: 'wx', mode: 0o600 });
41
+ return { contractVersion: 0, transport, format, output, containsCredentials: transport === 'http', configurationOnly: true };
42
+ }
@@ -0,0 +1,46 @@
1
+ import { connectionFail } from './core-connections.js';
2
+
3
+ export const MCP_CONFIG_FORMATS = Object.freeze(['mcpServers', 'entry', 'claude-json', 'codex-toml']);
4
+ const clients = [
5
+ { id: 'generic', title: '通用 MCP', format: 'mcpServers', documentationUrl: 'https://modelcontextprotocol.io/docs/develop/connect-local-servers', hint: '合并到客户端的 mcpServers 配置,或手动选择单条 entry。' },
6
+ { id: 'claude', title: 'Claude Code', format: 'claude-json', documentationUrl: 'https://code.claude.com/docs/en/mcp', hint: '将 iris 条目合并到项目 .mcp.json 的 mcpServers;已有配置请保留,随后在 /mcp 确认。' },
7
+ { id: 'codex', title: 'Codex', format: 'codex-toml', documentationUrl: 'https://learn.chatgpt.com/docs/extend/mcp?surface=cli', hint: '将 [mcp_servers.iris] 段合并到明确选择的 config.toml;保留其他段,随后在 /mcp 确认。' },
8
+ { id: 'kimi', title: 'Kimi Code', format: 'mcpServers', documentationUrl: 'https://moonshotai.github.io/kimi-code/en/customization/mcp', hint: '合并到明确选择的 mcp.json 的 mcpServers,创建新会话后在 /mcp 确认;安装位置由用户决定。' }
9
+ ];
10
+ export const MCP_CONNECTION_PRESETS = Object.freeze(clients.flatMap(client => ['stdio', 'http'].map(transport => Object.freeze({
11
+ presetId: client.id + '-' + transport, title: client.title + ' · ' + (transport === 'stdio' ? '本机 stdio' : 'HTTP'),
12
+ transport, format: client.format, documentationUrl: client.documentationUrl, hint: client.hint
13
+ }))));
14
+
15
+ export function resolveMcpConfigurationChoice({ presetId, transport, format } = {}) {
16
+ if (presetId !== undefined) {
17
+ if (transport !== undefined || format !== undefined) connectionFail('IRIS_CONNECTION_INPUT_INVALID', '接入预设不能与手动传输/格式混用');
18
+ const preset = MCP_CONNECTION_PRESETS.find(item => item.presetId === presetId);
19
+ if (!preset) connectionFail('IRIS_CONNECTION_INPUT_INVALID', '没有此接入预设;可选择通用 MCP 或手动格式');
20
+ return { transport: preset.transport, format: preset.format };
21
+ }
22
+ if (transport === undefined) transport = 'stdio';
23
+ if (format === undefined) format = 'mcpServers';
24
+ if (!['stdio', 'http'].includes(transport) || !MCP_CONFIG_FORMATS.includes(format)) connectionFail('IRIS_CONNECTION_INPUT_INVALID', '配置格式或传输方式无效');
25
+ return { transport, format };
26
+ }
27
+
28
+ /** Fixed, typed fields only. No shell interpolation, executable template or arbitrary TOML fragment. */
29
+ export function formatMcpConfiguration(entry, { transport, format }) {
30
+ let configuration, content, filename = 'iris-mcp.json', mediaType = 'application/json';
31
+ if (format === 'codex-toml') {
32
+ const native = transport === 'stdio' ? { command: entry.command, args: entry.args } : { url: entry.url, http_headers: entry.headers };
33
+ configuration = { mcp_servers: { iris: native } };
34
+ const lines = ['[mcp_servers.iris]'];
35
+ for (const [key, value] of Object.entries(native)) {
36
+ const quote = item => JSON.stringify(item).replace(/\x7f/g, '\\u007f');
37
+ const text = key === 'http_headers' ? '{ ' + Object.entries(value).map(([name, item]) => quote(name) + ' = ' + quote(item)).join(', ') + ' }' : quote(value);
38
+ lines.push(key + ' = ' + text);
39
+ }
40
+ content = lines.join('\n') + '\n'; filename = 'iris-mcp.toml'; mediaType = 'text/plain';
41
+ } else {
42
+ configuration = format === 'entry' ? entry : { mcpServers: { iris: format === 'claude-json' ? { type: transport, ...entry } : entry } };
43
+ content = JSON.stringify(configuration, null, 2) + '\n';
44
+ }
45
+ return { configuration, content, filename, mediaType };
46
+ }
@@ -0,0 +1,101 @@
1
+ import { Readable } from 'node:stream';
2
+ import { pipeline } from 'node:stream/promises';
3
+ import { createCoreServiceClient } from './core-service-client.js';
4
+ import { createMcpServerFactory, reportMcpClientInfo } from './mcp-server.js';
5
+ import { projectedMcpClient } from './core-connections.js';
6
+
7
+ /** Standard HTTP MCP edge. Core owns every business write; this edge only owns its local connections. */
8
+ export function createMcpHttpGateway({ getOrigin, authenticate, availableScopes, version }) {
9
+ const entries = new Map(), lifetime = new AbortController();
10
+ let state = 'new', createMcpHandler, disposal, starting;
11
+ function start() {
12
+ if (state !== 'new') return Promise.reject(Object.assign(new Error('MCP HTTP 入口只能显式启动一次'), { code: 'IRIS_MCP_CLOSED' }));
13
+ state = 'starting';
14
+ starting = (async () => {
15
+ ({ createMcpHandler } = await import('@modelcontextprotocol/server'));
16
+ if (state !== 'starting') throw Object.assign(new Error('MCP HTTP 启动已停止'), { code: 'IRIS_MCP_CLOSED' });
17
+ state = 'open';
18
+ })();
19
+ return starting;
20
+ }
21
+ function entryFor(grant, accessToken) {
22
+ if (entries.has(grant.connectionId)) return entries.get(grant.connectionId).ready;
23
+ const { scopes, ...identity } = grant;
24
+ const client = createCoreServiceClient({ connection: { schemaVersion: 0, origin: getOrigin(), ...identity, accessToken },
25
+ requestedScopes: scopes.filter(scope => availableScopes.includes(scope)), harnessVersion: 'Iris HTTP MCP bridge ' + version });
26
+ const entry = { client, ready: null };
27
+ entry.ready = (async () => {
28
+ try {
29
+ await client.connect();
30
+ const { factory } = await createMcpServerFactory({ client, version, transport: 'http' });
31
+ const handler = createMcpHandler(context => {
32
+ const server = factory();
33
+ if (context.authInfo?.extra?.irisObservation) context.authInfo.extra.irisObservation.server = server;
34
+ return server;
35
+ }, { legacy: 'stateless', maxRequestBodySize: 9 * 1024 * 1024,
36
+ maxSubscriptions: 4, onerror: () => console.error('[iris] MCP HTTP protocol error; private input omitted') });
37
+ return { client, handler };
38
+ } catch (error) {
39
+ if (entries.get(grant.connectionId) === entry) entries.delete(grant.connectionId);
40
+ await client.dispose(); throw error;
41
+ }
42
+ })();
43
+ entries.set(grant.connectionId, entry);
44
+ return entry.ready;
45
+ }
46
+ async function handle(request, response, accessToken) {
47
+ if (state !== 'open') throw Object.assign(new Error('MCP HTTP 服务正在关闭'), { code: 'IRIS_API_UNAVAILABLE' });
48
+ const grant = authenticate(accessToken);
49
+ const { handler, client } = await entryFor(grant, accessToken);
50
+ const disconnected = new AbortController();
51
+ const onClose = () => { if (!response.writableFinished) disconnected.abort(); };
52
+ response.once('close', onClose);
53
+ const signal = AbortSignal.any([disconnected.signal, lifetime.signal]);
54
+ try {
55
+ const headers = new Headers();
56
+ for (const [name, value] of Object.entries(request.headers)) {
57
+ if (Array.isArray(value)) for (const item of value) headers.append(name, item);
58
+ else if (value !== undefined) headers.set(name, value);
59
+ }
60
+ const input = new Request(getOrigin() + '/mcp', { method: request.method, headers, signal,
61
+ ...(['GET', 'HEAD'].includes(request.method) ? {} : { body: Readable.toWeb(request), duplex: 'half' }) });
62
+ const observation = {};
63
+ const result = await handler.fetch(input, { authInfo: { token: accessToken, clientId: grant.connectionId,
64
+ scopes: grant.scopes, extra: { irisObservation: observation } } });
65
+ if (result.ok && observation.server) void reportMcpClientInfo(client, projectedMcpClient(observation.server.server.getClientVersion()), 'http');
66
+ if (signal.aborted || response.destroyed) { await result.body?.cancel(); return; }
67
+ response.writeHead(result.status, { ...Object.fromEntries(result.headers), 'Cache-Control': 'no-store',
68
+ 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer' });
69
+ if (result.body && request.method !== 'HEAD') await pipeline(Readable.fromWeb(result.body), response);
70
+ else response.end();
71
+ } catch (error) {
72
+ if (!signal.aborted) throw error;
73
+ } finally { response.off('close', onClose); }
74
+ }
75
+ function dispose() {
76
+ if (disposal) return disposal;
77
+ state = 'closing'; lifetime.abort();
78
+ const pending = [...entries.values()];
79
+ const disconnecting = pending.map(entry => entry.client.dispose());
80
+ for (const result of disconnecting) void result.catch(() => {});
81
+ disposal = (async () => {
82
+ if (starting) await Promise.allSettled([starting]);
83
+ const ready = await Promise.allSettled(pending.map(entry => entry.ready));
84
+ const results = await Promise.allSettled([...disconnecting, ...ready.filter(result => result.status === 'fulfilled').map(result => result.value.handler.close())]);
85
+ entries.clear(); state = 'closed';
86
+ const failed = results.find(result => result.status === 'rejected');
87
+ if (failed) throw failed.reason;
88
+ })();
89
+ return disposal;
90
+ }
91
+ async function invalidateConnection(id) {
92
+ const pending = entries.get(id); entries.delete(id);
93
+ if (pending) {
94
+ const closing = pending.client.dispose(); void closing.catch(() => {});
95
+ const [ready] = await Promise.allSettled([pending.ready]);
96
+ const results = await Promise.allSettled([closing, ...(ready.status === 'fulfilled' ? [ready.value.handler.close()] : [])]);
97
+ const failed = results.find(result => result.status === 'rejected'); if (failed) throw failed.reason;
98
+ }
99
+ }
100
+ return Object.freeze({ start, handle, invalidateConnection, dispose, snapshot: () => ({ state, connections: entries.size }) });
101
+ }