@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
@@ -0,0 +1,216 @@
1
+ // Durable, non-executing local intentions. Call mutations under the supplied Core writer.
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { atomicCreatePrivate, atomicWritePrivate } from './private-storage.js';
6
+ import { inspectLocalRequestRoot, initializeLocalRequestRoot, requestDirectory } from './local-request-root.js';
7
+ import { listCreationRequests } from './creation-request-store.js';
8
+ import { readFlowRun, flowStep } from './flow-store.js';
9
+ import { findFlowTask } from './core-tasks.js';
10
+ import { findCoreArtifactMetadata } from './core-artifacts.js';
11
+ import { validatePluginOperation, validatePluginBinding, transitionPluginOperation, projectPluginOperation } from './plugin-operation-store.js';
12
+
13
+ const ID = /^op_[a-f0-9]{64}$/;
14
+ const RUN = /^run_[a-f0-9]{24}$/;
15
+ const TASK = /^task_[a-f0-9]{24}$/;
16
+ const ARTIFACT = /^artifact_[a-f0-9]{24}$/;
17
+ const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
18
+ const DIGEST = /^sha256:[a-f0-9]{64}$/;
19
+ const STATES = new Set(['queued', 'running', 'succeeded', 'failed', 'canceled', 'interrupted']);
20
+ const ACTIVE = new Set(['queued', 'running']);
21
+ const FAILURE_CODES = new Set(['run_changed', 'execution_failed']);
22
+ export const MAX_ACTIVE_OPERATIONS = 8;
23
+ export function operationFail(code, message) { throw Object.assign(new Error(message), { code }); }
24
+ const invalid = () => operationFail('IRIS_OPERATION_STORE_INVALID', 'Operation 记录未知、损坏或与请求容器不一致');
25
+ const hash = value => crypto.createHash('sha256').update(value).digest('hex');
26
+ const iso = value => typeof value === 'string' && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
27
+ const positive = value => Number.isSafeInteger(value) && value >= 1;
28
+ function keys(value, allowed, required = allowed) {
29
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value)
30
+ && Object.keys(value).every(key => allowed.includes(key)) && required.every(key => Object.hasOwn(value, key)));
31
+ }
32
+ function input(value, allowed, required = allowed) {
33
+ if (!keys(value, allowed, required)) operationFail('IRIS_OPERATION_INPUT_INVALID', 'Operation 输入包含缺失或不支持的字段');
34
+ }
35
+ export function inspectExecutionRoot(root) {
36
+ const marker = inspectLocalRequestRoot(root, { allowAbsent: true });
37
+ if (![1, 2, 3, 4].includes(marker?.schemaVersion)) operationFail('IRIS_OPERATION_ROOT_REQUIRED', 'Operation 需要新的专用执行根,请显式执行 execution init;不转换原根');
38
+ return marker;
39
+ }
40
+ export function initializeExecutionRoot(root, { imageGeneration = false, audio = false, video = false } = {}) {
41
+ const marker = initializeLocalRequestRoot(root, video ? 4 : audio ? 3 : imageGeneration ? 2 : 1);
42
+ listCreationRequests(root); readAllOperations(root);
43
+ return marker;
44
+ }
45
+ export function operationId(marker, key) {
46
+ if (!UUID.test(key)) operationFail('IRIS_OPERATION_KEY_INVALID', 'Operation Key 须为小写 UUIDv4');
47
+ return 'op_' + hash(JSON.stringify(['operation/v0', marker.rootId, key]));
48
+ }
49
+ function inputDigest(runId, revision) {
50
+ return 'sha256:' + hash(JSON.stringify(['operation-call/v0', 'flow.continue', runId, revision]));
51
+ }
52
+ const bindingFor = run => ({ taskId: flowStep(run).taskId, sourceArtifactId: run.source?.artifactId ?? null,
53
+ comparisonArtifactId: run.compare?.artifactId ?? null, inputDigest: run.inputDigest });
54
+ const artifactsFor = run => [...new Set([run.source?.artifactId, run.output?.artifactId, run.comparison?.artifactId, run.selection?.artifactId].filter(Boolean))];
55
+ function validate(record, id, marker) {
56
+ if (record?.schemaVersion === 1) return validatePluginOperation(record, id, marker);
57
+ if (!keys(record, ['schemaVersion', 'id', 'rootId', 'revision', 'action', 'runId', 'expectedRunRevision', 'inputDigest',
58
+ 'ownerInstanceId', 'binding', 'createdAt', 'updatedAt', 'state', 'cancelRequested', 'startedAt', 'finishedAt', 'result', 'failureCode'],
59
+ ['schemaVersion', 'id', 'rootId', 'revision', 'action', 'runId', 'expectedRunRevision', 'inputDigest',
60
+ 'ownerInstanceId', 'binding', 'createdAt', 'updatedAt', 'state', 'cancelRequested'])
61
+ || record.schemaVersion !== 0 || !ID.test(id) || record.id !== id || record.rootId !== marker.rootId
62
+ || !positive(record.revision) || record.action !== 'flow.continue' || !RUN.test(record.runId)
63
+ || !positive(record.expectedRunRevision) || record.inputDigest !== inputDigest(record.runId, record.expectedRunRevision)
64
+ || !UUID.test(record.ownerInstanceId) || !STATES.has(record.state) || typeof record.cancelRequested !== 'boolean'
65
+ || !iso(record.createdAt) || !iso(record.updatedAt) || record.updatedAt < record.createdAt
66
+ || !keys(record.binding, ['taskId', 'sourceArtifactId', 'comparisonArtifactId', 'inputDigest'])
67
+ || !TASK.test(record.binding.taskId) || !([3, 4].includes(marker.schemaVersion) && (record.binding.sourceArtifactId === null || ARTIFACT.test(record.binding.sourceArtifactId))
68
+ && (record.binding.comparisonArtifactId === null || ARTIFACT.test(record.binding.comparisonArtifactId))
69
+ || record.binding.sourceArtifactId === null && record.binding.comparisonArtifactId === null && marker.schemaVersion === 2
70
+ || ARTIFACT.test(record.binding.sourceArtifactId) && ARTIFACT.test(record.binding.comparisonArtifactId))
71
+ || !DIGEST.test(record.binding.inputDigest)) invalid();
72
+ const hasStarted = Object.hasOwn(record, 'startedAt');
73
+ if (record.revision !== (record.state === 'queued' ? 1 : record.state === 'running' || !hasStarted ? 2 : 3)) invalid();
74
+ if (hasStarted && (!iso(record.startedAt) || record.startedAt < record.createdAt || record.startedAt > record.updatedAt)) invalid();
75
+ if (record.state === 'queued' && (record.revision !== 1 || record.updatedAt !== record.createdAt || hasStarted || record.cancelRequested)
76
+ || record.state === 'running' && (!hasStarted || record.cancelRequested)
77
+ || record.state === 'succeeded' && (!hasStarted || record.cancelRequested)
78
+ || record.cancelRequested !== (record.state === 'canceled')
79
+ || record.state === 'failed' && !FAILURE_CODES.has(record.failureCode)
80
+ || record.state !== 'failed' && Object.hasOwn(record, 'failureCode')) invalid();
81
+ if (ACTIVE.has(record.state)) {
82
+ if (Object.hasOwn(record, 'finishedAt') || Object.hasOwn(record, 'result')) invalid();
83
+ } else {
84
+ if (!iso(record.finishedAt) || record.finishedAt !== record.updatedAt || record.finishedAt < record.createdAt
85
+ || !keys(record.result, ['runRevision', 'taskId', 'artifactIds']) || !positive(record.result.runRevision)
86
+ || record.result.runRevision < record.expectedRunRevision
87
+ || record.result.taskId !== null && record.result.taskId !== record.binding.taskId
88
+ || !Array.isArray(record.result.artifactIds) || record.binding.sourceArtifactId !== null && !record.result.artifactIds.length
89
+ || new Set(record.result.artifactIds).size !== record.result.artifactIds.length
90
+ || !record.result.artifactIds.every(value => ARTIFACT.test(value))
91
+ || record.binding.sourceArtifactId !== null && !record.result.artifactIds.includes(record.binding.sourceArtifactId)
92
+ || record.state === 'succeeded' && !record.result.taskId) invalid();
93
+ }
94
+ return record;
95
+ }
96
+ function validateBinding(root, record) {
97
+ if (record.action === 'plugin.execute') return validatePluginBinding(root, record);
98
+ const run = readFlowRun(root, record.runId), task = findFlowTask(root, run);
99
+ if (Object.entries(bindingFor(run)).some(([key, value]) => record.binding[key] !== value) || run.revision < record.expectedRunRevision
100
+ || record.result && (record.result.runRevision > run.revision || record.result.taskId && !task
101
+ || record.result.artifactIds.some(id => !artifactsFor(run).includes(id)))) {
102
+ operationFail('IRIS_OPERATION_BINDING_CONFLICT', 'Operation 与原 Run/Task/作品绑定冲突,停止操作并保留原记录');
103
+ }
104
+ return { run, task };
105
+ }
106
+ function fileFor(root, marker, id) {
107
+ if (!ID.test(id)) operationFail('IRIS_OPERATION_INPUT_INVALID', 'Operation ID 格式无效');
108
+ return path.join(requestDirectory(root, marker, 'operations'), id + '.json');
109
+ }
110
+ export function readOperation(root, id, marker = inspectExecutionRoot(root)) {
111
+ const file = fileFor(root, marker, id);
112
+ let record;
113
+ try {
114
+ const stat = fs.lstatSync(file);
115
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256 * 1024) invalid();
116
+ try { record = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) { invalid(); }
117
+ } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
118
+ validate(record, id, marker); validateBinding(root, record);
119
+ return structuredClone(record);
120
+ }
121
+ export function readAllOperations(root) {
122
+ const marker = inspectLocalRequestRoot(root, { allowAbsent: true });
123
+ if (!marker || marker.schemaVersion === 0) return [];
124
+ const records = [];
125
+ for (const name of fs.readdirSync(requestDirectory(root, marker, 'operations'))) {
126
+ if (/^op_[a-f0-9]{64}\.json\.tmp-[1-9][0-9]*-[a-f0-9]{12}$/.test(name)) continue;
127
+ if (!/^op_[a-f0-9]{64}\.json$/.test(name)) invalid();
128
+ const record = readOperation(root, name.slice(0, -5), marker);
129
+ if (!record) invalid();
130
+ records.push(record);
131
+ }
132
+ const active = records.filter(record => ACTIVE.has(record.state));
133
+ if (active.length > MAX_ACTIVE_OPERATIONS || new Set(active.filter(record => record.action === 'flow.continue').map(record => record.runId)).size !== active.filter(record => record.action === 'flow.continue').length) invalid();
134
+ return records;
135
+ }
136
+
137
+ export function acceptOperation(root, value) {
138
+ input(value, ['key', 'runId', 'expectedRunRevision', 'ownerInstanceId']);
139
+ if (!RUN.test(value.runId) || !positive(value.expectedRunRevision) || !UUID.test(value.ownerInstanceId)) {
140
+ operationFail('IRIS_OPERATION_INPUT_INVALID', '需要明确 Run、正整数 revision 与 owner 实例 UUID');
141
+ }
142
+ const marker = inspectExecutionRoot(root), id = operationId(marker, value.key);
143
+ const fingerprint = inputDigest(value.runId, value.expectedRunRevision), previous = readOperation(root, id, marker);
144
+ if (previous) {
145
+ if (previous.inputDigest !== fingerprint) operationFail('IRIS_OPERATION_KEY_CONFLICT', '该 Key 已绑定其他操作输入');
146
+ return { operation: previous, idempotentReplay: true };
147
+ }
148
+ const run = readFlowRun(root, value.runId); findFlowTask(root, run);
149
+ if (run.revision !== value.expectedRunRevision) operationFail('IRIS_OPERATION_RUN_CHANGED', 'Run revision 已改变,请读取原流程后明确操作');
150
+ const active = readAllOperations(root).filter(record => ACTIVE.has(record.state));
151
+ const conflicting = active.find(record => record.runId === run.id);
152
+ if (conflicting) throw Object.assign(new Error('此 Run 已有未结束的 Operation'), { code: 'IRIS_OPERATION_RUN_BUSY', operationId: conflicting.id });
153
+ if (active.length >= MAX_ACTIVE_OPERATIONS) operationFail('IRIS_OPERATION_QUEUE_FULL', '未结束的 Operation 已达上限,本次未接收');
154
+ const now = new Date().toISOString();
155
+ const record = { schemaVersion: 0, id, rootId: marker.rootId, revision: 1, action: 'flow.continue', runId: run.id,
156
+ expectedRunRevision: value.expectedRunRevision, inputDigest: fingerprint, ownerInstanceId: value.ownerInstanceId,
157
+ binding: bindingFor(run), createdAt: now, updatedAt: now, state: 'queued', cancelRequested: false };
158
+ validate(record, id, marker);
159
+ atomicCreatePrivate(fileFor(root, marker, id), JSON.stringify(record, null, 2) + '\n');
160
+ return { operation: structuredClone(record), idempotentReplay: false };
161
+ }
162
+
163
+ /** Store-only CAS. It neither runs flow.continue nor delivers an abort signal or remote cancel. */
164
+ export function transitionOperation(root, value) {
165
+ input(value, ['id', 'expectedRevision', 'state', 'failureCode', 'value'], ['id', 'expectedRevision', 'state']);
166
+ if (!positive(value.expectedRevision) || !STATES.has(value.state)) operationFail('IRIS_OPERATION_INPUT_INVALID', 'Operation 状态或 revision 无效');
167
+ const marker = inspectExecutionRoot(root), current = readOperation(root, value.id, marker);
168
+ if (!current) operationFail('IRIS_OPERATION_NOT_FOUND', 'Operation 不存在');
169
+ if (current.action === 'plugin.execute') return transitionPluginOperation(root, marker, value);
170
+ if (Object.hasOwn(value, 'value')) operationFail('IRIS_OPERATION_INPUT_INVALID', '媒体流程不接受插件业务结果');
171
+ if (current.revision !== value.expectedRevision) operationFail('IRIS_OPERATION_REVISION_CONFLICT', 'Operation 已改变,请先读取原记录');
172
+ const allowed = current.state === 'queued' ? ['running', 'canceled', 'interrupted', 'failed']
173
+ : current.state === 'running' ? ['succeeded', 'failed', 'canceled', 'interrupted'] : [];
174
+ if (!allowed.includes(value.state)) operationFail('IRIS_OPERATION_TRANSITION_INVALID', '不允许此 Operation 状态转换;结束记录不重新排队');
175
+ if (value.state === 'failed' ? !FAILURE_CODES.has(value.failureCode)
176
+ : Object.hasOwn(value, 'failureCode')) operationFail('IRIS_OPERATION_INPUT_INVALID', '失败记录只接受有限错误类别,不保存异常原文');
177
+ const { run, task } = validateBinding(root, current);
178
+ if (value.state === 'running' && run.revision !== current.expectedRunRevision) operationFail('IRIS_OPERATION_RUN_CHANGED', 'Run revision 已改变,未开始本次调用');
179
+ if (value.state === 'succeeded' && !task) operationFail('IRIS_OPERATION_RESULT_INVALID', '没有原 Task 事实,不能记录调用成功');
180
+ const now = new Date(Math.max(Date.now(), Date.parse(current.updatedAt))).toISOString();
181
+ const next = { ...current, revision: current.revision + 1, state: value.state, updatedAt: now,
182
+ cancelRequested: value.state === 'canceled',
183
+ ...(value.state === 'running' ? { startedAt: now } : { finishedAt: now,
184
+ result: { runRevision: run.revision, taskId: task?.id || null, artifactIds: artifactsFor(run) } }),
185
+ ...(value.state === 'failed' ? { failureCode: value.failureCode } : {}) };
186
+ validate(next, next.id, marker);
187
+ atomicWritePrivate(fileFor(root, marker, next.id), JSON.stringify(next, null, 2) + '\n');
188
+ return structuredClone(next);
189
+ }
190
+
191
+ export function listOperations(root, value = {}) {
192
+ input(value, ['runId', 'limit', 'cursor'], []);
193
+ if (value.runId !== undefined && !RUN.test(value.runId) || value.limit !== undefined && (!positive(value.limit) || value.limit > 30)
194
+ || value.cursor !== undefined && !ID.test(value.cursor)) operationFail('IRIS_OPERATION_INPUT_INVALID', 'Operation 列表参数无效');
195
+ inspectExecutionRoot(root);
196
+ const ordered = readAllOperations(root).filter(record => value.runId === undefined || record.runId === value.runId)
197
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.id.localeCompare(a.id));
198
+ const offset = value.cursor === undefined ? 0 : ordered.findIndex(record => record.id === value.cursor) + 1;
199
+ if (value.cursor !== undefined && offset === 0) operationFail('IRIS_OPERATION_INPUT_INVALID', 'Operation 游标不属于当前列表');
200
+ const page = ordered.slice(offset, offset + (value.limit ?? 20));
201
+ return { operations: page, total: ordered.length, nextCursor: offset + page.length < ordered.length ? page.at(-1).id : null };
202
+ }
203
+ export function operationReferences(root) {
204
+ return readAllOperations(root).map(record => record.action === 'plugin.execute' ? { operationId: record.id, runId: null, taskIds: [],
205
+ artifactIds: [...new Set([...record.sourceArtifactIds, ...record.reservedArtifactIds.filter(id => findCoreArtifactMetadata(root, id))])] } : ({ operationId: record.id, runId: record.runId,
206
+ taskIds: [record.binding.taskId], artifactIds: [...new Set([record.binding.sourceArtifactId,
207
+ record.binding.comparisonArtifactId, ...(record.result?.artifactIds || [])].filter(Boolean))] }));
208
+ }
209
+ export function projectOperation(record) {
210
+ if (record.action === 'plugin.execute') return projectPluginOperation(record);
211
+ return { contractVersion: 0, id: record.id, action: record.action, runId: record.runId,
212
+ revision: record.revision, expectedRunRevision: record.expectedRunRevision, state: record.state,
213
+ createdAt: record.createdAt, updatedAt: record.updatedAt, cancelRequested: record.cancelRequested,
214
+ ...(record.startedAt ? { startedAt: record.startedAt } : {}), ...(record.finishedAt ? { finishedAt: record.finishedAt } : {}),
215
+ ...(record.result ? { result: structuredClone(record.result) } : {}), ...(record.failureCode ? { failureCode: record.failureCode } : {}) };
216
+ }
@@ -0,0 +1,36 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ export const PANEL_CONTRACT_VERSION = 0;
5
+ const TYPES = new Map([['.html', 'text/html; charset=utf-8'], ['.js', 'text/javascript; charset=utf-8'],
6
+ ['.css', 'text/css; charset=utf-8'], ['.svg', 'image/svg+xml'], ['.png', 'image/png'], ['.jpg', 'image/jpeg'],
7
+ ['.webp', 'image/webp'], ['.woff2', 'font/woff2']]);
8
+ const fail = () => { throw Object.assign(new TypeError('Panel 清单、版本或固定资源无效'), { code: 'IRIS_PANEL_INVALID' }); };
9
+
10
+ /** Explicit trusted static Panel. Pin its finite assets before exposing any public route. */
11
+ export function loadPanelExtension(manifestPath) {
12
+ if (typeof manifestPath !== 'string' || !path.isAbsolute(manifestPath)) fail();
13
+ const file = fs.realpathSync(manifestPath), root = path.dirname(file);
14
+ if (!fs.statSync(file).isFile() || fs.statSync(file).size > 65536) fail();
15
+ let manifest;
16
+ try { manifest = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) { fail(); }
17
+ if (!manifest || manifest.contractVersion !== PANEL_CONTRACT_VERSION || !/^[a-z][a-z0-9._-]{0,79}$/.test(manifest.id)
18
+ || !/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/.test(manifest.version)
19
+ || Object.keys(manifest).some(key => !['contractVersion', 'id', 'version', 'assets'].includes(key))
20
+ || !manifest.assets || typeof manifest.assets !== 'object' || Array.isArray(manifest.assets)) fail();
21
+ const entries = Object.entries(manifest.assets), assets = new Map();
22
+ if (!entries.length || entries.length > 128 || !Object.hasOwn(manifest.assets, '/')) fail();
23
+ let total = 0;
24
+ for (const [route, relative] of entries) {
25
+ if (route !== '/' && (!/^\/(?:[a-zA-Z0-9_-]+\/)*[a-zA-Z0-9_.-]+$/.test(route)
26
+ || route.startsWith('/api/') || route === '/mcp' || route.startsWith('/mcp/'))
27
+ || typeof relative !== 'string' || path.isAbsolute(relative) || relative.split(/[\\/]/).includes('..')) fail();
28
+ const target = fs.realpathSync(path.resolve(root, relative)), rel = path.relative(root, target), stat = fs.statSync(target);
29
+ const mediaType = TYPES.get(path.extname(target));
30
+ if (!rel || rel.startsWith('..' + path.sep) || path.isAbsolute(rel) || !stat.isFile() || !mediaType
31
+ || stat.size > 8 * 1024 * 1024 || (total += stat.size) > 16 * 1024 * 1024 || route === '/' && path.extname(target) !== '.html') fail();
32
+ assets.set(route, { bytes: fs.readFileSync(target), mediaType });
33
+ }
34
+ return Object.freeze({ identity: Object.freeze({ contractVersion: PANEL_CONTRACT_VERSION, id: manifest.id, version: manifest.version }),
35
+ has: route => assets.has(route), read: async route => assets.get(route) || null });
36
+ }
@@ -0,0 +1,194 @@
1
+ // Experimental contribution contract. No Host, filesystem, registry or execution state.
2
+ export const PLUGIN_CONTRACT_VERSION = 0;
3
+ export const PLUGIN_KINDS = Object.freeze(['providers', 'operations', 'pages', 'renderers', 'settings']);
4
+ export const PLUGIN_CORE_COMMANDS = Object.freeze({
5
+ 'artifact.inspect': 'reader', 'artifact.list': 'reader', 'artifact.read': 'reader',
6
+ 'task.inspect': 'reader', 'task.list': 'reader', 'artifact.import': 'writer'
7
+ });
8
+
9
+ const ID = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/;
10
+ const VERSION = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
11
+ const MIME = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/;
12
+ const CAPS = new Set(['image-gen', 'video-gen', 'tts', 'music-gen', 'transcribe']);
13
+ const CORE_NAMESPACES = new Set(['iris', 'artifact', 'task', 'media', 'flow', 'creation', 'execution', 'operation', 'core']);
14
+
15
+ export class PluginContractError extends Error {
16
+ constructor(code, message) { super(message); this.name = 'PluginContractError'; this.code = code; }
17
+ }
18
+ export function pluginFail(code, message) { throw new PluginContractError(code, message); }
19
+ const invalid = message => pluginFail('IRIS_PLUGIN_INVALID', message);
20
+ export const pluginObject = value => value !== null && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype;
21
+ export function pluginFields(value, allowed, required = allowed) {
22
+ return pluginObject(value) && Object.keys(value).every(key => allowed.includes(key)) && required.every(key => Object.hasOwn(value, key));
23
+ }
24
+ function versionParts(value) {
25
+ const match = typeof value === 'string' && value.length <= 160 && value.match(VERSION);
26
+ if (!match || match[4]?.split('.').some(part => /^[0-9]+$/.test(part) && part.length > 1 && part[0] === '0')) invalid('插件版本须为有效 SemVer');
27
+ return { core: match.slice(1, 4), pre: match[4]?.split('.') || [] };
28
+ }
29
+ const numericOrder = (a, b) => a.length === b.length ? a === b ? 0 : a < b ? -1 : 1 : a.length < b.length ? -1 : 1;
30
+ /** SemVer precedence only; compatibility is a closed/open interval, not npm range syntax. */
31
+ export function comparePluginVersions(a, b) {
32
+ const left = versionParts(a), right = versionParts(b);
33
+ for (let i = 0; i < 3; i++) { const order = numericOrder(left.core[i], right.core[i]); if (order) return order; }
34
+ if (!left.pre.length || !right.pre.length) return left.pre.length === right.pre.length ? 0 : left.pre.length ? -1 : 1;
35
+ for (let i = 0; i < Math.max(left.pre.length, right.pre.length); i++) {
36
+ const x = left.pre[i], y = right.pre[i];
37
+ if (x === undefined || y === undefined) return x === y ? 0 : x === undefined ? -1 : 1;
38
+ if (x === y) continue;
39
+ const xn = /^[0-9]+$/.test(x), yn = /^[0-9]+$/.test(y);
40
+ return xn && yn ? numericOrder(x, y) : xn !== yn ? xn ? -1 : 1 : x < y ? -1 : 1;
41
+ }
42
+ return 0;
43
+ }
44
+ function checkedId(value) {
45
+ if (typeof value !== 'string' || value.length > 80 || !ID.test(value)) invalid('插件及贡献 ID 须为有界小写标识');
46
+ return value;
47
+ }
48
+ export function qualifiedPluginId(pluginId, localId) { return checkedId(checkedId(pluginId) + '.' + checkedId(localId)); }
49
+ function label(value) {
50
+ if (typeof value !== 'string' || !value.trim() || value.length > 200 || /[\x00-\x1f\x7f]/.test(value)) invalid('贡献标题无效');
51
+ return value;
52
+ }
53
+ function asset(value, extension) {
54
+ if (typeof value !== 'string' || !/^(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_.-]+$/.test(value)
55
+ || !extension.test(value) || value.split('/').some(part => part === '.' || part === '..')) invalid('Web 入口须为插件内相对 JS/MJS 路径,样式须为 CSS');
56
+ return value;
57
+ }
58
+ /** JSON boundaries never coerce, strip fields or preserve mutable author-owned objects. */
59
+ export function pluginJson(value, ancestors = new Set()) {
60
+ if (value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number' && Number.isFinite(value)) return value;
61
+ if ((!Array.isArray(value) && !pluginObject(value)) || ancestors.has(value)) invalid('插件描述、输入和结果须为无循环的 JSON 数据');
62
+ ancestors.add(value);
63
+ const result = Array.isArray(value) ? value.map(item => pluginJson(item, ancestors))
64
+ : Object.fromEntries(Object.entries(value).map(([key, item]) => [key, pluginJson(item, ancestors)]));
65
+ ancestors.delete(value);
66
+ return result;
67
+ }
68
+ export function freezePluginValue(value) {
69
+ if (value && typeof value === 'object') { for (const item of Object.values(value)) freezePluginValue(item); Object.freeze(value); }
70
+ return value;
71
+ }
72
+
73
+ // A documented JSON Schema subset; unknown/default/ref keywords must not be silently ignored.
74
+ function checkedInputSchema(value) {
75
+ const fields = ['type', 'title', 'description', 'properties', 'required', 'additionalProperties', 'items', 'anyOf', 'enum', 'const',
76
+ 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf', 'minLength', 'maxLength', 'pattern', 'minItems', 'maxItems'];
77
+ if (!pluginFields(value, fields, []) || value.type !== undefined && !['object', 'array', 'string', 'integer', 'number', 'boolean', 'null'].includes(value.type)) invalid('业务输入包含未支持的 JSON Schema 字段或类型');
78
+ if (value.properties !== undefined) {
79
+ if (!pluginObject(value.properties)) invalid('Schema properties 必须为对象');
80
+ for (const item of Object.values(value.properties)) checkedInputSchema(item);
81
+ }
82
+ if (value.required !== undefined && (!Array.isArray(value.required) || new Set(value.required).size !== value.required.length
83
+ || value.required.some(key => typeof key !== 'string' || !Object.hasOwn(value.properties || {}, key)))) invalid('Schema required 必须引用已声明属性');
84
+ if (value.items !== undefined) checkedInputSchema(value.items);
85
+ if (value.additionalProperties !== undefined && typeof value.additionalProperties !== 'boolean') checkedInputSchema(value.additionalProperties);
86
+ if (value.anyOf !== undefined) {
87
+ if (!Array.isArray(value.anyOf) || !value.anyOf.length) invalid('Schema anyOf 须为非空 Schema 数组');
88
+ value.anyOf.forEach(checkedInputSchema);
89
+ }
90
+ for (const key of ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf']) {
91
+ if (value[key] !== undefined && (typeof value[key] !== 'number' || !Number.isFinite(value[key]) || key === 'multipleOf' && value[key] <= 0)) invalid('Schema 数值约束类型无效');
92
+ }
93
+ for (const key of ['minLength', 'maxLength', 'minItems', 'maxItems']) {
94
+ if (value[key] !== undefined && (!Number.isSafeInteger(value[key]) || value[key] < 0)) invalid('Schema 长度约束须为非负整数');
95
+ }
96
+ if (value.pattern !== undefined && typeof value.pattern !== 'string' || value.enum !== undefined && (!Array.isArray(value.enum) || !value.enum.length)
97
+ || value.title !== undefined && typeof value.title !== 'string' || value.description !== undefined && typeof value.description !== 'string') invalid('Schema 文本或枚举约束无效');
98
+ return value;
99
+ }
100
+
101
+ function checkedContribution(kind, value) {
102
+ const common = ['id', 'version', 'title'];
103
+ const fields = kind === 'providers' ? ['capabilities', 'configuration'] : kind === 'operations' ? ['mode', 'commands', 'inputSchema', 'artifactInputs']
104
+ : kind === 'renderers' ? ['entry', 'styles', 'mediaTypes'] : ['entry', 'styles'];
105
+ const required = [...common, ...fields.filter(field => !['configuration', 'styles', 'artifactInputs'].includes(field))];
106
+ if (!pluginFields(value, [...common, ...fields], required)) invalid('贡献字段不符合 ' + kind + ' 契约');
107
+ const result = { id: checkedId(value.id), version: value.version, title: label(value.title) }; versionParts(value.version);
108
+ if (kind === 'providers') {
109
+ if (!Array.isArray(value.capabilities) || !value.capabilities.length || value.capabilities.some(cap => !CAPS.has(cap))
110
+ || new Set(value.capabilities).size !== value.capabilities.length) invalid('Provider 能力须为支持的明确列表');
111
+ const configuration = value.configuration === undefined ? [] : value.configuration;
112
+ if (!Array.isArray(configuration) || new Set(configuration.map(field => field?.key)).size !== configuration.length) invalid('Provider 配置字段重复或无效');
113
+ for (const field of configuration) {
114
+ if (!pluginFields(field, ['key', 'label', 'type', 'required', 'advanced'], ['key', 'label', 'type'])
115
+ || !['baseUrl', 'mediaBaseUrl', 'apiKey'].includes(field.key) || field.type !== (field.key === 'apiKey' ? 'secret' : 'url')
116
+ || field.required !== undefined && typeof field.required !== 'boolean' || field.advanced !== undefined && typeof field.advanced !== 'boolean') invalid('Provider 配置描述无效');
117
+ label(field.label);
118
+ }
119
+ Object.assign(result, { capabilities: [...value.capabilities], configuration: pluginJson(configuration) });
120
+ } else if (kind === 'operations') {
121
+ if (!['reader', 'writer'].includes(value.mode) || !Array.isArray(value.commands) || new Set(value.commands).size !== value.commands.length
122
+ || value.commands.some(name => typeof name !== 'string' || !Object.hasOwn(PLUGIN_CORE_COMMANDS, name)
123
+ || value.mode === 'reader' && PLUGIN_CORE_COMMANDS[name] !== 'reader')) invalid('业务模式或 Core 命令声明无效');
124
+ if (!pluginObject(value.inputSchema) || value.inputSchema.type !== 'object' || value.inputSchema.additionalProperties !== false) invalid('业务输入须声明封闭的 JSON 对象 Schema');
125
+ Object.assign(result, { mode: value.mode, commands: [...value.commands], inputSchema: checkedInputSchema(pluginJson(value.inputSchema)) });
126
+ if (value.artifactInputs !== undefined) {
127
+ if (!Array.isArray(value.artifactInputs) || new Set(value.artifactInputs).size !== value.artifactInputs.length
128
+ || value.artifactInputs.some(key => { const field = result.inputSchema.properties?.[key];
129
+ return typeof key !== 'string' || !(field?.type === 'string' || field?.type === 'array' && field.items?.type === 'string'); })) invalid('artifactInputs 须引用顶层 Artifact ID 字符串或数组字段');
130
+ result.artifactInputs = [...value.artifactInputs];
131
+ }
132
+ } else {
133
+ result.entry = asset(value.entry, /\.m?js$/);
134
+ if (value.styles !== undefined) {
135
+ if (!Array.isArray(value.styles) || new Set(value.styles).size !== value.styles.length) invalid('样式列表无效');
136
+ result.styles = value.styles.map(file => asset(file, /\.css$/));
137
+ }
138
+ if (kind === 'renderers') {
139
+ if (!Array.isArray(value.mediaTypes) || !value.mediaTypes.length || new Set(value.mediaTypes).size !== value.mediaTypes.length
140
+ || value.mediaTypes.some(type => typeof type !== 'string' || !MIME.test(type))) invalid('作品渲染器须声明准确媒体类型,不接受通配符');
141
+ result.mediaTypes = [...value.mediaTypes];
142
+ }
143
+ }
144
+ return result;
145
+ }
146
+
147
+ export function checkedPluginManifest(value) {
148
+ if (!pluginFields(value, ['contractVersion', 'id', 'version', 'compatibility', 'contributions'])
149
+ || value.contractVersion !== PLUGIN_CONTRACT_VERSION || !pluginFields(value.compatibility, ['min', 'maxExclusive'])
150
+ || !pluginFields(value.contributions, PLUGIN_KINDS, [])) invalid('插件清单或契约版本无效');
151
+ const id = checkedId(value.id); versionParts(value.version);
152
+ if (CORE_NAMESPACES.has(id) || id.startsWith('iris.')) invalid('插件 ID 不能使用 Core 保留的命名空间');
153
+ if (comparePluginVersions(value.compatibility.min, value.compatibility.maxExclusive) >= 0) invalid('Core 兼容区间须为 min <= version < maxExclusive');
154
+ const contributions = {};
155
+ for (const kind of PLUGIN_KINDS) {
156
+ const entries = value.contributions[kind] === undefined ? [] : value.contributions[kind];
157
+ if (!Array.isArray(entries)) invalid('贡献必须为数组');
158
+ contributions[kind] = entries.map(entry => checkedContribution(kind, entry));
159
+ const ids = contributions[kind].map(entry => qualifiedPluginId(id, entry.id));
160
+ if (new Set(ids).size !== ids.length) invalid('同类贡献 ID 重复');
161
+ }
162
+ if (!Object.values(contributions).some(entries => entries.length)) invalid('插件必须声明至少一个贡献');
163
+ return freezePluginValue({ contractVersion: PLUGIN_CONTRACT_VERSION, id, version: value.version,
164
+ compatibility: { ...value.compatibility }, contributions });
165
+ }
166
+
167
+ export function definePlugin(value) {
168
+ if (!pluginFields(value, ['manifest', 'activate']) || typeof value.activate !== 'function') invalid('插件需要 manifest 与 activate');
169
+ return Object.freeze({ manifest: checkedPluginManifest(value.manifest), activate: value.activate });
170
+ }
171
+ export function assertPluginCompatibility(manifest, coreVersion) {
172
+ if (comparePluginVersions(coreVersion, manifest.compatibility.min) < 0 || comparePluginVersions(coreVersion, manifest.compatibility.maxExclusive) >= 0) {
173
+ pluginFail('IRIS_PLUGIN_INCOMPATIBLE', '插件 ' + manifest.id + ' 不支持当前 Core 版本');
174
+ }
175
+ }
176
+ export function checkedPluginDependency(value) {
177
+ if (!pluginFields(value, ['contractVersion', 'pluginId', 'pluginVersion', 'kind', 'contributionId', 'contributionVersion'])
178
+ || value.contractVersion !== PLUGIN_CONTRACT_VERSION || !PLUGIN_KINDS.includes(value.kind)) invalid('持久插件依赖无效');
179
+ checkedId(value.pluginId); checkedId(value.contributionId); versionParts(value.pluginVersion); versionParts(value.contributionVersion);
180
+ if (!value.contributionId.startsWith(value.pluginId + '.')) invalid('持久贡献不属于原插件');
181
+ return freezePluginValue({ contractVersion: PLUGIN_CONTRACT_VERSION, pluginId: value.pluginId, pluginVersion: value.pluginVersion,
182
+ kind: value.kind, contributionId: value.contributionId, contributionVersion: value.contributionVersion });
183
+ }
184
+ export function assertPluginDependencyMatches(expected, current) {
185
+ const original = expected === undefined ? undefined : checkedPluginDependency(expected);
186
+ const installed = current === undefined ? undefined : checkedPluginDependency(current);
187
+ if (JSON.stringify(original) !== JSON.stringify(installed)) pluginFail('IRIS_PLUGIN_DEPENDENCY_MISMATCH', '原任务依赖的插件或贡献版本不一致;请恢复准确版本,不会重新提交');
188
+ }
189
+ export function pluginDependency(manifest, kind, localId) {
190
+ const entry = manifest.contributions[kind]?.find(item => item.id === localId);
191
+ if (!entry) pluginFail('IRIS_PLUGIN_CONTRIBUTION_MISSING', '插件未声明此贡献');
192
+ return checkedPluginDependency({ contractVersion: PLUGIN_CONTRACT_VERSION, pluginId: manifest.id, pluginVersion: manifest.version,
193
+ kind, contributionId: qualifiedPluginId(manifest.id, localId), contributionVersion: entry.version });
194
+ }
@@ -0,0 +1,51 @@
1
+ import { createCommandService } from './command-service.js';
2
+ import crypto from 'node:crypto';
3
+ import { readCoreArtifactBytes, inspectCoreArtifactMetadata, createCoreArtifact } from './core-artifacts.js';
4
+ import { readImportImage } from './image-import.js';
5
+ import { PLUGIN_CORE_COMMANDS, pluginFail } from './plugin-contract.js';
6
+ import { isPluginSourceImage } from './plugin-source-image.js';
7
+
8
+ /** One Core command boundary, shared by local Hosts and the durable runner. */
9
+ export async function invokeReservedPlugin(runtime, reserved, { imports } = {}) {
10
+ const pending = new Set(); let accepting = true, commandTail = Promise.resolve();
11
+ const context = Object.freeze({ signal: runtime.signal, dependency: reserved.dependency,
12
+ core: Object.freeze({ execute(name, args) {
13
+ if (!accepting) return Promise.reject(Object.assign(new Error('插件调用已结束'), { name: 'AbortError' }));
14
+ const owned = structuredClone(args);
15
+ const call = commandTail.then(async () => {
16
+ runtime.signal.throwIfAborted();
17
+ imports?.authorize?.();
18
+ if (!reserved.commands.includes(name) || reserved.mode === 'reader' && PLUGIN_CORE_COMMANDS[name] !== 'reader') pluginFail('IRIS_PLUGIN_OPERATION_DENIED', '插件未声明此 Core 命令或模式不允许写入');
19
+ if (name === 'artifact.read') return runtime.run('inspect', ({ dataRoot }) => {
20
+ if (!owned || Object.keys(owned).join(',') !== 'artifact_id') pluginFail('IRIS_PLUGIN_INPUT_INVALID', '作品读取只接受 Artifact ID');
21
+ if (imports && !imports.sources.includes(owned.artifact_id)) pluginFail('IRIS_PLUGIN_OPERATION_DENIED', '持久业务只能读取其声明并绑定的来源作品');
22
+ const metadata = inspectCoreArtifactMetadata(dataRoot, owned.artifact_id);
23
+ if (!isPluginSourceImage(metadata)) pluginFail('IRIS_PLUGIN_INPUT_INVALID', '插件图片读取支持 8 MiB 内 PNG/JPEG/WebP');
24
+ const result = readCoreArtifactBytes(dataRoot, owned.artifact_id);
25
+ return { contractVersion: 0, artifact: result.artifact, imageBase64: result.bytes.toString('base64') };
26
+ });
27
+ if (name === 'artifact.import') {
28
+ if (!owned || Object.keys(owned).join(',') !== 'image_bytes' || !(owned.image_bytes instanceof Uint8Array)) pluginFail('IRIS_PLUGIN_INPUT_INVALID', '插件导入只接受图片字节,不接受主机路径');
29
+ if (imports) return runtime.run('execute', async ({ dataRoot, signal }) => {
30
+ const source = await readImportImage({ imageBytes: owned.image_bytes }, signal); signal.throwIfAborted(); imports.authorize?.();
31
+ const artifactId = imports.reserve();
32
+ const artifact = createCoreArtifact(dataRoot, { ...source, artifactId, expectedSize: source.bytes.length,
33
+ expectedDigest: crypto.createHash('sha256').update(source.bytes).digest('hex'), kind: 'plugin-image', metadata: { ...source.metadata, pluginOperationId: imports.operationId },
34
+ relations: imports.sources.map(id => ({ type: 'derived-from', artifactId: id })) });
35
+ return { contractVersion: 0, artifact };
36
+ });
37
+ }
38
+ return createCommandService(runtime).execute(name, owned);
39
+ });
40
+ commandTail = call; void call.catch(() => {}); pending.add(call); return call;
41
+ } }) });
42
+ runtime.signal.throwIfAborted(); let result, failure;
43
+ try { result = await reserved.invoke(context); } catch (error) { failure = error; }
44
+ accepting = false;
45
+ const results = await Promise.allSettled([...pending]);
46
+ if (failure) throw failure;
47
+ const rejected = results.find(item => item.status === 'rejected'); if (rejected) throw rejected.reason;
48
+ runtime.signal.throwIfAborted();
49
+ if (Buffer.byteLength(JSON.stringify(result.value)) > 128 * 1024) pluginFail('IRIS_PLUGIN_RESULT_INVALID', '插件业务结果超过 128 KiB');
50
+ return result;
51
+ }
@@ -0,0 +1,86 @@
1
+ // Explicit startup selection, separate from plugin installation and connection grants.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { loadCorePluginRegistry } from './plugins.js';
6
+ import { createCoreCapabilityCatalog } from './core-capability-catalog.js';
7
+ import { createPluginPanelAssembly } from './plugin-web-assets.js';
8
+ import { pluginFields } from './plugin-contract.js';
9
+
10
+ const builtins = new Map([['builtin:contact-sheet', fileURLToPath(new URL('../plugins/contact-sheet/index.mjs', import.meta.url))]]);
11
+ export const builtinLaunchPlugins = () => [...builtins.keys()];
12
+
13
+ function invalid(message, file, field) {
14
+ const error = Object.assign(new Error(message), { code: 'IRIS_PLUGIN_LAUNCH_INVALID' });
15
+ error.diagnostic = { code: error.code, stage: 'plugin-config', message, ...(file ? { file } : {}), ...(field ? { field } : {}) };
16
+ throw error;
17
+ }
18
+ function contextualize(error, file, stage) {
19
+ if (!(error instanceof Error)) error = new Error(String(error));
20
+ error.diagnostic ??= { code: error.code || 'IRIS_PLUGIN_LAUNCH_FAILED', stage, message: error.message, file };
21
+ return error;
22
+ }
23
+
24
+ export function readPluginLaunchConfiguration(file) {
25
+ if (typeof file !== 'string' || !path.isAbsolute(file)) invalid('plugin-config 必须是明确的 JSON 文件绝对路径', undefined, 'plugin-config');
26
+ let input, fd;
27
+ try {
28
+ fd = fs.openSync(file, 'r');
29
+ const stat = fs.fstatSync(fd);
30
+ if (!stat.isFile() || stat.size > 65536) invalid('插件启动配置须为不超过 64 KiB 的普通 JSON 文件', file);
31
+ const buffer = Buffer.alloc(65537); let size = 0, count;
32
+ while (size < buffer.length && (count = fs.readSync(fd, buffer, size, buffer.length - size, null))) size += count;
33
+ if (size > 65536) invalid('插件启动配置超过 64 KiB', file);
34
+ try { input = JSON.parse(buffer.subarray(0, size).toString('utf8')); }
35
+ catch (_) { invalid('插件启动配置不是有效 JSON', file); }
36
+ } catch (error) { throw contextualize(error, file, 'plugin-config'); }
37
+ finally { if (fd !== undefined) fs.closeSync(fd); }
38
+ if (!pluginFields(input, ['schemaVersion', 'entries', 'operations', 'panel'], ['schemaVersion', 'entries']) || input.schemaVersion !== 0) {
39
+ invalid('插件启动配置须使用 schemaVersion:0,仅接受 entries、可选 operations/panel;不接受凭据或服务开关', file);
40
+ }
41
+ if (!Array.isArray(input.entries) || input.entries.some(entry => typeof entry !== 'string') || new Set(input.entries).size !== input.entries.length) {
42
+ invalid('entries 须为不重复的内置引用或 JS/MJS 绝对路径列表', file, 'entries');
43
+ }
44
+ const entries = input.entries.map((entry, index) => {
45
+ if (builtins.has(entry)) return builtins.get(entry);
46
+ if (!path.isAbsolute(entry) || !/\.m?js$/.test(entry)) invalid('入口须为已知 builtin: 引用或明确的 JS/MJS 绝对路径', file, 'entries[' + index + ']');
47
+ return entry;
48
+ });
49
+ if (input.operations !== undefined && (!Array.isArray(input.operations) || input.operations.some(id => typeof id !== 'string')
50
+ || new Set(input.operations).size !== input.operations.length)) invalid('operations 须为不重复的完整业务 ID 列表;空数组仅接管历史', file, 'operations');
51
+ // Reuse the same Panel validator as the server after the real registry is loaded.
52
+ if (input.panel !== undefined && !pluginFields(input.panel, ['pages', 'settings', 'renderers'], [])) invalid('panel 只接受 pages/settings/renderers 的明确列表', file, 'panel');
53
+ for (const [kind, values] of Object.entries(input.panel || {})) {
54
+ if (!Array.isArray(values) || values.some(id => typeof id !== 'string') || new Set(values).size !== values.length) {
55
+ invalid('panel 的每一项须为不重复的贡献 ID 列表', file, 'panel.' + kind);
56
+ }
57
+ }
58
+ return { entries, ...(input.operations !== undefined ? { operations: input.operations } : {}), ...(input.panel !== undefined ? { panel: input.panel } : {}) };
59
+ }
60
+
61
+ /** Imports/activate execute trusted code; inspect is not a sandbox or a media health test. */
62
+ export async function loadPluginLaunch(file, { serveWorkbench = true, panelManifestPath } = {}) {
63
+ const config = readPluginLaunchConfiguration(file);
64
+ if (config.panel !== undefined && (!serveWorkbench || panelManifestPath !== undefined)) {
65
+ invalid('panel 组合仅用于默认 web 工作台;不能用于 api serve 或与 --panel 整套替换同时选择', file, 'panel');
66
+ }
67
+ let registry;
68
+ try {
69
+ // Resolve before importing anything, including duplicate aliases to the same file.
70
+ const entries = config.entries.map(entry => fs.realpathSync(entry));
71
+ if (new Set(entries).size !== entries.length) invalid('entries 不能重复引用同一实际文件', file, 'entries');
72
+ registry = await loadCorePluginRegistry({ entries });
73
+ const catalog = createCoreCapabilityCatalog({ registry });
74
+ if (config.operations !== undefined) catalog.selectOperations(config.operations);
75
+ if (config.panel !== undefined) createPluginPanelAssembly(registry, config.panel);
76
+ const facts = catalog.snapshot({ allowedOperations: config.operations ?? [], panelContributions: config.panel });
77
+ const summary = { contractVersion: 0, plugins: facts.plugins, contributions: facts.contributions,
78
+ pluginExecution: config.operations !== undefined,
79
+ selection: { operations: config.operations ?? null, panel: config.panel ?? null } };
80
+ return { registry, allowedOperations: config.operations, panelContributions: config.panel, summary, dispose: () => registry.dispose() };
81
+ } catch (error) {
82
+ try { await registry?.dispose(); }
83
+ catch (cleanup) { throw contextualize(Object.assign(new AggregateError([error, cleanup], '插件启动校验与释放均失败'), { code: 'IRIS_PLUGIN_LAUNCH_CLEANUP_FAILED' }), file, 'plugin-cleanup'); }
84
+ throw contextualize(error, file, 'plugin-load');
85
+ }
86
+ }