@mokuyoaxis/dsh-iris 0.2.7 → 0.3.0-beta.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 (234) hide show
  1. package/CHANGELOG.md +143 -1
  2. package/README.en.md +109 -311
  3. package/README.md +106 -309
  4. package/bin/dsh-iris.js +452 -92
  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/ARTIFACT_MANIFEST.md +4 -0
  9. package/docs/AUDIO_FLOWS.md +87 -0
  10. package/docs/BETA_SCOPE.md +42 -0
  11. package/docs/CONTACT_SHEET.md +62 -0
  12. package/docs/CORE_CONNECTIONS.md +101 -0
  13. package/docs/CORE_QUICKSTART.md +110 -0
  14. package/docs/CORE_RUNTIME_CONTRACT.md +7 -2
  15. package/docs/CORE_SERVICE.md +101 -0
  16. package/docs/CORE_SKILLS.md +35 -3
  17. package/docs/DSH_COMPATIBILITY.md +21 -1
  18. package/docs/DSH_CORE_MIGRATION.md +6 -0
  19. package/docs/DSH_RC2_ADAPTATION.md +1 -1
  20. package/docs/DSH_RUNTIME.md +6 -2
  21. package/docs/HEADLESS_CLI.md +30 -5
  22. package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
  23. package/docs/IMAGE_FLOW.md +4 -0
  24. package/docs/IMAGE_GENERATION.md +127 -0
  25. package/docs/LOCAL_API.md +4 -2
  26. package/docs/LOCAL_API_CONTRACT.md +42 -15
  27. package/docs/LOCAL_CREATION.md +7 -1
  28. package/docs/LOCAL_OPERATIONS.md +46 -0
  29. package/docs/LOCAL_SELECTION.md +57 -0
  30. package/docs/LOCAL_WEB.md +207 -0
  31. package/docs/MCP.md +212 -0
  32. package/docs/MEDIA_SCHEDULING.md +72 -0
  33. package/docs/MUSIC.md +2 -0
  34. package/docs/NETWORK_RESOURCES.md +106 -0
  35. package/docs/NETWORK_ROUTING.md +103 -0
  36. package/docs/NETWORK_WORKBENCH.md +65 -0
  37. package/docs/PANEL_EXTENSIONS.md +64 -0
  38. package/docs/PLUGINS.md +193 -0
  39. package/docs/PLUGIN_DEVELOPMENT.md +39 -0
  40. package/docs/PLUGIN_STARTUP.md +113 -0
  41. package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
  42. package/docs/PROVIDER_CREDENTIALS.md +67 -0
  43. package/docs/PROVIDER_EXTENSIONS.md +82 -0
  44. package/docs/PROVIDER_HEALTH.md +5 -1
  45. package/docs/PROVIDER_NETWORK.md +45 -0
  46. package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
  47. package/docs/PROVIDER_SETTINGS.md +91 -0
  48. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +8 -0
  49. package/docs/ROADMAP.md +18 -8
  50. package/docs/SECURITY.md +1 -1
  51. package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
  52. package/docs/VIDEO_FLOWS.md +89 -0
  53. package/docs/VISION_CLI.md +1 -1
  54. package/docs/VISION_MODEL.md +2 -0
  55. package/docs/WEB_PROTOTYPE.md +21 -6
  56. package/docs/WEB_RECOVERY.md +46 -0
  57. package/docs/WORKBENCH_ARTIFACTS.md +6 -0
  58. package/docs/WORKBENCH_LOGIN.md +63 -0
  59. package/docs/assets/screenshots/core-connections-preview.webp +0 -0
  60. package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
  61. package/docs/assets/screenshots/core-overview-preview.webp +0 -0
  62. package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
  63. package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
  64. package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
  65. package/docs/assets/screenshots/core-studio-preview.webp +0 -0
  66. package/docs/releases/0.3.0-alpha.1.md +51 -0
  67. package/docs/releases/0.3.0-beta.1.md +34 -0
  68. package/docs/screenshots.md +41 -1
  69. package/lib/actions.js +56 -4
  70. package/lib/adapters.js +75 -49
  71. package/lib/api.js +23 -13
  72. package/lib/application-runtime.js +73 -17
  73. package/lib/application-writer.js +193 -0
  74. package/lib/audio-flow-input.js +37 -0
  75. package/lib/client.d.ts +3 -3
  76. package/lib/client.js +402 -72
  77. package/lib/comfyui-adapter.js +95 -0
  78. package/lib/comfyui-client.js +60 -0
  79. package/lib/comfyui-inspection.js +73 -0
  80. package/lib/comfyui-workflow.js +94 -0
  81. package/lib/command-service.js +18 -5
  82. package/lib/config.js +49 -10
  83. package/lib/configured-image-runtime.js +69 -0
  84. package/lib/connection-skill.js +98 -0
  85. package/lib/connection-template.js +23 -0
  86. package/lib/core-artifact-store.js +84 -28
  87. package/lib/core-bridge-selection.js +31 -0
  88. package/lib/core-bridge-tools.js +85 -0
  89. package/lib/core-capability-catalog.js +192 -0
  90. package/lib/core-connection-diagnostics.js +49 -0
  91. package/lib/core-connection-store.js +277 -0
  92. package/lib/core-connections.js +225 -0
  93. package/lib/core-delivery-cache.js +8 -3
  94. package/lib/core-maintenance.js +9 -3
  95. package/lib/core-runtime.js +2 -1
  96. package/lib/core-service-client.js +174 -0
  97. package/lib/core-tasks.js +113 -21
  98. package/lib/core-writer-lease.js +16 -0
  99. package/lib/creation-request-store.js +41 -64
  100. package/lib/doctor.js +2 -2
  101. package/lib/dsh-core-adapter.js +61 -9
  102. package/lib/dsh-host-adapter.js +4 -1
  103. package/lib/dsh-provider-config.js +1 -1
  104. package/lib/dsh-service-bridge.js +67 -0
  105. package/lib/execution-ownership.js +43 -0
  106. package/lib/flow-store.js +156 -14
  107. package/lib/generation-input.js +10 -1
  108. package/lib/headless-vision.js +7 -7
  109. package/lib/image-edit-input.js +1 -1
  110. package/lib/image-flow.js +97 -31
  111. package/lib/index.js +47 -22
  112. package/lib/local-api-projection.js +15 -6
  113. package/lib/local-api-token-directory.js +31 -0
  114. package/lib/local-api.js +861 -71
  115. package/lib/local-creation.js +96 -24
  116. package/lib/local-operations.js +36 -0
  117. package/lib/local-request-root.js +86 -0
  118. package/lib/local-web-assets.js +39 -0
  119. package/lib/mcp-bridge.js +58 -0
  120. package/lib/mcp-check.js +26 -0
  121. package/lib/mcp-config.js +42 -0
  122. package/lib/mcp-delivery.js +46 -0
  123. package/lib/mcp-http.js +174 -0
  124. package/lib/mcp-server.js +69 -0
  125. package/lib/media-scheduler.js +194 -0
  126. package/lib/model-directory-service.js +37 -10
  127. package/lib/model-directory.js +3 -3
  128. package/lib/models.js +2 -2
  129. package/lib/music-transport.js +6 -3
  130. package/lib/network-config-service.js +123 -0
  131. package/lib/network-config.js +127 -0
  132. package/lib/network-doctor.js +94 -0
  133. package/lib/network-management.js +51 -0
  134. package/lib/network-policy.js +19 -0
  135. package/lib/network-route-evidence.js +38 -0
  136. package/lib/network-routing.js +283 -0
  137. package/lib/operation-runner.js +281 -0
  138. package/lib/operation-store.js +216 -0
  139. package/lib/panel-extension.js +36 -0
  140. package/lib/plugin-contract.js +194 -0
  141. package/lib/plugin-execution.js +51 -0
  142. package/lib/plugin-launch.js +86 -0
  143. package/lib/plugin-operation-store.js +101 -0
  144. package/lib/plugin-operations.js +28 -0
  145. package/lib/plugin-registry.js +109 -0
  146. package/lib/plugin-selection.js +15 -0
  147. package/lib/plugin-source-image.js +5 -0
  148. package/lib/plugin-web-assets.js +54 -0
  149. package/lib/plugins.d.ts +307 -0
  150. package/lib/plugins.js +59 -0
  151. package/lib/provider-adapter.js +7 -3
  152. package/lib/provider-adapters.js +92 -22
  153. package/lib/provider-catalog.js +64 -36
  154. package/lib/provider-config-format.js +51 -0
  155. package/lib/provider-config-service.js +143 -31
  156. package/lib/provider-context.d.ts +90 -0
  157. package/lib/provider-context.js +42 -0
  158. package/lib/provider-credentials.js +97 -0
  159. package/lib/provider-diagnostics.js +56 -0
  160. package/lib/provider-extensions.js +29 -0
  161. package/lib/provider-health.js +18 -6
  162. package/lib/provider-monitor.js +81 -0
  163. package/lib/provider-network.js +52 -0
  164. package/lib/provider-protocol.js +73 -22
  165. package/lib/provider-registry.js +128 -0
  166. package/lib/provider-response.js +38 -0
  167. package/lib/provider-settings.js +136 -0
  168. package/lib/provider-task-runner.js +132 -43
  169. package/lib/provider-transport.js +16 -0
  170. package/lib/render.js +11 -1
  171. package/lib/scheduling-policy.js +66 -0
  172. package/lib/skill-installation.js +23 -11
  173. package/lib/tasks.js +3 -2
  174. package/lib/video-flow-input.js +16 -0
  175. package/lib/video-input.js +12 -5
  176. package/lib/vision-model-routing.js +5 -0
  177. package/lib/web/app.js +703 -0
  178. package/lib/web/appearance.js +111 -0
  179. package/lib/web/comfyui.js +146 -0
  180. package/lib/web/connections.js +516 -0
  181. package/lib/web/creation.js +359 -0
  182. package/lib/web/flow-view.js +34 -0
  183. package/lib/web/gallery.js +133 -0
  184. package/lib/web/index.html +344 -0
  185. package/lib/web/media-client.js +218 -0
  186. package/lib/web/network.js +232 -0
  187. package/lib/web/plugin-panel.js +223 -0
  188. package/lib/web/plugin-status.js +140 -0
  189. package/lib/web/plugin-surface.css +17 -0
  190. package/lib/web/providers.js +513 -0
  191. package/lib/web/recovery.js +105 -0
  192. package/lib/web/startup.js +18 -0
  193. package/lib/web/styles.css +571 -0
  194. package/lib/web/transport.js +101 -0
  195. package/lib/web/workbench-login.js +165 -0
  196. package/lib/workbench-artifacts.js +3 -2
  197. package/lib/workbench-auth-cli.js +47 -0
  198. package/lib/workbench-auth.js +200 -0
  199. package/package.json +24 -6
  200. package/plugins/contact-sheet/index.d.mts +4 -0
  201. package/plugins/contact-sheet/index.mjs +33 -0
  202. package/plugins/contact-sheet/web/board.css +14 -0
  203. package/plugins/contact-sheet/web/board.mjs +93 -0
  204. package/skills/iris-core/SKILL.md +8 -4
  205. package/skills/iris-core/references/cli.md +2 -2
  206. package/skills/iris-core/references/comfyui.md +69 -0
  207. package/skills/iris-core/references/tasks.md +2 -0
  208. package/templates/plugin-starter/LICENSE +21 -0
  209. package/templates/plugin-starter/README.md +74 -0
  210. package/templates/plugin-starter/assets/peach.png +0 -0
  211. package/templates/plugin-starter/assets/violet.png +0 -0
  212. package/templates/plugin-starter/index.mjs +27 -0
  213. package/templates/plugin-starter/package.json +18 -0
  214. package/templates/plugin-starter/scripts/core-cli.mjs +20 -0
  215. package/templates/plugin-starter/scripts/environment.mjs +65 -0
  216. package/templates/plugin-starter/scripts/mcp-probe.mjs +22 -0
  217. package/templates/plugin-starter/scripts/setup.mjs +42 -0
  218. package/templates/plugin-starter/scripts/smoke.mjs +76 -0
  219. package/templates/plugin-starter/web/board.css +27 -0
  220. package/templates/plugin-starter/web/board.mjs +160 -0
  221. package/templates/plugin-starter/web/png.mjs +23 -0
  222. package/templates/provider-starter/LICENSE +21 -0
  223. package/templates/provider-starter/README.md +50 -0
  224. package/templates/provider-starter/index.d.mts +5 -0
  225. package/templates/provider-starter/index.mjs +165 -0
  226. package/templates/provider-starter/package.json +13 -0
  227. package/templates/provider-starter/scripts/core-cli.mjs +22 -0
  228. package/templates/provider-starter/scripts/environment.mjs +122 -0
  229. package/templates/provider-starter/scripts/fixture.mjs +54 -0
  230. package/templates/provider-starter/scripts/mcp-probe.mjs +21 -0
  231. package/templates/provider-starter/scripts/protocol-smoke.mjs +48 -0
  232. package/templates/provider-starter/scripts/setup.mjs +44 -0
  233. package/templates/provider-starter/scripts/smoke.mjs +145 -0
  234. package/user_guide.md +5 -1
