@mokuyoaxis/dsh-iris 0.2.6 → 0.3.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -1
- package/README.en.md +104 -304
- package/README.md +101 -302
- package/bin/dsh-iris.js +446 -64
- package/docs/AGENT_CORE_QUICKSTART.md +64 -0
- package/docs/AGENT_QUICKSTART.md +7 -5
- package/docs/ARCHITECTURE.md +20 -4
- package/docs/AUDIO_FLOWS.md +71 -0
- package/docs/CLI_MANAGEMENT.md +1 -1
- package/docs/CONTACT_SHEET.md +62 -0
- package/docs/CORE_CONNECTIONS.md +99 -0
- package/docs/CORE_QUICKSTART.md +108 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +41 -5
- package/docs/CORE_SERVICE.md +97 -0
- package/docs/CORE_SKILLS.md +94 -0
- package/docs/DSH_COMPATIBILITY.md +14 -4
- package/docs/DSH_CORE_MIGRATION.md +4 -2
- package/docs/DSH_PROVIDER_CONNECTIONS.md +1 -1
- package/docs/DSH_RC2_ADAPTATION.md +1 -1
- package/docs/DSH_RUNTIME.md +40 -0
- package/docs/FUNCTION_MODEL_POOLS.md +1 -1
- package/docs/HEADLESS_CLI.md +12 -4
- package/docs/HOST_DOCTOR.md +13 -1
- package/docs/IMAGE_FLOW.md +75 -0
- package/docs/IMAGE_GENERATION.md +127 -0
- package/docs/LOCAL_API.md +62 -0
- package/docs/LOCAL_API_CONTRACT.md +140 -0
- package/docs/LOCAL_CREATION.md +56 -0
- package/docs/LOCAL_OPERATIONS.md +46 -0
- package/docs/LOCAL_SELECTION.md +57 -0
- package/docs/LOCAL_WEB.md +201 -0
- package/docs/MCP.md +191 -0
- package/docs/MUSIC.md +2 -0
- package/docs/PANEL_EXTENSIONS.md +64 -0
- package/docs/PLUGINS.md +189 -0
- package/docs/PLUGIN_STARTUP.md +100 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +1 -1
- package/docs/PROVIDER_EXTENSIONS.md +78 -0
- package/docs/PROVIDER_HEALTH.md +4 -2
- package/docs/PROVIDER_SETTINGS.md +85 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +6 -0
- package/docs/ROADMAP.md +28 -6
- package/docs/SECURITY.md +9 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
- package/docs/VISION_CLI.md +2 -2
- package/docs/WEB_PROTOTYPE.md +62 -0
- package/docs/WEB_RECOVERY.md +46 -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.2.7.md +62 -0
- package/docs/releases/0.3.0-alpha.1.md +51 -0
- package/docs/screenshots.md +41 -1
- package/lib/actions.js +132 -70
- package/lib/adapters.js +20 -11
- package/lib/api.js +38 -38
- package/lib/application-runtime.js +213 -0
- package/lib/audio-flow-input.js +37 -0
- package/lib/client.js +434 -55
- package/lib/comfyui-adapter.js +94 -0
- package/lib/comfyui-client.js +51 -0
- package/lib/comfyui-inspection.js +73 -0
- package/lib/comfyui-workflow.js +94 -0
- package/lib/config.js +36 -1
- package/lib/configured-image-runtime.js +50 -0
- package/lib/connection-skill.js +89 -0
- package/lib/connection-template.js +23 -0
- package/lib/core-artifact-store.js +21 -2
- package/lib/core-bridge-selection.js +31 -0
- package/lib/core-bridge-tools.js +83 -0
- package/lib/core-capability-catalog.js +182 -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-maintenance.js +31 -2
- package/lib/core-runtime.js +20 -0
- package/lib/core-service-client.js +170 -0
- package/lib/core-tasks.js +84 -13
- package/lib/core-writer-lease.js +16 -0
- package/lib/creation-request-store.js +133 -0
- package/lib/doctor.js +12 -1
- package/lib/dsh-core-adapter.js +666 -572
- package/lib/dsh-host-adapter.js +8 -2
- package/lib/dsh-service-bridge.js +66 -0
- package/lib/execution-ownership.js +43 -0
- package/lib/flow-store.js +290 -0
- 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 +232 -0
- package/lib/index.js +94 -68
- package/lib/local-api-projection.js +32 -0
- package/lib/local-api-token-directory.js +31 -0
- package/lib/local-api.js +1083 -0
- package/lib/local-creation.js +150 -0
- package/lib/local-operations.js +36 -0
- package/lib/local-request-root.js +86 -0
- package/lib/local-web-assets.js +37 -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 +101 -0
- package/lib/mcp-server.js +47 -0
- package/lib/model-directory-service.js +16 -6
- package/lib/model-directory.js +3 -3
- package/lib/models.js +2 -2
- package/lib/music-transport.js +2 -1
- package/lib/operation-runner.js +236 -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 +288 -0
- package/lib/plugins.js +58 -0
- package/lib/private-storage.js +12 -0
- package/lib/provider-adapter.js +5 -1
- package/lib/provider-adapters.js +56 -18
- package/lib/provider-catalog.js +40 -29
- package/lib/provider-config-service.js +105 -23
- package/lib/provider-diagnostics.js +56 -0
- package/lib/provider-extensions.js +29 -0
- package/lib/provider-health.js +8 -1
- package/lib/provider-monitor.js +79 -0
- package/lib/provider-protocol.js +70 -20
- package/lib/provider-registry.js +118 -0
- package/lib/provider-response.js +28 -0
- package/lib/provider-settings.js +134 -0
- package/lib/provider-task-runner.js +40 -7
- package/lib/render-address.js +35 -0
- package/lib/render.js +52 -13
- package/lib/skill-installation.js +211 -0
- package/lib/vision-model-routing.js +3 -0
- package/lib/web/app.js +642 -0
- package/lib/web/appearance.js +111 -0
- package/lib/web/comfyui.js +146 -0
- package/lib/web/connections.js +377 -0
- package/lib/web/creation.js +355 -0
- package/lib/web/flow-view.js +33 -0
- package/lib/web/gallery.js +132 -0
- package/lib/web/index.html +291 -0
- package/lib/web/media-client.js +204 -0
- package/lib/web/plugin-panel.js +221 -0
- package/lib/web/plugin-surface.css +17 -0
- package/lib/web/providers.js +492 -0
- package/lib/web/recovery.js +105 -0
- package/lib/web/startup.js +18 -0
- package/lib/web/styles.css +518 -0
- package/lib/web/transport.js +96 -0
- package/lib/web/workbench-login.js +165 -0
- package/lib/workbench-artifacts.js +1 -2
- package/lib/workbench-auth-cli.js +47 -0
- package/lib/workbench-auth.js +200 -0
- package/lib/workbench-management.js +8 -9
- package/package.json +22 -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 +44 -0
- package/skills/iris-core/agents/openai.yaml +4 -0
- package/skills/iris-core/references/cli.md +95 -0
- package/skills/iris-core/references/comfyui.md +69 -0
- package/skills/iris-core/references/tasks.md +60 -0
- package/user_guide.md +19 -9
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { checkedPluginDependency, pluginFail } from './plugin-contract.js';
|
|
2
|
+
|
|
3
|
+
/** Saved authority is an exact dependency list, never a live registry expansion. */
|
|
4
|
+
export function checkedPluginSelection(value = []) {
|
|
5
|
+
if (!Array.isArray(value) || value.length > 16) pluginFail('IRIS_CONNECTION_INPUT_INVALID', '插件业务须为最多 16 项明确依赖');
|
|
6
|
+
let result;
|
|
7
|
+
try { result = value.map(checkedPluginDependency); }
|
|
8
|
+
catch (error) { if (error.code !== 'IRIS_PLUGIN_INVALID') throw error; pluginFail('IRIS_CONNECTION_INPUT_INVALID', '插件业务依赖格式无效'); }
|
|
9
|
+
if (result.some(item => item.kind !== 'operations') || new Set(result.map(item => item.contributionId)).size !== result.length) pluginFail('IRIS_CONNECTION_INPUT_INVALID', '插件业务依赖重复或类型无效');
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
12
|
+
export function hasPluginSelection(values, dependency) {
|
|
13
|
+
const pin = checkedPluginDependency(dependency);
|
|
14
|
+
return values.some(value => JSON.stringify(checkedPluginDependency(value)) === JSON.stringify(pin));
|
|
15
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Shared source-image limits for the catalog and the plugin command boundary.
|
|
2
|
+
const MEDIA_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp']);
|
|
3
|
+
export function isPluginSourceImage(artifact) {
|
|
4
|
+
return MEDIA_TYPES.has(artifact.mediaType) && artifact.size <= 8 * 1024 * 1024;
|
|
5
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { freezePluginValue, pluginFail, pluginFields } from './plugin-contract.js';
|
|
4
|
+
|
|
5
|
+
const WEB_KINDS = ['pages', 'settings', 'renderers'];
|
|
6
|
+
const resources = new WeakMap();
|
|
7
|
+
const invalid = message => pluginFail('IRIS_PLUGIN_WEB_INVALID', message);
|
|
8
|
+
|
|
9
|
+
/** Only explicitly declared entry/styles. Never serve a plugin directory or evaluate browser JS. */
|
|
10
|
+
export function pinPluginWebAssets(definition, file) {
|
|
11
|
+
const root = path.dirname(file), entries = new Map();
|
|
12
|
+
let total = 0;
|
|
13
|
+
for (const kind of WEB_KINDS) for (const item of definition.manifest.contributions[kind]) {
|
|
14
|
+
const assets = new Map();
|
|
15
|
+
for (const relative of [item.entry, ...(item.styles || [])]) {
|
|
16
|
+
const target = fs.realpathSync(path.join(root, relative)), rel = path.relative(root, target), stat = fs.statSync(target);
|
|
17
|
+
if (!rel || rel.startsWith('..' + path.sep) || path.isAbsolute(rel) || !stat.isFile()) {
|
|
18
|
+
pluginFail('IRIS_PLUGIN_INVALID', 'Web 入口必须位于插件目录内');
|
|
19
|
+
}
|
|
20
|
+
if (stat.size > 8 * 1024 * 1024 || (total += stat.size) > 16 * 1024 * 1024) invalid('插件 Web 资源超过 8 MiB 单文件 / 16 MiB 总量限制');
|
|
21
|
+
assets.set(relative, { bytes: fs.readFileSync(target), mediaType: relative.endsWith('.css') ? 'text/css; charset=utf-8' : 'text/javascript; charset=utf-8' });
|
|
22
|
+
}
|
|
23
|
+
entries.set(kind + ':' + definition.manifest.id + '.' + item.id, assets);
|
|
24
|
+
}
|
|
25
|
+
return entries;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function registerPluginWebAssets(registry, entries) { resources.set(registry, entries); }
|
|
29
|
+
|
|
30
|
+
/** A Core service opts in to exact contribution IDs. Installing a plugin never opts in. */
|
|
31
|
+
export function createPluginPanelAssembly(registry, selection = {}) {
|
|
32
|
+
if (!pluginFields(selection, WEB_KINDS, [])) invalid('Panel 组合选择须为 pages/settings/renderers 的明确 ID 列表');
|
|
33
|
+
const source = registry?.snapshot(), pinned = resources.get(registry), routes = new Map(), manifest = { contractVersion: 0 }, selected = {};
|
|
34
|
+
if (source && source.state !== 'active') pluginFail('IRIS_PLUGIN_CLOSED', '插件注册实例正在关闭或已关闭');
|
|
35
|
+
for (const kind of WEB_KINDS) {
|
|
36
|
+
const ids = selection[kind] ?? [];
|
|
37
|
+
if (!Array.isArray(ids) || ids.some(id => typeof id !== 'string') || new Set(ids).size !== ids.length) invalid('Panel 选择须为不重复的完整贡献 ID');
|
|
38
|
+
selected[kind] = [...ids];
|
|
39
|
+
manifest[kind] = ids.map(id => {
|
|
40
|
+
const item = source?.contributions[kind].find(value => value.id === id);
|
|
41
|
+
if (!item) pluginFail('IRIS_PLUGIN_CONTRIBUTION_MISSING', 'Panel 贡献未加载:' + kind + ':' + id);
|
|
42
|
+
const assets = pinned?.get(kind + ':' + id);
|
|
43
|
+
if (!assets) pluginFail('IRIS_PLUGIN_WEB_ASSET_MISSING', 'Panel 贡献须经显式文件入口加载并固定资源:' + id);
|
|
44
|
+
const base = '/plugin-assets/' + item.dependency.pluginId + '/' + item.dependency.pluginVersion + '/' + kind + '/' + id + '/';
|
|
45
|
+
for (const [name, asset] of assets) routes.set(base + name, asset);
|
|
46
|
+
return { id, title: item.title, dependency: item.dependency, entry: base + item.entry,
|
|
47
|
+
styles: (item.styles || []).map(name => base + name), ...(kind === 'renderers' ? { mediaTypes: item.mediaTypes } : {}) };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const active = () => !registry || registry.snapshot().state === 'active';
|
|
51
|
+
freezePluginValue(manifest); freezePluginValue(selected);
|
|
52
|
+
return Object.freeze({ selection: selected, manifest,
|
|
53
|
+
has: route => active() && routes.has(route), read: async route => active() ? routes.get(route) || null : null });
|
|
54
|
+
}
|
package/lib/plugins.d.ts
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/** Experimental v0. Trusted local code; no sandbox, discovery, installation or implicit grant. */
|
|
2
|
+
export type JsonValue = null | boolean | number | string | readonly JsonValue[] | { readonly [key: string]: JsonValue };
|
|
3
|
+
export type PluginKind = 'providers' | 'operations' | 'pages' | 'renderers' | 'settings';
|
|
4
|
+
export const PLUGIN_CONTRACT_VERSION: 0;
|
|
5
|
+
export const PLUGIN_KINDS: readonly PluginKind[];
|
|
6
|
+
export type PluginCoreCommand = 'artifact.inspect' | 'artifact.read' | 'artifact.list' | 'task.inspect' | 'task.list' | 'artifact.import';
|
|
7
|
+
export const PLUGIN_CORE_COMMANDS: Readonly<Record<PluginCoreCommand, 'reader' | 'writer'>>;
|
|
8
|
+
export class PluginContractError extends Error { constructor(code: string, message: string); readonly code: string; }
|
|
9
|
+
export interface PluginDependency {
|
|
10
|
+
readonly contractVersion: 0;
|
|
11
|
+
readonly pluginId: string;
|
|
12
|
+
readonly pluginVersion: string;
|
|
13
|
+
readonly kind: PluginKind;
|
|
14
|
+
/** Fully qualified pluginId.localId. */
|
|
15
|
+
readonly contributionId: string;
|
|
16
|
+
readonly contributionVersion: string;
|
|
17
|
+
}
|
|
18
|
+
export interface Contribution { readonly id: string; readonly version: string; readonly title: string; }
|
|
19
|
+
export interface ProviderContribution extends Contribution {
|
|
20
|
+
readonly capabilities: readonly ('image-gen' | 'video-gen' | 'tts' | 'music-gen' | 'transcribe')[];
|
|
21
|
+
readonly configuration?: readonly { key: 'baseUrl' | 'mediaBaseUrl' | 'apiKey'; label: string; type: 'url' | 'secret'; required?: boolean; advanced?: boolean }[];
|
|
22
|
+
}
|
|
23
|
+
export interface OperationContribution extends Contribution {
|
|
24
|
+
readonly mode: 'reader' | 'writer';
|
|
25
|
+
readonly commands: readonly PluginCoreCommand[];
|
|
26
|
+
/** Top-level string or string-array input fields containing source Artifact IDs. Bound at acceptance. */
|
|
27
|
+
readonly artifactInputs?: readonly string[];
|
|
28
|
+
/** Closed root object; supported JSON Schema subset documented in PLUGINS.md. */
|
|
29
|
+
readonly inputSchema: { readonly type: 'object'; readonly additionalProperties: false; readonly [key: string]: JsonValue };
|
|
30
|
+
}
|
|
31
|
+
export interface PageContribution extends Contribution {
|
|
32
|
+
/** Plugin-relative bundled JS/MJS module. Explicit entry loader pins it before Panel selection. */
|
|
33
|
+
readonly entry: string;
|
|
34
|
+
readonly styles?: readonly string[];
|
|
35
|
+
}
|
|
36
|
+
export interface RendererContribution extends PageContribution { readonly mediaTypes: readonly string[]; }
|
|
37
|
+
export interface SettingsContribution extends PageContribution {}
|
|
38
|
+
export interface PluginPanelSelection {
|
|
39
|
+
readonly pages?: readonly string[];
|
|
40
|
+
readonly settings?: readonly string[];
|
|
41
|
+
readonly renderers?: readonly string[];
|
|
42
|
+
}
|
|
43
|
+
export interface PluginManifest {
|
|
44
|
+
readonly contractVersion: 0;
|
|
45
|
+
readonly id: string;
|
|
46
|
+
readonly version: string;
|
|
47
|
+
/** SemVer precedence interval, min inclusive/maxExclusive exclusive. Not npm range syntax. */
|
|
48
|
+
readonly compatibility: { readonly min: string; readonly maxExclusive: string };
|
|
49
|
+
readonly contributions: {
|
|
50
|
+
readonly providers?: readonly ProviderContribution[];
|
|
51
|
+
readonly operations?: readonly OperationContribution[];
|
|
52
|
+
readonly pages?: readonly PageContribution[];
|
|
53
|
+
readonly renderers?: readonly RendererContribution[];
|
|
54
|
+
readonly settings?: readonly SettingsContribution[];
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export interface PluginOperationContext {
|
|
58
|
+
readonly signal: AbortSignal;
|
|
59
|
+
readonly dependency: PluginDependency;
|
|
60
|
+
readonly core: { execute(command: PluginCoreCommand, input: Record<string, unknown>): Promise<unknown> };
|
|
61
|
+
}
|
|
62
|
+
export type PluginOperation = (input: Readonly<Record<string, JsonValue>>, context: PluginOperationContext) => JsonValue | Promise<JsonValue>;
|
|
63
|
+
export type ProviderCapability = 'image' | 'video' | 'tts' | 'music' | 'transcribe';
|
|
64
|
+
export type ProviderMethod = 'discover' | 'submit' | 'poll' | 'cancel' | 'download' | 'mapError';
|
|
65
|
+
export interface ProviderAdapterInput {
|
|
66
|
+
id: string; protocol: string; capabilities: ProviderCapability[]; contractVersion?: 0;
|
|
67
|
+
operations: Partial<Record<ProviderMethod, (...args: any[]) => unknown>>;
|
|
68
|
+
unsupported?: Partial<Record<ProviderMethod, string>>;
|
|
69
|
+
prepareInput?: (...args: any[]) => unknown;
|
|
70
|
+
unsupportedPreparation?: string;
|
|
71
|
+
pluginDependency?: PluginDependency;
|
|
72
|
+
}
|
|
73
|
+
export type ProviderAdapter = Readonly<Omit<ProviderAdapterInput, 'contractVersion' | 'capabilities' | 'operations' | 'unsupported'> & {
|
|
74
|
+
contractVersion: 0;
|
|
75
|
+
capabilities: readonly ProviderCapability[];
|
|
76
|
+
operations: Readonly<Partial<Record<ProviderMethod, (...args: any[]) => unknown>>>;
|
|
77
|
+
unsupported: Readonly<Partial<Record<ProviderMethod, { readonly reason: string }>>>;
|
|
78
|
+
}>;
|
|
79
|
+
export function defineProviderAdapter(input: ProviderAdapterInput): ProviderAdapter;
|
|
80
|
+
export function normalizeMediaEndpoint(value: string): string;
|
|
81
|
+
export interface ProviderImplementation {
|
|
82
|
+
normalizeEndpoint(value: string): string;
|
|
83
|
+
createAdapter(provider: Record<string, any>, options?: Record<string, any>): ProviderAdapter;
|
|
84
|
+
supportsModel?(input: { provider: Record<string, unknown>; modelId: string; capability: string; checkEndpoint?: boolean }): boolean;
|
|
85
|
+
supportsImageEdit?(modelId: string): boolean;
|
|
86
|
+
}
|
|
87
|
+
export interface PluginActivationContext {
|
|
88
|
+
readonly coreVersion: string;
|
|
89
|
+
readonly plugin: { readonly id: string; readonly version: string };
|
|
90
|
+
/** Register during activate, including before awaited initialization; reverse order cleanup. */
|
|
91
|
+
onDispose(cleanup: () => void | Promise<void>): void;
|
|
92
|
+
}
|
|
93
|
+
export interface PluginImplementation {
|
|
94
|
+
readonly providers?: Readonly<Record<string, ProviderImplementation>>;
|
|
95
|
+
readonly operations?: Readonly<Record<string, PluginOperation>>;
|
|
96
|
+
}
|
|
97
|
+
export interface CorePlugin {
|
|
98
|
+
readonly manifest: PluginManifest;
|
|
99
|
+
activate(context: PluginActivationContext): PluginImplementation | Promise<PluginImplementation>;
|
|
100
|
+
}
|
|
101
|
+
export function definePlugin(definition: CorePlugin): CorePlugin;
|
|
102
|
+
export interface ProviderRegistry {
|
|
103
|
+
descriptor(id: string): Readonly<Record<string, any>> | undefined;
|
|
104
|
+
snapshot(): { state: string; protocols: Record<string, any>[]; extensions: { id: string; version: string }[]; activeCalls: number; cleanupFailed: boolean; contractVersion: 0 };
|
|
105
|
+
createAdapter(id: string, provider: Record<string, any>, options?: Record<string, any>): ProviderAdapter;
|
|
106
|
+
dispose(): Promise<void>;
|
|
107
|
+
}
|
|
108
|
+
export interface CorePluginRegistry {
|
|
109
|
+
readonly providers: ProviderRegistry;
|
|
110
|
+
snapshot(): {
|
|
111
|
+
contractVersion: 0; coreVersion: string; state: string; activeCalls: number; cleanupFailed: boolean;
|
|
112
|
+
plugins: PluginManifest[];
|
|
113
|
+
contributions: {
|
|
114
|
+
providers: (ProviderContribution & { dependency: PluginDependency })[];
|
|
115
|
+
operations: (OperationContribution & { dependency: PluginDependency })[];
|
|
116
|
+
pages: (PageContribution & { dependency: PluginDependency })[];
|
|
117
|
+
renderers: (RendererContribution & { dependency: PluginDependency })[];
|
|
118
|
+
settings: (SettingsContribution & { dependency: PluginDependency })[];
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
dependency(kind: PluginKind, qualifiedId: string): PluginDependency;
|
|
122
|
+
assertDependency(dependency: PluginDependency): PluginDependency;
|
|
123
|
+
dispose(): Promise<void>;
|
|
124
|
+
}
|
|
125
|
+
export function createCorePluginRegistry(options?: { plugins?: CorePlugin[]; coreVersion?: string }): Promise<CorePluginRegistry>;
|
|
126
|
+
export function loadCorePluginRegistry(options?: { entries?: string[]; coreVersion?: string }): Promise<CorePluginRegistry>;
|
|
127
|
+
export type CoreCapabilityScope = 'read' | 'create' | 'execute' | 'stop';
|
|
128
|
+
export type CoreBridgeCapability = 'image-edit' | 'image-generate' | 'tts' | 'transcribe';
|
|
129
|
+
export interface ContributionAccess {
|
|
130
|
+
readonly local: 'requires-configuration' | 'requires-grant' | 'descriptor-only' | 'unavailable';
|
|
131
|
+
readonly web: 'via-media-flow' | 'via-operation' | 'not-exposed' | 'durable-operation-required' | 'not-mounted' | 'available-in-panel' | 'unavailable';
|
|
132
|
+
readonly mcp: 'via-media-flow' | 'requires-selection' | 'not-exposed' | 'durable-operation-required' | 'not-applicable' | 'unavailable';
|
|
133
|
+
}
|
|
134
|
+
export interface CatalogContribution extends Contribution {
|
|
135
|
+
readonly dependency: PluginDependency;
|
|
136
|
+
/** Declaration/assembly facts only; never the caller's authorization or service health. */
|
|
137
|
+
readonly access: ContributionAccess;
|
|
138
|
+
}
|
|
139
|
+
export interface CoreCapabilitySnapshot {
|
|
140
|
+
readonly contractVersion: 0;
|
|
141
|
+
readonly state: string;
|
|
142
|
+
readonly coreVersion?: string;
|
|
143
|
+
readonly scopeOptions: readonly { readonly id: CoreCapabilityScope; readonly title: string }[];
|
|
144
|
+
readonly controlTools: readonly string[];
|
|
145
|
+
readonly groups: readonly {
|
|
146
|
+
readonly id: CoreBridgeCapability; readonly recipe: string; readonly title: string; readonly label: string; readonly shortTitle: string;
|
|
147
|
+
readonly media: string; readonly requiresAudio?: boolean; readonly tools: readonly string[];
|
|
148
|
+
}[];
|
|
149
|
+
readonly tools: readonly { readonly name: string; readonly scope: CoreCapabilityScope; readonly description: string }[];
|
|
150
|
+
readonly pluginTools: CoreCapabilitySnapshot['tools'];
|
|
151
|
+
readonly pluginControlTools: readonly string[];
|
|
152
|
+
readonly media: readonly { readonly id: string; readonly title: string; readonly note?: string; readonly capabilities: readonly CoreBridgeCapability[] }[];
|
|
153
|
+
readonly plugins: readonly { readonly id: string; readonly version: string; readonly compatibility: PluginManifest['compatibility'] }[];
|
|
154
|
+
readonly contributions: {
|
|
155
|
+
readonly providers: readonly (ProviderContribution & CatalogContribution)[];
|
|
156
|
+
readonly operations: readonly (OperationContribution & CatalogContribution)[];
|
|
157
|
+
/** No browser module path is exposed or mounted by discovery. */
|
|
158
|
+
readonly pages: readonly CatalogContribution[];
|
|
159
|
+
readonly renderers: readonly (CatalogContribution & { readonly mediaTypes: readonly string[] })[];
|
|
160
|
+
readonly settings: readonly CatalogContribution[];
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
export interface CoreCapabilityCatalog {
|
|
164
|
+
snapshot(options?: { audioEnabled?: boolean; panelContributions?: PluginPanelSelection; allowedOperations?: readonly string[] }): CoreCapabilitySnapshot;
|
|
165
|
+
/** Validate an explicit local selection. Does not invoke, persist a grant or enable MCP/Web. */
|
|
166
|
+
selectOperations(ids?: readonly string[]): readonly (OperationContribution & CatalogContribution)[];
|
|
167
|
+
}
|
|
168
|
+
export function createCoreCapabilityCatalog(options?: { registry?: CorePluginRegistry }): CoreCapabilityCatalog;
|
|
169
|
+
export interface CorePluginHost {
|
|
170
|
+
call(id: string, input: Record<string, JsonValue>, options?: { dependency?: PluginDependency; signal?: AbortSignal }): Promise<{ dependency: PluginDependency; value: JsonValue }>;
|
|
171
|
+
snapshot(): { state: string; queuedWrites: number; activeWrites: number; activeReaders: number; cleanupFailed: boolean };
|
|
172
|
+
/** Caller disposes the shared registry after every host has closed. */
|
|
173
|
+
dispose(): Promise<ReturnType<CorePluginHost['snapshot']>>;
|
|
174
|
+
}
|
|
175
|
+
export function createCorePluginHost(options: { dataRoot: string; registry: CorePluginRegistry; allowedOperations?: string[] }): CorePluginHost;
|
|
176
|
+
export interface CorePluginWorkbenchSnapshot {
|
|
177
|
+
readonly contractVersion: 0;
|
|
178
|
+
readonly state: string;
|
|
179
|
+
readonly origin: string | null;
|
|
180
|
+
/** Media writes remain disabled; optional Provider editing has a separate owner gate. */
|
|
181
|
+
readonly readOnly: true;
|
|
182
|
+
readonly activeReads: number;
|
|
183
|
+
readonly cleanupFailed?: boolean;
|
|
184
|
+
}
|
|
185
|
+
export interface CorePluginWorkbench {
|
|
186
|
+
start(options?: { port?: number }): Promise<{ readonly origin: string; readonly accessToken: string; readonly loginUrl: string; readonly loginExpiresAt: string }>;
|
|
187
|
+
snapshot(): CorePluginWorkbenchSnapshot;
|
|
188
|
+
readonly closed: Promise<CorePluginWorkbenchSnapshot>;
|
|
189
|
+
dispose(): Promise<CorePluginWorkbenchSnapshot>;
|
|
190
|
+
}
|
|
191
|
+
export interface CorePasswordPluginWorkbench extends Omit<CorePluginWorkbench, 'start'> {
|
|
192
|
+
start(options?: { port?: number }): Promise<{ readonly origin: string; readonly accessToken: string; readonly loginUrl: string;
|
|
193
|
+
readonly authentication: { readonly mode: 'password'; readonly configured: boolean; readonly authId: string;
|
|
194
|
+
readonly rememberDays: 30; readonly minPasswordLength: 8; readonly maxPasswordLength: 128 } }>;
|
|
195
|
+
}
|
|
196
|
+
export interface CorePluginWorkbenchOptions {
|
|
197
|
+
dataRoot: string; registry?: CorePluginRegistry; panelContributions?: PluginPanelSelection;
|
|
198
|
+
panelManifestPath?: string; providerConfigPath?: string; allowProviderManagement?: boolean;
|
|
199
|
+
/** Omitted: no business runner. []: reconcile/read history only. IDs: explicitly enabled durable businesses. */
|
|
200
|
+
allowedOperations?: string[];
|
|
201
|
+
/** Explicit private directory outside dataRoot. Enables owner connection management. */
|
|
202
|
+
connectionStore?: string;
|
|
203
|
+
/** Enable the standard HTTP /mcp edge. Does not start a second Core. */
|
|
204
|
+
allowMcp?: boolean;
|
|
205
|
+
/** Optional persistent browser password/session store, in a private directory outside dataRoot. */
|
|
206
|
+
workbenchAuth?: { directory: string };
|
|
207
|
+
}
|
|
208
|
+
export function createCorePluginWorkbench(options: CorePluginWorkbenchOptions & { workbenchAuth: { directory: string } }): CorePasswordPluginWorkbench;
|
|
209
|
+
export function createCorePluginWorkbench(options: Omit<CorePluginWorkbenchOptions, 'workbenchAuth'> & { workbenchAuth?: undefined }): CorePluginWorkbench;
|
|
210
|
+
export function createCorePluginWorkbench(options: CorePluginWorkbenchOptions): CorePluginWorkbench | CorePasswordPluginWorkbench;
|
|
211
|
+
export function checkedPluginDependency(value: unknown): PluginDependency;
|
|
212
|
+
export function comparePluginVersions(a: string, b: string): -1 | 0 | 1;
|
|
213
|
+
|
|
214
|
+
export interface PluginTheme { readonly mode: 'light' | 'dark'; readonly density: string; readonly accent: 'iris'; }
|
|
215
|
+
export interface PluginWebSession {
|
|
216
|
+
readonly contractVersion: 0;
|
|
217
|
+
readonly instanceId: string;
|
|
218
|
+
readonly rootId?: string | null;
|
|
219
|
+
readonly operations: readonly string[];
|
|
220
|
+
readonly readOnly: boolean;
|
|
221
|
+
readonly audio: boolean;
|
|
222
|
+
readonly providerManagement: boolean;
|
|
223
|
+
readonly pluginExecution: boolean;
|
|
224
|
+
readonly pluginOperations: readonly PluginDependency[];
|
|
225
|
+
}
|
|
226
|
+
export interface PluginArtifact { readonly id: string; readonly mediaType: string; readonly label?: string; }
|
|
227
|
+
export interface PluginWebRequestOptions { readonly signal?: AbortSignal; }
|
|
228
|
+
export interface PluginBusinessRequest {
|
|
229
|
+
readonly key: string;
|
|
230
|
+
readonly operationId: string;
|
|
231
|
+
readonly dependency: PluginDependency;
|
|
232
|
+
readonly input: Readonly<Record<string, JsonValue>>;
|
|
233
|
+
}
|
|
234
|
+
export interface PluginDurableOperation {
|
|
235
|
+
readonly contractVersion: 0;
|
|
236
|
+
readonly id: string;
|
|
237
|
+
readonly rootId: string;
|
|
238
|
+
readonly action: 'plugin.execute';
|
|
239
|
+
readonly operationId: string;
|
|
240
|
+
readonly dependency: PluginDependency;
|
|
241
|
+
readonly input: Readonly<Record<string, JsonValue>>;
|
|
242
|
+
readonly inputDigest: string;
|
|
243
|
+
readonly sourceArtifactIds: readonly string[];
|
|
244
|
+
readonly revision: number;
|
|
245
|
+
readonly state: 'queued' | 'running' | 'succeeded' | 'failed' | 'canceled' | 'interrupted';
|
|
246
|
+
readonly cancelRequested: boolean;
|
|
247
|
+
readonly createdAt: string;
|
|
248
|
+
readonly updatedAt: string;
|
|
249
|
+
readonly startedAt?: string;
|
|
250
|
+
readonly finishedAt?: string;
|
|
251
|
+
readonly failureCode?: 'dependency_unavailable' | 'permission_denied' | 'execution_failed';
|
|
252
|
+
readonly result?: { readonly value: JsonValue; readonly artifactIds: readonly string[] };
|
|
253
|
+
}
|
|
254
|
+
export interface PluginWebApi {
|
|
255
|
+
runs(query?: Readonly<Record<string, string>>, options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
256
|
+
run(id: string, options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
257
|
+
models(options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
258
|
+
audioModels(options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
259
|
+
artifact(artifact: PluginArtifact, options?: PluginWebRequestOptions): Promise<Blob>;
|
|
260
|
+
/** Only settings mounts in an explicitly enabled owner Provider-management session. */
|
|
261
|
+
readonly providers?: {
|
|
262
|
+
read(options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
263
|
+
save(input: Record<string, JsonValue>, options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
264
|
+
};
|
|
265
|
+
/** Present only when the service explicitly owns a durable plugin runner. Core checks each request. */
|
|
266
|
+
readonly pluginOperations?: {
|
|
267
|
+
catalog(query?: Readonly<Record<string, string>>, options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
268
|
+
history(options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
269
|
+
execute(input: PluginBusinessRequest, options?: PluginWebRequestOptions): Promise<{ contractVersion: 0; operation: PluginDurableOperation; idempotentReplay: boolean }>;
|
|
270
|
+
inspect(id: string, options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
271
|
+
lookup(key: string, options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
272
|
+
stop(id: string, options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/** Same-origin trusted modules; Shadow DOM is a style boundary, not a security sandbox. */
|
|
276
|
+
export interface PluginWebContext<Container = unknown> {
|
|
277
|
+
readonly container: Container;
|
|
278
|
+
readonly signal: AbortSignal;
|
|
279
|
+
readonly dependency: PluginDependency;
|
|
280
|
+
readonly session: PluginWebSession;
|
|
281
|
+
readonly api: PluginWebApi;
|
|
282
|
+
readonly theme: { current(): PluginTheme; subscribe(listener: (theme: PluginTheme) => void): () => void };
|
|
283
|
+
}
|
|
284
|
+
export interface PluginPageModule<Container = unknown> { mount(context: PluginWebContext<Container>): void | (() => void) | Promise<void | (() => void)>; }
|
|
285
|
+
export interface PluginSettingsModule<Container = unknown> extends PluginPageModule<Container> {}
|
|
286
|
+
export interface PluginRendererModule<Container = unknown> {
|
|
287
|
+
mount(context: PluginWebContext<Container> & { readonly artifact: PluginArtifact }): void | (() => void) | Promise<void | (() => void)>;
|
|
288
|
+
}
|
package/lib/plugins.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Experimental v0 public plugin author/embedding boundary. No automatic discovery or installation.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { createPluginRegistry } from './plugin-registry.js';
|
|
6
|
+
import { createApplicationRuntime } from './application-runtime.js';
|
|
7
|
+
import { definePlugin, pluginFail, pluginFields } from './plugin-contract.js';
|
|
8
|
+
import { createCoreCapabilityCatalog } from './core-capability-catalog.js';
|
|
9
|
+
import { pinPluginWebAssets, registerPluginWebAssets } from './plugin-web-assets.js';
|
|
10
|
+
import { createLocalReadApi } from './local-api.js';
|
|
11
|
+
|
|
12
|
+
export { createCoreCapabilityCatalog } from './core-capability-catalog.js';
|
|
13
|
+
|
|
14
|
+
export { definePlugin, PluginContractError, PLUGIN_CONTRACT_VERSION, PLUGIN_KINDS, PLUGIN_CORE_COMMANDS,
|
|
15
|
+
checkedPluginDependency, comparePluginVersions } from './plugin-contract.js';
|
|
16
|
+
export { defineProviderAdapter, normalizeMediaEndpoint } from './provider-extensions.js';
|
|
17
|
+
|
|
18
|
+
export function createCorePluginRegistry({ plugins = [], coreVersion } = {}) {
|
|
19
|
+
coreVersion ??= JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
20
|
+
return createPluginRegistry({ plugins, coreVersion });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Importing a module executes trusted code. Compatibility gates activate, not module evaluation. */
|
|
24
|
+
export async function loadCorePluginRegistry({ entries = [], coreVersion } = {}) {
|
|
25
|
+
if (!Array.isArray(entries)) pluginFail('IRIS_PLUGIN_INVALID', '入口须为明确文件列表');
|
|
26
|
+
const plugins = [], assets = new Map();
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (typeof entry !== 'string' || !path.isAbsolute(entry) || !/\.m?js$/.test(entry) || !fs.statSync(entry).isFile()) pluginFail('IRIS_PLUGIN_INVALID', '插件入口须为 JS/MJS 文件绝对路径');
|
|
29
|
+
const file = fs.realpathSync(entry), definition = definePlugin((await import(pathToFileURL(file).href)).default);
|
|
30
|
+
for (const [id, files] of pinPluginWebAssets(definition, file)) assets.set(id, files);
|
|
31
|
+
plugins.push(definition);
|
|
32
|
+
}
|
|
33
|
+
const registry = await createCorePluginRegistry({ plugins, coreVersion });
|
|
34
|
+
registerPluginWebAssets(registry, assets);
|
|
35
|
+
return registry;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Trusted Core assembly. Grants are captured once; individual calls cannot enlarge them. */
|
|
39
|
+
export function createCorePluginHost({ dataRoot, registry, allowedOperations = [] } = {}) {
|
|
40
|
+
const selection = createCoreCapabilityCatalog({ registry }).selectOperations(allowedOperations);
|
|
41
|
+
const grants = Object.freeze(selection.map(item => item.id)), application = createApplicationRuntime({ dataRoot });
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
call: (id, input, { dependency, signal } = {}) => application.executePluginOperation(registry, id, input,
|
|
44
|
+
{ allowedOperations: grants, dependency, signal }),
|
|
45
|
+
snapshot: () => application.snapshot(),
|
|
46
|
+
dispose: () => application.dispose()
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Public, bounded Panel assembly. Media remains read-only; Provider editing is an explicit owner gate. */
|
|
51
|
+
export function createCorePluginWorkbench(options) {
|
|
52
|
+
if (!pluginFields(options, ['dataRoot', 'registry', 'panelContributions', 'panelManifestPath', 'providerConfigPath', 'allowProviderManagement', 'allowedOperations', 'connectionStore', 'allowMcp', 'workbenchAuth'], ['dataRoot'])) {
|
|
53
|
+
pluginFail('IRIS_PLUGIN_WEB_INVALID', '工作台只接受明确的数据根、注册器、Panel 选择及供应商管理设置');
|
|
54
|
+
}
|
|
55
|
+
const { dataRoot, registry: pluginRegistry, panelContributions, panelManifestPath, providerConfigPath, allowProviderManagement = false, allowedOperations, connectionStore, allowMcp = false, workbenchAuth } = options;
|
|
56
|
+
return createLocalReadApi({ dataRoot, serveWorkbench: true, pluginRegistry, panelContributions, panelManifestPath, providerConfigPath, allowProviderManagement,
|
|
57
|
+
allowedOperations, connectionStore, allowConnectionManagement: connectionStore !== undefined, allowMcp, workbenchAuth });
|
|
58
|
+
}
|
package/lib/private-storage.js
CHANGED
|
@@ -51,6 +51,18 @@ export function atomicWritePrivate(file, data) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** 原子且排他地发布新记录;已存在的身份绝不被 rename 覆盖。 */
|
|
55
|
+
export function atomicCreatePrivate(file, data) {
|
|
56
|
+
ensurePrivateDir(path.dirname(file));
|
|
57
|
+
const tmp = privateSibling(file);
|
|
58
|
+
try {
|
|
59
|
+
fs.writeFileSync(tmp, data, { mode: 0o600, flag: 'wx' });
|
|
60
|
+
fs.linkSync(tmp, file);
|
|
61
|
+
} finally {
|
|
62
|
+
try { fs.rmSync(tmp, { force: true }); } catch (_) { /* 仅本次未发布的临时链接 */ }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
54
66
|
/** 收紧既有 Iris 树;不跟随符号链接,也不修改文件内容。 */
|
|
55
67
|
export function hardenPrivateTree(root) {
|
|
56
68
|
ensurePrivateDir(root);
|
package/lib/provider-adapter.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
providerErrorRecord,
|
|
14
14
|
resultFromThrown
|
|
15
15
|
} from './provider-contract.js';
|
|
16
|
+
import { checkedPluginDependency } from './plugin-contract.js';
|
|
16
17
|
|
|
17
18
|
export const PROVIDER_ADAPTER_CONTRACT_VERSION = 0;
|
|
18
19
|
|
|
@@ -72,7 +73,7 @@ export function defineProviderAdapter(input) {
|
|
|
72
73
|
if (!isPlainObject(input)) throw new TypeError('Provider Adapter 必须是对象');
|
|
73
74
|
const allowed = new Set([
|
|
74
75
|
'contractVersion', 'id', 'protocol', 'capabilities', 'operations', 'unsupported',
|
|
75
|
-
'prepareInput', 'unsupportedPreparation'
|
|
76
|
+
'prepareInput', 'unsupportedPreparation', 'pluginDependency'
|
|
76
77
|
]);
|
|
77
78
|
for (const key of Object.keys(input)) {
|
|
78
79
|
if (!allowed.has(key)) throw new Error('Provider Adapter 不允许顶层字段:' + key);
|
|
@@ -129,6 +130,8 @@ export function defineProviderAdapter(input) {
|
|
|
129
130
|
}
|
|
130
131
|
const unsupportedPreparation = input.prepareInput ? undefined
|
|
131
132
|
: normalizedReason(input.unsupportedPreparation, '协议未实现临时输入上传');
|
|
133
|
+
const pluginDependency = input.pluginDependency === undefined ? undefined : checkedPluginDependency(input.pluginDependency);
|
|
134
|
+
if (pluginDependency && (pluginDependency.kind !== 'providers' || pluginDependency.contributionId !== protocol)) throw new TypeError('Provider Adapter 的插件依赖与协议不一致');
|
|
132
135
|
|
|
133
136
|
return Object.freeze({
|
|
134
137
|
contractVersion,
|
|
@@ -137,6 +140,7 @@ export function defineProviderAdapter(input) {
|
|
|
137
140
|
capabilities: Object.freeze(capabilities),
|
|
138
141
|
operations: Object.freeze(operations),
|
|
139
142
|
unsupported: Object.freeze(unsupported),
|
|
143
|
+
...(pluginDependency ? { pluginDependency } : {}),
|
|
140
144
|
prepareInput: input.prepareInput,
|
|
141
145
|
unsupportedPreparation
|
|
142
146
|
});
|
package/lib/provider-adapters.js
CHANGED
|
@@ -10,10 +10,12 @@ import * as defaultTransport from './adapters.js';
|
|
|
10
10
|
import { ProviderContractError, providerErrorRecord } from './provider-contract.js';
|
|
11
11
|
import { writePrivateFile } from './private-storage.js';
|
|
12
12
|
import { defineProviderAdapter } from './provider-adapter.js';
|
|
13
|
-
import { selectMediaProtocol, providerApiKey, providerMediaBaseUrl, unsupportedProtocolError, supportsImageEdit, dashscopeTtsInputIssue } from './provider-protocol.js';
|
|
13
|
+
import { selectMediaProtocol, providerApiKey, providerMediaBaseUrl, unsupportedProtocolError, supportsImageEdit, dashscopeTtsInputIssue, BUILTIN_PROVIDER_PROTOCOLS } from './provider-protocol.js';
|
|
14
14
|
import { modelRateLimit, reportModelRateLimit, isFreeQuotaExhausted, recordModelAvailability } from './provider-health.js';
|
|
15
15
|
import { modelUnavailable, modelUnavailableReason, modelAvailabilityBinding } from './model-directory.js';
|
|
16
16
|
import { prepareVisionImage, visionInputLimits } from './vision-image-input.js';
|
|
17
|
+
import { createComfyUiProviderAdapter } from './comfyui-adapter.js';
|
|
18
|
+
import { PROVIDER_RESPONSE_LIMITS } from './provider-response.js';
|
|
17
19
|
|
|
18
20
|
const AMBIGUOUS_4XX = new Set([408, 409, 425, 499]);
|
|
19
21
|
|
|
@@ -26,7 +28,7 @@ function providerIdentity(provider) {
|
|
|
26
28
|
return { id, key, baseUrl };
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
function mappedCategory(error, status) {
|
|
31
|
+
function mappedCategory(error, status, stage) {
|
|
30
32
|
const raw = String(error?.category || '').toLowerCase();
|
|
31
33
|
if (error?.name === 'AbortError' || error?.code === 'ABORT_ERR') return 'aborted';
|
|
32
34
|
if (error?.name === 'TimeoutError' || raw === 'timeout'
|
|
@@ -37,7 +39,10 @@ function mappedCategory(error, status) {
|
|
|
37
39
|
if (status === 429 || raw === 'rate_limit') return 'rate_limit';
|
|
38
40
|
if (raw === 'invalid_parameter' || raw === 'invalid_request') return 'invalid_request';
|
|
39
41
|
if (raw === 'protocol') return 'protocol';
|
|
42
|
+
if (raw === 'local_io') return 'local_io';
|
|
40
43
|
if (raw === 'network' || error instanceof TypeError) return 'network';
|
|
44
|
+
if (!status && ['download', 'persist'].includes(stage)
|
|
45
|
+
&& ['ENOSPC', 'EDQUOT', 'EACCES', 'EPERM', 'ENOENT', 'EEXIST', 'EROFS', 'EIO'].includes(error?.code)) return 'local_io';
|
|
41
46
|
if (raw === 'server' || status >= 500) return 'provider';
|
|
42
47
|
if (!status && /未返回|响应|协议|结构|json/i.test(String(error?.message || ''))) return 'protocol';
|
|
43
48
|
return 'provider';
|
|
@@ -50,7 +55,7 @@ export function mapProviderAdapterError(error, context = {}) {
|
|
|
50
55
|
? statusValue
|
|
51
56
|
: undefined;
|
|
52
57
|
const stage = String(context.stage || error?.stage || 'submit');
|
|
53
|
-
const category = mappedCategory(error, httpStatus);
|
|
58
|
+
const category = mappedCategory(error, httpStatus, stage);
|
|
54
59
|
let acceptance = context.acceptance || error?.acceptance;
|
|
55
60
|
if (stage === 'validate' || stage === 'prepare' || stage === 'upload') {
|
|
56
61
|
acceptance = 'not_accepted';
|
|
@@ -117,8 +122,16 @@ function openAiImageArtifacts(items) {
|
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
function writeInlineArtifact(artifact, targetPath) {
|
|
125
|
+
const maxBytes = String(artifact.mediaType || '').startsWith('image/') ? PROVIDER_RESPONSE_LIMITS.image
|
|
126
|
+
: artifact.mediaType === 'text/plain' ? PROVIDER_RESPONSE_LIMITS.transcript : PROVIDER_RESPONSE_LIMITS.audio;
|
|
127
|
+
if (typeof artifact.data === 'string' && artifact.data.length > Math.ceil(maxBytes / 3) * 4) {
|
|
128
|
+
throw new ProviderContractError('内嵌媒体超过允许的大小', { stage: 'download', category: 'protocol', acceptance: 'accepted' });
|
|
129
|
+
}
|
|
120
130
|
const data = String(artifact?.data || '').trim();
|
|
121
131
|
const body = Buffer.from(data, 'base64');
|
|
132
|
+
if (body.length > maxBytes) {
|
|
133
|
+
throw new ProviderContractError('内嵌媒体超过允许的大小', { stage: 'download', category: 'protocol', acceptance: 'accepted' });
|
|
134
|
+
}
|
|
122
135
|
if (!data || body.length === 0 || body.toString('base64') !== data) {
|
|
123
136
|
throw new ProviderContractError('inline-base64 媒体数据无效', {
|
|
124
137
|
stage: 'download', category: 'protocol', acceptance: 'accepted'
|
|
@@ -262,6 +275,8 @@ function operationsForDashScope(provider, transport) {
|
|
|
262
275
|
return {
|
|
263
276
|
bytes: await transport.downloadTo(artifact.url, targetPath, {
|
|
264
277
|
signal: request.signal,
|
|
278
|
+
maxBytes: { image: PROVIDER_RESPONSE_LIMITS.image, video: PROVIDER_RESPONSE_LIMITS.video,
|
|
279
|
+
tts: PROVIDER_RESPONSE_LIMITS.audio, transcribe: PROVIDER_RESPONSE_LIMITS.transcript }[request.capability] || PROVIDER_RESPONSE_LIMITS.video,
|
|
265
280
|
...(request.timeoutMs ? { timeoutMs: request.timeoutMs } : {})
|
|
266
281
|
})
|
|
267
282
|
};
|
|
@@ -278,8 +293,8 @@ export function createDashScopeProviderAdapter(provider, { transport = defaultTr
|
|
|
278
293
|
protocol: 'dashscope',
|
|
279
294
|
capabilities: ['image', 'video', 'tts', 'transcribe'],
|
|
280
295
|
operations: operationsForDashScope(provider, transport),
|
|
281
|
-
async prepareInput({ model, filePath, signal, timeoutMs } = {}) {
|
|
282
|
-
return { url: await transport.uploadTempFile({ key, baseUrl, model, filePath, signal, timeoutMs }) };
|
|
296
|
+
async prepareInput({ model, filePath, bytes, filename, signal, timeoutMs } = {}) {
|
|
297
|
+
return { url: await transport.uploadTempFile({ key, baseUrl, model, filePath, ...(bytes !== undefined ? { bytes, filename } : {}), signal, timeoutMs }) };
|
|
283
298
|
},
|
|
284
299
|
unsupported: {
|
|
285
300
|
cancel: '当前 DashScope 媒体协议未提供经过验证的远端取消实现'
|
|
@@ -345,6 +360,7 @@ function operationsForOpenAiImages(provider, transport, protocol = 'openai-image
|
|
|
345
360
|
return {
|
|
346
361
|
bytes: await transport.downloadTo(artifact.url, targetPath, {
|
|
347
362
|
signal: request.signal,
|
|
363
|
+
maxBytes: PROVIDER_RESPONSE_LIMITS.image,
|
|
348
364
|
...(request.timeoutMs ? { timeoutMs: request.timeoutMs } : {})
|
|
349
365
|
})
|
|
350
366
|
};
|
|
@@ -418,22 +434,44 @@ export function createGeminiWebMusicProviderAdapter(provider, { transport = defa
|
|
|
418
434
|
});
|
|
419
435
|
}
|
|
420
436
|
|
|
421
|
-
const PROTOCOL_FACTORIES =
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
437
|
+
const PROTOCOL_FACTORIES = Object.freeze({
|
|
438
|
+
'gemini-web-music': createGeminiWebMusicProviderAdapter,
|
|
439
|
+
dashscope: createDashScopeProviderAdapter,
|
|
440
|
+
'openai-images': createOpenAiImagesProviderAdapter,
|
|
441
|
+
'openai-chat-images': createOpenAiChatImagesProviderAdapter,
|
|
442
|
+
'openai-responses-images': createOpenAiResponsesImagesProviderAdapter,
|
|
443
|
+
comfyui: createComfyUiProviderAdapter
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
export function builtinProviderExtension() {
|
|
447
|
+
return { contractVersion: 0, id: 'iris.builtin-media', version: '0.1.0',
|
|
448
|
+
protocols: BUILTIN_PROVIDER_PROTOCOLS.map(descriptor => ({ ...descriptor, createAdapter: PROTOCOL_FACTORIES[descriptor.id] })) };
|
|
449
|
+
}
|
|
428
450
|
|
|
429
451
|
export function createConfiguredProviderAdapter(provider, options = {}) {
|
|
430
452
|
const selection = options.protocol
|
|
431
453
|
? selectMediaProtocol({ ...provider, mediaProtocol: options.protocol, protocolInferred: false })
|
|
432
454
|
: selectMediaProtocol(provider);
|
|
433
|
-
const factory =
|
|
434
|
-
if (!factory) throw unsupportedProtocolError(selection.mediaProtocol);
|
|
435
|
-
const adapter = factory(provider, options);
|
|
455
|
+
const factory = Object.hasOwn(PROTOCOL_FACTORIES, selection.mediaProtocol) ? PROTOCOL_FACTORIES[selection.mediaProtocol] : null;
|
|
456
|
+
if (!options.registry && !factory) throw unsupportedProtocolError(selection.mediaProtocol);
|
|
457
|
+
const adapter = options.registry ? options.registry.createAdapter(selection.mediaProtocol, provider, options) : factory(provider, options);
|
|
436
458
|
const operations = { ...adapter.operations };
|
|
459
|
+
const key = providerApiKey(provider);
|
|
460
|
+
const scrub = value => typeof value === 'string' && key ? value.split(key).join('[REDACTED]') : value;
|
|
461
|
+
operations.mapError = (error, context) => {
|
|
462
|
+
let mapped;
|
|
463
|
+
try { mapped = adapter.operations.mapError(error, context); } catch (failure) { mapped = failure; }
|
|
464
|
+
mapped ||= error;
|
|
465
|
+
const defaults = {};
|
|
466
|
+
for (const field of ['stage', 'category', 'acceptance', 'retryable']) {
|
|
467
|
+
if (mapped?.[field] === undefined) defaults[field] = context?.[field];
|
|
468
|
+
}
|
|
469
|
+
return providerErrorRecord(mapped, {
|
|
470
|
+
...defaults,
|
|
471
|
+
safeMessage: scrub(String(mapped?.safeMessage || mapped?.message || mapped)),
|
|
472
|
+
providerCode: scrub(mapped?.providerCode ?? mapped?.code)
|
|
473
|
+
});
|
|
474
|
+
};
|
|
437
475
|
for (const name of ['submit', 'poll']) {
|
|
438
476
|
if (!operations[name]) continue;
|
|
439
477
|
const run = operations[name];
|
|
@@ -441,15 +479,15 @@ export function createConfiguredProviderAdapter(provider, options = {}) {
|
|
|
441
479
|
const modelId = request.model || options.modelId;
|
|
442
480
|
const report = error => reportModelRateLimit(provider, modelId, {
|
|
443
481
|
httpStatus: error.httpStatus ?? error.status, category: error.category,
|
|
444
|
-
code: error.code, providerCode: error.providerCode, message: error.safeMessage || error.message,
|
|
482
|
+
code: scrub(error.code), providerCode: scrub(error.providerCode), message: scrub(error.safeMessage || error.message),
|
|
445
483
|
retryAfterMs: error.retryAfterMs,
|
|
446
484
|
source: options.allowRateLimited ? 'probe' : 'task'
|
|
447
485
|
}, options.onRateLimit, request.capability);
|
|
448
486
|
const availability = result => {
|
|
449
487
|
if (name !== 'submit') return;
|
|
450
488
|
const event = { ok: result.ok === true, category: result.category,
|
|
451
|
-
httpStatus: result.httpStatus ?? result.status, providerCode: result.providerCode || result.code,
|
|
452
|
-
note: result.safeMessage || result.message,
|
|
489
|
+
httpStatus: result.httpStatus ?? result.status, providerCode: scrub(result.providerCode || result.code),
|
|
490
|
+
note: scrub(result.safeMessage || result.message),
|
|
453
491
|
stage: 'submit', source: options.allowRateLimited ? 'probe' : 'task',
|
|
454
492
|
binding: modelAvailabilityBinding(provider, modelId, capability) };
|
|
455
493
|
if (!recordModelAvailability(provider, modelId, request.capability, event)) return;
|