@easyai-canvas/canvas-plugin-sdk 0.1.0
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/LICENSE +5 -0
- package/README.md +22 -0
- package/bin/easyai-plugin.mjs +267 -0
- package/dist/customer-main.d.ts +1 -0
- package/dist/customer-main.js +256 -0
- package/dist/protocol/index.d.ts +295 -0
- package/dist/protocol/index.js +454 -0
- package/dist/runtime/auth.d.ts +1 -0
- package/dist/runtime/auth.js +68 -0
- package/dist/runtime/engine.d.ts +49 -0
- package/dist/runtime/engine.js +296 -0
- package/dist/runtime/host-files.d.ts +16 -0
- package/dist/runtime/host-files.js +208 -0
- package/dist/runtime/instance-lock.d.ts +1 -0
- package/dist/runtime/instance-lock.js +52 -0
- package/dist/runtime/package-runtime.d.ts +44 -0
- package/dist/runtime/package-runtime.js +361 -0
- package/dist/runtime/packaged-plugins.d.ts +14 -0
- package/dist/runtime/packaged-plugins.js +58 -0
- package/dist/runtime/plugin-config.d.ts +4 -0
- package/dist/runtime/plugin-config.js +31 -0
- package/dist/runtime/store.d.ts +48 -0
- package/dist/runtime/store.js +151 -0
- package/dist/runtime/worker.d.ts +1 -0
- package/dist/runtime/worker.js +38 -0
- package/dist/sdk/frontend.d.ts +62 -0
- package/dist/sdk/frontend.js +40 -0
- package/dist/sdk/index.d.ts +38 -0
- package/dist/sdk/index.js +10 -0
- package/dist/studio/index.html +649 -0
- package/dist/studio/remote-connector.d.ts +60 -0
- package/dist/studio/remote-connector.js +295 -0
- package/package.json +45 -0
- package/tools/build-plugin-package.mjs +99 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { ContractError } from '../protocol/index.js';
|
|
2
|
+
import Database from 'better-sqlite3';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { mkdirSync } from 'node:fs';
|
|
5
|
+
import { dirname } from 'node:path';
|
|
6
|
+
function canonical(value) {
|
|
7
|
+
if (Array.isArray(value))
|
|
8
|
+
return `[${value.map(canonical).join(',')}]`;
|
|
9
|
+
if (value && typeof value === 'object')
|
|
10
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(',')}}`;
|
|
11
|
+
return JSON.stringify(value);
|
|
12
|
+
}
|
|
13
|
+
export class RuntimeStore {
|
|
14
|
+
db;
|
|
15
|
+
constructor(path) {
|
|
16
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
17
|
+
this.db = new Database(path);
|
|
18
|
+
this.db.pragma('journal_mode = WAL');
|
|
19
|
+
this.db.pragma('synchronous = FULL');
|
|
20
|
+
this.db.exec(`
|
|
21
|
+
CREATE TABLE IF NOT EXISTS meta (id INTEGER PRIMARY KEY, revision INTEGER NOT NULL);
|
|
22
|
+
INSERT OR IGNORE INTO meta VALUES (1, 0);
|
|
23
|
+
CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
24
|
+
CREATE TABLE IF NOT EXISTS deployments (id INTEGER PRIMARY KEY, digest TEXT NOT NULL);
|
|
25
|
+
CREATE TABLE IF NOT EXISTS plugins (id TEXT PRIMARY KEY, enabled INTEGER NOT NULL, config TEXT NOT NULL, config_version INTEGER NOT NULL DEFAULT 0);
|
|
26
|
+
CREATE TABLE IF NOT EXISTS executions (id TEXT PRIMARY KEY, plugin TEXT NOT NULL, hash TEXT NOT NULL, request TEXT NOT NULL, record TEXT NOT NULL);
|
|
27
|
+
CREATE INDEX IF NOT EXISTS executions_plugin ON executions(plugin);
|
|
28
|
+
CREATE TABLE IF NOT EXISTS execution_snapshots (id TEXT PRIMARY KEY, snapshot TEXT NOT NULL);
|
|
29
|
+
`);
|
|
30
|
+
const pluginColumns = this.db.prepare('PRAGMA table_info(plugins)').all();
|
|
31
|
+
if (!pluginColumns.some(column => column.name === 'config_version')) {
|
|
32
|
+
this.db.exec('ALTER TABLE plugins ADD COLUMN config_version INTEGER NOT NULL DEFAULT 0');
|
|
33
|
+
}
|
|
34
|
+
this.db.prepare('INSERT OR IGNORE INTO settings(key, value) VALUES (?, ?)').run('instance_id', randomUUID());
|
|
35
|
+
}
|
|
36
|
+
syncDeployment(digest) {
|
|
37
|
+
this.db.transaction(() => {
|
|
38
|
+
const old = this.db.prepare('SELECT digest FROM deployments WHERE id=1').get();
|
|
39
|
+
if (old?.digest === digest)
|
|
40
|
+
return;
|
|
41
|
+
this.db.prepare('INSERT INTO deployments VALUES (1, ?) ON CONFLICT(id) DO UPDATE SET digest=excluded.digest').run(digest);
|
|
42
|
+
this.db.prepare('UPDATE meta SET revision=revision+1 WHERE id=1').run();
|
|
43
|
+
})();
|
|
44
|
+
}
|
|
45
|
+
get revision() { return this.db.prepare('SELECT revision FROM meta WHERE id = 1').get().revision; }
|
|
46
|
+
get instanceId() { return this.db.prepare("SELECT value FROM settings WHERE key='instance_id'").get().value; }
|
|
47
|
+
getPlugin(id) {
|
|
48
|
+
const row = this.db.prepare('SELECT enabled, config, config_version FROM plugins WHERE id = ?').get(id);
|
|
49
|
+
return row ? { enabled: Boolean(row.enabled), config: JSON.parse(row.config), configVersion: row.config_version } : { enabled: false, config: {}, configVersion: 0 };
|
|
50
|
+
}
|
|
51
|
+
setPlugin(id, patch) {
|
|
52
|
+
this.db.transaction(() => {
|
|
53
|
+
const prev = this.getPlugin(id);
|
|
54
|
+
if (patch.expectedVersion !== undefined && patch.expectedVersion !== prev.configVersion) {
|
|
55
|
+
throw new ContractError('插件配置已被其他页面修改,请重新加载后再保存', 409, 'PLUGIN_CONFIG_CONFLICT');
|
|
56
|
+
}
|
|
57
|
+
this.db.prepare(`INSERT INTO plugins(id, enabled, config, config_version) VALUES (?, ?, ?, 1)
|
|
58
|
+
ON CONFLICT(id) DO UPDATE SET enabled=excluded.enabled, config=excluded.config, config_version=plugins.config_version+1`)
|
|
59
|
+
.run(id, Number(patch.enabled ?? prev.enabled), JSON.stringify(patch.config ?? prev.config));
|
|
60
|
+
this.db.prepare('UPDATE meta SET revision=revision+1 WHERE id=1').run();
|
|
61
|
+
})();
|
|
62
|
+
return this.getPlugin(id);
|
|
63
|
+
}
|
|
64
|
+
accept(request, snapshot = { config: {}, release: 'legacy', safeRetry: false, maxAttempts: 1 }) {
|
|
65
|
+
return this.db.transaction(() => {
|
|
66
|
+
const hash = createHash('sha256').update(canonical(request)).digest('hex');
|
|
67
|
+
const prev = this.db.prepare('SELECT hash, record FROM executions WHERE id=?').get(request.executionId);
|
|
68
|
+
if (prev) {
|
|
69
|
+
if (prev.hash !== hash)
|
|
70
|
+
throw new ContractError('执行标识与请求内容冲突', 409, 'EXECUTION_CONFLICT');
|
|
71
|
+
return { created: false, record: JSON.parse(prev.record) };
|
|
72
|
+
}
|
|
73
|
+
const now = new Date().toISOString();
|
|
74
|
+
const record = { executionId: request.executionId, pluginId: request.pluginId, nodeType: request.nodeType, status: 'queued', progress: 0, phase: '排队', attempt: 0, maxAttempts: snapshot.maxAttempts, cancelRequested: false, createdAt: now, updatedAt: now };
|
|
75
|
+
this.db.prepare('INSERT INTO executions VALUES (?, ?, ?, ?, ?)').run(request.executionId, request.pluginId, hash, JSON.stringify(request), JSON.stringify(record));
|
|
76
|
+
this.db.prepare('INSERT INTO execution_snapshots VALUES (?, ?)').run(request.executionId, JSON.stringify(snapshot));
|
|
77
|
+
return { created: true, record };
|
|
78
|
+
})();
|
|
79
|
+
}
|
|
80
|
+
getSnapshot(id) {
|
|
81
|
+
const row = this.db.prepare('SELECT snapshot FROM execution_snapshots WHERE id=?').get(id);
|
|
82
|
+
return row ? JSON.parse(row.snapshot) : { config: {}, release: 'legacy', safeRetry: false, maxAttempts: 1 };
|
|
83
|
+
}
|
|
84
|
+
getExecution(id) {
|
|
85
|
+
const row = this.db.prepare('SELECT record FROM executions WHERE id=?').get(id);
|
|
86
|
+
return row ? JSON.parse(row.record) : undefined;
|
|
87
|
+
}
|
|
88
|
+
getRequest(id) {
|
|
89
|
+
const row = this.db.prepare('SELECT request FROM executions WHERE id=?').get(id);
|
|
90
|
+
if (!row)
|
|
91
|
+
throw new Error('任务不存在');
|
|
92
|
+
return JSON.parse(row.request);
|
|
93
|
+
}
|
|
94
|
+
update(id, patch) {
|
|
95
|
+
const previous = this.getExecution(id);
|
|
96
|
+
if (!previous)
|
|
97
|
+
throw new Error('任务不存在');
|
|
98
|
+
if (['succeeded', 'failed', 'cancelled', 'interrupted'].includes(previous.status))
|
|
99
|
+
return previous;
|
|
100
|
+
const record = { ...previous, ...patch, executionId: id, updatedAt: new Date().toISOString() };
|
|
101
|
+
this.db.prepare('UPDATE executions SET record=? WHERE id=?').run(JSON.stringify(record), id);
|
|
102
|
+
return record;
|
|
103
|
+
}
|
|
104
|
+
claim(id) {
|
|
105
|
+
return this.db.transaction(() => {
|
|
106
|
+
const previous = this.getExecution(id);
|
|
107
|
+
if (!previous || previous.status !== 'queued' || previous.cancelRequested)
|
|
108
|
+
return undefined;
|
|
109
|
+
const record = { ...previous, status: 'running', phase: '启动任务', attempt: previous.attempt + 1, updatedAt: new Date().toISOString() };
|
|
110
|
+
this.db.prepare('UPDATE executions SET record=? WHERE id=?').run(JSON.stringify(record), id);
|
|
111
|
+
return record;
|
|
112
|
+
})();
|
|
113
|
+
}
|
|
114
|
+
requeue(id, error) {
|
|
115
|
+
const previous = this.getExecution(id);
|
|
116
|
+
if (!previous || previous.cancelRequested || ['succeeded', 'cancelled'].includes(previous.status))
|
|
117
|
+
return previous;
|
|
118
|
+
const record = { ...previous, status: 'queued', progress: 0, phase: '等待重试', ...(error ? { error } : {}), updatedAt: new Date().toISOString() };
|
|
119
|
+
this.db.prepare('UPDATE executions SET record=? WHERE id=?').run(JSON.stringify(record), id);
|
|
120
|
+
return record;
|
|
121
|
+
}
|
|
122
|
+
queued(pluginId) {
|
|
123
|
+
const rows = this.db.prepare(pluginId ? 'SELECT id, request, record FROM executions WHERE plugin=? ORDER BY rowid' : 'SELECT id, request, record FROM executions ORDER BY rowid').all(...(pluginId ? [pluginId] : []));
|
|
124
|
+
return rows.flatMap(row => {
|
|
125
|
+
const record = JSON.parse(row.record);
|
|
126
|
+
return record.status === 'queued' && !record.cancelRequested ? [{ id: row.id, request: JSON.parse(row.request) }] : [];
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
list(limit = 100) {
|
|
130
|
+
return this.db.prepare('SELECT record FROM executions ORDER BY rowid DESC LIMIT ?').all(Math.min(limit, 1000)).map(r => JSON.parse(r.record));
|
|
131
|
+
}
|
|
132
|
+
recover() {
|
|
133
|
+
const recovered = [];
|
|
134
|
+
const rows = this.db.prepare('SELECT record FROM executions').all();
|
|
135
|
+
for (const row of rows) {
|
|
136
|
+
const record = JSON.parse(row.record);
|
|
137
|
+
if (!['running', 'queued'].includes(record.status))
|
|
138
|
+
continue;
|
|
139
|
+
const snapshot = this.getSnapshot(record.executionId);
|
|
140
|
+
if (snapshot.safeRetry && record.attempt < snapshot.maxAttempts && !record.cancelRequested) {
|
|
141
|
+
this.requeue(record.executionId, { code: 'SERVICE_RESTARTED', message: '服务重启,任务已重新排队' });
|
|
142
|
+
recovered.push(record.executionId);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
this.update(record.executionId, { status: 'interrupted', phase: '执行中断', error: { code: 'SERVICE_RESTARTED', message: '服务重启,任务未自动重跑,请核实结果后重试' } });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return recovered;
|
|
149
|
+
}
|
|
150
|
+
close() { this.db.close(); }
|
|
151
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { validateParameters } from '../protocol/index.js';
|
|
2
|
+
import { createHostFileService } from './host-files.js';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
const abort = new AbortController();
|
|
7
|
+
process.on('message', async (message) => {
|
|
8
|
+
if (message.type === 'cancel') {
|
|
9
|
+
abort.abort();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (message.type !== 'run')
|
|
13
|
+
return;
|
|
14
|
+
try {
|
|
15
|
+
const request = message.request;
|
|
16
|
+
if (message.packageEntry &&
|
|
17
|
+
createHash('sha256').update(await readFile(message.packageEntry)).digest('hex') !== message.packageEntryHash)
|
|
18
|
+
throw new Error('插件运行文件摘要与任务固定版本不一致');
|
|
19
|
+
const plugin = message.packageEntry
|
|
20
|
+
? (await import(pathToFileURL(message.packageEntry).href)).default
|
|
21
|
+
: (await import('../plugins/catalog.js')).getPlugins().find(p => p.manifest.id === request.pluginId);
|
|
22
|
+
if (!plugin || plugin.manifest.id !== request.pluginId)
|
|
23
|
+
throw new Error('插件包与任务标识不一致');
|
|
24
|
+
const definition = plugin.manifest.contributes.nodes.find(n => n.type === request.nodeType);
|
|
25
|
+
const normalized = validateParameters(definition.parameters ?? [], plugin.normalizeParameters?.(request.nodeType, request.parameters) ?? request.parameters);
|
|
26
|
+
const result = await plugin.handlers[request.nodeType]({ ...request, parameters: normalized }, {
|
|
27
|
+
signal: abort.signal, config: message.config,
|
|
28
|
+
files: createHostFileService(message.hostFiles, request.executionId),
|
|
29
|
+
progress: (value, phase) => process.send?.({ type: 'progress', value, phase }),
|
|
30
|
+
});
|
|
31
|
+
process.send?.({ type: 'result', result });
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const message = error instanceof Error ? error.message : '插件执行失败';
|
|
35
|
+
const retryable = !abort.signal.aborted && /(ECONNRESET|ETIMEDOUT|fetch failed|网络|暂时|上传|存储服务|socket hang up)/i.test(message);
|
|
36
|
+
process.send?.({ type: 'error', message, retryable });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type Ref } from 'vue';
|
|
2
|
+
import type { NodeDefinition, DataPacket, PluginNodeTheme, AssetSource, PluginAction, PluginRuntimeContext, SelectedResource } from '../protocol/index.js';
|
|
3
|
+
export declare const NODE_CONTEXT = "easyai.plugin.node.v1";
|
|
4
|
+
export declare const ASSET_SELECTOR_CONTEXT = "easyai.plugin.asset-selector.v1";
|
|
5
|
+
export declare const ACTION_CONTEXT = "easyai.plugin.action.v1";
|
|
6
|
+
export interface PluginNodeContext {
|
|
7
|
+
definition: NodeDefinition;
|
|
8
|
+
environment: 'canvas';
|
|
9
|
+
runtime: Readonly<PluginRuntimeContext>;
|
|
10
|
+
parameters: Readonly<Ref<Record<string, unknown>>>;
|
|
11
|
+
inputs: Readonly<Ref<Record<string, DataPacket[]>>>;
|
|
12
|
+
output: Readonly<Ref<DataPacket | undefined>>;
|
|
13
|
+
outputs: Readonly<Ref<Record<string, DataPacket | undefined>>>;
|
|
14
|
+
document: Readonly<Ref<unknown>>;
|
|
15
|
+
resultVersion: Readonly<Ref<string | undefined>>;
|
|
16
|
+
progress: Readonly<Ref<number | undefined>>;
|
|
17
|
+
isExecuting: Readonly<Ref<boolean>>;
|
|
18
|
+
error: Readonly<Ref<string | undefined>>;
|
|
19
|
+
theme: Readonly<Ref<PluginNodeTheme>>;
|
|
20
|
+
updateParameters(patch: Record<string, unknown>): void;
|
|
21
|
+
updateDocument(value: unknown): void;
|
|
22
|
+
disconnectInput(sourceNodeId: string, sourcePortId?: string): void;
|
|
23
|
+
uploadMedia(files: File[]): Promise<DataPacket[]>;
|
|
24
|
+
setSourceOutputs(outputs: Record<string, DataPacket>): void;
|
|
25
|
+
execute(confirm?: boolean): Promise<void>;
|
|
26
|
+
cancelExecution(): Promise<void>;
|
|
27
|
+
addOutputToCanvas(portId: string, options?: {
|
|
28
|
+
title?: string;
|
|
29
|
+
connect?: boolean;
|
|
30
|
+
}): Promise<{
|
|
31
|
+
created: boolean;
|
|
32
|
+
nodeId?: string;
|
|
33
|
+
}>;
|
|
34
|
+
openWorkspace?(): void;
|
|
35
|
+
closeWorkspace?(): void;
|
|
36
|
+
query(name: string, input: Record<string, unknown>): Promise<unknown>;
|
|
37
|
+
}
|
|
38
|
+
export declare function usePluginNode(): PluginNodeContext;
|
|
39
|
+
export interface PluginAssetSelectorContext {
|
|
40
|
+
source: AssetSource;
|
|
41
|
+
runtime: Readonly<PluginRuntimeContext>;
|
|
42
|
+
constraints: Readonly<{
|
|
43
|
+
mediaTypes: string[];
|
|
44
|
+
maxCount: number;
|
|
45
|
+
}>;
|
|
46
|
+
query(name: string, input: Record<string, unknown>): Promise<unknown>;
|
|
47
|
+
submit(resources: SelectedResource[]): void | Promise<void>;
|
|
48
|
+
cancel(): void;
|
|
49
|
+
}
|
|
50
|
+
export interface PluginActionContext {
|
|
51
|
+
action: PluginAction;
|
|
52
|
+
runtime: Readonly<PluginRuntimeContext>;
|
|
53
|
+
constraints: Readonly<{
|
|
54
|
+
mediaTypes: string[];
|
|
55
|
+
maxCount: number;
|
|
56
|
+
}>;
|
|
57
|
+
query(name: string, input: Record<string, unknown>): Promise<unknown>;
|
|
58
|
+
submit(resources: SelectedResource[]): void | Promise<void>;
|
|
59
|
+
cancel(): void;
|
|
60
|
+
}
|
|
61
|
+
export declare function usePluginAction(): PluginActionContext;
|
|
62
|
+
export declare function usePluginAssetSelector(): PluginAssetSelectorContext;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { inject } from 'vue';
|
|
2
|
+
export const NODE_CONTEXT = 'easyai.plugin.node.v1';
|
|
3
|
+
export const ASSET_SELECTOR_CONTEXT = 'easyai.plugin.asset-selector.v1';
|
|
4
|
+
export const ACTION_CONTEXT = 'easyai.plugin.action.v1';
|
|
5
|
+
export function usePluginNode() {
|
|
6
|
+
const ctx = inject(NODE_CONTEXT);
|
|
7
|
+
if (!ctx)
|
|
8
|
+
throw new Error('请在画布内加载该组件');
|
|
9
|
+
return ctx;
|
|
10
|
+
}
|
|
11
|
+
export function usePluginAction() {
|
|
12
|
+
const ctx = inject(ACTION_CONTEXT);
|
|
13
|
+
if (!ctx)
|
|
14
|
+
throw new Error('请在画布操作容器内加载该组件');
|
|
15
|
+
return ctx;
|
|
16
|
+
}
|
|
17
|
+
export function usePluginAssetSelector() {
|
|
18
|
+
const ctx = inject(ASSET_SELECTOR_CONTEXT);
|
|
19
|
+
if (ctx)
|
|
20
|
+
return ctx;
|
|
21
|
+
const action = inject(ACTION_CONTEXT);
|
|
22
|
+
if (!action)
|
|
23
|
+
throw new Error('请在画布素材弹框内加载该组件');
|
|
24
|
+
return {
|
|
25
|
+
source: {
|
|
26
|
+
id: action.action.id,
|
|
27
|
+
name: action.action.name,
|
|
28
|
+
description: action.action.description,
|
|
29
|
+
mediaTypes: action.action.mediaTypes,
|
|
30
|
+
...(action.action.ui.component ? { ui: { selector: action.action.ui.component } } : {}),
|
|
31
|
+
...(action.action.ui.url ? { url: action.action.ui.url, origin: action.action.ui.origin } : {}),
|
|
32
|
+
menu: action.action.menu,
|
|
33
|
+
},
|
|
34
|
+
runtime: action.runtime,
|
|
35
|
+
constraints: action.constraints,
|
|
36
|
+
query: action.query,
|
|
37
|
+
submit: action.submit,
|
|
38
|
+
cancel: action.cancel,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type PluginManifest, type NodeDefinition, type ExecutionRequest, type ExecutionResult, type PluginRuntimeContext } from '../protocol/index.js';
|
|
2
|
+
export type HostFileRecord = {
|
|
3
|
+
id: string;
|
|
4
|
+
url: string;
|
|
5
|
+
name: string;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
size: number;
|
|
8
|
+
sha256: string;
|
|
9
|
+
path?: string;
|
|
10
|
+
};
|
|
11
|
+
export type HostFileService = {
|
|
12
|
+
put(input: {
|
|
13
|
+
key: string;
|
|
14
|
+
path: string;
|
|
15
|
+
mimeType: string;
|
|
16
|
+
}): Promise<HostFileRecord>;
|
|
17
|
+
readJson<T = unknown>(key: string): Promise<T | undefined>;
|
|
18
|
+
info(key: string): Promise<Pick<HostFileRecord, 'id' | 'url' | 'size' | 'sha256'> | undefined>;
|
|
19
|
+
exists(key: string): Promise<boolean>;
|
|
20
|
+
};
|
|
21
|
+
export type HandlerContext = {
|
|
22
|
+
signal: AbortSignal;
|
|
23
|
+
config: Record<string, unknown>;
|
|
24
|
+
files: HostFileService;
|
|
25
|
+
progress(value: number, phase?: string): void;
|
|
26
|
+
};
|
|
27
|
+
export type NodeHandler = (request: ExecutionRequest, context: HandlerContext) => Promise<ExecutionResult> | ExecutionResult;
|
|
28
|
+
export type Plugin = {
|
|
29
|
+
manifest: PluginManifest;
|
|
30
|
+
handlers: Record<string, NodeHandler>;
|
|
31
|
+
resolveManifest?: (config: Record<string, unknown>, manifest: PluginManifest) => PluginManifest;
|
|
32
|
+
queries?: Record<string, (input: Record<string, unknown>, config: Record<string, unknown>, context: PluginRuntimeContext, signal?: AbortSignal) => Promise<unknown>>;
|
|
33
|
+
/** Optional real dependency check. Absence means hosts may report format validation only. */
|
|
34
|
+
checkConfig?: (config: Record<string, unknown>) => Promise<unknown>;
|
|
35
|
+
normalizeParameters?: (nodeType: string, parameters: Record<string, unknown>) => Record<string, unknown>;
|
|
36
|
+
};
|
|
37
|
+
export declare function definePlugin(plugin: Plugin): Plugin;
|
|
38
|
+
export declare function defineNode(node: NodeDefinition): NodeDefinition;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { validateManifest } from '../protocol/index.js';
|
|
2
|
+
export function definePlugin(plugin) {
|
|
3
|
+
validateManifest(plugin.manifest);
|
|
4
|
+
for (const node of plugin.manifest.contributes.nodes ?? []) {
|
|
5
|
+
if (['remote', 'confirmed'].includes(node.execution) && !plugin.handlers[node.type])
|
|
6
|
+
throw new Error(`缺少 ${node.type} 的业务处理器`);
|
|
7
|
+
}
|
|
8
|
+
return plugin;
|
|
9
|
+
}
|
|
10
|
+
export function defineNode(node) { return node; }
|