@@ -4,45 +4,122 @@ 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';
9
+ import { VIDEO_RECIPE, normalizeVideoFlowInput } from './video-flow-input.js';
7
10
  import { initializeCreationRoot, inspectCreationRoot, creationRequestId, creationInputDigest,
8
- readCreationRequest, publishCreationRequest, completeCreationRequest, creationFail } from './creation-request-store.js';
11
+ readCreationRequest, publishCreationRequest, completeCreationRequest, creationFail, CREATION_KINDS, isImportCreation } from './creation-request-store.js';
9
12
 
10
13
  const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
11
- function flowInput(input) {
14
+ function verifyImported(artifact, intent) {
15
+ if (artifact.digest.value !== intent.sha256 || artifact.size !== intent.size || artifact.mediaType !== intent.mediaType
16
+ || artifact.kind !== (intent.mediaType.startsWith('audio/') ? 'imported-audio' : 'imported-image') || artifact.createdAt !== intent.createdAt
17
+ || JSON.stringify(artifact.metadata) !== JSON.stringify(intent.metadata) || artifact.relations.length) {
18
+ creationFail('IRIS_CREATION_RESULT_CONFLICT', '原作品与请求意图不一致');
19
+ }
20
+ return artifact;
21
+ }
22
+ function flowInput(input, generate = false) {
23
+ const required = generate ? ['modelRef', 'prompt'] : ['sourceArtifactId', 'modelRef', 'prompt'];
12
24
  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')
25
+ || Object.keys(input).some(key => ![...required, 'size', ...(generate ? ['workflowParameters'] : [])].includes(key))
26
+ || !required.every(key => typeof input[key] === 'string')
15
27
  || input.size !== undefined && typeof input.size !== 'string') {
16
28
  creationFail('IRIS_CREATION_INPUT_INVALID', '流程创建只接受来源作品、明确模型、Prompt 和可选 size');
17
29
  }
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,
30
+ const normalized = normalizeGenerationInput('image', { ...(!generate ? { source_artifact_id: input.sourceArtifactId } : {}),
31
+ model_ref: input.modelRef, prompt: input.prompt, ...(input.size !== undefined ? { size: input.size } : {}),
32
+ ...(input.workflowParameters !== undefined ? { workflow_parameters: input.workflowParameters } : {}) });
33
+ return { ...(normalized.sourceArtifactId ? { source_artifact_id: normalized.sourceArtifactId } : {}), model_ref: normalized.modelRef,
21
34
  ...normalized.providerInput };
22
35
  }
