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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +103 -1
  2. package/README.en.md +104 -304
  3. package/README.md +101 -302
  4. package/bin/dsh-iris.js +446 -64
  5. package/docs/AGENT_CORE_QUICKSTART.md +64 -0
  6. package/docs/AGENT_QUICKSTART.md +7 -5
  7. package/docs/ARCHITECTURE.md +20 -4
  8. package/docs/AUDIO_FLOWS.md +71 -0
  9. package/docs/CLI_MANAGEMENT.md +1 -1
  10. package/docs/CONTACT_SHEET.md +62 -0
  11. package/docs/CORE_CONNECTIONS.md +99 -0
  12. package/docs/CORE_QUICKSTART.md +108 -0
  13. package/docs/CORE_RUNTIME_CONTRACT.md +41 -5
  14. package/docs/CORE_SERVICE.md +97 -0
  15. package/docs/CORE_SKILLS.md +94 -0
  16. package/docs/DSH_COMPATIBILITY.md +14 -4
  17. package/docs/DSH_CORE_MIGRATION.md +4 -2
  18. package/docs/DSH_PROVIDER_CONNECTIONS.md +1 -1
  19. package/docs/DSH_RC2_ADAPTATION.md +1 -1
  20. package/docs/DSH_RUNTIME.md +40 -0
  21. package/docs/FUNCTION_MODEL_POOLS.md +1 -1
  22. package/docs/HEADLESS_CLI.md +12 -4
  23. package/docs/HOST_DOCTOR.md +13 -1
  24. package/docs/IMAGE_FLOW.md +75 -0
  25. package/docs/IMAGE_GENERATION.md +127 -0
  26. package/docs/LOCAL_API.md +62 -0
  27. package/docs/LOCAL_API_CONTRACT.md +140 -0
  28. package/docs/LOCAL_CREATION.md +56 -0
  29. package/docs/LOCAL_OPERATIONS.md +46 -0
  30. package/docs/LOCAL_SELECTION.md +57 -0
  31. package/docs/LOCAL_WEB.md +201 -0
  32. package/docs/MCP.md +191 -0
  33. package/docs/MUSIC.md +2 -0
  34. package/docs/PANEL_EXTENSIONS.md +64 -0
  35. package/docs/PLUGINS.md +189 -0
  36. package/docs/PLUGIN_STARTUP.md +100 -0
  37. package/docs/PROVIDER_ADAPTER_CONTRACT.md +1 -1
  38. package/docs/PROVIDER_EXTENSIONS.md +78 -0
  39. package/docs/PROVIDER_HEALTH.md +4 -2
  40. package/docs/PROVIDER_SETTINGS.md +85 -0
  41. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +6 -0
  42. package/docs/ROADMAP.md +28 -6
  43. package/docs/SECURITY.md +9 -1
  44. package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
  45. package/docs/VISION_CLI.md +2 -2
  46. package/docs/WEB_PROTOTYPE.md +62 -0
  47. package/docs/WEB_RECOVERY.md +46 -0
  48. package/docs/WORKBENCH_LOGIN.md +63 -0
  49. package/docs/assets/screenshots/core-connections-preview.webp +0 -0
  50. package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
  51. package/docs/assets/screenshots/core-overview-preview.webp +0 -0
  52. package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
  53. package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
  54. package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
  55. package/docs/assets/screenshots/core-studio-preview.webp +0 -0
  56. package/docs/releases/0.2.7.md +62 -0
  57. package/docs/releases/0.3.0-alpha.1.md +51 -0
  58. package/docs/screenshots.md +41 -1
  59. package/lib/actions.js +132 -70
  60. package/lib/adapters.js +20 -11
  61. package/lib/api.js +38 -38
  62. package/lib/application-runtime.js +213 -0
  63. package/lib/audio-flow-input.js +37 -0
  64. package/lib/client.js +434 -55
  65. package/lib/comfyui-adapter.js +94 -0
  66. package/lib/comfyui-client.js +51 -0
  67. package/lib/comfyui-inspection.js +73 -0
  68. package/lib/comfyui-workflow.js +94 -0
  69. package/lib/config.js +36 -1
  70. package/lib/configured-image-runtime.js +50 -0
  71. package/lib/connection-skill.js +89 -0
  72. package/lib/connection-template.js +23 -0
  73. package/lib/core-artifact-store.js +21 -2
  74. package/lib/core-bridge-selection.js +31 -0
  75. package/lib/core-bridge-tools.js +83 -0
  76. package/lib/core-capability-catalog.js +182 -0
  77. package/lib/core-connection-diagnostics.js +49 -0
  78. package/lib/core-connection-store.js +277 -0
  79. package/lib/core-connections.js +225 -0
  80. package/lib/core-maintenance.js +31 -2
  81. package/lib/core-runtime.js +20 -0
  82. package/lib/core-service-client.js +170 -0
  83. package/lib/core-tasks.js +84 -13
  84. package/lib/core-writer-lease.js +16 -0
  85. package/lib/creation-request-store.js +133 -0
  86. package/lib/doctor.js +12 -1
  87. package/lib/dsh-core-adapter.js +666 -572
  88. package/lib/dsh-host-adapter.js +8 -2
  89. package/lib/dsh-service-bridge.js +66 -0
  90. package/lib/execution-ownership.js +43 -0
  91. package/lib/flow-store.js +290 -0
  92. package/lib/generation-input.js +10 -1
  93. package/lib/headless-vision.js +7 -7
  94. package/lib/image-edit-input.js +1 -1
  95. package/lib/image-flow.js +232 -0
  96. package/lib/index.js +94 -68
  97. package/lib/local-api-projection.js +32 -0
  98. package/lib/local-api-token-directory.js +31 -0
  99. package/lib/local-api.js +1083 -0
  100. package/lib/local-creation.js +150 -0
  101. package/lib/local-operations.js +36 -0
  102. package/lib/local-request-root.js +86 -0
  103. package/lib/local-web-assets.js +37 -0
  104. package/lib/mcp-bridge.js +58 -0
  105. package/lib/mcp-check.js +26 -0
  106. package/lib/mcp-config.js +42 -0
  107. package/lib/mcp-delivery.js +46 -0
  108. package/lib/mcp-http.js +101 -0
  109. package/lib/mcp-server.js +47 -0
  110. package/lib/model-directory-service.js +16 -6
  111. package/lib/model-directory.js +3 -3
  112. package/lib/models.js +2 -2
  113. package/lib/music-transport.js +2 -1
  114. package/lib/operation-runner.js +236 -0
  115. package/lib/operation-store.js +216 -0
  116. package/lib/panel-extension.js +36 -0
  117. package/lib/plugin-contract.js +194 -0
  118. package/lib/plugin-execution.js +51 -0
  119. package/lib/plugin-launch.js +86 -0
  120. package/lib/plugin-operation-store.js +101 -0
  121. package/lib/plugin-operations.js +28 -0
  122. package/lib/plugin-registry.js +109 -0
  123. package/lib/plugin-selection.js +15 -0
  124. package/lib/plugin-source-image.js +5 -0
  125. package/lib/plugin-web-assets.js +54 -0
  126. package/lib/plugins.d.ts +288 -0
  127. package/lib/plugins.js +58 -0
  128. package/lib/private-storage.js +12 -0
  129. package/lib/provider-adapter.js +5 -1
  130. package/lib/provider-adapters.js +56 -18
  131. package/lib/provider-catalog.js +40 -29
  132. package/lib/provider-config-service.js +105 -23
  133. package/lib/provider-diagnostics.js +56 -0
  134. package/lib/provider-extensions.js +29 -0
  135. package/lib/provider-health.js +8 -1
  136. package/lib/provider-monitor.js +79 -0
  137. package/lib/provider-protocol.js +70 -20
  138. package/lib/provider-registry.js +118 -0
  139. package/lib/provider-response.js +28 -0
  140. package/lib/provider-settings.js +134 -0
  141. package/lib/provider-task-runner.js +40 -7
  142. package/lib/render-address.js +35 -0
  143. package/lib/render.js +52 -13
  144. package/lib/skill-installation.js +211 -0
  145. package/lib/vision-model-routing.js +3 -0
  146. package/lib/web/app.js +642 -0
  147. package/lib/web/appearance.js +111 -0
  148. package/lib/web/comfyui.js +146 -0
  149. package/lib/web/connections.js +377 -0
  150. package/lib/web/creation.js +355 -0
  151. package/lib/web/flow-view.js +33 -0
  152. package/lib/web/gallery.js +132 -0
  153. package/lib/web/index.html +291 -0
  154. package/lib/web/media-client.js +204 -0
  155. package/lib/web/plugin-panel.js +221 -0
  156. package/lib/web/plugin-surface.css +17 -0
  157. package/lib/web/providers.js +492 -0
  158. package/lib/web/recovery.js +105 -0
  159. package/lib/web/startup.js +18 -0
  160. package/lib/web/styles.css +518 -0
  161. package/lib/web/transport.js +96 -0
  162. package/lib/web/workbench-login.js +165 -0
  163. package/lib/workbench-artifacts.js +1 -2
  164. package/lib/workbench-auth-cli.js +47 -0
  165. package/lib/workbench-auth.js +200 -0
  166. package/lib/workbench-management.js +8 -9
  167. package/package.json +22 -6
  168. package/plugins/contact-sheet/index.d.mts +4 -0
  169. package/plugins/contact-sheet/index.mjs +33 -0
  170. package/plugins/contact-sheet/web/board.css +14 -0
  171. package/plugins/contact-sheet/web/board.mjs +93 -0
  172. package/skills/iris-core/SKILL.md +44 -0
  173. package/skills/iris-core/agents/openai.yaml +4 -0
  174. package/skills/iris-core/references/cli.md +95 -0
  175. package/skills/iris-core/references/comfyui.md +69 -0
  176. package/skills/iris-core/references/tasks.md +60 -0
  177. package/user_guide.md +19 -9
