@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.
- package/CHANGELOG.md +143 -1
- package/README.en.md +109 -311
- package/README.md +106 -309
- package/bin/dsh-iris.js +452 -92
- package/docs/AGENT_CORE_QUICKSTART.md +64 -0
- package/docs/AGENT_QUICKSTART.md +2 -0
- package/docs/ARCHITECTURE.md +13 -3
- package/docs/ARTIFACT_MANIFEST.md +4 -0
- package/docs/AUDIO_FLOWS.md +87 -0
- package/docs/BETA_SCOPE.md +42 -0
- package/docs/CONTACT_SHEET.md +62 -0
- package/docs/CORE_CONNECTIONS.md +101 -0
- package/docs/CORE_QUICKSTART.md +110 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +7 -2
- package/docs/CORE_SERVICE.md +101 -0
- package/docs/CORE_SKILLS.md +35 -3
- package/docs/DSH_COMPATIBILITY.md +21 -1
- package/docs/DSH_CORE_MIGRATION.md +6 -0
- package/docs/DSH_RC2_ADAPTATION.md +1 -1
- package/docs/DSH_RUNTIME.md +6 -2
- package/docs/HEADLESS_CLI.md +30 -5
- package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
- package/docs/IMAGE_FLOW.md +4 -0
- package/docs/IMAGE_GENERATION.md +127 -0
- package/docs/LOCAL_API.md +4 -2
- package/docs/LOCAL_API_CONTRACT.md +42 -15
- package/docs/LOCAL_CREATION.md +7 -1
- package/docs/LOCAL_OPERATIONS.md +46 -0
- package/docs/LOCAL_SELECTION.md +57 -0
- package/docs/LOCAL_WEB.md +207 -0
- package/docs/MCP.md +212 -0
- package/docs/MEDIA_SCHEDULING.md +72 -0
- package/docs/MUSIC.md +2 -0
- package/docs/NETWORK_RESOURCES.md +106 -0
- package/docs/NETWORK_ROUTING.md +103 -0
- package/docs/NETWORK_WORKBENCH.md +65 -0
- package/docs/PANEL_EXTENSIONS.md +64 -0
- package/docs/PLUGINS.md +193 -0
- package/docs/PLUGIN_DEVELOPMENT.md +39 -0
- package/docs/PLUGIN_STARTUP.md +113 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
- package/docs/PROVIDER_CREDENTIALS.md +67 -0
- package/docs/PROVIDER_EXTENSIONS.md +82 -0
- package/docs/PROVIDER_HEALTH.md +5 -1
- package/docs/PROVIDER_NETWORK.md +45 -0
- package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
- package/docs/PROVIDER_SETTINGS.md +91 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +8 -0
- package/docs/ROADMAP.md +18 -8
- package/docs/SECURITY.md +1 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
- package/docs/VIDEO_FLOWS.md +89 -0
- package/docs/VISION_CLI.md +1 -1
- package/docs/VISION_MODEL.md +2 -0
- package/docs/WEB_PROTOTYPE.md +21 -6
- package/docs/WEB_RECOVERY.md +46 -0
- package/docs/WORKBENCH_ARTIFACTS.md +6 -0
- package/docs/WORKBENCH_LOGIN.md +63 -0
- package/docs/assets/screenshots/core-connections-preview.webp +0 -0
- package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
- package/docs/assets/screenshots/core-overview-preview.webp +0 -0
- package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-preview.webp +0 -0
- package/docs/releases/0.3.0-alpha.1.md +51 -0
- package/docs/releases/0.3.0-beta.1.md +34 -0
- package/docs/screenshots.md +41 -1
- package/lib/actions.js +56 -4
- package/lib/adapters.js +75 -49
- package/lib/api.js +23 -13
- package/lib/application-runtime.js +73 -17
- package/lib/application-writer.js +193 -0
- package/lib/audio-flow-input.js +37 -0
- package/lib/client.d.ts +3 -3
- package/lib/client.js +402 -72
- package/lib/comfyui-adapter.js +95 -0
- package/lib/comfyui-client.js +60 -0
- package/lib/comfyui-inspection.js +73 -0
- package/lib/comfyui-workflow.js +94 -0
- package/lib/command-service.js +18 -5
- package/lib/config.js +49 -10
- package/lib/configured-image-runtime.js +69 -0
- package/lib/connection-skill.js +98 -0
- package/lib/connection-template.js +23 -0
- package/lib/core-artifact-store.js +84 -28
- package/lib/core-bridge-selection.js +31 -0
- package/lib/core-bridge-tools.js +85 -0
- package/lib/core-capability-catalog.js +192 -0
- package/lib/core-connection-diagnostics.js +49 -0
- package/lib/core-connection-store.js +277 -0
- package/lib/core-connections.js +225 -0
- package/lib/core-delivery-cache.js +8 -3
- package/lib/core-maintenance.js +9 -3
- package/lib/core-runtime.js +2 -1
- package/lib/core-service-client.js +174 -0
- package/lib/core-tasks.js +113 -21
- package/lib/core-writer-lease.js +16 -0
- package/lib/creation-request-store.js +41 -64
- package/lib/doctor.js +2 -2
- package/lib/dsh-core-adapter.js +61 -9
- package/lib/dsh-host-adapter.js +4 -1
- package/lib/dsh-provider-config.js +1 -1
- package/lib/dsh-service-bridge.js +67 -0
- package/lib/execution-ownership.js +43 -0
- package/lib/flow-store.js +156 -14
- package/lib/generation-input.js +10 -1
- package/lib/headless-vision.js +7 -7
- package/lib/image-edit-input.js +1 -1
- package/lib/image-flow.js +97 -31
- package/lib/index.js +47 -22
- package/lib/local-api-projection.js +15 -6
- package/lib/local-api-token-directory.js +31 -0
- package/lib/local-api.js +861 -71
- package/lib/local-creation.js +96 -24
- package/lib/local-operations.js +36 -0
- package/lib/local-request-root.js +86 -0
- package/lib/local-web-assets.js +39 -0
- package/lib/mcp-bridge.js +58 -0
- package/lib/mcp-check.js +26 -0
- package/lib/mcp-config.js +42 -0
- package/lib/mcp-delivery.js +46 -0
- package/lib/mcp-http.js +174 -0
- package/lib/mcp-server.js +69 -0
- package/lib/media-scheduler.js +194 -0
- package/lib/model-directory-service.js +37 -10
- package/lib/model-directory.js +3 -3
- package/lib/models.js +2 -2
- package/lib/music-transport.js +6 -3
- package/lib/network-config-service.js +123 -0
- package/lib/network-config.js +127 -0
- package/lib/network-doctor.js +94 -0
- package/lib/network-management.js +51 -0
- package/lib/network-policy.js +19 -0
- package/lib/network-route-evidence.js +38 -0
- package/lib/network-routing.js +283 -0
- package/lib/operation-runner.js +281 -0
- package/lib/operation-store.js +216 -0
- package/lib/panel-extension.js +36 -0
- package/lib/plugin-contract.js +194 -0
- package/lib/plugin-execution.js +51 -0
- package/lib/plugin-launch.js +86 -0
- package/lib/plugin-operation-store.js +101 -0
- package/lib/plugin-operations.js +28 -0
- package/lib/plugin-registry.js +109 -0
- package/lib/plugin-selection.js +15 -0
- package/lib/plugin-source-image.js +5 -0
- package/lib/plugin-web-assets.js +54 -0
- package/lib/plugins.d.ts +307 -0
- package/lib/plugins.js +59 -0
- package/lib/provider-adapter.js +7 -3
- package/lib/provider-adapters.js +92 -22
- package/lib/provider-catalog.js +64 -36
- package/lib/provider-config-format.js +51 -0
- package/lib/provider-config-service.js +143 -31
- package/lib/provider-context.d.ts +90 -0
- package/lib/provider-context.js +42 -0
- package/lib/provider-credentials.js +97 -0
- package/lib/provider-diagnostics.js +56 -0
- package/lib/provider-extensions.js +29 -0
- package/lib/provider-health.js +18 -6
- package/lib/provider-monitor.js +81 -0
- package/lib/provider-network.js +52 -0
- package/lib/provider-protocol.js +73 -22
- package/lib/provider-registry.js +128 -0
- package/lib/provider-response.js +38 -0
- package/lib/provider-settings.js +136 -0
- package/lib/provider-task-runner.js +132 -43
- package/lib/provider-transport.js +16 -0
- package/lib/render.js +11 -1
- package/lib/scheduling-policy.js +66 -0
- package/lib/skill-installation.js +23 -11
- package/lib/tasks.js +3 -2
- package/lib/video-flow-input.js +16 -0
- package/lib/video-input.js +12 -5
- package/lib/vision-model-routing.js +5 -0
- package/lib/web/app.js +703 -0
- package/lib/web/appearance.js +111 -0
- package/lib/web/comfyui.js +146 -0
- package/lib/web/connections.js +516 -0
- package/lib/web/creation.js +359 -0
- package/lib/web/flow-view.js +34 -0
- package/lib/web/gallery.js +133 -0
- package/lib/web/index.html +344 -0
- package/lib/web/media-client.js +218 -0
- package/lib/web/network.js +232 -0
- package/lib/web/plugin-panel.js +223 -0
- package/lib/web/plugin-status.js +140 -0
- package/lib/web/plugin-surface.css +17 -0
- package/lib/web/providers.js +513 -0
- package/lib/web/recovery.js +105 -0
- package/lib/web/startup.js +18 -0
- package/lib/web/styles.css +571 -0
- package/lib/web/transport.js +101 -0
- package/lib/web/workbench-login.js +165 -0
- package/lib/workbench-artifacts.js +3 -2
- package/lib/workbench-auth-cli.js +47 -0
- package/lib/workbench-auth.js +200 -0
- package/package.json +24 -6
- package/plugins/contact-sheet/index.d.mts +4 -0
- package/plugins/contact-sheet/index.mjs +33 -0
- package/plugins/contact-sheet/web/board.css +14 -0
- package/plugins/contact-sheet/web/board.mjs +93 -0
- package/skills/iris-core/SKILL.md +8 -4
- package/skills/iris-core/references/cli.md +2 -2
- package/skills/iris-core/references/comfyui.md +69 -0
- package/skills/iris-core/references/tasks.md +2 -0
- package/templates/plugin-starter/LICENSE +21 -0
- package/templates/plugin-starter/README.md +74 -0
- package/templates/plugin-starter/assets/peach.png +0 -0
- package/templates/plugin-starter/assets/violet.png +0 -0
- package/templates/plugin-starter/index.mjs +27 -0
- package/templates/plugin-starter/package.json +18 -0
- package/templates/plugin-starter/scripts/core-cli.mjs +20 -0
- package/templates/plugin-starter/scripts/environment.mjs +65 -0
- package/templates/plugin-starter/scripts/mcp-probe.mjs +22 -0
- package/templates/plugin-starter/scripts/setup.mjs +42 -0
- package/templates/plugin-starter/scripts/smoke.mjs +76 -0
- package/templates/plugin-starter/web/board.css +27 -0
- package/templates/plugin-starter/web/board.mjs +160 -0
- package/templates/plugin-starter/web/png.mjs +23 -0
- package/templates/provider-starter/LICENSE +21 -0
- package/templates/provider-starter/README.md +50 -0
- package/templates/provider-starter/index.d.mts +5 -0
- package/templates/provider-starter/index.mjs +165 -0
- package/templates/provider-starter/package.json +13 -0
- package/templates/provider-starter/scripts/core-cli.mjs +22 -0
- package/templates/provider-starter/scripts/environment.mjs +122 -0
- package/templates/provider-starter/scripts/fixture.mjs +54 -0
- package/templates/provider-starter/scripts/mcp-probe.mjs +21 -0
- package/templates/provider-starter/scripts/protocol-smoke.mjs +48 -0
- package/templates/provider-starter/scripts/setup.mjs +44 -0
- package/templates/provider-starter/scripts/smoke.mjs +145 -0
- package/user_guide.md +5 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { inspectExecutionRoot } from './operation-store.js';
|
|
5
|
+
import { checkedBridgeCapabilities, checkedBridgeTools, selectedBridgeTools, DEFAULT_BRIDGE_CAPABILITIES, LEGACY_BRIDGE_TOOLS_V0 } from './core-bridge-selection.js';
|
|
6
|
+
import { CORE_SCOPE_DEFINITIONS } from './core-capability-catalog.js';
|
|
7
|
+
import { checkedPluginSelection, hasPluginSelection } from './plugin-selection.js';
|
|
8
|
+
|
|
9
|
+
export const CONNECTION_CONTRACT_VERSION = 0;
|
|
10
|
+
export const CONNECTION_SCOPES = Object.freeze(CORE_SCOPE_DEFINITIONS.map(scope => scope.id));
|
|
11
|
+
export const HARNESS_ADAPTERS = Object.freeze([
|
|
12
|
+
Object.freeze({ adapterId: 'iris.mcp', harnessKinds: ['kimi', 'generic'], transport: 'stdio-to-local-http', contractVersions: [0] }),
|
|
13
|
+
Object.freeze({ adapterId: 'iris.dsh-service', harnessKinds: ['dsh'], transport: 'local-http', contractVersions: [0] })
|
|
14
|
+
]);
|
|
15
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
|
|
16
|
+
const TOKEN = /^[A-Za-z0-9_-]{43}$/;
|
|
17
|
+
const hash = value => crypto.createHash('sha256').update(value).digest();
|
|
18
|
+
export function connectionFail(code, message) { throw Object.assign(new Error(message), { code }); }
|
|
19
|
+
function exact(value, fields, optional = []) {
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !fields.includes(key) && !optional.includes(key))
|
|
21
|
+
|| !fields.every(field => Object.hasOwn(value, field))) connectionFail('IRIS_CONNECTION_INPUT_INVALID', '连接配置字段无效');
|
|
22
|
+
}
|
|
23
|
+
export function checkedCoreOrigin(value) {
|
|
24
|
+
const match = typeof value === 'string' && /^http:\/\/127\.0\.0\.1:([1-9][0-9]{0,4})$/.exec(value);
|
|
25
|
+
if (!match || Number(match[1]) > 65535) {
|
|
26
|
+
connectionFail('IRIS_CONNECTION_INPUT_INVALID', 'Core 地址必须是 http://127.0.0.1:端口,不含路径或凭据');
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function checkedIdentity(value) {
|
|
31
|
+
const adapter = HARNESS_ADAPTERS.find(item => item.adapterId === value.adapterId);
|
|
32
|
+
if (!adapter?.harnessKinds.includes(value.harnessKind) || !UUID.test(value.connectionId) || !UUID.test(value.rootId)
|
|
33
|
+
|| typeof value.label !== 'string' || !value.label.trim() || value.label.length > 80
|
|
34
|
+
|| /[\x00-\x1f\x7f]/.test(value.label)) connectionFail('IRIS_CONNECTION_INPUT_INVALID', '接入身份或标签无效');
|
|
35
|
+
}
|
|
36
|
+
function checkedScopes(scopes) {
|
|
37
|
+
if (!Array.isArray(scopes) || !scopes.includes('read') || scopes.length > CONNECTION_SCOPES.length || new Set(scopes).size !== scopes.length
|
|
38
|
+
|| scopes.some(scope => !CONNECTION_SCOPES.includes(scope))) connectionFail('IRIS_CONNECTION_INPUT_INVALID', '连接权限必须包含 read,且仅允许已知的有限范围');
|
|
39
|
+
}
|
|
40
|
+
export { checkedScopes as checkedConnectionScopes };
|
|
41
|
+
export function checkedDeclaredClient(value) {
|
|
42
|
+
exact(value, ['name', 'version'], ['title']);
|
|
43
|
+
for (const item of Object.values(value)) {
|
|
44
|
+
if (typeof item !== 'string' || !item.trim() || item.length > 160 || /[\x00-\x1f\x7f]/.test(item)) {
|
|
45
|
+
connectionFail('IRIS_CONNECTION_INPUT_INVALID', '客户端声明需要有界名称与版本');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return structuredClone(value);
|
|
49
|
+
}
|
|
50
|
+
export function projectedMcpClient(value) {
|
|
51
|
+
if (!value || typeof value.name !== 'string' || typeof value.version !== 'string') return null;
|
|
52
|
+
const text = item => item.replace(/[\x00-\x1f\x7f]/g, ' ').trim().slice(0, 160) || 'unknown';
|
|
53
|
+
return { name: text(value.name), version: text(value.version), ...(typeof value.title === 'string' && value.title.trim() ? { title: text(value.title) } : {}) };
|
|
54
|
+
}
|
|
55
|
+
export function readPrivateConnectionJson(file) {
|
|
56
|
+
if (typeof file !== 'string' || !path.isAbsolute(file)) connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '连接文件必须为显式绝对路径');
|
|
57
|
+
let stat, value;
|
|
58
|
+
try {
|
|
59
|
+
stat = fs.lstatSync(file);
|
|
60
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 64 * 1024 || process.platform !== 'win32' && stat.mode & 0o077) throw new Error('private file required');
|
|
61
|
+
value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
62
|
+
} catch (_) { connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '连接文件不可读、格式损坏或权限过宽'); }
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
export function checkedClientConnection(value) {
|
|
66
|
+
exact(value, ['schemaVersion', 'origin', 'rootId', 'connectionId', 'adapterId', 'harnessKind', 'label', 'accessToken'], ['capabilities', 'tools', 'pluginOperations']);
|
|
67
|
+
if (value.schemaVersion !== 0 || !TOKEN.test(value.accessToken)) connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '连接凭据版本或格式不受支持');
|
|
68
|
+
checkedIdentity(value); checkedCoreOrigin(value.origin);
|
|
69
|
+
checkedPluginSelection(value.pluginOperations);
|
|
70
|
+
return { ...structuredClone(value), capabilities: checkedBridgeCapabilities(value.capabilities === undefined ? DEFAULT_BRIDGE_CAPABILITIES : value.capabilities),
|
|
71
|
+
tools: checkedBridgeTools(value.tools === undefined ? LEGACY_BRIDGE_TOOLS_V0 : value.tools) };
|
|
72
|
+
}
|
|
73
|
+
export function readConnectionGrants(file) {
|
|
74
|
+
const value = readPrivateConnectionJson(file);
|
|
75
|
+
exact(value, ['schemaVersion', 'grants']);
|
|
76
|
+
if (value.schemaVersion !== 0 || !Array.isArray(value.grants) || !value.grants.length || value.grants.length > 32) {
|
|
77
|
+
connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '需要 1–32 个显式连接授权');
|
|
78
|
+
}
|
|
79
|
+
const ids = new Set(), tokens = new Set();
|
|
80
|
+
for (const grant of value.grants) {
|
|
81
|
+
exact(grant, ['rootId', 'connectionId', 'adapterId', 'harnessKind', 'label', 'accessToken', 'scopes'], ['capabilities', 'tools', 'pluginOperations']);
|
|
82
|
+
checkedIdentity(grant); checkedScopes(grant.scopes);
|
|
83
|
+
checkedPluginSelection(grant.pluginOperations);
|
|
84
|
+
checkedBridgeCapabilities(grant.capabilities === undefined ? DEFAULT_BRIDGE_CAPABILITIES : grant.capabilities);
|
|
85
|
+
checkedBridgeTools(grant.tools === undefined ? LEGACY_BRIDGE_TOOLS_V0 : grant.tools);
|
|
86
|
+
if (!TOKEN.test(grant.accessToken) || ids.has(grant.connectionId) || tokens.has(grant.accessToken)) connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '连接身份或凭据重复/无效');
|
|
87
|
+
ids.add(grant.connectionId); tokens.add(grant.accessToken);
|
|
88
|
+
}
|
|
89
|
+
return value.grants.map(grant => ({ ...structuredClone(grant), capabilities: [...(grant.capabilities ?? DEFAULT_BRIDGE_CAPABILITIES)],
|
|
90
|
+
tools: [...(grant.tools ?? LEGACY_BRIDGE_TOOLS_V0)] }));
|
|
91
|
+
}
|
|
92
|
+
/** Explicit new pairing directory. No harness configuration discovery or modification. */
|
|
93
|
+
export function createConnectionPair({ dataRoot, directory, origin, harnessKind = 'generic', label, scopes = ['read'], capabilities = DEFAULT_BRIDGE_CAPABILITIES, tools, pluginOperations } = {}) {
|
|
94
|
+
checkedCoreOrigin(origin); checkedScopes(scopes);
|
|
95
|
+
checkedBridgeCapabilities(capabilities);
|
|
96
|
+
pluginOperations = checkedPluginSelection(pluginOperations);
|
|
97
|
+
tools = tools === undefined ? selectedBridgeTools(capabilities, undefined, pluginOperations) : checkedBridgeTools(tools);
|
|
98
|
+
if (tools.some(name => !selectedBridgeTools(capabilities, undefined, pluginOperations).includes(name))) connectionFail('IRIS_CONNECTION_INPUT_INVALID', '工具不属于所选功能分组');
|
|
99
|
+
if (typeof dataRoot !== 'string' || !path.isAbsolute(dataRoot)) connectionFail('IRIS_CONNECTION_INPUT_INVALID', '需要明确的 v1 数据根绝对路径');
|
|
100
|
+
const adapterId = harnessKind === 'dsh' ? 'iris.dsh-service' : 'iris.mcp';
|
|
101
|
+
const root = fs.realpathSync.native(dataRoot), marker = inspectExecutionRoot(root);
|
|
102
|
+
const client = { schemaVersion: 0, origin, rootId: marker.rootId, connectionId: crypto.randomUUID(), adapterId, harnessKind, label,
|
|
103
|
+
accessToken: crypto.randomBytes(32).toString('base64url'), capabilities: [...capabilities], tools: [...tools], ...(pluginOperations.length ? { pluginOperations } : {}) };
|
|
104
|
+
checkedClientConnection(client);
|
|
105
|
+
if (typeof directory !== 'string' || !path.isAbsolute(directory)) connectionFail('IRIS_CONNECTION_INPUT_INVALID', '输出目录须为新的绝对路径');
|
|
106
|
+
const relative = path.relative(root, path.join(fs.realpathSync.native(path.dirname(directory)), path.basename(directory)));
|
|
107
|
+
if (!relative || !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative)) connectionFail('IRIS_CONNECTION_INPUT_INVALID', '连接凭据须存放在数据根之外');
|
|
108
|
+
fs.mkdirSync(directory, { mode: 0o700 });
|
|
109
|
+
const serverFile = path.join(directory, 'server-grants.json'), clientFile = path.join(directory, 'client.json');
|
|
110
|
+
const { schemaVersion: _version, origin: _origin, ...grant } = client;
|
|
111
|
+
fs.writeFileSync(serverFile, JSON.stringify({ schemaVersion: 0, grants: [{ ...grant, scopes }] }, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
112
|
+
fs.writeFileSync(clientFile, JSON.stringify(client, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
113
|
+
return { contractVersion: 0, connectionId: client.connectionId, adapterId, harnessKind, scopes, capabilities, tools, serverFile, clientFile };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Authenticated, per-connection and per-runtime facts. No filesystem or Provider access. */
|
|
117
|
+
export function createConnectionRegistry(grants, availableScopes, { onClientInfo = () => {}, availableCapabilities, availableTools } = {}) {
|
|
118
|
+
const configured = new Map(grants.map(grant => {
|
|
119
|
+
const { accessToken, ...publicGrant } = grant;
|
|
120
|
+
return [grant.connectionId, { ...structuredClone(publicGrant), capabilities: [...(grant.capabilities ?? DEFAULT_BRIDGE_CAPABILITIES)],
|
|
121
|
+
tools: [...(grant.tools ?? LEGACY_BRIDGE_TOOLS_V0)], tokenDigest: hash(accessToken), instances: new Map() }];
|
|
122
|
+
}));
|
|
123
|
+
function identify(connectionId, token) {
|
|
124
|
+
const grant = configured.get(connectionId);
|
|
125
|
+
if (!grant || !TOKEN.test(token) || !crypto.timingSafeEqual(hash(token), grant.tokenDigest)) connectionFail('IRIS_CONNECTION_AUTH_REQUIRED', '连接身份或凭据无效');
|
|
126
|
+
return grant.connectionId;
|
|
127
|
+
}
|
|
128
|
+
function handshake(connectionId, value) {
|
|
129
|
+
exact(value, ['contractVersion', 'adapterId', 'harnessKind', 'runtimeInstanceId', 'harnessVersion', 'requestedScopes'], ['requestedCapabilities', 'requestedTools', 'requestedPluginOperations']);
|
|
130
|
+
const grant = configured.get(connectionId);
|
|
131
|
+
if (value.contractVersion !== 0) connectionFail('IRIS_CONNECTION_VERSION_UNSUPPORTED', '连接契约版本不受支持');
|
|
132
|
+
if (value.adapterId !== grant.adapterId || value.harnessKind !== grant.harnessKind || !UUID.test(value.runtimeInstanceId)
|
|
133
|
+
|| typeof value.harnessVersion !== 'string' || value.harnessVersion.length > 80 || /[\x00-\x1f\x7f]/.test(value.harnessVersion)) {
|
|
134
|
+
connectionFail('IRIS_CONNECTION_INPUT_INVALID', '握手身份不符合本地授权');
|
|
135
|
+
}
|
|
136
|
+
checkedScopes(value.requestedScopes);
|
|
137
|
+
const capabilities = checkedBridgeCapabilities(value.requestedCapabilities === undefined ? grant.capabilities : value.requestedCapabilities);
|
|
138
|
+
const tools = checkedBridgeTools(value.requestedTools === undefined ? grant.tools : value.requestedTools);
|
|
139
|
+
const pluginOperations = checkedPluginSelection(value.requestedPluginOperations === undefined ? grant.pluginOperations : value.requestedPluginOperations);
|
|
140
|
+
if (pluginOperations.some(pin => !hasPluginSelection(grant.pluginOperations || [], pin))) connectionFail('IRIS_CONNECTION_CAPABILITY_DENIED', '插件业务或版本不在明确授权中');
|
|
141
|
+
if (value.requestedScopes.some(scope => !grant.scopes.includes(scope) || !availableScopes.includes(scope))) {
|
|
142
|
+
connectionFail('IRIS_CONNECTION_CAPABILITY_DENIED', '请求的能力未获授权或服务未开启');
|
|
143
|
+
}
|
|
144
|
+
if (capabilities.some(id => !grant.capabilities.includes(id) || availableCapabilities && !availableCapabilities.includes(id)) || tools.some(name => !grant.tools.includes(name))) {
|
|
145
|
+
connectionFail('IRIS_CONNECTION_CAPABILITY_DENIED', '请求的功能或工具未获本地授权');
|
|
146
|
+
}
|
|
147
|
+
if (!grant.instances.has(value.runtimeInstanceId) && grant.instances.size >= 8) {
|
|
148
|
+
const retired = [...grant.instances.values()].find(instance => instance.state === 'disconnected');
|
|
149
|
+
if (retired) grant.instances.delete(retired.runtimeInstanceId);
|
|
150
|
+
else connectionFail('IRIS_CONNECTION_BUSY', '该连接实例数量已达上限,请由 owner 明确断开旧实例');
|
|
151
|
+
}
|
|
152
|
+
const now = new Date().toISOString(), instance = { runtimeInstanceId: value.runtimeInstanceId, harnessVersion: value.harnessVersion,
|
|
153
|
+
connectedAt: now, lastSeenAt: now, state: 'connected', scopes: [...value.requestedScopes], capabilities,
|
|
154
|
+
tools: selectedBridgeTools(capabilities, tools, pluginOperations).filter(name => availableTools === undefined || availableTools.includes(name)),
|
|
155
|
+
...(pluginOperations.length ? { pluginOperations } : {}),
|
|
156
|
+
verification: 'handshake', liveness: 'not_probed' };
|
|
157
|
+
grant.instances.set(value.runtimeInstanceId, instance);
|
|
158
|
+
return { contractVersion: 0, rootId: grant.rootId, connectionId, adapterId: grant.adapterId, harnessKind: grant.harnessKind,
|
|
159
|
+
instance: structuredClone(instance), remoteHostPorts: [], connectionFeatures: ['client-info'] };
|
|
160
|
+
}
|
|
161
|
+
function authorize(connectionId, runtimeInstanceId, scope, tool) {
|
|
162
|
+
const grant = configured.get(connectionId), instance = grant?.instances.get(runtimeInstanceId);
|
|
163
|
+
if (!instance || instance.state !== 'connected') connectionFail('IRIS_CONNECTION_HANDSHAKE_REQUIRED', '请先完成本次服务的连接握手');
|
|
164
|
+
if (!instance.scopes.includes(scope)) connectionFail('IRIS_CONNECTION_CAPABILITY_DENIED', '此连接未获该操作权限');
|
|
165
|
+
if (tool && !instance.tools.includes(tool)) connectionFail('IRIS_CONNECTION_CAPABILITY_DENIED', '此功能或工具未被用户选择');
|
|
166
|
+
instance.lastSeenAt = new Date().toISOString();
|
|
167
|
+
return [...instance.scopes];
|
|
168
|
+
}
|
|
169
|
+
function disconnect(connectionId, runtimeInstanceId) {
|
|
170
|
+
const instance = configured.get(connectionId)?.instances.get(runtimeInstanceId);
|
|
171
|
+
if (!instance) connectionFail('IRIS_CONNECTION_HANDSHAKE_REQUIRED', '没有此连接实例');
|
|
172
|
+
instance.state = 'disconnected'; instance.lastSeenAt = new Date().toISOString();
|
|
173
|
+
return { contractVersion: 0, disconnected: true };
|
|
174
|
+
}
|
|
175
|
+
function list(connectionId) {
|
|
176
|
+
return { contractVersion: 0, adapters: structuredClone(HARNESS_ADAPTERS), connections: [...configured.values()]
|
|
177
|
+
.filter(grant => connectionId === undefined || grant.connectionId === connectionId)
|
|
178
|
+
.map(({ tokenDigest: _secret, instances, ...grant }) => ({ ...structuredClone(grant), configured: true,
|
|
179
|
+
harnessInstalled: 'unknown', instances: [...instances.values()].map(instance => structuredClone(instance)) })) };
|
|
180
|
+
}
|
|
181
|
+
function dispose() { for (const grant of configured.values()) grant.tokenDigest.fill(0); configured.clear(); }
|
|
182
|
+
function selection(connectionId, runtimeInstanceId) {
|
|
183
|
+
const instance = configured.get(connectionId).instances.get(runtimeInstanceId);
|
|
184
|
+
return { capabilities: [...instance.capabilities], tools: [...instance.tools], ...(instance.pluginOperations?.length ? { pluginOperations: structuredClone(instance.pluginOperations) } : {}) };
|
|
185
|
+
}
|
|
186
|
+
function clientInfo(connectionId, runtimeInstanceId, value) {
|
|
187
|
+
exact(value, ['clientInfo', 'transport']);
|
|
188
|
+
const info = checkedDeclaredClient(value.clientInfo);
|
|
189
|
+
if (!['stdio', 'http'].includes(value.transport)) connectionFail('IRIS_CONNECTION_INPUT_INVALID', 'MCP 传输声明无效');
|
|
190
|
+
const instance = configured.get(connectionId)?.instances.get(runtimeInstanceId);
|
|
191
|
+
if (!instance || instance.state !== 'connected') connectionFail('IRIS_CONNECTION_HANDSHAKE_REQUIRED', '请先连接当前实例');
|
|
192
|
+
const contact = { clientInfo: info, transport: value.transport, source: 'client-declared', at: new Date().toISOString() };
|
|
193
|
+
onClientInfo(connectionId, contact);
|
|
194
|
+
instance.declaredClient = contact; instance.lastSeenAt = contact.at;
|
|
195
|
+
return { contractVersion: 0, recorded: true };
|
|
196
|
+
}
|
|
197
|
+
function setGrant(grant) {
|
|
198
|
+
const old = configured.get(grant.connectionId), { accessToken, ...publicGrant } = grant;
|
|
199
|
+
const same = old && crypto.timingSafeEqual(old.tokenDigest, hash(accessToken))
|
|
200
|
+
&& ['adapterId', 'harnessKind', 'scopes', 'capabilities', 'tools', 'pluginOperations'].every(key => JSON.stringify(old[key]) === JSON.stringify(publicGrant[key]));
|
|
201
|
+
if (old) old.tokenDigest.fill(0);
|
|
202
|
+
configured.set(grant.connectionId, { ...structuredClone(publicGrant), tokenDigest: hash(accessToken), instances: same ? old.instances : new Map() });
|
|
203
|
+
return { authorizationChanged: !same };
|
|
204
|
+
}
|
|
205
|
+
function revoke(connectionId) {
|
|
206
|
+
const grant = configured.get(connectionId);
|
|
207
|
+
if (grant) { grant.tokenDigest.fill(0); configured.delete(connectionId); }
|
|
208
|
+
}
|
|
209
|
+
function identifyMcp(token) {
|
|
210
|
+
if (!TOKEN.test(token)) connectionFail('IRIS_CONNECTION_AUTH_REQUIRED', '需要明确的 MCP 连接凭据');
|
|
211
|
+
const tokenDigest = hash(token);
|
|
212
|
+
const grant = [...configured.values()].find(item => crypto.timingSafeEqual(tokenDigest, item.tokenDigest));
|
|
213
|
+
if (!grant) connectionFail('IRIS_CONNECTION_AUTH_REQUIRED', 'MCP 连接凭据无效');
|
|
214
|
+
if (grant.adapterId !== 'iris.mcp') connectionFail('IRIS_CONNECTION_CAPABILITY_DENIED', '此凭据没有授权 MCP 接入');
|
|
215
|
+
const { tokenDigest: _secret, instances: _instances, ...publicGrant } = grant;
|
|
216
|
+
return structuredClone(publicGrant);
|
|
217
|
+
}
|
|
218
|
+
function authorizePlugin(connectionId, dependency) {
|
|
219
|
+
const grant = configured.get(connectionId);
|
|
220
|
+
if (!grant?.scopes.includes('execute') || !grant.tools.includes('iris_core_plugin_call') || !hasPluginSelection(grant.pluginOperations || [], dependency)) {
|
|
221
|
+
connectionFail('IRIS_CONNECTION_CAPABILITY_DENIED', '原连接的插件业务执行授权已缩减或撤销');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return Object.freeze({ identify, identifyMcp, handshake, authorize, authorizePlugin, selection, disconnect, list, clientInfo, setGrant, revoke, dispose });
|
|
225
|
+
}
|
|
@@ -5,6 +5,7 @@ import crypto from 'node:crypto';
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { atomicWritePrivate, ensurePrivateDir } from './private-storage.js';
|
|
8
|
+
import { assertProviderMediaSize } from './provider-response.js';
|
|
8
9
|
|
|
9
10
|
const TASK_ID = /^task_[a-f0-9]{24}$/;
|
|
10
11
|
const ARTIFACT_ID = /^artifact_[a-f0-9]{24}$/;
|
|
@@ -42,9 +43,10 @@ function regular(file) {
|
|
|
42
43
|
return stat;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
function fingerprint(file) {
|
|
46
|
+
function fingerprint(file, capability) {
|
|
46
47
|
const stat = regular(file);
|
|
47
48
|
if (!stat.size) invalid();
|
|
49
|
+
assertProviderMediaSize(stat.size, capability);
|
|
48
50
|
const hash = crypto.createHash('sha256');
|
|
49
51
|
const chunk = Buffer.alloc(1024 * 1024);
|
|
50
52
|
const fd = fs.openSync(file, 'r');
|
|
@@ -75,6 +77,8 @@ export function createDeliveryCache(dataRoot, task, artifacts) {
|
|
|
75
77
|
if (remote.kind === 'remote-url') {
|
|
76
78
|
item.source = { kind: 'remote-url', url: remote.url, ...(remote.mediaType ? { mediaType: remote.mediaType } : {}) };
|
|
77
79
|
} else if (remote.kind === 'inline-base64') {
|
|
80
|
+
const padding = remote.data.endsWith('==') ? 2 : remote.data.endsWith('=') ? 1 : 0;
|
|
81
|
+
assertProviderMediaSize(remote.data.length / 4 * 3 - padding, task.capability);
|
|
78
82
|
const bytes = Buffer.from(remote.data, 'base64');
|
|
79
83
|
if (!bytes.length || bytes.toString('base64') !== remote.data) invalid();
|
|
80
84
|
const file = 'payload-' + crypto.randomBytes(12).toString('hex') + '.bin';
|
|
@@ -130,7 +134,7 @@ export function newDeliveryPayloadPath(dataRoot, cache) {
|
|
|
130
134
|
export function recordDeliveryPayload(dataRoot, cache, index, file, mediaType) {
|
|
131
135
|
const root = directory(dataRoot, cache.taskId);
|
|
132
136
|
if (path.dirname(file) !== root || !PAYLOAD_FILE.test(path.basename(file))) invalid();
|
|
133
|
-
cache.items[index].payload = { file: path.basename(file), ...fingerprint(file), mediaType };
|
|
137
|
+
cache.items[index].payload = { file: path.basename(file), ...fingerprint(file, cache.capability), mediaType };
|
|
134
138
|
delete cache.items[index].source;
|
|
135
139
|
save(dataRoot, cache);
|
|
136
140
|
}
|
|
@@ -139,7 +143,8 @@ export function readDeliveryPayload(dataRoot, cache, index) {
|
|
|
139
143
|
const payload = cache.items[index].payload;
|
|
140
144
|
const file = path.join(directory(dataRoot, cache.taskId), payload.file);
|
|
141
145
|
let actual;
|
|
142
|
-
try { actual = fingerprint(file
|
|
146
|
+
try { actual = fingerprint(file, cache.capability); }
|
|
147
|
+
catch (error) { if (error.code === 'IRIS_PROVIDER_RESPONSE_TOO_LARGE') throw error; invalid(); }
|
|
143
148
|
if (actual.size !== payload.size || actual.sha256 !== payload.sha256) invalid();
|
|
144
149
|
return file;
|
|
145
150
|
}
|
package/lib/core-maintenance.js
CHANGED
|
@@ -5,8 +5,9 @@ import { inspectCoreArtifact, refreshCoreArtifactInventory } from './core-artifa
|
|
|
5
5
|
import { assertFlowTaskMatches, inspectCoreTask, listCoreTasks } from './core-tasks.js';
|
|
6
6
|
import { atomicWritePrivate } from './private-storage.js';
|
|
7
7
|
import { readDeliveryCache } from './core-delivery-cache.js';
|
|
8
|
-
import { flowReferences } from './flow-store.js';
|
|
8
|
+
import { flowReferences, flowStep } from './flow-store.js';
|
|
9
9
|
import { creationReferences } from './creation-request-store.js';
|
|
10
|
+
import { operationReferences } from './operation-store.js';
|
|
10
11
|
|
|
11
12
|
const TASK = /^task_[a-f0-9]{24}$/;
|
|
12
13
|
const ARTIFACT = /^artifact_[a-f0-9]{24}$/;
|
|
@@ -55,7 +56,7 @@ function inventory(root) {
|
|
|
55
56
|
.map(name => inspectCoreArtifact(root, name.slice(0, -5)));
|
|
56
57
|
const flows = flowReferences(root);
|
|
57
58
|
for (const reference of flows) {
|
|
58
|
-
if (reference.run.
|
|
59
|
+
if (flowStep(reference.run).claimed && !tasks.some(task => task.id === flowStep(reference.run).taskId)) {
|
|
59
60
|
fail('IRIS_FLOW_TASK_CONFLICT', '流程已建立的 Task 丢失,停止管理');
|
|
60
61
|
}
|
|
61
62
|
}
|
|
@@ -64,7 +65,7 @@ function inventory(root) {
|
|
|
64
65
|
if (!reference) fail('IRIS_FLOW_TASK_CONFLICT', '流程 Task 的 Run 记录丢失,停止管理');
|
|
65
66
|
assertFlowTaskMatches(task, reference.run);
|
|
66
67
|
}
|
|
67
|
-
return { tasks, artifacts, flows, creations: creationReferences(root) };
|
|
68
|
+
return { tasks, artifacts, flows, creations: creationReferences(root), operations: operationReferences(root) };
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
function ids(value, pattern, label) {
|
|
@@ -78,6 +79,10 @@ export function planCoreDeletion(root, input) {
|
|
|
78
79
|
if (!taskIds.length && !artifactIds.length) fail('IRIS_COMMAND_INPUT_INVALID', '请明确指定 Task 或 Artifact ID');
|
|
79
80
|
const existing = inventory(root), selectedTasks = new Set(taskIds), selectedArtifacts = new Set(artifactIds);
|
|
80
81
|
const blockers = [], files = [];
|
|
82
|
+
for (const reference of existing.operations) {
|
|
83
|
+
for (const id of reference.taskIds) if (selectedTasks.has(id)) blockers.push({ id, referencedBy: reference.operationId, reason: 'operation_task_reference' });
|
|
84
|
+
for (const id of reference.artifactIds) if (selectedArtifacts.has(id)) blockers.push({ id, referencedBy: reference.operationId, reason: 'operation_artifact_reference' });
|
|
85
|
+
}
|
|
81
86
|
for (const reference of existing.creations) {
|
|
82
87
|
for (const id of reference.taskIds) if (selectedTasks.has(id)) blockers.push({ id, referencedBy: reference.requestId, reason: 'creation_task_reference' });
|
|
83
88
|
for (const id of reference.artifactIds) if (selectedArtifacts.has(id)) blockers.push({ id, referencedBy: reference.requestId, reason: 'creation_artifact_reference' });
|
|
@@ -136,6 +141,7 @@ export function planCoreCleanup(root, { older_than_ms = 3600000 } = {}) {
|
|
|
136
141
|
if (!Number.isSafeInteger(older_than_ms) || older_than_ms < 60000) fail('IRIS_COMMAND_INPUT_INVALID', 'older_than_ms 至少为 60000');
|
|
137
142
|
const existing = inventory(root);
|
|
138
143
|
const referenced = new Set(existing.artifacts.map(artifact => artifact.id));
|
|
144
|
+
for (const reference of existing.operations) for (const id of reference.artifactIds) referenced.add(id);
|
|
139
145
|
for (const reference of existing.creations) for (const id of reference.artifactIds) referenced.add(id);
|
|
140
146
|
for (const reference of existing.flows) for (const id of reference.artifactIds) referenced.add(id);
|
|
141
147
|
for (const artifact of existing.artifacts) for (const edge of artifact.relations) referenced.add(edge.artifactId);
|
package/lib/core-runtime.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from './core-contract.js';
|
|
15
15
|
import { ensurePrivateDir } from './private-storage.js';
|
|
16
16
|
import { listCreationRequests } from './creation-request-store.js';
|
|
17
|
+
import { readAllOperations } from './operation-store.js';
|
|
17
18
|
import { WRITER_LEASE_DIR, WRITER_INTENT_FILE, publishLeaseRecord, readWriterLease,
|
|
18
19
|
removeWriterIntent, retireLeaseDirectory, writerOwnerStatus } from './core-writer-lease.js';
|
|
19
20
|
|
|
@@ -164,7 +165,7 @@ export function createCoreRuntime(input) {
|
|
|
164
165
|
const candidateRoot = physicalDataRoot(options);
|
|
165
166
|
const candidateLease = options.mode === 'writer' ? acquireWriterLease(candidateRoot, instanceId) : null;
|
|
166
167
|
if (candidateLease) {
|
|
167
|
-
try { listCreationRequests(candidateRoot); }
|
|
168
|
+
try { listCreationRequests(candidateRoot); readAllOperations(candidateRoot); }
|
|
168
169
|
catch (error) {
|
|
169
170
|
try { releaseWriterLease(candidateLease); }
|
|
170
171
|
catch (cleanupError) {
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { checkedClientConnection, readPrivateConnectionJson, connectionFail } from './core-connections.js';
|
|
3
|
+
import { MAX_IMPORT_IMAGE_BYTES } from './image-import.js';
|
|
4
|
+
import { MAX_VIDEO_BYTES, VIDEO_TYPES } from './video-flow-input.js';
|
|
5
|
+
import { annotateCoreConnectionError } from './core-connection-diagnostics.js';
|
|
6
|
+
import { checkedPluginSelection, hasPluginSelection } from './plugin-selection.js';
|
|
7
|
+
|
|
8
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
|
|
9
|
+
const RUN = /^run_[a-f0-9]{24}$/, OP = /^op_[a-f0-9]{64}$/, ARTIFACT = /^artifact_[a-f0-9]{24}$/;
|
|
10
|
+
const input = () => connectionFail('IRIS_CONNECTION_INPUT_INVALID', 'Core 服务工具输入无效');
|
|
11
|
+
function reference(value, pattern) { if (typeof value !== 'string' || !pattern.test(value)) input(); return value; }
|
|
12
|
+
|
|
13
|
+
/** A bounded client of one explicitly authorized Core. It never owns a Core writer or falls back. */
|
|
14
|
+
export function createCoreServiceClient({ connectionFile, connection, requestedScopes = ['read'], requestedCapabilities, requestedTools, requestedPluginOperations,
|
|
15
|
+
harnessVersion = 'unknown', fetch: fetchImpl = globalThis.fetch } = {}) {
|
|
16
|
+
if (connectionFile !== undefined && connection !== undefined) throw new TypeError('连接配置只能明确提供一种来源');
|
|
17
|
+
const runtimeInstanceId = crypto.randomUUID(), lifetime = new AbortController();
|
|
18
|
+
let state = 'new', config, negotiated, connectionPromise, metadataPromise, metadataKey, metadataAt = 0, disposal;
|
|
19
|
+
async function request(route, { method = 'GET', value, bytes, key, mediaType, binary = false, signal, handshake = false, disconnect = false } = {}) {
|
|
20
|
+
if (state !== 'open' && !(handshake && state === 'connecting') && !(disconnect && state === 'closing')) connectionFail('IRIS_CORE_SERVICE_DISCONNECTED', 'Core 服务尚未连接,请显式连接');
|
|
21
|
+
const headers = { Authorization: 'Bearer ' + config.accessToken, 'Iris-Connection-Id': config.connectionId, 'Iris-Runtime-Id': runtimeInstanceId };
|
|
22
|
+
if (key !== undefined) headers['Idempotency-Key'] = reference(key, UUID);
|
|
23
|
+
if (value !== undefined || bytes !== undefined) headers['Content-Type'] = mediaType || 'application/json';
|
|
24
|
+
const combined = AbortSignal.any([...(disconnect ? [] : [lifetime.signal]), AbortSignal.timeout(disconnect ? 2000 : 15000), ...(signal ? [signal] : [])]);
|
|
25
|
+
let response;
|
|
26
|
+
try {
|
|
27
|
+
response = await fetchImpl(config.origin + route, { method, headers, redirect: 'error', signal: combined,
|
|
28
|
+
...(value !== undefined ? { body: JSON.stringify(value) } : bytes !== undefined ? { body: bytes } : {}) });
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (signal?.aborted || !disconnect && lifetime.signal.aborted) throw Object.assign(new Error('Core 请求等待已停止;已接收操作需按原 Key 查询'), { name: 'AbortError', code: 'IRIS_CORE_SERVICE_ABORTED' });
|
|
31
|
+
throw annotateCoreConnectionError(Object.assign(new Error('Core 服务无法连接,请检查服务状态'), {
|
|
32
|
+
code: combined.reason?.name === 'TimeoutError' ? 'IRIS_CORE_SERVICE_TIMEOUT' : 'IRIS_CORE_SERVICE_UNAVAILABLE' }), 'transport');
|
|
33
|
+
}
|
|
34
|
+
const limit = binary && binary !== 'text' && response.ok ? binary === 'video' ? MAX_VIDEO_BYTES : 64 * 1024 * 1024 : 1024 * 1024;
|
|
35
|
+
if (Number(response.headers.get('content-length')) > limit) { await response.body?.cancel(); connectionFail('IRIS_CORE_SERVICE_RESPONSE_INVALID', 'Core 响应超过有界大小'); }
|
|
36
|
+
const chunks = []; let size = 0;
|
|
37
|
+
try {
|
|
38
|
+
if (response.body) for await (const chunk of response.body) {
|
|
39
|
+
size += chunk.length;
|
|
40
|
+
if (size > limit) connectionFail('IRIS_CORE_SERVICE_RESPONSE_INVALID', 'Core 响应超过有界大小');
|
|
41
|
+
chunks.push(chunk);
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error.code === 'IRIS_CORE_SERVICE_RESPONSE_INVALID') throw error;
|
|
45
|
+
connectionFail('IRIS_CORE_SERVICE_UNAVAILABLE', 'Core 响应中断;请读取原 Operation,不要自动再次提交');
|
|
46
|
+
}
|
|
47
|
+
const result = Buffer.concat(chunks, size);
|
|
48
|
+
if (binary && response.ok) {
|
|
49
|
+
const mediaType = response.headers.get('content-type');
|
|
50
|
+
const allowed = binary === 'video' ? Object.keys(VIDEO_TYPES) : binary === 'audio' ? ['audio/wav', 'audio/mpeg', 'audio/ogg', 'audio/flac'] : binary === 'text' ? ['text/plain'] : ['image/png', 'image/jpeg', 'image/webp'];
|
|
51
|
+
if (!allowed.includes(mediaType)) connectionFail('IRIS_CORE_SERVICE_RESPONSE_INVALID', 'Core返回的作品类型与所选入口不一致');
|
|
52
|
+
return { bytes: result, mediaType };
|
|
53
|
+
}
|
|
54
|
+
let body;
|
|
55
|
+
try { body = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(result)); }
|
|
56
|
+
catch (_) { connectionFail('IRIS_CORE_SERVICE_RESPONSE_INVALID', 'Core 返回无效 JSON'); }
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
const code = /^IRIS_[A-Z_]+$/.test(body?.error?.code) ? body.error.code : 'IRIS_CORE_SERVICE_REJECTED';
|
|
59
|
+
throw annotateCoreConnectionError(Object.assign(new Error('Core 服务拒绝本次请求;请检查授权、原操作及服务状态'), {
|
|
60
|
+
code, status: response.status, ...(UUID.test(body?.error?.requestId) ? { requestId: body.error.requestId } : {}) }), handshake ? 'handshake' : disconnect ? 'disconnect' : 'tool');
|
|
61
|
+
}
|
|
62
|
+
if (body?.contractVersion !== 0) connectionFail('IRIS_CORE_SERVICE_RESPONSE_INVALID', 'Core 响应契约不受支持');
|
|
63
|
+
return body;
|
|
64
|
+
}
|
|
65
|
+
function connect() {
|
|
66
|
+
if (['closing', 'closed'].includes(state)) return Promise.reject(Object.assign(new Error('Core 连接已关闭'), { code: 'IRIS_CORE_SERVICE_DISCONNECTED' }));
|
|
67
|
+
if (connectionPromise) return connectionPromise;
|
|
68
|
+
connectionPromise = (async () => {
|
|
69
|
+
let stage = 'configuration';
|
|
70
|
+
try {
|
|
71
|
+
config = checkedClientConnection(connection ?? readPrivateConnectionJson(connectionFile));
|
|
72
|
+
state = 'connecting'; stage = 'handshake';
|
|
73
|
+
const pluginChoice = requestedPluginOperations === undefined ? config.pluginOperations : requestedPluginOperations;
|
|
74
|
+
const result = await request('/api/v0/connections/' + config.connectionId + '/handshake', { method: 'POST', handshake: true,
|
|
75
|
+
value: { contractVersion: 0, adapterId: config.adapterId, harnessKind: config.harnessKind, runtimeInstanceId,
|
|
76
|
+
harnessVersion, requestedScopes, requestedCapabilities: requestedCapabilities ?? config.capabilities,
|
|
77
|
+
requestedTools: requestedTools ?? config.tools,
|
|
78
|
+
...(pluginChoice === undefined ? {} : { requestedPluginOperations: checkedPluginSelection(pluginChoice) }) } });
|
|
79
|
+
if (state !== 'connecting') connectionFail('IRIS_CORE_SERVICE_ABORTED', '连接启动等待已停止');
|
|
80
|
+
if (result.rootId !== config.rootId || result.connectionId !== config.connectionId || result.instance?.runtimeInstanceId !== runtimeInstanceId
|
|
81
|
+
|| !Array.isArray(result.instance.scopes) || result.instance.scopes.some(scope => !requestedScopes.includes(scope))
|
|
82
|
+
|| !Array.isArray(result.instance.capabilities) || result.instance.capabilities.some(id => !(requestedCapabilities ?? config.capabilities).includes(id))
|
|
83
|
+
|| !Array.isArray(result.instance.tools) || result.instance.tools.some(name => !(requestedTools ?? config.tools).includes(name))) {
|
|
84
|
+
connectionFail('IRIS_CORE_SERVICE_RESPONSE_INVALID', 'Core 数据空间或协商身份不一致');
|
|
85
|
+
}
|
|
86
|
+
if (checkedPluginSelection(result.instance.pluginOperations).some(pin => !hasPluginSelection(pluginChoice ?? [], pin))) {
|
|
87
|
+
connectionFail('IRIS_CORE_SERVICE_RESPONSE_INVALID', 'Core 返回了未请求的插件业务依赖');
|
|
88
|
+
}
|
|
89
|
+
negotiated = result; metadataKey = null; metadataAt = 0; state = 'open'; return structuredClone(result);
|
|
90
|
+
} catch (error) { if (!['closing', 'closed'].includes(state)) state = 'disconnected'; throw annotateCoreConnectionError(error, stage); }
|
|
91
|
+
})();
|
|
92
|
+
void connectionPromise.then(() => { connectionPromise = null; }, () => { connectionPromise = null; });
|
|
93
|
+
return connectionPromise;
|
|
94
|
+
}
|
|
95
|
+
function snapshot() {
|
|
96
|
+
return { contractVersion: 0, state, ...(config ? { connectionId: config.connectionId, adapterId: config.adapterId,
|
|
97
|
+
harnessKind: config.harnessKind, label: config.label, runtimeInstanceId, scopes: negotiated?.instance.scopes || [],
|
|
98
|
+
capabilities: negotiated?.instance.capabilities || [], tools: negotiated?.instance.tools || [],
|
|
99
|
+
...(negotiated?.instance.pluginOperations?.length ? { pluginOperations: structuredClone(negotiated.instance.pluginOperations) } : {}) } : {}) };
|
|
100
|
+
}
|
|
101
|
+
async function reportClientInfo(clientInfo, transport) {
|
|
102
|
+
if (!clientInfo || !negotiated?.connectionFeatures?.includes('client-info')) return;
|
|
103
|
+
const key = JSON.stringify({ clientInfo, transport });
|
|
104
|
+
if (key === metadataKey && Date.now() - metadataAt < 60000) return metadataPromise;
|
|
105
|
+
metadataKey = key; metadataAt = Date.now();
|
|
106
|
+
const attempt = request('/api/v0/connections/' + config.connectionId + '/client-info', { method: 'POST', value: { clientInfo, transport } });
|
|
107
|
+
metadataPromise = attempt;
|
|
108
|
+
void attempt.catch(() => { if (metadataPromise === attempt) { metadataKey = null; metadataAt = 0; } });
|
|
109
|
+
return metadataPromise;
|
|
110
|
+
}
|
|
111
|
+
function dispose() {
|
|
112
|
+
if (disposal) return disposal;
|
|
113
|
+
const wasOpen = state === 'open', starting = connectionPromise;
|
|
114
|
+
state = 'closing'; lifetime.abort();
|
|
115
|
+
disposal = (async () => {
|
|
116
|
+
let remoteDisconnect = wasOpen ? 'unconfirmed' : 'not_connected';
|
|
117
|
+
try {
|
|
118
|
+
if (wasOpen) {
|
|
119
|
+
await request('/api/v0/connections/' + config.connectionId + '/disconnect', { method: 'POST', value: {}, disconnect: true });
|
|
120
|
+
remoteDisconnect = 'confirmed';
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (!['IRIS_CORE_SERVICE_UNAVAILABLE', 'IRIS_CORE_SERVICE_TIMEOUT', 'IRIS_CORE_SERVICE_ABORTED', 'IRIS_CONNECTION_HANDSHAKE_REQUIRED',
|
|
124
|
+
'IRIS_CONNECTION_AUTH_REQUIRED', 'IRIS_API_AUTH_REQUIRED', 'IRIS_API_UNAVAILABLE'].includes(error.code)) throw error;
|
|
125
|
+
} finally {
|
|
126
|
+
await Promise.allSettled([starting, metadataPromise].filter(Boolean));
|
|
127
|
+
state = 'closed'; negotiated = null; config = null;
|
|
128
|
+
}
|
|
129
|
+
return { state, remoteDisconnect };
|
|
130
|
+
})();
|
|
131
|
+
return disposal;
|
|
132
|
+
}
|
|
133
|
+
function pageQuery(value = {}) {
|
|
134
|
+
if (!value || typeof value !== 'object' || Object.keys(value).some(key => !['limit', 'cursor'].includes(key))
|
|
135
|
+
|| value.limit !== undefined && (!Number.isInteger(value.limit) || value.limit < 1 || value.limit > 30)
|
|
136
|
+
|| value.cursor !== undefined && typeof value.cursor !== 'string') input();
|
|
137
|
+
return new URLSearchParams(Object.fromEntries(Object.entries(value).map(([key, value]) => [key, String(value)]))).toString();
|
|
138
|
+
}
|
|
139
|
+
return Object.freeze({ connect, dispose, snapshot, reportClientInfo,
|
|
140
|
+
pluginCatalog: (page, options) => request('/api/v0/plugin-operations/catalog?' + new URLSearchParams(page), options),
|
|
141
|
+
callPlugin({ key, ...value }, options) { return request('/api/v0/plugin-operations', { ...options, method: 'POST', value, key }); },
|
|
142
|
+
models: options => request('/api/v0/models', options),
|
|
143
|
+
audioModels: options => request('/api/v0/audio-models', options),
|
|
144
|
+
videoModels: options => request('/api/v0/video-models', options),
|
|
145
|
+
connections: options => request('/api/v0/connections', options),
|
|
146
|
+
runs: (page, options) => request('/api/v0/runs?' + pageQuery(page), options),
|
|
147
|
+
run: (id, options) => request('/api/v0/runs/' + reference(id, RUN), options),
|
|
148
|
+
image: (id, options) => request('/api/v0/artifacts/' + reference(id, ARTIFACT) + '/content', { ...options, binary: true }),
|
|
149
|
+
audio: (id, options) => request('/api/v0/artifacts/' + reference(id, ARTIFACT) + '/audio', { ...options, binary: 'audio' }),
|
|
150
|
+
video: (id, options) => request('/api/v0/artifacts/' + reference(id, ARTIFACT) + '/video', { ...options, binary: 'video' }),
|
|
151
|
+
transcript: (id, options) => request('/api/v0/artifacts/' + reference(id, ARTIFACT) + '/text', { ...options, binary: 'text' }),
|
|
152
|
+
createImage({ key, bytes, mediaType }, options) {
|
|
153
|
+
if (!(bytes instanceof Uint8Array) || !bytes.length || bytes.length > MAX_IMPORT_IMAGE_BYTES || !['image/png', 'image/jpeg', 'image/webp'].includes(mediaType)) input();
|
|
154
|
+
return request('/api/v0/images', { ...options, method: 'POST', bytes, key, mediaType });
|
|
155
|
+
},
|
|
156
|
+
createRun({ key, value }, options) { return request('/api/v0/runs', { ...options, method: 'POST', value, key }); },
|
|
157
|
+
createGeneration({ key, value }, options) { return request('/api/v0/generations', { ...options, method: 'POST', value, key }); },
|
|
158
|
+
createVideo({ key, value }, options) { return request('/api/v0/videos', { ...options, method: 'POST', value, key }); },
|
|
159
|
+
createAudio({ key, bytes, mediaType }, options) {
|
|
160
|
+
if (!(bytes instanceof Uint8Array) || !bytes.length || bytes.length > 20 * 1024 * 1024 || !['audio/wav', 'audio/mpeg', 'audio/ogg', 'audio/flac'].includes(mediaType)) input();
|
|
161
|
+
return request('/api/v0/audio', { ...options, method: 'POST', bytes, key, mediaType });
|
|
162
|
+
},
|
|
163
|
+
createSpeech({ key, value }, options) { return request('/api/v0/speeches', { ...options, method: 'POST', value, key }); },
|
|
164
|
+
createTranscription({ key, value }, options) { return request('/api/v0/transcriptions', { ...options, method: 'POST', value, key }); },
|
|
165
|
+
continueRun({ key, runId, expectedRevision }, options) {
|
|
166
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 1) input();
|
|
167
|
+
return request('/api/v0/runs/' + reference(runId, RUN) + '/continue', { ...options, method: 'POST', value: { expectedRevision }, key });
|
|
168
|
+
},
|
|
169
|
+
operation: (id, options) => request('/api/v0/operations/' + reference(id, OP), options),
|
|
170
|
+
lookup: (key, options) => request('/api/v0/operations?key=' + reference(key, UUID), options),
|
|
171
|
+
operations: (runId, page, options) => request('/api/v0/runs/' + reference(runId, RUN) + '/operations?' + pageQuery(page), options),
|
|
172
|
+
stop: (id, options) => request('/api/v0/operations/' + reference(id, OP) + '/cancel', { ...options, method: 'POST', value: {} })
|
|
173
|
+
});
|
|
174
|
+
}
|