@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.
@@ -0,0 +1,361 @@
1
+ /** Shared package transport/runtime. The platform copy is generated, never hand edited. */
2
+ import JSZip from 'jszip';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { mkdir, readFile, writeFile, rename, rm, lstat, } from 'node:fs/promises';
5
+ import { join, dirname } from 'node:path';
6
+ import { fork } from 'node:child_process';
7
+ import { ContractError, validateManifest, validateExecution, object, } from '../protocol/index.js';
8
+ export const PLUGIN_PACKAGE_MAX_BYTES = 16 * 1024 * 1024;
9
+ const MAX_EXPANDED_BYTES = 64 * 1024 * 1024;
10
+ const MAX_FILES = 128;
11
+ export function packageSha256(bytes) {
12
+ return createHash('sha256').update(bytes).digest('hex');
13
+ }
14
+ function fail(message) {
15
+ throw new ContractError(message, 400, 'INVALID_PACKAGE');
16
+ }
17
+ function record(value) {
18
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
19
+ }
20
+ function relativePath(value) {
21
+ if (typeof value !== 'string' ||
22
+ value.length > 240 ||
23
+ !/^[a-zA-Z0-9_.\/-]+$/.test(value) ||
24
+ value.split('/').some((p) => !p || p === '.' || p === '..') ||
25
+ /(^|\/)(node_modules|\.env[^/]*|package\.json|\.git)(\/|$)/i.test(value))
26
+ fail('插件文件路径无效或包含不允许的文件');
27
+ return value;
28
+ }
29
+ // Reject oversized/ambiguous central directories before JSZip allocates entries.
30
+ function checkZipDirectory(bytes) {
31
+ const end = bytes.lastIndexOf(Buffer.from([0x50, 0x4b, 0x05, 0x06]));
32
+ if (end < 0 ||
33
+ end + 22 > bytes.length ||
34
+ end + 22 + bytes.readUInt16LE(end + 20) !== bytes.length)
35
+ fail('插件压缩包目录损坏');
36
+ const count = bytes.readUInt16LE(end + 10);
37
+ if (!count ||
38
+ count > MAX_FILES ||
39
+ bytes.readUInt16LE(end + 4) ||
40
+ bytes.readUInt16LE(end + 6))
41
+ fail('插件包文件数量超限或压缩格式不支持');
42
+ let offset = bytes.readUInt32LE(end + 16), expanded = 0;
43
+ const names = new Set();
44
+ for (let i = 0; i < count; i++) {
45
+ if (offset + 46 > end || bytes.readUInt32LE(offset) !== 0x02014b50)
46
+ fail('插件压缩包目录损坏');
47
+ const size = bytes.readUInt32LE(offset + 24), nameLength = bytes.readUInt16LE(offset + 28);
48
+ const next = offset +
49
+ 46 +
50
+ nameLength +
51
+ bytes.readUInt16LE(offset + 30) +
52
+ bytes.readUInt16LE(offset + 32);
53
+ if (next > end || bytes.readUInt16LE(offset + 8) & 1)
54
+ fail('插件压缩包目录损坏或已加密');
55
+ const name = bytes
56
+ .subarray(offset + 46, offset + 46 + nameLength)
57
+ .toString('utf8');
58
+ if (names.has(name))
59
+ fail('插件包包含重复文件路径');
60
+ names.add(name);
61
+ expanded += size;
62
+ if (size > PLUGIN_PACKAGE_MAX_BYTES || expanded > MAX_EXPANDED_BYTES)
63
+ fail('插件解压大小超过限制');
64
+ offset = next;
65
+ }
66
+ if (offset !== end)
67
+ fail('插件压缩格式不支持');
68
+ }
69
+ export async function inspectPluginPackage(bytes) {
70
+ if (!bytes.length || bytes.length > PLUGIN_PACKAGE_MAX_BYTES)
71
+ fail('插件包大小超过限制');
72
+ checkZipDirectory(bytes);
73
+ const zip = await JSZip.loadAsync(bytes).catch(() => fail('插件压缩包损坏'));
74
+ const entries = Object.values(zip.files);
75
+ if (entries.length > MAX_FILES)
76
+ fail('插件包文件数量超过限制');
77
+ const files = new Map();
78
+ let total = 0;
79
+ for (const entry of entries) {
80
+ const original = entry.unsafeOriginalName;
81
+ relativePath((original ?? entry.name).replace(/\/$/, ''));
82
+ if (original && original !== entry.name)
83
+ fail('插件文件路径越界');
84
+ const mode = typeof entry.unixPermissions === 'string'
85
+ ? parseInt(entry.unixPermissions, 8)
86
+ : (entry.unixPermissions ?? 0);
87
+ if ((mode & 0o170000) === 0o120000)
88
+ fail('插件包不允许符号链接');
89
+ if (entry.dir)
90
+ continue;
91
+ const chunks = [];
92
+ let size = 0;
93
+ await new Promise((resolve, reject) => {
94
+ const stream = entry.nodeStream();
95
+ stream.on('data', (chunk) => {
96
+ const buffer = Buffer.from(chunk);
97
+ size += buffer.length;
98
+ total += buffer.length;
99
+ if (size > PLUGIN_PACKAGE_MAX_BYTES || total > MAX_EXPANDED_BYTES) {
100
+ stream.destroy();
101
+ reject(new ContractError('插件解压大小超过限制', 400, 'INVALID_PACKAGE'));
102
+ return;
103
+ }
104
+ chunks.push(buffer);
105
+ });
106
+ stream.once('error', reject);
107
+ stream.once('end', resolve);
108
+ });
109
+ files.set(entry.name, Buffer.concat(chunks));
110
+ }
111
+ let d;
112
+ try {
113
+ d = JSON.parse(files.get('plugin.json')?.toString('utf8') ?? '');
114
+ }
115
+ catch {
116
+ fail('插件包缺少有效的 plugin.json');
117
+ }
118
+ if (!record(d) ||
119
+ !['easyai.plugin-package/v1', 'easyai.plugin-package/v2'].includes(d.format) ||
120
+ !record(d.runtime) ||
121
+ !record(d.ui) ||
122
+ !record(d.files))
123
+ fail('插件包格式不支持');
124
+ const descriptor = d;
125
+ descriptor.manifest = validateManifest(descriptor.manifest);
126
+ if (d.runtime.nodeMajor !== 22 ||
127
+ !Array.isArray(d.runtime.capabilities) ||
128
+ !d.runtime.capabilities.length ||
129
+ d.runtime.capabilities.some((v) => !['actions', 'queries', ...(d.format === 'easyai.plugin-package/v2' ? ['nodes'] : [])].includes(String(v))) ||
130
+ (d.format === 'easyai.plugin-package/v1' && descriptor.manifest.contributes.nodes?.length))
131
+ fail('当前包运行环境不支持所需能力');
132
+ if (!Array.isArray(d.runtime.queries) ||
133
+ d.runtime.queries.some((q) => typeof q !== 'string' || !/^[a-zA-Z][\w.-]{0,100}$/.test(q)) ||
134
+ new Set(d.runtime.queries).size !== d.runtime.queries.length)
135
+ fail('插件查询声明无效');
136
+ const expectedHandlers = (descriptor.manifest.contributes.nodes ?? [])
137
+ .filter(node => node.execution === 'remote' || node.execution === 'confirmed')
138
+ .map(node => node.type).sort();
139
+ const handlers = d.runtime.handlers ?? [];
140
+ if (d.format === 'easyai.plugin-package/v2' && (!Array.isArray(handlers) ||
141
+ handlers.some((name) => typeof name !== 'string' || !/^[a-zA-Z][\w.-]{0,100}$/.test(name)) ||
142
+ JSON.stringify([...handlers].sort()) !== JSON.stringify(expectedHandlers) ||
143
+ Boolean(descriptor.manifest.contributes.nodes?.length) !== d.runtime.capabilities.includes('nodes')))
144
+ fail('插件节点处理器声明无效');
145
+ if (!relativePath(d.runtime.entry).endsWith('.mjs') ||
146
+ !relativePath(d.ui.entry).endsWith('.js') ||
147
+ !/^[A-Za-z_$][\w$]{0,127}$/.test(d.ui.globalName))
148
+ fail('插件入口声明无效');
149
+ if (d.ui.css && !relativePath(d.ui.css).endsWith('.css'))
150
+ fail('插件样式入口无效');
151
+ if (!d.files[d.runtime.entry] ||
152
+ !d.files[d.ui.entry] ||
153
+ (d.ui.css && !d.files[d.ui.css]))
154
+ fail('插件包缺少声明的入口');
155
+ for (const [path, hash] of Object.entries(d.files)) {
156
+ relativePath(path);
157
+ if (path === 'plugin.json' ||
158
+ typeof hash !== 'string' ||
159
+ !/^[a-f0-9]{64}$/.test(hash) ||
160
+ !files.has(path) ||
161
+ packageSha256(files.get(path)) !== hash)
162
+ fail('插件文件摘要校验失败');
163
+ }
164
+ for (const path of files.keys())
165
+ if (path !== 'plugin.json' && !d.files[path])
166
+ fail('插件包包含未声明文件');
167
+ return { digest: packageSha256(bytes), descriptor, files };
168
+ }
169
+ const materializations = new Map();
170
+ export async function materializePluginPackage(pkg, root) {
171
+ const key = join(root, pkg.digest);
172
+ const pending = materializations.get(key);
173
+ if (pending)
174
+ return pending;
175
+ const operation = materialize(pkg, root);
176
+ materializations.set(key, operation);
177
+ try {
178
+ return await operation;
179
+ }
180
+ finally {
181
+ if (materializations.get(key) === operation)
182
+ materializations.delete(key);
183
+ }
184
+ }
185
+ async function materialize(pkg, root) {
186
+ const directory = join(root, pkg.digest);
187
+ async function valid() {
188
+ try {
189
+ if (!(await lstat(directory)).isDirectory())
190
+ return false;
191
+ for (const [name, content] of pkg.files) {
192
+ if (!(await lstat(join(directory, name))).isFile() ||
193
+ packageSha256(await readFile(join(directory, name))) !==
194
+ packageSha256(content))
195
+ return false;
196
+ }
197
+ return true;
198
+ }
199
+ catch {
200
+ return false;
201
+ }
202
+ }
203
+ if (!(await valid())) {
204
+ await mkdir(root, { recursive: true, mode: 0o700 });
205
+ const staging = join(root, `.staging-${randomUUID()}`);
206
+ await mkdir(staging, { mode: 0o700 });
207
+ try {
208
+ for (const [name, content] of pkg.files) {
209
+ const path = join(staging, name);
210
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
211
+ await writeFile(path, content, { mode: 0o600 });
212
+ }
213
+ if (!(await valid())) {
214
+ // Local cache is disposable. Move corrupt content aside before publishing
215
+ // the fully verified replacement, tolerating other replica-local callers.
216
+ const quarantine = `${directory}.invalid-${randomUUID()}`;
217
+ await rename(directory, quarantine).catch((error) => {
218
+ if (error.code !== 'ENOENT')
219
+ throw error;
220
+ });
221
+ await rm(quarantine, { recursive: true, force: true });
222
+ }
223
+ try {
224
+ await rename(staging, directory);
225
+ }
226
+ catch (error) {
227
+ if (!(await valid()))
228
+ throw error;
229
+ }
230
+ }
231
+ finally {
232
+ await rm(staging, { recursive: true, force: true });
233
+ }
234
+ }
235
+ return { digest: pkg.digest, descriptor: pkg.descriptor, directory };
236
+ }
237
+ // A short-lived, trusted-code runner. Deliberately not a security sandbox.
238
+ const RUNNER = `
239
+ import { pathToFileURL } from 'node:url';
240
+ process.once('message', async ({ entry, manifest, queries, handlers, call }) => {
241
+ try {
242
+ const mod = await import(pathToFileURL(entry).href);
243
+ const plugin = mod.default;
244
+ const canonical = value => value && typeof value === 'object' ? (Array.isArray(value) ? value.map(canonical) : Object.fromEntries(Object.keys(value).sort().map(k => [k, canonical(value[k])]))) : value;
245
+ if (!plugin || JSON.stringify(canonical(plugin.manifest)) !== JSON.stringify(canonical(manifest))) throw Object.assign(new Error('运行模块与插件声明不一致'), { code: 'INVALID_PACKAGE' });
246
+ if (JSON.stringify(Object.keys(plugin.queries ?? {}).sort()) !== JSON.stringify([...queries].sort()) || JSON.stringify(Object.keys(plugin.handlers ?? {}).sort()) !== JSON.stringify([...handlers].sort())) throw Object.assign(new Error('运行模块能力与包声明不一致'), { code: 'INVALID_PACKAGE' });
247
+ let result;
248
+ if (call.operation === 'inspect') result = plugin.manifest;
249
+ else if (call.operation === 'query') {
250
+ if (!Object.hasOwn(plugin.queries ?? {}, call.name) || typeof plugin.queries[call.name] !== 'function') throw Object.assign(new Error('插件查询不存在'), { code: 'QUERY_NOT_FOUND' });
251
+ result = await plugin.queries[call.name](call.input, call.config, call.context);
252
+ } else if (call.operation === 'check') result = plugin.checkConfig ? await plugin.checkConfig(call.config) : { supported: false };
253
+ else if (call.operation === 'resolve') result = plugin.resolveManifest ? await plugin.resolveManifest(call.config, structuredClone(plugin.manifest)) : plugin.manifest;
254
+ else if (call.operation === 'execute') {
255
+ const handler = plugin.handlers?.[call.request?.nodeType];
256
+ if (typeof handler !== 'function') throw Object.assign(new Error('插件节点处理器不存在'), { code: 'HANDLER_NOT_FOUND' });
257
+ result = await handler(call.request, { signal: new AbortController().signal, config: call.config ?? {}, progress() {}, files: { put() { throw new Error('托管节点暂不支持文件写入'); }, readJson() { throw new Error('托管节点暂不支持文件读取'); }, info() { throw new Error('托管节点暂不支持文件查询'); }, exists() { throw new Error('托管节点暂不支持文件查询'); } } });
258
+ }
259
+ else throw new Error('不支持的插件操作');
260
+ const text = JSON.stringify({ ok: true, result: result ?? null });
261
+ if (Buffer.byteLength(text) > 1048576) throw Object.assign(new Error('插件返回内容超过限制'), { code: 'RESULT_TOO_LARGE' });
262
+ process.send(JSON.parse(text));
263
+ } catch (error) {
264
+ let message = error instanceof Error ? error.message : '插件查询失败';
265
+ for (const field of manifest.configFields ?? []) if (field.type === 'secret' && typeof call.config?.[field.key] === 'string' && call.config[field.key]) message = message.split(call.config[field.key]).join('[已隐藏]');
266
+ process.send({ ok: false, error: { message: message.slice(0, 500), code: typeof error.code === 'string' ? error.code.slice(0, 80) : 'PLUGIN_ERROR', status: Number.isInteger(error.status) && error.status >= 400 && error.status <= 599 ? error.status : 502 } });
267
+ }
268
+ });
269
+ `;
270
+ let activeCalls = 0;
271
+ export async function invokePluginPackage(pkg, call, options = {}) {
272
+ if (call.operation === 'execute') {
273
+ const request = object(call.request, 'request');
274
+ const node = pkg.descriptor.manifest.contributes.nodes?.find(item => item.type === request.nodeType);
275
+ if (request.pluginId !== pkg.descriptor.manifest.id || !node ||
276
+ !['remote', 'confirmed'].includes(node.execution) ||
277
+ !pkg.descriptor.runtime.handlers?.includes(node.type))
278
+ throw new ContractError('插件节点处理器与安装包声明不匹配', 400, 'INVALID_PACKAGE');
279
+ call = { ...call, request: validateExecution(request, node) };
280
+ }
281
+ if (Number(process.versions.node.split('.')[0]) !==
282
+ pkg.descriptor.runtime.nodeMajor)
283
+ throw new ContractError('平台 Node 运行版本不兼容', 400, 'INVALID_PACKAGE');
284
+ if (options.signal?.aborted)
285
+ throw new ContractError('插件操作已取消', 409, 'CANCELLED');
286
+ const runner = join(dirname(pkg.directory), `runner-${packageSha256(RUNNER)}.mjs`);
287
+ const temporaryRunner = `${runner}.${randomUUID()}`;
288
+ await writeFile(temporaryRunner, RUNNER, { mode: 0o600 });
289
+ await rename(temporaryRunner, runner);
290
+ if (activeCalls >= 4)
291
+ throw new ContractError('插件正在处理其他请求,请稍后重试', 429, 'PLUGIN_BUSY');
292
+ activeCalls++;
293
+ try {
294
+ return await new Promise((resolve, reject) => {
295
+ const child = fork(runner, [], {
296
+ execArgv: ['--max-old-space-size=128'],
297
+ cwd: pkg.directory,
298
+ env: {
299
+ NODE_ENV: 'production',
300
+ ...(process.env.NODE_EXTRA_CA_CERTS
301
+ ? { NODE_EXTRA_CA_CERTS: process.env.NODE_EXTRA_CA_CERTS }
302
+ : {}),
303
+ ...(process.env.SSL_CERT_FILE
304
+ ? { SSL_CERT_FILE: process.env.SSL_CERT_FILE }
305
+ : {}),
306
+ },
307
+ stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
308
+ });
309
+ let settled = false;
310
+ const finish = (error, value) => {
311
+ if (settled)
312
+ return;
313
+ settled = true;
314
+ clearTimeout(timer);
315
+ options.signal?.removeEventListener('abort', cancel);
316
+ const complete = () => {
317
+ if (error)
318
+ reject(error);
319
+ else
320
+ resolve(value);
321
+ };
322
+ if (!child.pid || child.exitCode !== null || child.signalCode !== null)
323
+ complete();
324
+ else {
325
+ child.once('exit', complete);
326
+ child.kill('SIGKILL');
327
+ }
328
+ };
329
+ const cancel = () => finish(new ContractError('插件操作已取消', 409, 'CANCELLED'));
330
+ const timer = setTimeout(() => finish(new ContractError(call.operation === 'execute'
331
+ ? '托管节点超过 20 秒执行限制,请使用独立服务部署'
332
+ : '插件查询超时,请重试', 504, 'PLUGIN_TIMEOUT')), options.timeoutMs ?? 20_000);
333
+ options.signal?.addEventListener('abort', cancel, { once: true });
334
+ if (options.signal?.aborted) {
335
+ cancel();
336
+ return;
337
+ }
338
+ child.once('error', () => finish(new ContractError('插件进程无法启动', 502, 'PLUGIN_UNAVAILABLE')));
339
+ child.once('exit', () => finish(new ContractError('插件进程意外退出', 502, 'PLUGIN_UNAVAILABLE')));
340
+ child.once('message', (message) => {
341
+ if (message?.ok === true)
342
+ finish(undefined, message.result);
343
+ else
344
+ finish(new ContractError(message?.error?.message ?? '插件查询失败', message?.error?.status ?? 502, message?.error?.code ?? 'PLUGIN_ERROR'));
345
+ });
346
+ child.send({
347
+ entry: join(pkg.directory, pkg.descriptor.runtime.entry),
348
+ manifest: pkg.descriptor.manifest,
349
+ queries: pkg.descriptor.runtime.queries,
350
+ handlers: pkg.descriptor.runtime.handlers ?? [],
351
+ call,
352
+ }, (error) => {
353
+ if (error)
354
+ finish(new ContractError('插件进程通信失败', 502, 'PLUGIN_UNAVAILABLE'));
355
+ });
356
+ });
357
+ }
358
+ finally {
359
+ activeCalls--;
360
+ }
361
+ }
@@ -0,0 +1,14 @@
1
+ import type { Plugin } from '../sdk/index.js';
2
+ export declare function loadPackagedPlugins(paths: string[], directory: string): Promise<{
3
+ plugins: Plugin[];
4
+ presentations: Record<string, Record<string, unknown>>;
5
+ downloads: Record<string, {
6
+ bytes: Buffer;
7
+ digest: string;
8
+ }>;
9
+ executionEntries: Record<string, {
10
+ entry: string;
11
+ digest: string;
12
+ entryHash: string;
13
+ }>;
14
+ }>;
@@ -0,0 +1,58 @@
1
+ import { pathToFileURL } from 'node:url';
2
+ import { join } from 'node:path';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { inspectPluginPackage, materializePluginPackage, invokePluginPackage, } from './package-runtime.js';
5
+ export async function loadPackagedPlugins(paths, directory) {
6
+ const plugins = [];
7
+ const executionEntries = {};
8
+ const presentations = {};
9
+ const downloads = {};
10
+ for (const path of paths) {
11
+ const bytes = await readFile(path);
12
+ const inspected = await inspectPluginPackage(bytes);
13
+ const pkg = await materializePluginPackage(inspected, directory);
14
+ await invokePluginPackage(pkg, { operation: 'inspect' });
15
+ const { descriptor: d, digest } = pkg;
16
+ if (presentations[d.manifest.id])
17
+ throw new Error('插件包标识重复');
18
+ downloads[d.manifest.id] = { bytes, digest };
19
+ // The independent service already runs trusted plugin code in its own process.
20
+ // Preserve the SDK's synchronous declaration resolver there. Platform imports
21
+ // and all queries continue to use the bounded child runner.
22
+ const definition = (await import(pathToFileURL(join(pkg.directory, d.runtime.entry)).href)).default;
23
+ if (d.format === 'easyai.plugin-package/v2' && d.runtime.handlers?.length) {
24
+ executionEntries[d.manifest.id] = { entry: join(pkg.directory, d.runtime.entry), digest,
25
+ entryHash: d.files[d.runtime.entry] };
26
+ }
27
+ plugins.push({
28
+ resolveManifest: definition.resolveManifest,
29
+ normalizeParameters: definition.normalizeParameters,
30
+ ...(definition.checkConfig
31
+ ? {
32
+ checkConfig: (config) => invokePluginPackage(pkg, { operation: 'check', config }),
33
+ }
34
+ : {}),
35
+ manifest: d.manifest,
36
+ handlers: {},
37
+ queries: Object.fromEntries(d.runtime.queries.map((name) => [
38
+ name,
39
+ (input, config, context, signal) => invokePluginPackage(pkg, { operation: 'query', name, input, config, context }, { signal }),
40
+ ])),
41
+ });
42
+ presentations[d.manifest.id] = {
43
+ packageDigest: digest,
44
+ uiBundle: {
45
+ url: `/packages/${digest}/${d.ui.entry}`,
46
+ digest: d.files[d.ui.entry],
47
+ globalName: d.ui.globalName,
48
+ ...(d.ui.css
49
+ ? {
50
+ cssUrl: `/packages/${digest}/${d.ui.css}`,
51
+ cssDigest: d.files[d.ui.css],
52
+ }
53
+ : {}),
54
+ },
55
+ };
56
+ }
57
+ return { plugins, presentations, downloads, executionEntries };
58
+ }
@@ -0,0 +1,4 @@
1
+ import { type ConfigField } from '../protocol/index.js';
2
+ export declare function fieldsConfig(fields: ConfigField[], previous: Record<string, unknown>, patch: Record<string, unknown>): {
3
+ [x: string]: unknown;
4
+ };
@@ -0,0 +1,31 @@
1
+ import { ContractError } from '../protocol/index.js';
2
+ export function fieldsConfig(fields, previous, patch) {
3
+ const declared = new Set(fields.map((field) => field.key));
4
+ const retained = Object.fromEntries(Object.entries(previous).filter(([key]) => declared.has(key)));
5
+ const config = {
6
+ ...Object.fromEntries(fields
7
+ .filter((f) => f.default !== undefined)
8
+ .map((f) => [f.key, f.default])),
9
+ ...retained,
10
+ };
11
+ for (const [key, value] of Object.entries(patch)) {
12
+ const field = fields.find((f) => f.key === key);
13
+ if (!field)
14
+ throw new ContractError(`未知配置字段 ${key}`);
15
+ if (value === null) {
16
+ delete config[key];
17
+ continue;
18
+ }
19
+ if ((['string', 'secret', 'select'].includes(field.type) &&
20
+ typeof value !== 'string') ||
21
+ (field.type === 'number' &&
22
+ (typeof value !== 'number' || !Number.isFinite(value))) ||
23
+ (field.type === 'boolean' && typeof value !== 'boolean'))
24
+ throw new ContractError(`${key} 类型错误`);
25
+ if (field.type === 'select' &&
26
+ !field.options?.some((o) => o.value === value))
27
+ throw new ContractError(`${key} 选项无效`);
28
+ config[key] = value;
29
+ }
30
+ return config;
31
+ }
@@ -0,0 +1,48 @@
1
+ import type { ExecutionRecord, ExecutionRequest } from '../protocol/index.js';
2
+ export type ExecutionSnapshot = {
3
+ config: Record<string, unknown>;
4
+ release: string;
5
+ safeRetry: boolean;
6
+ maxAttempts: number;
7
+ packageEntry?: string;
8
+ packageDigest?: string;
9
+ packageEntryHash?: string;
10
+ };
11
+ export declare class RuntimeStore {
12
+ private db;
13
+ constructor(path: string);
14
+ syncDeployment(digest: string): void;
15
+ get revision(): number;
16
+ get instanceId(): string;
17
+ getPlugin(id: string): {
18
+ enabled: boolean;
19
+ config: Record<string, unknown>;
20
+ configVersion: number;
21
+ };
22
+ setPlugin(id: string, patch: {
23
+ enabled?: boolean;
24
+ config?: Record<string, unknown>;
25
+ expectedVersion?: number;
26
+ }): {
27
+ enabled: boolean;
28
+ config: Record<string, unknown>;
29
+ configVersion: number;
30
+ };
31
+ accept(request: ExecutionRequest, snapshot?: ExecutionSnapshot): {
32
+ created: boolean;
33
+ record: ExecutionRecord;
34
+ };
35
+ getSnapshot(id: string): ExecutionSnapshot;
36
+ getExecution(id: string): ExecutionRecord | undefined;
37
+ getRequest(id: string): ExecutionRequest;
38
+ update(id: string, patch: Partial<ExecutionRecord>): ExecutionRecord;
39
+ claim(id: string): ExecutionRecord | undefined;
40
+ requeue(id: string, error?: ExecutionRecord['error']): ExecutionRecord;
41
+ queued(pluginId?: string): Array<{
42
+ id: string;
43
+ request: ExecutionRequest;
44
+ }>;
45
+ list(limit?: number): ExecutionRecord[];
46
+ recover(): string[];
47
+ close(): void;
48
+ }