@@ -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
+ }
@@ -0,0 +1,101 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { atomicCreatePrivate, atomicWritePrivate } from './private-storage.js';
5
+ import { requestDirectory } from './local-request-root.js';
6
+ import { checkedPluginDependency, pluginFields, pluginJson, pluginFail, pluginObject } from './plugin-contract.js';
7
+ import { inspectCoreArtifactMetadata, findCoreArtifactMetadata } from './core-artifacts.js';
8
+
9
+ const ID = /^op_[a-f0-9]{64}$/, ARTIFACT = /^artifact_[a-f0-9]{24}$/, UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
10
+ const STATES = ['queued', 'running', 'succeeded', 'failed', 'canceled', 'interrupted'];
11
+ const CODES = ['dependency_unavailable', 'permission_denied', 'execution_failed'];
12
+ const invalid = () => pluginFail('IRIS_OPERATION_STORE_INVALID', '持久插件 Operation 记录损坏或绑定不一致');
13
+ const hash = value => crypto.createHash('sha256').update(value).digest('hex');
14
+ const canonical = value => Array.isArray(value) ? value.map(canonical) : value !== null && typeof value === 'object'
15
+ ? Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])) : value;
16
+ export const pluginInputDigest = (id, input, dependency, sourceArtifactIds = []) => 'sha256:' + hash(JSON.stringify(canonical(['plugin.execute/v1', id, input, checkedPluginDependency(dependency), sourceArtifactIds])));
17
+ const iso = value => typeof value === 'string' && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
18
+ const ids = value => Array.isArray(value) && value.length <= 16 && new Set(value).size === value.length && value.every(id => typeof id === 'string' && ARTIFACT.test(id));
19
+ const fileFor = (root, marker, id) => { if (!ID.test(id)) invalid(); return path.join(requestDirectory(root, marker, 'operations'), id + '.json'); };
20
+ const save = (root, marker, record, create = false) => {
21
+ validatePluginOperation(record, record.id, marker);
22
+ (create ? atomicCreatePrivate : atomicWritePrivate)(fileFor(root, marker, record.id), JSON.stringify(record) + '\n');
23
+ return structuredClone(record);
24
+ };
25
+ const now = record => new Date(Math.max(Date.now(), Date.parse(record.updatedAt))).toISOString();
26
+
27
+ export function validatePluginOperation(record, id, marker) {
28
+ if (!pluginFields(record, ['schemaVersion', 'id', 'rootId', 'revision', 'action', 'operationId', 'dependency', 'input', 'inputDigest',
29
+ 'sourceArtifactIds', 'reservedArtifactIds', 'ownerInstanceId', 'createdAt', 'updatedAt', 'state', 'cancelRequested', 'startedAt', 'finishedAt', 'result', 'failureCode'],
30
+ ['schemaVersion', 'id', 'rootId', 'revision', 'action', 'operationId', 'dependency', 'input', 'inputDigest', 'sourceArtifactIds', 'reservedArtifactIds',
31
+ 'ownerInstanceId', 'createdAt', 'updatedAt', 'state', 'cancelRequested']) || record.schemaVersion !== 1 || record.action !== 'plugin.execute'
32
+ || !ID.test(id) || record.id !== id || record.rootId !== marker.rootId || !Number.isSafeInteger(record.revision) || record.revision < 1
33
+ || !UUID.test(record.ownerInstanceId) || !STATES.includes(record.state) || !iso(record.createdAt) || !iso(record.updatedAt) || record.updatedAt < record.createdAt
34
+ || record.cancelRequested !== (record.state === 'canceled') || !ids(record.sourceArtifactIds) || !ids(record.reservedArtifactIds)) invalid();
35
+ const dependency = checkedPluginDependency(record.dependency);
36
+ if (dependency.kind !== 'operations' || dependency.contributionId !== record.operationId
37
+ || !pluginObject(record.input) || record.inputDigest !== pluginInputDigest(record.operationId, record.input, dependency, record.sourceArtifactIds)
38
+ || Buffer.byteLength(JSON.stringify(record.input)) > 65536) invalid();
39
+ if (record.reservedArtifactIds.some((value, i) => value !== 'artifact_' + hash(JSON.stringify([record.id, i])).slice(0, 24))) invalid();
40
+ const active = ['queued', 'running'].includes(record.state), started = Object.hasOwn(record, 'startedAt');
41
+ if (record.state === 'queued' && (record.revision !== 1 || started || record.reservedArtifactIds.length || record.updatedAt !== record.createdAt)
42
+ || record.state === 'running' && !started || started && (!iso(record.startedAt) || record.startedAt < record.createdAt || record.startedAt > record.updatedAt)
43
+ || record.state === 'failed' && !CODES.includes(record.failureCode) || record.state !== 'failed' && Object.hasOwn(record, 'failureCode')) invalid();
44
+ if (active) { if (Object.hasOwn(record, 'finishedAt') || Object.hasOwn(record, 'result')) invalid(); }
45
+ else if (!iso(record.finishedAt) || record.finishedAt !== record.updatedAt || !pluginFields(record.result, ['value', 'artifactIds'])
46
+ || !ids(record.result.artifactIds) || record.result.artifactIds.some(value => !record.reservedArtifactIds.includes(value))
47
+ || record.state === 'succeeded' && (!started || record.result.artifactIds.length !== record.reservedArtifactIds.length)
48
+ || record.state !== 'succeeded' && record.result.value !== null
49
+ || Buffer.byteLength(JSON.stringify(record.result.value)) > 128 * 1024) invalid();
50
+ return record;
51
+ }
52
+
53
+ export function validatePluginBinding(root, record) {
54
+ for (const id of record.sourceArtifactIds) inspectCoreArtifactMetadata(root, id);
55
+ for (const id of record.result?.artifactIds || []) {
56
+ const artifact = inspectCoreArtifactMetadata(root, id);
57
+ if (artifact.kind !== 'plugin-image' || artifact.metadata.pluginOperationId !== record.id
58
+ || JSON.stringify(artifact.relations) !== JSON.stringify(record.sourceArtifactIds.map(artifactId => ({ type: 'derived-from', artifactId })))) invalid();
59
+ }
60
+ }
61
+ function read(root, marker, id) {
62
+ const file = fileFor(root, marker, id), stat = fs.lstatSync(file);
63
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256 * 1024) invalid();
64
+ let record; try { record = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) { invalid(); }
65
+ return validatePluginOperation(record, id, marker);
66
+ }
67
+ export function pluginSourceArtifacts(input, fields) {
68
+ const result = [...new Set(fields.flatMap(key => input[key] === undefined ? [] : Array.isArray(input[key]) ? input[key] : [input[key]]))];
69
+ if (!ids(result)) pluginFail('IRIS_PLUGIN_INPUT_INVALID', '业务来源须为最多 16 个有效 Artifact ID');
70
+ return result;
71
+ }
72
+ export function acceptPluginOperation(root, marker, id, { operationId, input, dependency, ownerInstanceId, sourceArtifactIds }) {
73
+ for (const artifactId of sourceArtifactIds) inspectCoreArtifactMetadata(root, artifactId);
74
+ const time = new Date().toISOString();
75
+ return save(root, marker, { schemaVersion: 1, id, rootId: marker.rootId, revision: 1, action: 'plugin.execute', operationId,
76
+ dependency: checkedPluginDependency(dependency), input: pluginJson(input), inputDigest: pluginInputDigest(operationId, input, dependency, sourceArtifactIds),
77
+ sourceArtifactIds, reservedArtifactIds: [], ownerInstanceId, createdAt: time, updatedAt: time, state: 'queued', cancelRequested: false }, true);
78
+ }
79
+ export function reservePluginArtifact(root, marker, id) {
80
+ const record = read(root, marker, id);
81
+ if (record.state !== 'running' || record.reservedArtifactIds.length >= 16) pluginFail('IRIS_PLUGIN_OPERATION_DENIED', '当前业务不可导入更多作品');
82
+ const artifactId = 'artifact_' + hash(JSON.stringify([record.id, record.reservedArtifactIds.length])).slice(0, 24);
83
+ save(root, marker, { ...record, revision: record.revision + 1, updatedAt: now(record), reservedArtifactIds: [...record.reservedArtifactIds, artifactId] });
84
+ return artifactId;
85
+ }
86
+ export function transitionPluginOperation(root, marker, value) {
87
+ const record = read(root, marker, value.id);
88
+ if (record.revision !== value.expectedRevision) pluginFail('IRIS_OPERATION_REVISION_CONFLICT', 'Operation 已改变');
89
+ const allowed = record.state === 'queued' ? ['running', 'failed', 'canceled', 'interrupted'] : record.state === 'running' ? STATES.filter(state => !['queued', 'running'].includes(state)) : [];
90
+ if (!allowed.includes(value.state)) pluginFail('IRIS_OPERATION_TRANSITION_INVALID', '结束业务不重新排队或重放');
91
+ if (value.state === 'failed' ? !CODES.includes(value.failureCode) : value.failureCode !== undefined) pluginFail('IRIS_OPERATION_INPUT_INVALID', '业务失败类别无效');
92
+ const time = now(record), terminal = value.state !== 'running';
93
+ const artifacts = terminal ? record.reservedArtifactIds.filter(id => findCoreArtifactMetadata(root, id)) : [];
94
+ return save(root, marker, { ...record, revision: record.revision + 1, state: value.state, updatedAt: time, cancelRequested: value.state === 'canceled',
95
+ ...(terminal ? { finishedAt: time, result: { value: value.state === 'succeeded' ? pluginJson(value.value) : null, artifactIds: artifacts } } : { startedAt: time }),
96
+ ...(value.failureCode ? { failureCode: value.failureCode } : {}) });
97
+ }
98
+ export function projectPluginOperation(record) {
99
+ const { schemaVersion: _version, ownerInstanceId: _owner, reservedArtifactIds: _planned, ...value } = record;
100
+ return { contractVersion: 0, ...structuredClone(value) };
101
+ }
@@ -0,0 +1,28 @@
1
+ // Internal Core invocation boundary. No Provider implementations, Zod, module loading or filesystem.
2
+ import { freezePluginValue, pluginJson, pluginFail } from './plugin-contract.js';
3
+
4
+ const registries = new WeakMap();
5
+ export function registerPluginOperations(registry, state) { registries.set(registry, state); }
6
+
7
+ /** Reserve across the Core queue; the public registry never exposes an executable handler. */
8
+ export function reservePluginOperation(registry, id, input, allowedOperations, expectedDependency) {
9
+ const owned = registries.get(registry);
10
+ if (!owned) pluginFail('IRIS_PLUGIN_INVALID', '需要当前插件注册实例');
11
+ owned.requireActive();
12
+ const operation = owned.operations.get(id);
13
+ if (!operation) pluginFail('IRIS_PLUGIN_CONTRIBUTION_MISSING', '业务贡献未加载');
14
+ if (!Array.isArray(allowedOperations) || !allowedOperations.includes(id)) pluginFail('IRIS_PLUGIN_OPERATION_DENIED', '业务贡献未获本次 Core 装配授权');
15
+ const dependency = registry.dependency('operations', id);
16
+ if (expectedDependency !== undefined) {
17
+ registry.assertDependency(expectedDependency);
18
+ if (expectedDependency.kind !== 'operations' || expectedDependency.contributionId !== id) pluginFail('IRIS_PLUGIN_DEPENDENCY_MISMATCH', '不能用其他贡献的持久依赖执行当前业务');
19
+ }
20
+ const checked = operation.schema.safeParse(pluginJson(input));
21
+ if (!checked.success) pluginFail('IRIS_PLUGIN_INPUT_INVALID', '输入不符合 ' + id + ' 的 Schema');
22
+ let release;
23
+ const done = new Promise(resolve => { release = () => { owned.active.delete(done); resolve(); }; });
24
+ owned.active.add(done);
25
+ return { mode: operation.descriptor.mode, commands: operation.descriptor.commands, artifactInputs: operation.descriptor.artifactInputs || [],
26
+ input: freezePluginValue(checked.data), dependency, release,
27
+ async invoke(context) { return { dependency, value: pluginJson(await operation.handler(freezePluginValue(checked.data), context)) }; } };
28
+ }
@@ -0,0 +1,109 @@
1
+ import { z } from 'zod';
2
+ import { createProviderRegistry } from './provider-extensions.js';
3
+ import { definePlugin, assertPluginCompatibility, checkedPluginDependency, pluginDependency, qualifiedPluginId,
4
+ pluginFields, pluginJson, pluginFail, comparePluginVersions, PLUGIN_KINDS } from './plugin-contract.js';
5
+ import { registerPluginOperations } from './plugin-operations.js';
6
+
7
+ const unavailable = () => pluginFail('IRIS_PLUGIN_CLOSED', '插件注册实例正在关闭或已关闭');
8
+
9
+ /** Manifest validation precedes every activation. Core owns call reservations and cleanup. */
10
+ export async function createPluginRegistry({ plugins, coreVersion }) {
11
+ if (!Array.isArray(plugins)) pluginFail('IRIS_PLUGIN_INVALID', 'plugins 须为明确的插件数组');
12
+ comparePluginVersions(coreVersion, coreVersion);
13
+ const definitions = plugins.map(definePlugin), entries = new Map(), contributionIds = new Set(), operations = new Map(), cleanups = [], active = new Set();
14
+ for (const definition of definitions) {
15
+ const { manifest } = definition; assertPluginCompatibility(manifest, coreVersion);
16
+ if (entries.has(manifest.id)) pluginFail('IRIS_PLUGIN_INVALID', '插件 ID 重复:' + manifest.id);
17
+ entries.set(manifest.id, definition);
18
+ for (const kind of PLUGIN_KINDS) for (const contribution of manifest.contributions[kind]) {
19
+ const key = kind + ':' + qualifiedPluginId(manifest.id, contribution.id);
20
+ if (contributionIds.has(key)) pluginFail('IRIS_PLUGIN_INVALID', '贡献完整 ID 重复:' + key);
21
+ contributionIds.add(key);
22
+ }
23
+ for (const operation of manifest.contributions.operations) {
24
+ let schema;
25
+ try { schema = z.fromJSONSchema(operation.inputSchema); }
26
+ catch (cause) { throw Object.assign(new Error('插件输入 Schema 无法编译:' + manifest.id + '.' + operation.id, { cause }), { code: 'IRIS_PLUGIN_INVALID' }); }
27
+ operations.set(qualifiedPluginId(manifest.id, operation.id), { manifest, descriptor: operation, schema });
28
+ }
29
+ }
30
+ let state = 'loading', providers, disposal, cleanupFailed = false;
31
+
32
+ function dispose() {
33
+ if (!disposal) {
34
+ state = 'closing';
35
+ const providerDisposal = providers?.dispose();
36
+ void providerDisposal?.catch(() => {}); // Awaited below after operation reservations drain.
37
+ disposal = (async () => {
38
+ await Promise.allSettled([...active]);
39
+ const errors = [];
40
+ try { await providerDisposal; } catch (error) { errors.push(error); }
41
+ for (const cleanup of [...cleanups].reverse()) {
42
+ try { await cleanup(); } catch (error) { errors.push(error); }
43
+ }
44
+ operations.clear(); state = 'closed'; cleanupFailed = errors.length > 0;
45
+ if (errors.length) throw new AggregateError(errors, '插件释放失败');
46
+ })();
47
+ }
48
+ return disposal;
49
+ }
50
+
51
+ try {
52
+ const factories = [];
53
+ for (const { manifest, activate } of definitions) {
54
+ let activating = true;
55
+ const context = Object.freeze({ coreVersion, plugin: Object.freeze({ id: manifest.id, version: manifest.version }),
56
+ onDispose(cleanup) {
57
+ if (!activating || typeof cleanup !== 'function') pluginFail('IRIS_PLUGIN_LIFECYCLE', '资源释放须在 activate 完成前注册');
58
+ cleanups.push(cleanup);
59
+ } });
60
+ let implementation;
61
+ try { implementation = await activate(context); } finally { activating = false; }
62
+ if (!pluginFields(implementation, ['providers', 'operations'], [])) pluginFail('IRIS_PLUGIN_INVALID', 'activate 须返回 Provider/业务实现映射;资源通过 onDispose 释放');
63
+ for (const kind of ['providers', 'operations']) {
64
+ const handlers = implementation[kind] === undefined ? {} : implementation[kind];
65
+ if (!pluginFields(handlers, manifest.contributions[kind].map(item => item.id))) pluginFail('IRIS_PLUGIN_INVALID', '实现必须与声明的 ' + kind + ' 一一对应');
66
+ if (kind === 'operations') for (const descriptor of manifest.contributions.operations) {
67
+ if (typeof handlers[descriptor.id] !== 'function') pluginFail('IRIS_PLUGIN_INVALID', '业务操作必须为函数');
68
+ operations.get(qualifiedPluginId(manifest.id, descriptor.id)).handler = handlers[descriptor.id];
69
+ }
70
+ else if (manifest.contributions.providers.length) factories.push(() => ({ contractVersion: 0, id: manifest.id, version: manifest.version,
71
+ protocols: manifest.contributions.providers.map(descriptor => {
72
+ const handler = handlers[descriptor.id];
73
+ if (!pluginFields(handler, ['normalizeEndpoint', 'createAdapter', 'supportsModel', 'supportsImageEdit'], ['normalizeEndpoint', 'createAdapter'])) {
74
+ pluginFail('IRIS_PLUGIN_INVALID', 'Provider 实现字段无效');
75
+ }
76
+ return { ...handler, id: qualifiedPluginId(manifest.id, descriptor.id), label: descriptor.title,
77
+ capabilities: descriptor.capabilities, configuration: descriptor.configuration,
78
+ dependency: pluginDependency(manifest, 'providers', descriptor.id) };
79
+ }) }));
80
+ }
81
+ }
82
+ providers = await createProviderRegistry({ extensions: factories });
83
+ state = 'active';
84
+ } catch (error) {
85
+ try { await dispose(); } catch (cleanup) { throw new AggregateError([error, cleanup], '插件激活与释放均失败'); }
86
+ throw error;
87
+ }
88
+
89
+ function dependency(kind, id) {
90
+ const manifest = definitions.map(item => item.manifest).find(item => item.contributions[kind]?.some(value => qualifiedPluginId(item.id, value.id) === id));
91
+ if (!manifest) pluginFail('IRIS_PLUGIN_CONTRIBUTION_MISSING', '所需插件贡献未加载');
92
+ return pluginDependency(manifest, kind, manifest.contributions[kind].find(item => qualifiedPluginId(manifest.id, item.id) === id).id);
93
+ }
94
+ function assertDependency(value) {
95
+ const expected = checkedPluginDependency(value), current = dependency(expected.kind, expected.contributionId);
96
+ if (Object.keys(expected).some(key => expected[key] !== current[key])) pluginFail('IRIS_PLUGIN_DEPENDENCY_MISMATCH', '原任务依赖的插件或贡献版本不一致;请恢复准确版本,不会重新提交');
97
+ return current;
98
+ }
99
+ const registry = Object.freeze({
100
+ providers,
101
+ snapshot: () => ({ contractVersion: 0, coreVersion, state, activeCalls: active.size, cleanupFailed,
102
+ plugins: definitions.map(({ manifest }) => pluginJson(manifest)),
103
+ contributions: Object.fromEntries(PLUGIN_KINDS.map(kind => [kind, definitions.flatMap(({ manifest }) => manifest.contributions[kind]
104
+ .map(entry => ({ ...pluginJson(entry), id: qualifiedPluginId(manifest.id, entry.id), dependency: pluginJson(pluginDependency(manifest, kind, entry.id)) })))])) }),
105
+ dependency, assertDependency, dispose
106
+ });
107
+ registerPluginOperations(registry, { operations, active, requireActive: () => { if (state !== 'active') unavailable(); } });
108
+ return registry;
109
+ }