23
- export function createLocalCreationService(runtime, { resolveImageCandidates, onPrepared } = {}) {
24
- const flows = createImageFlowService(runtime, { resolveImageCandidates });
36
+ export function createLocalCreationService(runtime, { resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates, onPrepared } = {}) {
37
+ const flows = createImageFlowService(runtime, { resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates });
25
38
  function execute(name, input) {
39
+ if (name === 'creation.root' || name === 'creation.lookup') {
40
+ return runtime.run('inspect', ({ dataRoot, signal }) => {
41
+ signal.throwIfAborted();
42
+ const fields = name === 'creation.root' ? [] : ['kind', 'key'];
43
+ if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).length !== fields.length
44
+ || fields.some(key => !Object.hasOwn(input, key))
45
+ || name === 'creation.lookup' && (!CREATION_KINDS.includes(input.kind) || typeof input.key !== 'string')) {
46
+ creationFail('IRIS_CREATION_INPUT_INVALID', '需要明确的创建类型和原 Key');
47
+ }
48
+ const marker = inspectCreationRoot(dataRoot, { allowAbsent: true });
49
+ if (name === 'creation.root') return { contractVersion: 0, rootId: marker?.rootId ?? null,
50
+ ...([2, 3, 4].includes(marker?.schemaVersion) ? { imageGeneration: true } : {}), ...([3, 4].includes(marker?.schemaVersion) ? { audio: true } : {}),
51
+ ...(marker?.schemaVersion === 4 ? { video: true } : {}) };
52
+ if (!marker) creationFail('IRIS_CREATION_NOT_FOUND', '此数据根尚无创建请求区');
53
+ const id = creationRequestId(marker, input.kind, input.key);
54
+ const request = readCreationRequest(dataRoot, id, marker);
55
+ if (!request) creationFail('IRIS_CREATION_NOT_FOUND', '此数据根没有对应的创建请求');
56
+ if (request.kind !== input.kind) creationFail('IRIS_CREATION_RESULT_CONFLICT', '原请求类型与查询身份不一致');
57
+ const intent = request.intent;
58
+ let result = null;
59
+ if (isImportCreation(request.kind)) {
60
+ let artifact;
61
+ try { artifact = readCoreArtifactBytes(dataRoot, intent.artifactId).artifact; }
62
+ catch (error) {
63
+ if (error.code !== 'IRIS_ARTIFACT_NOT_FOUND') throw error;
64
+ if (request.state === 'succeeded') creationFail('IRIS_CREATION_RESULT_UNAVAILABLE', '原创建结果已不可用,不会重新创建');
65
+ }
66
+ if (artifact) result = { artifactId: verifyImported(artifact, intent).id };
67
+ } else {
68
+ let run;
69
+ try { run = readFlowRun(dataRoot, intent.runId); }
70
+ catch (error) {
71
+ if (error.code !== 'IRIS_FLOW_NOT_FOUND') throw error;
72
+ if (request.state === 'succeeded') creationFail('IRIS_CREATION_RESULT_UNAVAILABLE', '原创建流程已不可用,不会重新创建');
73
+ }
74
+ if (run) {
75
+ try { flows.inspectCreation(dataRoot, intent, { allowCreate: false }); }
76
+ catch (error) {
77
+ if (['IRIS_FLOW_NOT_FOUND', 'IRIS_ARTIFACT_NOT_FOUND'].includes(error.code)) {
78
+ creationFail('IRIS_CREATION_RESULT_UNAVAILABLE', '原创建流程的作品已不可用,不会恢复数据');
79
+ }
80
+ throw error;
81
+ }
82
+ result = { runId: run.id };
83
+ }
84
+ }
85
+ return { contractVersion: 0, kind: request.kind, state: request.state, result };
86
+ });
87
+ }
26
88
  return runtime.run('execute', async ({ dataRoot, signal }) => {
27
89
  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';
90
+ const kinds = { 'creation.image': 'image-import', 'creation.flow': 'flow-create', 'creation.generate': 'generation-create',
91
+ 'creation.audio': 'audio-import', 'creation.speech': 'speech-create', 'creation.transcribe': 'transcription-create', 'creation.video': 'video-create' };
92
+ const kind = kinds[name];
93
+ if (!kind) throw new TypeError('未知本地创建操作');
94
+ if (['audio-import', 'speech-create', 'transcription-create'].includes(kind)) requireAudioRoot(dataRoot);
30
95
  const marker = inspectCreationRoot(dataRoot), id = creationRequestId(marker, kind, input.key);
31
96
  let source, owned;
32
- if (kind === 'image-import') {
33
- source = await readImportImage({ imageBytes: input.bytes }, signal);
97
+ if (isImportCreation(kind)) {
98
+ source = kind === 'audio-import' ? readImportAudio(input.bytes, input.mediaType) : await readImportImage({ imageBytes: input.bytes }, signal);
34
99
  if (source.mediaType !== input.mediaType) creationFail('IRIS_CREATION_MEDIA_MISMATCH', '图片真实格式与 Content-Type 不一致');
35
100
  owned = { mediaType: source.mediaType, size: source.bytes.length, sha256: sha(source.bytes) };
36
- } else owned = flowInput(input.value);
101
+ } else if (['speech-create', 'transcription-create'].includes(kind)) {
102
+ const value = input.value, speech = kind === 'speech-create';
103
+ const allowed = speech ? ['modelRef', 'text', 'voice'] : ['modelRef', 'sourceArtifactId'];
104
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !allowed.includes(key))) creationFail('IRIS_CREATION_INPUT_INVALID', '音频创建输入无效');
105
+ owned = normalizeAudioFlowInput(speech ? 'speech/v0' : 'transcribe/v0', { model_ref: value.modelRef,
106
+ ...(speech ? { text: value.text, ...(value.voice !== undefined ? { voice: value.voice } : {}) } : { source_artifact_id: value.sourceArtifactId }) });
107
+ } else if (kind === 'video-create') {
108
+ const value = input.value;
109
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !['modelRef', 'prompt', 'size', 'duration'].includes(key))) creationFail('IRIS_CREATION_INPUT_INVALID', '文生视频输入无效');
110
+ owned = normalizeVideoFlowInput({ model_ref: value.modelRef, prompt: value.prompt,
111
+ ...(value.size !== undefined ? { size: value.size } : {}), ...(value.duration !== undefined ? { duration: value.duration } : {}) });
112
+ } else owned = flowInput(input.value, kind === 'generation-create');
37
113
  const inputDigest = creationInputDigest(kind, owned);
