@mokuyoaxis/dsh-iris 0.3.0-alpha.1 → 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 +63 -1
- package/README.en.md +20 -13
- package/README.md +20 -13
- package/bin/dsh-iris.js +143 -51
- package/docs/AGENT_CORE_QUICKSTART.md +2 -2
- package/docs/AGENT_QUICKSTART.md +1 -1
- package/docs/ARCHITECTURE.md +5 -5
- package/docs/ARTIFACT_MANIFEST.md +4 -0
- package/docs/AUDIO_FLOWS.md +20 -4
- package/docs/BETA_SCOPE.md +42 -0
- package/docs/CONTACT_SHEET.md +1 -1
- package/docs/CORE_CONNECTIONS.md +3 -1
- package/docs/CORE_QUICKSTART.md +7 -5
- package/docs/CORE_RUNTIME_CONTRACT.md +3 -3
- package/docs/CORE_SERVICE.md +9 -5
- package/docs/CORE_SKILLS.md +2 -2
- package/docs/DSH_COMPATIBILITY.md +11 -1
- package/docs/DSH_CORE_MIGRATION.md +6 -0
- package/docs/DSH_RUNTIME.md +2 -2
- package/docs/HEADLESS_CLI.md +29 -4
- package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
- package/docs/IMAGE_FLOW.md +2 -2
- package/docs/IMAGE_GENERATION.md +1 -1
- package/docs/LOCAL_API.md +2 -2
- package/docs/LOCAL_API_CONTRACT.md +17 -15
- package/docs/LOCAL_CREATION.md +2 -2
- package/docs/LOCAL_OPERATIONS.md +2 -2
- package/docs/LOCAL_SELECTION.md +1 -1
- package/docs/LOCAL_WEB.md +11 -5
- package/docs/MCP.md +30 -9
- package/docs/MEDIA_SCHEDULING.md +72 -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 +1 -1
- package/docs/PLUGINS.md +9 -5
- package/docs/PLUGIN_DEVELOPMENT.md +39 -0
- package/docs/PLUGIN_STARTUP.md +15 -2
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
- package/docs/PROVIDER_CREDENTIALS.md +67 -0
- package/docs/PROVIDER_EXTENSIONS.md +8 -4
- package/docs/PROVIDER_HEALTH.md +4 -2
- package/docs/PROVIDER_NETWORK.md +45 -0
- package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
- package/docs/PROVIDER_SETTINGS.md +13 -7
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +3 -1
- package/docs/ROADMAP.md +6 -6
- package/docs/SECURITY.md +1 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
- package/docs/VIDEO_FLOWS.md +89 -0
- package/docs/VISION_MODEL.md +2 -0
- package/docs/WEB_PROTOTYPE.md +3 -3
- package/docs/WEB_RECOVERY.md +2 -2
- package/docs/WORKBENCH_ARTIFACTS.md +6 -0
- package/docs/WORKBENCH_LOGIN.md +2 -2
- package/docs/releases/0.3.0-beta.1.md +34 -0
- package/docs/screenshots.md +2 -2
- package/lib/actions.js +5 -4
- package/lib/adapters.js +58 -41
- package/lib/api.js +23 -13
- package/lib/application-runtime.js +44 -15
- package/lib/application-writer.js +193 -0
- package/lib/client.d.ts +3 -3
- package/lib/client.js +62 -25
- package/lib/comfyui-adapter.js +3 -2
- package/lib/comfyui-client.js +10 -1
- package/lib/command-service.js +18 -5
- package/lib/config.js +13 -9
- package/lib/configured-image-runtime.js +28 -9
- package/lib/connection-skill.js +9 -0
- package/lib/core-artifact-store.js +68 -26
- package/lib/core-bridge-tools.js +5 -3
- package/lib/core-capability-catalog.js +21 -11
- package/lib/core-delivery-cache.js +8 -3
- package/lib/core-service-client.js +6 -2
- package/lib/core-tasks.js +86 -11
- package/lib/creation-request-store.js +8 -5
- package/lib/doctor.js +2 -2
- package/lib/dsh-core-adapter.js +61 -9
- package/lib/dsh-provider-config.js +1 -1
- package/lib/dsh-service-bridge.js +1 -0
- package/lib/flow-store.js +64 -17
- package/lib/image-flow.js +39 -17
- package/lib/index.js +10 -5
- package/lib/local-api-projection.js +6 -2
- package/lib/local-api.js +137 -49
- package/lib/local-creation.js +12 -5
- package/lib/local-operations.js +2 -2
- package/lib/local-request-root.js +2 -2
- package/lib/local-web-assets.js +2 -0
- package/lib/mcp-http.js +82 -9
- package/lib/mcp-server.js +23 -1
- package/lib/media-scheduler.js +194 -0
- package/lib/model-directory-service.js +25 -8
- package/lib/music-transport.js +4 -2
- 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 +83 -38
- package/lib/operation-store.js +6 -6
- package/lib/plugins.d.ts +25 -6
- package/lib/plugins.js +2 -1
- package/lib/provider-adapter.js +2 -2
- package/lib/provider-adapters.js +37 -5
- package/lib/provider-catalog.js +33 -16
- package/lib/provider-config-format.js +51 -0
- package/lib/provider-config-service.js +52 -22
- 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 +5 -5
- package/lib/provider-health.js +10 -5
- package/lib/provider-monitor.js +5 -3
- package/lib/provider-network.js +52 -0
- package/lib/provider-protocol.js +3 -2
- package/lib/provider-registry.js +15 -5
- package/lib/provider-response.js +10 -0
- package/lib/provider-settings.js +14 -12
- package/lib/provider-task-runner.js +113 -37
- package/lib/provider-transport.js +16 -0
- package/lib/render.js +11 -1
- package/lib/scheduling-policy.js +66 -0
- 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 +2 -0
- package/lib/web/app.js +107 -46
- package/lib/web/connections.js +151 -12
- package/lib/web/creation.js +29 -25
- package/lib/web/flow-view.js +4 -3
- package/lib/web/gallery.js +17 -16
- package/lib/web/index.html +58 -5
- package/lib/web/media-client.js +18 -4
- package/lib/web/network.js +232 -0
- package/lib/web/plugin-panel.js +4 -2
- package/lib/web/plugin-status.js +140 -0
- package/lib/web/providers.js +34 -13
- package/lib/web/recovery.js +2 -2
- package/lib/web/styles.css +54 -1
- package/lib/web/transport.js +6 -1
- package/lib/workbench-artifacts.js +3 -2
- package/package.json +5 -2
- package/skills/iris-core/references/comfyui.md +1 -1
- 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 +3 -3
package/lib/comfyui-client.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Native HTTP transport shared by generation and explicit read-only workflow checks.
|
|
2
2
|
import { ProviderContractError } from './provider-contract.js';
|
|
3
3
|
import { normalizeMediaEndpoint, providerApiKey, providerMediaBaseUrl } from './provider-protocol.js';
|
|
4
|
+
import { resolveProviderCredential } from './provider-credentials.js';
|
|
5
|
+
import { inheritedProviderFetch } from './provider-network.js';
|
|
6
|
+
import { providerNetworkRequirement, sameNetworkRequirement, networkPolicyError } from './network-policy.js';
|
|
4
7
|
|
|
5
8
|
export function comfyFail(message, stage, category = 'protocol', acceptance = stage === 'validate' ? 'not_accepted' : 'accepted', details = {}) {
|
|
6
9
|
throw new ProviderContractError(message, { stage, category, acceptance, ...details });
|
|
@@ -15,7 +18,12 @@ async function bounded(response, limit, stage) {
|
|
|
15
18
|
}
|
|
16
19
|
return Buffer.concat(chunks, size);
|
|
17
20
|
}
|
|
18
|
-
export function createComfyUiClient(provider, { fetchPort =
|
|
21
|
+
export function createComfyUiClient(provider, { fetchPort = inheritedProviderFetch, network } = {}) {
|
|
22
|
+
const requirement = providerNetworkRequirement(provider);
|
|
23
|
+
if (requirement && (!network || !sameNetworkRequirement(requirement, network.networkRequirement) || network.providerId !== provider.id)) {
|
|
24
|
+
throw networkPolicyError('IRIS_NETWORK_CONTEXT_REQUIRED', 'ComfyUI 需要供应商匹配的网络上下文,不会静默直连');
|
|
25
|
+
}
|
|
26
|
+
provider = resolveProviderCredential(provider);
|
|
19
27
|
const base = normalizeMediaEndpoint(providerMediaBaseUrl(provider)), key = providerApiKey(provider);
|
|
20
28
|
const route = leaf => base + '/' + leaf;
|
|
21
29
|
async function request(url, { method = 'GET', value, signal, stage, timeoutMs = 15000, binary = false } = {}) {
|
|
@@ -26,6 +34,7 @@ export function createComfyUiClient(provider, { fetchPort = globalThis.fetch } =
|
|
|
26
34
|
headers: { ...(key ? { Authorization: 'Bearer ' + key } : {}), ...(value ? { 'Content-Type': 'application/json' } : {}) },
|
|
27
35
|
...(value ? { body: JSON.stringify(value) } : {}) });
|
|
28
36
|
} catch (error) {
|
|
37
|
+
if (error instanceof ProviderContractError) throw error;
|
|
29
38
|
comfyFail('ComfyUI 服务通信失败,请核对原任务与服务状态', stage,
|
|
30
39
|
error.name === 'AbortError' ? 'aborted' : error.name === 'TimeoutError' ? 'timeout' : 'network',
|
|
31
40
|
stage === 'submit' ? 'unknown' : stage === 'validate' ? 'not_accepted' : 'accepted');
|
package/lib/command-service.js
CHANGED
|
@@ -429,23 +429,34 @@ export function createCommandService(runtime, ports = {}) {
|
|
|
429
429
|
if (input.confirm_billing !== true) {
|
|
430
430
|
throw commandError('IRIS_COMMAND_BILLING_CONFIRM_REQUIRED', '重试会创建新任务并可能产生重复生成费用;必须显式确认计费');
|
|
431
431
|
}
|
|
432
|
-
return runtime.run('execute', async ({ dataRoot }) => {
|
|
432
|
+
return runtime.run('execute', async ({ dataRoot, signal }) => {
|
|
433
433
|
const before = inspectCoreTask(dataRoot, input.task_id);
|
|
434
434
|
if (before.phase !== 'terminal' || (before.outcome === 'succeeded' && before.deliveryState === 'ready')) {
|
|
435
435
|
throw commandError('IRIS_TASK_NOT_RETRYABLE', '只有终态且未成功交付的 Task 可以重试为新任务');
|
|
436
436
|
}
|
|
437
|
-
let normalized;
|
|
437
|
+
let normalized, requestedVideoMode;
|
|
438
|
+
let suppliedInput = input.provider_input;
|
|
439
|
+
if (before.capability === 'video') {
|
|
440
|
+
if (!plain(suppliedInput) || !['t2v', 'i2v', 's2v'].includes(suppliedInput.video_mode)) {
|
|
441
|
+
throw commandError('IRIS_COMMAND_INPUT_INVALID', '视频重试必须明确 video_mode(t2v/i2v/s2v),并重新提供该模式所需的首帧和音频;旧 Task 不保存这些输入');
|
|
442
|
+
}
|
|
443
|
+
const { video_mode, ...generation } = suppliedInput;
|
|
444
|
+
requestedVideoMode = video_mode; suppliedInput = generation;
|
|
445
|
+
}
|
|
438
446
|
if (before.capability === 'music' && input.provider_input &&
|
|
439
447
|
['retry_502', 'retry_delay_ms'].some(field => Object.hasOwn(input.provider_input, field))) {
|
|
440
448
|
throw commandError('IRIS_COMMAND_INPUT_INVALID', 'task.retry 只执行一次重新生成;502重试配置请使用Music生成入口');
|
|
441
449
|
}
|
|
442
|
-
try { normalized = normalizeGenerationInput(before.capability,
|
|
450
|
+
try { normalized = normalizeGenerationInput(before.capability, suppliedInput, { allowVideoPaths: before.capability === 'video' }); }
|
|
443
451
|
catch (error) {
|
|
444
452
|
if (error instanceof GenerationInputError) throw commandError('IRIS_COMMAND_INPUT_INVALID', error.message);
|
|
445
453
|
throw error;
|
|
446
454
|
}
|
|
455
|
+
if (before.capability === 'video' && normalized.videoMode !== requestedVideoMode) {
|
|
456
|
+
throw commandError('IRIS_COMMAND_INPUT_INVALID', 'video_mode 与重新提供的首帧/音频不符;i2v 需首帧,s2v 需首帧和音频,可通过 CLI 重新提供本地文件');
|
|
457
|
+
}
|
|
447
458
|
const providerInput = normalized.providerInput;
|
|
448
|
-
if (before.sourceArtifactId && !normalized.sourceArtifactId) {
|
|
459
|
+
if (before.capability === 'image' && before.sourceArtifactId && !normalized.sourceArtifactId) {
|
|
449
460
|
throw commandError('IRIS_COMMAND_INPUT_INVALID', '改图重试必须重新提供 source_artifact_id 和修改指令');
|
|
450
461
|
}
|
|
451
462
|
if (Object.hasOwn(input.provider_input, 'model_ref')) {
|
|
@@ -454,7 +465,9 @@ export function createCommandService(runtime, ports = {}) {
|
|
|
454
465
|
const modelRef = String(input.model_ref || '').trim();
|
|
455
466
|
if (input.model_ref !== undefined && !modelRef) throw commandError('IRIS_COMMAND_INPUT_INVALID', 'model_ref 必须是 providerId::modelId 复合引用');
|
|
456
467
|
if (typeof ports.resolveTaskCandidates !== 'function') throw commandError('IRIS_COMMAND_PROVIDER_REQUIRED', 'task.retry 需要宿主显式提供候选链 resolver');
|
|
457
|
-
|
|
468
|
+
signal.throwIfAborted();
|
|
469
|
+
let candidates = await ports.resolveTaskCandidates({ capability: before.capability, modelRef, signal });
|
|
470
|
+
signal.throwIfAborted();
|
|
458
471
|
if (before.capability === 'video') candidates = videoTaskCandidates(candidates, normalized);
|
|
459
472
|
const result = await createProviderTaskRunner(runtime).submit({
|
|
460
473
|
capability: before.capability,
|
package/lib/config.js
CHANGED
|
@@ -7,7 +7,7 @@ import { refreshModelDirectory } from './model-directory-service.js';
|
|
|
7
7
|
/**
|
|
8
8
|
* Iris 配置存储:$DSH_HOME/iris/v1/providers.json
|
|
9
9
|
* Iris 独立持有配置;旧工作台仅作为用户显式指定的一次性导入来源。
|
|
10
|
-
* Key 明文落盘(0600
|
|
10
|
+
* 内联 Key 明文落盘(0600);环境引用只存名称,解析值不持久化;接口层不回明文。
|
|
11
11
|
*/
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
@@ -17,6 +17,7 @@ import * as models from './models.js';
|
|
|
17
17
|
import { atomicWritePrivate, chmodPrivateFile, privateSibling } from './private-storage.js';
|
|
18
18
|
import { selectMediaProtocol, selectImageProtocol, normalizeImageProtocol, normalizeMusicProtocol, isConfiguredProvider } from './provider-protocol.js';
|
|
19
19
|
import { redactProviderMessage } from './provider-contract.js';
|
|
20
|
+
import { validateProviderCredential, providerCredentialStatus, providerCredentialValue, applyCredentialPatch } from './provider-credentials.js';
|
|
20
21
|
import {
|
|
21
22
|
PROVIDER_HEALTH_FRESH_MS,
|
|
22
23
|
aggregateHealthStates,
|
|
@@ -37,6 +38,7 @@ function validateStoredConfig(value) {
|
|
|
37
38
|
for (const [capability, assignment] of Object.entries(value.assignments)) models.functionPolicy(assignment, capability);
|
|
38
39
|
for (const provider of value.providers) {
|
|
39
40
|
if (!isPlainObject(provider)) throw new Error('provider 条目必须是对象');
|
|
41
|
+
validateProviderCredential(provider);
|
|
40
42
|
validateModelDirectory(provider.modelDirectory);
|
|
41
43
|
if (provider.id !== undefined && typeof provider.id !== 'string') throw new Error('provider.id 必须是字符串');
|
|
42
44
|
if (provider.baseUrl !== undefined && typeof provider.baseUrl !== 'string') throw new Error('provider.baseUrl 必须是字符串');
|
|
@@ -47,7 +49,7 @@ function validateStoredConfig(value) {
|
|
|
47
49
|
if (!isPlainObject(ref) || typeof ref.provider !== 'string' || !ref.provider.trim()
|
|
48
50
|
|| typeof ref.settingsNs !== 'string' || !ref.settingsNs.trim()
|
|
49
51
|
|| Object.keys(ref).some(key => !['provider', 'settingsNs'].includes(key))) throw new Error('DSH 连接引用无效');
|
|
50
|
-
if (provider.apiKey !== undefined || provider.baseUrl !== undefined) throw new Error('DSH
|
|
52
|
+
if (provider.apiKey !== undefined || provider.apiKeyEnv !== undefined || provider.baseUrl !== undefined) throw new Error('DSH 引用不能同时保存密钥、环境引用或普通 Base URL');
|
|
51
53
|
}
|
|
52
54
|
if (provider.models !== undefined && !Array.isArray(provider.models)) throw new Error('provider.models 必须是数组');
|
|
53
55
|
for (const model of provider.models || []) {
|
|
@@ -72,7 +74,7 @@ function authModeOf(provider) {
|
|
|
72
74
|
function missingCredentials(provider) {
|
|
73
75
|
if (!provider || authModeOf(provider) === 'none') return false;
|
|
74
76
|
if (provider.dshProvider) return false;
|
|
75
|
-
return
|
|
77
|
+
return providerCredentialStatus(provider) !== 'ready';
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
function normalizeStoredProvider(provider) {
|
|
@@ -133,6 +135,9 @@ export function load() {
|
|
|
133
135
|
for (const provider of cache.providers) normalized = normalizeStoredProvider(provider) || normalized;
|
|
134
136
|
if (normalized) persist();
|
|
135
137
|
} catch (err) {
|
|
138
|
+
// A rejected source/field combination is actionable configuration, not corrupt
|
|
139
|
+
// JSON. Keep it intact so a policy change cannot quarantine user providers.
|
|
140
|
+
if (err.code === 'IRIS_CONFIG_INPUT_INVALID') throw err;
|
|
136
141
|
// 文件存在但损坏:隔离,绝不静默覆盖证据
|
|
137
142
|
const backup = privateSibling(file, 'corrupted');
|
|
138
143
|
try {
|
|
@@ -400,9 +405,8 @@ function healthObservation(provider, modelId, capability) {
|
|
|
400
405
|
function safeHealthNote(provider, value) {
|
|
401
406
|
const raw = String(value ?? '').trim();
|
|
402
407
|
if (!raw) return '';
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (key) note = note.split(key).join('[REDACTED]');
|
|
408
|
+
const key = providerCredentialValue(provider && scopedProvider(provider));
|
|
409
|
+
let note = redactProviderMessage(key ? raw.split(key).join('[REDACTED]') : raw, 200);
|
|
406
410
|
return note;
|
|
407
411
|
}
|
|
408
412
|
|
|
@@ -587,7 +591,7 @@ export function upsert(provider) {
|
|
|
587
591
|
if (!isPlainObject(reference) || typeof reference.provider !== 'string' || !reference.provider.trim()
|
|
588
592
|
|| typeof reference.settingsNs !== 'string' || !reference.settingsNs.trim()
|
|
589
593
|
|| Object.keys(reference).some(key => !['provider', 'settingsNs'].includes(key))) throw new Error('DSH 连接引用无效');
|
|
590
|
-
if (['apiKey', 'baseUrl'].some(key => rest[key] !== undefined || existing?.[key] !== undefined)) {
|
|
594
|
+
if (['apiKey', 'apiKeyEnv', 'baseUrl'].some(key => rest[key] !== undefined || existing?.[key] !== undefined)) {
|
|
591
595
|
throw new Error('DSH 引用不保存密钥或普通 Base URL;请新建引用供应商');
|
|
592
596
|
}
|
|
593
597
|
rest.dshProvider = { provider: reference.provider.trim(), settingsNs: reference.settingsNs.trim() };
|
|
@@ -602,7 +606,7 @@ export function upsert(provider) {
|
|
|
602
606
|
const i = c.providers.findIndex((p) => p.id === id);
|
|
603
607
|
if (i >= 0) {
|
|
604
608
|
const previous = c.providers[i];
|
|
605
|
-
const next =
|
|
609
|
+
const next = applyCredentialPatch(previous, rest);
|
|
606
610
|
if (Object.prototype.hasOwnProperty.call(rest, 'mediaProtocol')) next.protocolInferred = false;
|
|
607
611
|
normalizeStoredProvider(next);
|
|
608
612
|
resetHealthOnCriticalChange(previous, next, rest);
|
|
@@ -613,7 +617,7 @@ export function upsert(provider) {
|
|
|
613
617
|
return next;
|
|
614
618
|
}
|
|
615
619
|
}
|
|
616
|
-
const fresh = { id: 'iris_' + Math.random().toString(36).slice(2, 8), ...rest };
|
|
620
|
+
const fresh = applyCredentialPatch(null, { id: 'iris_' + Math.random().toString(36).slice(2, 8), ...rest });
|
|
617
621
|
fresh.protocolInferred = false;
|
|
618
622
|
normalizeStoredProvider(fresh);
|
|
619
623
|
c.providers.push(fresh);
|
|
@@ -1,42 +1,61 @@
|
|
|
1
1
|
// Shared CLI/service assembly. Configuration is read only during explicit commands.
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { loadProviderCatalog, imageCandidatesFromCatalog, ttsCandidatesFromCatalog, transcribeCandidatesFromCatalog, providerForTaskFromCatalog, providerTaskBinding } from './provider-catalog.js';
|
|
3
|
+
import { loadProviderCatalog, imageCandidatesFromCatalog, ttsCandidatesFromCatalog, transcribeCandidatesFromCatalog, videoCandidatesFromCatalog, providerForTaskFromCatalog, providerTaskBinding } from './provider-catalog.js';
|
|
4
4
|
import { createConfiguredProviderAdapter } from './provider-adapters.js';
|
|
5
5
|
import { recordCatalogAvailability, recordCatalogRateLimit } from './provider-config-service.js';
|
|
6
6
|
import { parseModelRef } from './models.js';
|
|
7
7
|
import { supportsImageEdit } from './provider-protocol.js';
|
|
8
8
|
import { compileComfyWorkflow } from './comfyui-workflow.js';
|
|
9
|
+
import { providerNetworkRequirement, sameNetworkRequirement, networkPolicyError } from './network-policy.js';
|
|
9
10
|
|
|
10
|
-
export function createTrackedProviderAdapter(provider, providerConfigPath, task, registry) {
|
|
11
|
+
export function createTrackedProviderAdapter(provider, providerConfigPath, task, registry, { network, modelRef, deferCooling = false } = {}) {
|
|
12
|
+
if (task && !sameNetworkRequirement(task.networkRequirement, providerNetworkRequirement(provider))) {
|
|
13
|
+
throw networkPolicyError('IRIS_NETWORK_REQUIREMENT_MISMATCH', '原 Task 的网络要求与当前供应商目录不一致;不会隐式转换或解除');
|
|
14
|
+
}
|
|
11
15
|
return createConfiguredProviderAdapter(provider, {
|
|
12
16
|
registry,
|
|
13
|
-
|
|
17
|
+
...(network !== undefined ? { network } : {}),
|
|
18
|
+
modelId: parseModelRef(task?.modelRef || modelRef)?.modelId,
|
|
19
|
+
deferCooling,
|
|
14
20
|
onModelAvailability: event => recordCatalogAvailability(providerConfigPath, event),
|
|
15
21
|
onRateLimit: event => recordCatalogRateLimit(providerConfigPath, event)
|
|
16
22
|
});
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
export function configuredImagePorts(providerConfigPath, registry) {
|
|
25
|
+
export function configuredImagePorts(providerConfigPath, registry, { resolveProviderNetwork, deferCooling = false } = {}) {
|
|
20
26
|
if (typeof providerConfigPath !== 'string' || !path.isAbsolute(providerConfigPath)) throw new TypeError('Provider 配置须为显式绝对路径');
|
|
27
|
+
if (resolveProviderNetwork !== undefined && typeof resolveProviderNetwork !== 'function') throw new TypeError('resolveProviderNetwork 必须是函数');
|
|
28
|
+
const adapterFor = (provider, task, modelRef) => createTrackedProviderAdapter(provider, providerConfigPath, task, registry, {
|
|
29
|
+
modelRef, deferCooling,
|
|
30
|
+
network: resolveProviderNetwork?.({ providerId: provider.id, modelRef: modelRef || task?.modelRef, taskId: task?.id,
|
|
31
|
+
...(providerNetworkRequirement(provider) ? { networkRequirement: providerNetworkRequirement(provider) } : {}) })
|
|
32
|
+
});
|
|
21
33
|
return Object.freeze({
|
|
34
|
+
resolveVideoCandidates({ modelRef, signal }) {
|
|
35
|
+
signal?.throwIfAborted();
|
|
36
|
+
return videoCandidatesFromCatalog(loadProviderCatalog(providerConfigPath), modelRef, registry, { deferCooling }).map(route => ({
|
|
37
|
+
adapter: adapterFor(route.provider, undefined, route.modelRef), model: route.modelRef,
|
|
38
|
+
providerBinding: providerTaskBinding(route.provider, registry), selectionReason: 'explicit'
|
|
39
|
+
}));
|
|
40
|
+
},
|
|
22
41
|
resolveAudioCandidates({ capability, modelRef, signal }) {
|
|
23
42
|
signal?.throwIfAborted();
|
|
24
43
|
if (!['tts', 'transcribe'].includes(capability)) throw new TypeError('未知音频能力');
|
|
25
44
|
const select = capability === 'tts' ? ttsCandidatesFromCatalog : transcribeCandidatesFromCatalog;
|
|
26
|
-
return select(loadProviderCatalog(providerConfigPath), modelRef, registry).map(route => ({
|
|
27
|
-
adapter:
|
|
45
|
+
return select(loadProviderCatalog(providerConfigPath), modelRef, registry, { deferCooling }).map(route => ({
|
|
46
|
+
adapter: adapterFor(route.provider, undefined, route.modelRef), model: route.modelRef,
|
|
28
47
|
providerBinding: providerTaskBinding(route.provider, registry), selectionReason: 'explicit'
|
|
29
48
|
}));
|
|
30
49
|
},
|
|
31
50
|
resolveImageCandidates({ modelRef, signal, input }) {
|
|
32
51
|
signal?.throwIfAborted();
|
|
33
|
-
return imageCandidatesFromCatalog(loadProviderCatalog(providerConfigPath), modelRef, registry).map(route => {
|
|
52
|
+
return imageCandidatesFromCatalog(loadProviderCatalog(providerConfigPath), modelRef, registry, { deferCooling }).map(route => {
|
|
34
53
|
if (input?.workflow_parameters !== undefined) {
|
|
35
54
|
if (route.provider.mediaProtocol !== 'comfyui') throw Object.assign(new TypeError('工作流参数仅用于 ComfyUI 模板'), { code: 'IRIS_COMFYUI_PARAMETERS_INVALID' });
|
|
36
55
|
compileComfyWorkflow(route.provider.comfyWorkflow, input);
|
|
37
56
|
}
|
|
38
57
|
return {
|
|
39
|
-
adapter:
|
|
58
|
+
adapter: adapterFor(route.provider, undefined, route.modelRef), model: route.modelRef,
|
|
40
59
|
supportsImageEdit: supportsImageEdit(route.provider.mediaProtocol, route.model, registry),
|
|
41
60
|
providerBinding: providerTaskBinding(route.provider, registry), selectionReason: 'explicit'
|
|
42
61
|
};
|
|
@@ -44,7 +63,7 @@ export function configuredImagePorts(providerConfigPath, registry) {
|
|
|
44
63
|
},
|
|
45
64
|
resolveTaskAdapter(task, { signal } = {}) {
|
|
46
65
|
signal?.throwIfAborted();
|
|
47
|
-
return
|
|
66
|
+
return adapterFor(providerForTaskFromCatalog(loadProviderCatalog(providerConfigPath), task, registry), task);
|
|
48
67
|
}
|
|
49
68
|
});
|
|
50
69
|
}
|
package/lib/connection-skill.js
CHANGED
|
@@ -61,6 +61,14 @@ function imageGuidance(tools) {
|
|
|
61
61
|
];
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function videoGuidance(tools) {
|
|
65
|
+
if (!tools.includes('iris_core_create_video') && !tools.includes('iris_core_video')) return [];
|
|
66
|
+
return ['## Text to video · 文生视频', '',
|
|
67
|
+
'Use the actual video directory when selected. Keep an explicit permitted modelRef; do not guess models or supported sizes/durations. This fixed flow takes a prompt and optional size/duration (1–60 seconds). It has no image/audio input or S2V path. Omit optional parameters unless required by the user or model.', '',
|
|
68
|
+
'Creation saves an intent without generation. Only an authorized continue can submit it and incur charges. Keep distinct stable creation/execution UUIDs, the original Run, Task and Operation. An asynchronous submission needs another explicit continue to observe the SAME Task. Unknown acceptance must never cause another submission. Local stop stops waiting, not remote billing.', '',
|
|
69
|
+
'Video has one output and needs no image selection. If the video reading tool is selected, it returns an iris://video/ resource_link for the original MP4/WebM (at most 32 MiB). Use standard resources/read to obtain its bytes; each read checks current Core authorization. Client playback support varies. Do not replace it with a provider URL or claim to have watched it merely from a reference.', ''];
|
|
70
|
+
}
|
|
71
|
+
|
|
64
72
|
export function connectionSkillResources(input, metadata) {
|
|
65
73
|
const bundle = checkedConnectionSkillBundle(input), skillName = 'iris-mcp-' + bundle.connectionId;
|
|
66
74
|
const write = bundle.tools.some(name => toolDeclarations.get(name).scope !== 'read');
|
|
@@ -74,6 +82,7 @@ export function connectionSkillResources(input, metadata) {
|
|
|
74
82
|
...bundle.tools.map(name => '- `' + name + '`: ' + toolDeclarations.get(name).guidance), '',
|
|
75
83
|
...imageGuidance(bundle.tools),
|
|
76
84
|
...audioGuidance(bundle.tools),
|
|
85
|
+
...videoGuidance(bundle.tools),
|
|
77
86
|
'## Work within the selection · 遵循所选范围', '',
|
|
78
87
|
'Choose only the steps needed for the user\'s request and available in the list above. A partial selection may not support a complete workflow. Ask for missing input or report a missing tool rather than inventing IDs, changing grants, creating another connection, or bypassing permissions through a shell/CLI.', '',
|
|
79
88
|
'This is a saved instruction snapshot. Core checks actual current authorization on every request; loading this skill grants no permission. If a tool is unavailable or denied, report the mismatch and have the owner regenerate the bundle when appropriate. Do not automatically retry a write.', '',
|
|
@@ -46,7 +46,8 @@ function dirs(dataRoot) {
|
|
|
46
46
|
objects: path.join(base, 'objects'),
|
|
47
47
|
records: path.join(base, 'records'),
|
|
48
48
|
manifests: path.join(base, 'manifests'),
|
|
49
|
-
index: path.join(base, 'index.json')
|
|
49
|
+
index: path.join(base, 'index.json'),
|
|
50
|
+
indexDirty: path.join(base, 'index.dirty')
|
|
50
51
|
};
|
|
51
52
|
}
|
|
52
53
|
|
|
@@ -186,7 +187,7 @@ function verifyObject(dataRoot, manifest, verifyContent = true) {
|
|
|
186
187
|
return object;
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
function
|
|
190
|
+
function legacyObject(dataRoot, record, id) {
|
|
190
191
|
if (!record || record.schemaVersion !== 0 || record.id !== id
|
|
191
192
|
|| typeof record.kind !== 'string' || !MEDIA_EXTENSION[record.mediaType]
|
|
192
193
|
|| !Number.isSafeInteger(record.size) || record.size <= 0
|
|
@@ -197,6 +198,15 @@ function legacyManifest(dataRoot, record, id) {
|
|
|
197
198
|
const object = path.join(dirs(dataRoot).objects, record.object);
|
|
198
199
|
const stat = regularFile(object);
|
|
199
200
|
if (stat.size !== record.size) fail('IRIS_ARTIFACT_DIGEST_MISMATCH', '早期 Core Artifact 大小不一致');
|
|
201
|
+
return object;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function legacyManifest(dataRoot, record, id, indexed) {
|
|
205
|
+
const object = legacyObject(dataRoot, record, id);
|
|
206
|
+
if (indexed && (indexed.id !== id || indexed.createdAt !== record.createdAt
|
|
207
|
+
|| indexed.mediaType !== record.mediaType || indexed.size !== record.size || !HASH.test(indexed.digest))) {
|
|
208
|
+
fail('IRIS_ARTIFACT_INDEX_INVALID', '早期 Artifact 与缓存索引不一致,请显式重建索引');
|
|
209
|
+
}
|
|
200
210
|
return {
|
|
201
211
|
schemaVersion: 0,
|
|
202
212
|
id,
|
|
@@ -205,13 +215,13 @@ function legacyManifest(dataRoot, record, id) {
|
|
|
205
215
|
size: record.size,
|
|
206
216
|
createdAt: record.createdAt,
|
|
207
217
|
object: record.object,
|
|
208
|
-
digest: { algorithm: 'sha256', value: hashFile(object) },
|
|
218
|
+
digest: { algorithm: 'sha256', value: indexed ? indexed.digest : hashFile(object) },
|
|
209
219
|
relations: [],
|
|
210
220
|
metadata: { ...record.metadata }
|
|
211
221
|
};
|
|
212
222
|
}
|
|
213
223
|
|
|
214
|
-
function readArtifact(dataRoot, id, { allowLegacy = true, verifyContent = true } = {}) {
|
|
224
|
+
function readArtifact(dataRoot, id, { allowLegacy = true, verifyContent = true, legacyIndex } = {}) {
|
|
215
225
|
checkStore(dataRoot);
|
|
216
226
|
const stable = stableId(id);
|
|
217
227
|
let record;
|
|
@@ -226,7 +236,7 @@ function readArtifact(dataRoot, id, { allowLegacy = true, verifyContent = true }
|
|
|
226
236
|
manifest = validateManifest(
|
|
227
237
|
jsonFile(manifestPath(dataRoot, stable), 'IRIS_ARTIFACT_MANIFEST_INVALID'), stable
|
|
228
238
|
);
|
|
229
|
-
} else if (allowLegacy) manifest = legacyManifest(dataRoot, record, stable);
|
|
239
|
+
} else if (allowLegacy) manifest = legacyManifest(dataRoot, record, stable, legacyIndex?.(stable));
|
|
230
240
|
else fail('IRIS_ARTIFACT_INVALID', 'Artifact 提交记录无效');
|
|
231
241
|
const object = verifyObject(dataRoot, manifest, verifyContent);
|
|
232
242
|
return { manifest, object, legacy: record.schemaVersion === 0 };
|
|
@@ -252,11 +262,9 @@ function readIndexRevision(file) {
|
|
|
252
262
|
|
|
253
263
|
function committedIds(dataRoot) {
|
|
254
264
|
const directory = dirs(dataRoot).records;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
.filter((id) => ID.test(id));
|
|
259
|
-
} catch (_) { return []; }
|
|
265
|
+
return fs.readdirSync(directory)
|
|
266
|
+
.map((name) => name.endsWith('.json') ? name.slice(0, -5) : '')
|
|
267
|
+
.filter((id) => ID.test(id));
|
|
260
268
|
}
|
|
261
269
|
|
|
262
270
|
function writeIndex(dataRoot, manifests) {
|
|
@@ -275,23 +283,35 @@ function writeIndex(dataRoot, manifests) {
|
|
|
275
283
|
atomicWritePrivate(file, JSON.stringify({
|
|
276
284
|
schemaVersion: 0, revision, count: artifacts.length, inventoryDigest, artifacts
|
|
277
285
|
}, null, 2) + '\n');
|
|
286
|
+
try { fs.unlinkSync(dirs(dataRoot).indexDirty); }
|
|
287
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
278
288
|
return { revision, artifacts };
|
|
279
289
|
}
|
|
280
290
|
|
|
291
|
+
function markIndexDirty(dataRoot) {
|
|
292
|
+
atomicWritePrivate(dirs(dataRoot).indexDirty, 'Artifact index needs refresh\n');
|
|
293
|
+
}
|
|
294
|
+
|
|
281
295
|
function refreshIndex(dataRoot) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
296
|
+
// The new object was just hashed. Historical bytes are checked on use and rebuild,
|
|
297
|
+
// not on every unrelated commit. Do not silently publish a subset of bad records.
|
|
298
|
+
let previous;
|
|
299
|
+
const legacyIndex = id => {
|
|
300
|
+
previous ||= new Map(readArtifactIndex(dataRoot, { allowDirty: true }).artifacts.map(entry => [entry.id, entry]));
|
|
301
|
+
const entry = previous.get(id);
|
|
302
|
+
if (!entry) fail('IRIS_ARTIFACT_INDEX_INVALID', '早期 Artifact 缺少缓存摘要,请显式重建索引');
|
|
303
|
+
return entry;
|
|
304
|
+
};
|
|
305
|
+
const manifests = committedIds(dataRoot).map(id => readArtifact(dataRoot, id, { verifyContent: false, legacyIndex }).manifest);
|
|
286
306
|
return writeIndex(dataRoot, manifests);
|
|
287
307
|
}
|
|
288
308
|
|
|
289
309
|
/** 只更新已提交条目的派生索引,不升级旧记录或接回孤儿。 */
|
|
290
310
|
export function refreshCoreArtifactInventory(dataRoot) {
|
|
291
311
|
checkStore(dataRoot, { manifests: true });
|
|
292
|
-
|
|
293
|
-
const index =
|
|
294
|
-
return { revision: index.revision, total:
|
|
312
|
+
markIndexDirty(dataRoot);
|
|
313
|
+
const index = refreshIndex(dataRoot);
|
|
314
|
+
return { revision: index.revision, total: index.artifacts.length };
|
|
295
315
|
}
|
|
296
316
|
|
|
297
317
|
function validateRelationTargets(dataRoot, relations) {
|
|
@@ -329,13 +349,15 @@ function finishObject(dataRoot, input) {
|
|
|
329
349
|
|| JSON.stringify(previous.relations) !== JSON.stringify(relations)) {
|
|
330
350
|
fail('IRIS_ARTIFACT_INPUT_INVALID', '可恢复 Artifact 与已提交内容不一致');
|
|
331
351
|
}
|
|
332
|
-
try { refreshIndex(dataRoot); } catch (_) { /*
|
|
352
|
+
try { markIndexDirty(dataRoot); refreshIndex(dataRoot); } catch (_) { /* 已提交事实保留;脏索引由读取/Doctor 报告 */ }
|
|
333
353
|
return publicArtifact(previous);
|
|
334
354
|
}
|
|
335
|
-
let stat;
|
|
355
|
+
let stat, digest;
|
|
336
356
|
try {
|
|
337
357
|
if (resumable && exists(object)) {
|
|
338
|
-
|
|
358
|
+
stat = regularFile(object);
|
|
359
|
+
digest = hashFile(object);
|
|
360
|
+
if (stat.size !== input.expectedSize || digest !== input.expectedDigest) {
|
|
339
361
|
fail('IRIS_ARTIFACT_INPUT_INVALID', '未提交 Artifact 与恢复内容不一致');
|
|
340
362
|
}
|
|
341
363
|
} else input.writeObject(object);
|
|
@@ -357,7 +379,7 @@ function finishObject(dataRoot, input) {
|
|
|
357
379
|
size: stat.size,
|
|
358
380
|
createdAt: input.createdAt || new Date().toISOString(),
|
|
359
381
|
object: objectName,
|
|
360
|
-
digest: { algorithm: 'sha256', value: hashFile(object) },
|
|
382
|
+
digest: { algorithm: 'sha256', value: digest || hashFile(object) },
|
|
361
383
|
relations,
|
|
362
384
|
metadata
|
|
363
385
|
};
|
|
@@ -372,6 +394,7 @@ function finishObject(dataRoot, input) {
|
|
|
372
394
|
}
|
|
373
395
|
try {
|
|
374
396
|
writeManifest(dataRoot, manifest);
|
|
397
|
+
markIndexDirty(dataRoot);
|
|
375
398
|
commitRecord(dataRoot, id);
|
|
376
399
|
} catch (_) {
|
|
377
400
|
if (!resumable) {
|
|
@@ -380,7 +403,7 @@ function finishObject(dataRoot, input) {
|
|
|
380
403
|
}
|
|
381
404
|
fail('IRIS_ARTIFACT_WRITE_FAILED', 'Artifact 无法安全提交');
|
|
382
405
|
}
|
|
383
|
-
try { refreshIndex(dataRoot); } catch (_) { /*
|
|
406
|
+
try { refreshIndex(dataRoot); } catch (_) { /* 脏索引保留;不撤销已提交 Artifact 或触发重新生成 */ }
|
|
384
407
|
return publicArtifact(manifest);
|
|
385
408
|
}
|
|
386
409
|
|
|
@@ -441,10 +464,16 @@ export function readCoreArtifactBytes(dataRoot, id) {
|
|
|
441
464
|
const hit = readArtifact(dataRoot, id);
|
|
442
465
|
return Object.freeze({
|
|
443
466
|
artifact: publicArtifact(hit.manifest),
|
|
444
|
-
bytes:
|
|
467
|
+
bytes: fs.readFileSync(hit.object)
|
|
445
468
|
});
|
|
446
469
|
}
|
|
447
470
|
|
|
471
|
+
/** Caller owns the stream until close; content is verified in bounded chunks before delivery. */
|
|
472
|
+
export function readCoreArtifactStream(dataRoot, id, { signal } = {}) {
|
|
473
|
+
const hit = readArtifact(dataRoot, id);
|
|
474
|
+
return { artifact: publicArtifact(hit.manifest), stream: fs.createReadStream(hit.object, { signal }) };
|
|
475
|
+
}
|
|
476
|
+
|
|
448
477
|
export function exportCoreArtifact(dataRoot, id, outputPath) {
|
|
449
478
|
const destination = String(outputPath || '').trim();
|
|
450
479
|
if (!destination || !path.isAbsolute(destination)) {
|
|
@@ -462,13 +491,19 @@ export function exportCoreArtifact(dataRoot, id, outputPath) {
|
|
|
462
491
|
return Object.freeze({ artifact: publicArtifact(hit.manifest), exported: true, bytes: hit.manifest.size });
|
|
463
492
|
}
|
|
464
493
|
|
|
465
|
-
function readArtifactIndex(dataRoot) {
|
|
494
|
+
function readArtifactIndex(dataRoot, { allowDirty = false } = {}) {
|
|
466
495
|
try { fs.lstatSync(dirs(dataRoot).storeRoot); }
|
|
467
496
|
catch (error) {
|
|
468
497
|
if (error?.code !== 'ENOENT') fail('IRIS_ARTIFACT_STORE_INVALID', 'Artifact Store 无法读取');
|
|
469
498
|
return { schemaVersion: 0, revision: 0, artifacts: [], absent: true };
|
|
470
499
|
}
|
|
471
500
|
checkStore(dataRoot, { manifests: true });
|
|
501
|
+
if (!allowDirty) {
|
|
502
|
+
try {
|
|
503
|
+
fs.lstatSync(dirs(dataRoot).indexDirty);
|
|
504
|
+
fail('IRIS_ARTIFACT_INDEX_INVALID', 'Artifact Index 尚未刷新;写入结束后重试,持续失败请显式重建');
|
|
505
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
506
|
+
}
|
|
472
507
|
const index = jsonFile(dirs(dataRoot).index, 'IRIS_ARTIFACT_INDEX_INVALID');
|
|
473
508
|
const inventoryDigest = Array.isArray(index?.artifacts)
|
|
474
509
|
? crypto.createHash('sha256').update(JSON.stringify(index.artifacts)).digest('hex') : '';
|
|
@@ -483,16 +518,22 @@ function readArtifactIndex(dataRoot) {
|
|
|
483
518
|
/** 工作台目录只读已提交元数据和文件大小;媒体打开/识别仍须核验完整内容哈希。 */
|
|
484
519
|
export function listCoreArtifactMetadata(dataRoot) {
|
|
485
520
|
const index = readArtifactIndex(dataRoot), artifacts = [];
|
|
486
|
-
let dropped = 0;
|
|
521
|
+
let dropped = 0, legacySkipped = 0;
|
|
487
522
|
for (const entry of index.artifacts) {
|
|
488
523
|
try {
|
|
524
|
+
const record = jsonFile(recordPath(dataRoot, entry.id), 'IRIS_ARTIFACT_NOT_FOUND');
|
|
525
|
+
if (record?.schemaVersion === 0) {
|
|
526
|
+
legacyObject(dataRoot, record, entry.id);
|
|
527
|
+
if (entry.createdAt !== record.createdAt || entry.mediaType !== record.mediaType || entry.size !== record.size) throw new Error('index mismatch');
|
|
528
|
+
legacySkipped++; continue;
|
|
529
|
+
}
|
|
489
530
|
const { manifest } = readArtifact(dataRoot, entry.id, { allowLegacy: false, verifyContent: false });
|
|
490
531
|
if (entry.createdAt !== manifest.createdAt || entry.mediaType !== manifest.mediaType
|
|
491
532
|
|| entry.size !== manifest.size || entry.digest !== manifest.digest.value) throw new Error('index mismatch');
|
|
492
533
|
artifacts.push(publicArtifact(manifest));
|
|
493
534
|
} catch (_) { dropped++; }
|
|
494
535
|
}
|
|
495
|
-
return { revision: index.revision, artifacts, dropped };
|
|
536
|
+
return { revision: index.revision, artifacts, dropped, legacySkipped };
|
|
496
537
|
}
|
|
497
538
|
|
|
498
539
|
export function listCoreArtifacts(dataRoot, { offset = 0, limit = 50, kind, media_type, task_id } = {}) {
|
|
@@ -623,6 +664,7 @@ function inferredOrphanManifest(dataRoot, id, objectName) {
|
|
|
623
664
|
*/
|
|
624
665
|
export function rebuildCoreArtifactIndex(dataRoot) {
|
|
625
666
|
const store = ensureStore(dataRoot);
|
|
667
|
+
markIndexDirty(dataRoot);
|
|
626
668
|
const report = { recovered: 0, legacyUpgraded: 0, invalid: 0, unresolved: 0, danglingRelations: 0 };
|
|
627
669
|
const records = new Set(committedIds(dataRoot));
|
|
628
670
|
const manifests = new Set(names(store.manifests)
|
package/lib/core-bridge-tools.js
CHANGED
|
@@ -28,7 +28,7 @@ export function validateBridgeArguments(tool, value) {
|
|
|
28
28
|
try { normalizeComfyParameters(item); } catch (_) { invalid(); }
|
|
29
29
|
continue;
|
|
30
30
|
}
|
|
31
|
-
if (field.type === 'integer' ?
|
|
31
|
+
if (['integer', 'number'].includes(field.type) ? !(field.type === 'integer' ? Number.isSafeInteger(item) : typeof item === 'number' && Number.isFinite(item)) || item < field.minimum || field.maximum !== undefined && item > field.maximum
|
|
32
32
|
: typeof item !== 'string' || field.minLength !== undefined && item.length < field.minLength || field.maxLength !== undefined && item.length > field.maxLength
|
|
33
33
|
|| field.pattern && !new RegExp(field.pattern).test(item) || field.enum && !field.enum.includes(item)) invalid();
|
|
34
34
|
}
|
|
@@ -49,6 +49,7 @@ export async function callCoreBridgeTool(client, name, args, { signal } = {}) {
|
|
|
49
49
|
if (name === 'iris_core_plugin_call') return { value: await client.callPlugin(args, options) };
|
|
50
50
|
if (name === 'iris_core_models') return { value: await client.models(options) };
|
|
51
51
|
if (name === 'iris_core_audio_models') return { value: await client.audioModels(options) };
|
|
52
|
+
if (name === 'iris_core_video_models') return { value: await client.videoModels(options) };
|
|
52
53
|
if (name === 'iris_core_runs') return { value: await client.runs(args, options) };
|
|
53
54
|
if (name === 'iris_core_run') return { value: await client.run(args.runId, options) };
|
|
54
55
|
if (name === 'iris_core_import_image') {
|
|
@@ -66,13 +67,14 @@ export async function callCoreBridgeTool(client, name, args, { signal } = {}) {
|
|
|
66
67
|
return { value: await client[name === 'iris_core_create_speech' ? 'createSpeech' : 'createTranscription']({ key, value }, options) };
|
|
67
68
|
}
|
|
68
69
|
if (name === 'iris_core_audio') return { audio: { artifactId: args.artifactId, ...await client.audio(args.artifactId, options) } };
|
|
70
|
+
if (name === 'iris_core_video') return { video: { artifactId: args.artifactId, ...await client.video(args.artifactId, options) } };
|
|
69
71
|
if (name === 'iris_core_transcript') {
|
|
70
72
|
const result = await client.transcript(args.artifactId, options);
|
|
71
73
|
return { value: { contractVersion: 0, artifactId: args.artifactId, text: new TextDecoder('utf-8', { fatal: true }).decode(result.bytes) } };
|
|
72
74
|
}
|
|
73
|
-
if (['iris_core_create_run', 'iris_core_create_generation'].includes(name)) {
|
|
75
|
+
if (['iris_core_create_run', 'iris_core_create_generation', 'iris_core_create_video'].includes(name)) {
|
|
74
76
|
const { key, ...value } = args;
|
|
75
|
-
return { value: await client[name === 'iris_core_create_run' ? 'createRun' : 'createGeneration']({ key, value }, options) };
|
|
77
|
+
return { value: await client[name === 'iris_core_create_run' ? 'createRun' : name === 'iris_core_create_video' ? 'createVideo' : 'createGeneration']({ key, value }, options) };
|
|
76
78
|
}
|
|
77
79
|
if (name === 'iris_core_continue') return { value: await client.continueRun(args, options) };
|
|
78
80
|
if (name === 'iris_core_operation') return { value: await client.operation(args.operationId, options) };
|