38
114
  let request = readCreationRequest(dataRoot, id, marker);
39
115
  const replay = Boolean(request);
40
116
  if (request && request.inputDigest !== inputDigest) creationFail('IRIS_CREATION_KEY_CONFLICT', '该 Key 已绑定其他输入,请保留原请求或为新动作使用新 Key');
41
117
  if (!request) {
42
- const intent = kind === 'image-import'
118
+ const intent = isImportCreation(kind)
43
119
  ? { artifactId: 'artifact_' + crypto.randomBytes(12).toString('hex'), ...owned,
44
120
  metadata: source.metadata, createdAt: new Date().toISOString() }
45
- : await flows.prepareCreation(dataRoot, signal, owned);
121
+ : await flows.prepareCreation(dataRoot, signal, { ...owned, ...(kind === 'speech-create' ? { recipe: 'speech/v0' }
122
+ : kind === 'transcription-create' ? { recipe: 'transcribe/v0' } : kind === 'video-create' ? { recipe: VIDEO_RECIPE } : {}) });
46
123
  signal.throwIfAborted();
47
124
  request = publishCreationRequest(dataRoot, { schemaVersion: 0, id, rootId: marker.rootId, kind,
48
125
  inputDigest, createdAt: intent.createdAt, intent, state: 'prepared' });
@@ -51,10 +128,10 @@ export function createLocalCreationService(runtime, { resolveImageCandidates, on
51
128
  signal.throwIfAborted();
52
129
  let result;
53
130
  const intent = request.intent;
54
- if (kind === 'image-import') {
131
+ if (isImportCreation(kind)) {
55
132
  if (request.state === 'prepared') {
56
133
  result = createCoreArtifact(dataRoot, { artifactId: intent.artifactId, bytes: source.bytes,
57
- mediaType: intent.mediaType, kind: 'imported-image', metadata: intent.metadata,
134
+ mediaType: intent.mediaType, kind: kind === 'audio-import' ? 'imported-audio' : 'imported-image', metadata: intent.metadata,
58
135
  expectedDigest: intent.sha256, expectedSize: intent.size, createdAt: intent.createdAt });
59
136
  } else {
60
137
  try { result = readCoreArtifactBytes(dataRoot, intent.artifactId).artifact; }
@@ -63,12 +140,7 @@ export function createLocalCreationService(runtime, { resolveImageCandidates, on
63
140
  creationFail('IRIS_CREATION_RESULT_UNAVAILABLE', '该请求的原结果已不可用;不会重新创建或恢复已删除作品');
64
141
  }
65
142
  }
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 };
143
+ result = { artifact: verifyImported(result, intent) };
72
144
  } else {
73
145
  try { result = { flow: flows.inspectCreation(dataRoot, intent, { allowCreate: request.state === 'prepared' }) }; }
74
146
  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', 'video'].includes(key))
12
+ || ['imageGeneration', 'audio', 'video'].some(key => input[key] !== undefined && typeof input[key] !== 'boolean')) operationFail('IRIS_OPERATION_INPUT_INVALID', 'execution.init 只接受可选 imageGeneration/audio/video 开关');
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', 'v4'].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, 4].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,39 @@
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-status.js', ['plugin-status.js', 'text/javascript; charset=utf-8']],
16
+ ['/plugin-surface.css', ['plugin-surface.css', 'text/css; charset=utf-8']],
17
+ ['/flow-view.js', ['flow-view.js', 'text/javascript; charset=utf-8']],
18
+ ['/connections.js', ['connections.js', 'text/javascript; charset=utf-8']],
19
+ ['/providers.js', ['providers.js', 'text/javascript; charset=utf-8']],
20
+ ['/network.js', ['network.js', 'text/javascript; charset=utf-8']],
21
+ ['/comfyui.js', ['comfyui.js', 'text/javascript; charset=utf-8']],
22
+ ['/appearance.js', ['appearance.js', 'text/javascript; charset=utf-8']],
23
+ ['/styles.css', ['styles.css', 'text/css; charset=utf-8']],
24
+ ['/iris-mark.svg', ['../../docs/assets/logo/iris-mark.svg', 'image/svg+xml']]
25
+ ]);
26
+ 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'";
27
+
28
+ export async function readLocalWebAsset(pathname) {
29
+ const entry = ASSETS.get(pathname);
30
+ if (!entry) return null;
31
+ return { bytes: await fs.readFile(new URL('./web/' + entry[0], import.meta.url)), mediaType: entry[1] };
32
+ }
33
+
34
+ export const isLocalWebAsset = pathname => ASSETS.has(pathname);
35
+
36
+ export const DEFAULT_PANEL = Object.freeze({
37
+ identity: Object.freeze({ contractVersion: 0, id: 'iris.default-panel', version: '0.1.0' }),
38
+ has: isLocalWebAsset, read: readLocalWebAsset
39
+ });
@@ -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
+ }