@easyai-canvas/canvas-plugin-sdk 0.1.3 → 0.1.4
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/README.md +6 -2
- package/bin/easyai-plugin.mjs +76 -28
- package/dist/customer-main.js +20 -1
- package/dist/protocol/index.d.ts +23 -1
- package/dist/protocol/index.js +39 -2
- package/dist/studio/index.html +79 -6
- package/package.json +3 -2
- package/tools/build-plugin-package.mjs +1 -0
package/README.md
CHANGED
|
@@ -13,11 +13,15 @@ npm run build
|
|
|
13
13
|
npm start
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
`init --template node|action|combined`
|
|
16
|
+
`init --template node|action|combined` 选择首个插件模板;`--no-install` 跳过依赖安装。新项目将插件放在 `src/plugins/<目录>/`,用 `npx easyai-plugin add <目录> --template node|action|combined` 增加独立插件。每个目录拥有独立 ID、配置、UI 和 ZIP;同一插件的多个节点可继续放在该目录内。旧项目的 `src/plugin.ts`、`src/ui.ts` 与 `dist/plugin.zip` 仍可使用。
|
|
17
|
+
|
|
18
|
+
开发工作台位于 `http://localhost:4310/studio`,按插件展示配置表单和待配置状态。`npm run build` 为新结构分别输出 `dist/plugins/<pluginId>.zip` 与摘要文件;管理员可分别导入,或用 `npm start` 运行同一服务下的多个插件。
|
|
19
|
+
|
|
20
|
+
节点在 `contributes.nodes[].placement` 声明 `{ slot: 'canvas.add.node', group: 'tools', order: 100 }`,素材选择操作在 `contributes.actions[].placement` 声明 `{ slot: 'canvas.add.resource', order: 100 }`。节点分组还可选 `basic`、`storyboard`;素材入口不使用节点分组。插件的 Vue 组件负责内部样式与交互,画布负责添加入口及拖动、权限等边界。旧版 `menu` 声明继续可用;未知位置或贡献能力与位置不匹配会在打包校验时报错。
|
|
17
21
|
|
|
18
22
|
运行 `npm run dev` 后,工作台会显示当前 SDK 与 npm 最新版本。若有新版本,点击“一键升级”会在当前项目执行固定版本安装,更新 `package.json` 和 npm 锁文件;安装完成后重启 `npm run dev`,新运行时才会生效。版本检查失败时仍可继续开发。
|
|
19
23
|
|
|
20
|
-
平台托管只接纳运行不超过 20 秒的节点任务;需要长任务、GPU
|
|
24
|
+
平台托管只接纳运行不超过 20 秒的节点任务;需要长任务、GPU 或自有基础设施时使用独立服务。构建还会生成每个 ZIP 的 `.sha256` 供管理员核对。独立服务生产环境必须提供 `PLUGIN_SERVICE_SIGNING_SECRET`、与平台连接 ID 一致的 `PLUGIN_SERVICE_AUDIENCE` 和持久化的 `PLUGIN_DATA_DIR`。业务密钥放在插件配置或部署环境,不写入前端组件。
|
|
21
25
|
|
|
22
26
|
托管节点暂不支持文件和画布宿主能力。管理员导入 ZIP 后默认停用,审核插件代码、声明能力和版本摘要后启用。版本切换不会改变已提交任务使用的包摘要。
|
|
23
27
|
|
package/bin/easyai-plugin.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { existsSync, watch } from 'node:fs';
|
|
5
|
-
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
6
6
|
import net from 'node:net';
|
|
7
7
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
@@ -40,11 +40,12 @@ export default definePlugin({
|
|
|
40
40
|
inputs: [], outputs: [{ id: 'out', type: 'text' }],
|
|
41
41
|
parameters: [{ key: 'text', label: '文本', type: 'string', default: '你好,画布' }],
|
|
42
42
|
ui: { preview: 'EchoPreview', panel: 'EchoPanel' }, timeoutMs: 20000,
|
|
43
|
+
placement: { slot: 'canvas.add.node', group: 'tools' },
|
|
43
44
|
})],` : ''}
|
|
44
45
|
${action ? `actions: [{
|
|
45
46
|
id: 'select-image', name: '添加图片链接', action: 'asset-selection',
|
|
46
47
|
presentation: 'dialog', mediaTypes: ['image'], maxCount: 1,
|
|
47
|
-
ui: { component: 'ImageSelector' },
|
|
48
|
+
ui: { component: 'ImageSelector' }, placement: { slot: 'canvas.add.resource' },
|
|
48
49
|
}],` : ''}
|
|
49
50
|
},
|
|
50
51
|
},
|
|
@@ -92,12 +93,12 @@ ${action ? `export const ImageSelector = defineComponent({ setup() {
|
|
|
92
93
|
} });` : ''}
|
|
93
94
|
`;
|
|
94
95
|
}
|
|
95
|
-
function testSource(id, template) {
|
|
96
|
+
function testSource(id, template, directory) {
|
|
96
97
|
const node = template !== 'action';
|
|
97
98
|
const action = template !== 'node';
|
|
98
99
|
return `import test from 'node:test';
|
|
99
100
|
import assert from 'node:assert/strict';
|
|
100
|
-
import plugin from '../src/plugin.ts';
|
|
101
|
+
import plugin from '../src/plugins/${directory}/plugin.ts';
|
|
101
102
|
import { validateManifest, validateResult } from '${metadata.name}/protocol';
|
|
102
103
|
|
|
103
104
|
test('插件声明和业务处理器符合画布契约', async () => {
|
|
@@ -125,7 +126,7 @@ async function init() {
|
|
|
125
126
|
const slug = safeName(basename(target));
|
|
126
127
|
const id = `customer.${slug.replaceAll('-', '.')}`;
|
|
127
128
|
const title = slug.replaceAll('-', ' ');
|
|
128
|
-
await mkdir(join(target, 'src'), { recursive: true });
|
|
129
|
+
await mkdir(join(target, 'src/plugins', slug), { recursive: true });
|
|
129
130
|
await mkdir(join(target, 'test'), { recursive: true });
|
|
130
131
|
const spec = process.env.EASYAI_SDK_INSTALL_SPEC ?? metadata.version;
|
|
131
132
|
const project = {
|
|
@@ -137,13 +138,13 @@ async function init() {
|
|
|
137
138
|
};
|
|
138
139
|
const files = {
|
|
139
140
|
'package.json': `${JSON.stringify(project, null, 2)}\n`,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
[`src/plugins/${slug}/plugin.ts`]: pluginSource(id, title, template),
|
|
142
|
+
[`src/plugins/${slug}/ui.ts`]: uiSource(template),
|
|
143
|
+
[`test/${slug}.test.ts`]: testSource(id, template, slug),
|
|
143
144
|
'.gitignore': 'node_modules/\ndist/\n.easyai-plugin/\n.env\n',
|
|
144
145
|
'.env.example': 'PORT=4310\nPLUGIN_DATA_DIR=./.easyai-plugin\nPLUGIN_AUTH_MODE=token\nPLUGIN_SERVICE_AUDIENCE=customer-plugin\n# PLUGIN_SERVICE_SIGNING_SECRET=由部署环境安全注入\n',
|
|
145
|
-
'Dockerfile': 'FROM node:22.22.0\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci --omit=dev\nCOPY dist/
|
|
146
|
-
'README.md': `# ${title}\n\n运行 npm run dev,打开 http://localhost:4310/studio,在浏览器中连接 EasyAI 画布。\n运行 npm run test:contract && npm run build
|
|
146
|
+
'Dockerfile': 'FROM node:22.22.0\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci --omit=dev\nCOPY dist/plugins ./dist/plugins\nENV NODE_ENV=production PLUGIN_BUILTINS=0\nCMD ["npm", "start"]\n',
|
|
147
|
+
'README.md': `# ${title}\n\n每个插件放在 src/plugins/<目录>/,用 easyai-plugin add <目录> 增加独立插件。\n运行 npm run dev,打开 http://localhost:4310/studio,在浏览器中连接 EasyAI 画布。\n运行 npm run test:contract && npm run build,分别生成 dist/plugins/<pluginId>.zip。\n独立部署时配置插件服务签名密钥并运行 npm start;平台托管时由管理员审核并分别导入 ZIP。\n`,
|
|
147
148
|
'tsconfig.json': JSON.stringify({ compilerOptions: { target: 'ES2022', module: 'NodeNext', moduleResolution: 'NodeNext', strict: true, skipLibCheck: true, allowImportingTsExtensions: true, lib: ['ES2022', 'DOM'], types: ['node'], noEmit: true }, include: ['src/**/*.ts', 'test/**/*.ts'] }, null, 2) + '\n',
|
|
148
149
|
};
|
|
149
150
|
for (const [path, content] of Object.entries(files)) await writeFile(join(target, path), content, { flag: 'wx' });
|
|
@@ -158,34 +159,77 @@ async function run(executable, argv, directory = cwd, env = process.env) {
|
|
|
158
159
|
child.once('exit', code => code === 0 ? resolvePromise() : reject(new Error(`${executable} 退出码 ${code}`)));
|
|
159
160
|
});
|
|
160
161
|
}
|
|
162
|
+
function templateArg() {
|
|
163
|
+
return args.find(arg => arg.startsWith('--template='))?.slice('--template='.length)
|
|
164
|
+
?? (args.includes('--template') ? args[args.indexOf('--template') + 1] : 'node');
|
|
165
|
+
}
|
|
166
|
+
async function add() {
|
|
167
|
+
const template = templateArg();
|
|
168
|
+
if (!['node', 'action', 'combined'].includes(template)) fail('模板只支持 node、action 或 combined');
|
|
169
|
+
const directory = args.find((arg, index) => !arg.startsWith('--') && args[index - 1] !== '--template');
|
|
170
|
+
if (!directory) fail('用法:easyai-plugin add <插件目录> [--template node|action|combined]');
|
|
171
|
+
const slug = safeName(directory);
|
|
172
|
+
if (slug !== directory) fail('插件目录只能使用小写英文字母、数字和短横线');
|
|
173
|
+
const path = join(cwd, 'src/plugins', slug);
|
|
174
|
+
if (existsSync(path)) fail(`插件目录已存在:${path}`);
|
|
175
|
+
await mkdir(path, { recursive: true });
|
|
176
|
+
await mkdir(join(cwd, 'test'), { recursive: true });
|
|
177
|
+
const id = `customer.${slug.replaceAll('-', '.')}`;
|
|
178
|
+
await writeFile(join(path, 'plugin.ts'), pluginSource(id, slug.replaceAll('-', ' '), template), { flag: 'wx' });
|
|
179
|
+
await writeFile(join(path, 'ui.ts'), uiSource(template), { flag: 'wx' });
|
|
180
|
+
await writeFile(join(cwd, 'test', `${slug}.test.ts`), testSource(id, template, slug), { flag: 'wx' });
|
|
181
|
+
console.log(`已添加插件 ${id};运行 npm run test:contract && npm run build`);
|
|
182
|
+
}
|
|
183
|
+
async function pluginEntries() {
|
|
184
|
+
const root = join(cwd, 'src/plugins');
|
|
185
|
+
const folders = existsSync(root) ? (await readdir(root, { withFileTypes: true }))
|
|
186
|
+
.filter(item => item.isDirectory()).map(item => item.name).sort() : [];
|
|
187
|
+
const entries = folders.map(name => ({ name, server: join(root, name, 'plugin.ts'), ui: join(root, name, 'ui.ts'), legacy: false }));
|
|
188
|
+
if (existsSync(join(cwd, 'src/plugin.ts'))) entries.push({ name: 'plugin', server: join(cwd, 'src/plugin.ts'), ui: join(cwd, 'src/ui.ts'), legacy: true });
|
|
189
|
+
if (!entries.length) fail('缺少插件入口:src/plugins/<目录>/plugin.ts 或 src/plugin.ts');
|
|
190
|
+
for (const entry of entries) for (const path of [entry.server, entry.ui]) if (!existsSync(path)) fail(`缺少 ${path}`);
|
|
191
|
+
return entries;
|
|
192
|
+
}
|
|
161
193
|
async function build() {
|
|
162
194
|
nodeVersion();
|
|
163
|
-
|
|
195
|
+
const entries = await pluginEntries();
|
|
164
196
|
const { buildPluginPackage } = await import(builderEntry);
|
|
165
|
-
const output = join(cwd, 'dist/plugin.zip');
|
|
166
|
-
await buildPluginPackage({ serverEntry: join(cwd, 'src/plugin.ts'), uiEntry: join(cwd, 'src/ui.ts'),
|
|
167
|
-
globalName: `EasyAIPlugin${createHash('sha256').update(cwd).digest('hex').slice(0, 12)}`, output });
|
|
168
|
-
const bytes = await readFile(output);
|
|
169
197
|
const { inspectPluginPackage } = await import(join(packageRoot, 'dist/runtime/package-runtime.js'));
|
|
170
|
-
await
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
198
|
+
await rm(join(cwd, 'dist/plugins'), { recursive: true, force: true });
|
|
199
|
+
const ids = new Set();
|
|
200
|
+
for (const entry of entries) {
|
|
201
|
+
const temporary = join(cwd, 'dist/plugins', `${entry.name}.zip`);
|
|
202
|
+
await buildPluginPackage({ serverEntry: entry.server, uiEntry: entry.ui,
|
|
203
|
+
globalName: `EasyAIPlugin${createHash('sha256').update(`${cwd}/${entry.name}`).digest('hex').slice(0, 12)}`, output: temporary });
|
|
204
|
+
const bytes = await readFile(temporary);
|
|
205
|
+
const inspected = await inspectPluginPackage(bytes);
|
|
206
|
+
const id = inspected.descriptor.manifest.id;
|
|
207
|
+
if (ids.has(id)) fail(`插件 ID 重复:${id}`);
|
|
208
|
+
ids.add(id);
|
|
209
|
+
const output = entry.legacy ? join(cwd, 'dist/plugin.zip') : join(cwd, 'dist/plugins', `${id}.zip`);
|
|
210
|
+
if (output !== temporary) await rename(temporary, output);
|
|
211
|
+
const digest = createHash('sha256').update(bytes).digest('hex');
|
|
212
|
+
await writeFile(`${output}.sha256`, `${digest} ${basename(output)}\n`);
|
|
213
|
+
console.log(`${id} → ${output}(SHA-256 ${digest})`);
|
|
214
|
+
}
|
|
174
215
|
}
|
|
175
|
-
function serviceEnv(dev) {
|
|
176
|
-
const
|
|
177
|
-
|
|
216
|
+
async function serviceEnv(dev) {
|
|
217
|
+
const entries = await pluginEntries();
|
|
218
|
+
const folder = join(cwd, 'dist/plugins');
|
|
219
|
+
const zips = existsSync(folder) ? (await readdir(folder)).filter(name => name.endsWith('.zip')).map(name => join(folder, name)) : [];
|
|
220
|
+
if (entries.some(entry => entry.legacy)) zips.push(join(cwd, 'dist/plugin.zip'));
|
|
221
|
+
if (!zips.length || zips.some(zip => !existsSync(zip))) fail('缺少插件 ZIP;请先运行 npm run build');
|
|
178
222
|
if (!existsSync(runtimeEntry)) fail('SDK 运行时未构建');
|
|
179
223
|
if (!dev && !process.env.PLUGIN_SERVICE_SIGNING_SECRET) fail('生产服务需要 PLUGIN_SERVICE_SIGNING_SECRET');
|
|
180
224
|
return { ...process.env, NODE_ENV: dev ? 'development' : 'production',
|
|
181
|
-
PLUGIN_BUILTINS: '0', PLUGIN_PACKAGES: JSON.stringify(
|
|
225
|
+
PLUGIN_BUILTINS: '0', PLUGIN_PACKAGES: JSON.stringify(zips),
|
|
182
226
|
PLUGIN_SDK_VERSION: metadata.version,
|
|
183
227
|
PLUGIN_DATA_DIR: process.env.PLUGIN_DATA_DIR ?? join(cwd, '.easyai-plugin'),
|
|
184
228
|
...(dev ? { PLUGIN_AUTH_MODE: 'local', PLUGIN_BIND: '127.0.0.1', PLUGIN_DEV_WATCH: '1' } : {}) };
|
|
185
229
|
}
|
|
186
230
|
async function start() {
|
|
187
231
|
nodeVersion();
|
|
188
|
-
await run(process.execPath, [runtimeEntry], cwd, serviceEnv(false));
|
|
232
|
+
await run(process.execPath, [runtimeEntry], cwd, await serviceEnv(false));
|
|
189
233
|
}
|
|
190
234
|
async function dev() {
|
|
191
235
|
await build();
|
|
@@ -195,9 +239,10 @@ async function dev() {
|
|
|
195
239
|
const status = async (state, message = '') => writeFile(join(dataDir, 'dev-build-status.json'),
|
|
196
240
|
JSON.stringify({ state, message: message.slice(0, 500), updatedAt: new Date().toISOString() }));
|
|
197
241
|
const launch = () => {
|
|
198
|
-
child = spawn(process.execPath, [runtimeEntry], { cwd, env:
|
|
242
|
+
child = spawn(process.execPath, [runtimeEntry], { cwd, env: launchEnv, stdio: 'inherit' });
|
|
199
243
|
child.on('error', error => console.error(error));
|
|
200
244
|
};
|
|
245
|
+
let launchEnv = await serviceEnv(true);
|
|
201
246
|
await status('ready');
|
|
202
247
|
launch();
|
|
203
248
|
console.log(`真实画布调试工作台:http://localhost:${process.env.PORT ?? 4310}/studio`);
|
|
@@ -211,6 +256,7 @@ async function dev() {
|
|
|
211
256
|
await status('building');
|
|
212
257
|
try {
|
|
213
258
|
await build();
|
|
259
|
+
launchEnv = await serviceEnv(true);
|
|
214
260
|
child?.kill('SIGTERM');
|
|
215
261
|
await new Promise(resolvePromise => child?.once('exit', resolvePromise));
|
|
216
262
|
launch();
|
|
@@ -235,8 +281,9 @@ async function doctor() {
|
|
|
235
281
|
const checks = [];
|
|
236
282
|
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
237
283
|
checks.push({ name: 'Node 22.22+', ok: major === 22 && minor >= 22 });
|
|
238
|
-
|
|
239
|
-
checks.push({ name:
|
|
284
|
+
const entries = await pluginEntries().catch(() => []);
|
|
285
|
+
checks.push({ name: `插件入口(${entries.length} 个)`, ok: entries.length > 0 });
|
|
286
|
+
checks.push({ name: '插件 ID 唯一', ok: entries.length > 0 && (await build().then(() => true).catch(() => false)) });
|
|
240
287
|
const port = Number(process.env.PORT ?? 4310);
|
|
241
288
|
const available = await new Promise(resolvePromise => {
|
|
242
289
|
const server = net.createServer();
|
|
@@ -256,12 +303,13 @@ async function doctor() {
|
|
|
256
303
|
|
|
257
304
|
try {
|
|
258
305
|
if (command === 'init') await init();
|
|
306
|
+
else if (command === 'add') await add();
|
|
259
307
|
else if (command === 'dev') await dev();
|
|
260
308
|
else if (command === 'test:contract') await contractTest();
|
|
261
309
|
else if (command === 'build') await build();
|
|
262
310
|
else if (command === 'start') await start();
|
|
263
311
|
else if (command === 'doctor') await doctor();
|
|
264
|
-
else fail('用法:easyai-plugin <init|dev|test:contract|build|start|doctor>');
|
|
312
|
+
else fail('用法:easyai-plugin <init|add|dev|test:contract|build|start|doctor>');
|
|
265
313
|
} catch (error) {
|
|
266
314
|
console.error(error instanceof Error ? error.message : String(error));
|
|
267
315
|
process.exitCode = 1;
|
package/dist/customer-main.js
CHANGED
|
@@ -17,7 +17,7 @@ const sdkVersions = new SdkVersionManager(process.cwd(), process.env.PLUGIN_SDK_
|
|
|
17
17
|
if (process.env.PLUGIN_DEV_WATCH === '1') {
|
|
18
18
|
for (const plugin of packaged.plugins) {
|
|
19
19
|
const entry = runtime.catalog().plugins.find(item => item.manifest.id === plugin.manifest.id);
|
|
20
|
-
if (entry && !entry.enabled)
|
|
20
|
+
if (entry && !entry.enabled && !(entry.manifest.configFields ?? []).some(field => field.required && !(field.type === 'secret' ? entry.secretConfigured?.[field.key] : entry.config[field.key] !== undefined && entry.config[field.key] !== '')))
|
|
21
21
|
runtime.configure(plugin.manifest.id, { enabled: true });
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -73,6 +73,10 @@ async function studioCatalog() {
|
|
|
73
73
|
return {
|
|
74
74
|
id: entry.manifest.id, name: entry.manifest.name, release: entry.manifest.release,
|
|
75
75
|
enabled: entry.enabled, manifest: entry.manifest, uiBundle,
|
|
76
|
+
config: entry.config, secretConfigured: entry.secretConfigured,
|
|
77
|
+
configCheckAvailable: entry.configCheckAvailable,
|
|
78
|
+
configurationRequired: (entry.manifest.configFields ?? []).some(field => field.required &&
|
|
79
|
+
!(field.type === 'secret' ? entry.secretConfigured?.[field.key] : entry.config[field.key] !== undefined && entry.config[field.key] !== '')),
|
|
76
80
|
nodes: entry.manifest.contributes.nodes?.length ?? 0,
|
|
77
81
|
actions: entry.manifest.contributes.actions?.length ?? 0,
|
|
78
82
|
package: packaged.downloads[entry.manifest.id] ? { mode: 'download' } : null,
|
|
@@ -197,6 +201,21 @@ const server = createServer(async (request, response) => {
|
|
|
197
201
|
send(response, 200, await studioCatalog());
|
|
198
202
|
return;
|
|
199
203
|
}
|
|
204
|
+
const studioPlugin = /^\/studio\/plugins\/([A-Za-z0-9_.-]+)\/(configure|check)$/.exec(path);
|
|
205
|
+
if (method === 'POST' && studioPlugin) {
|
|
206
|
+
if (request.headers.origin && ![`http://localhost:${port}`, `http://127.0.0.1:${port}`, `http://[::1]:${port}`].includes(request.headers.origin))
|
|
207
|
+
throw new ContractError('本地配置请求来源无效', 403);
|
|
208
|
+
if (studioPlugin[2] === 'configure') {
|
|
209
|
+
const input = await body(request);
|
|
210
|
+
const configured = runtime.configure(studioPlugin[1], {
|
|
211
|
+
config: object(input.config, 'config'), enabled: input.enabled === true,
|
|
212
|
+
});
|
|
213
|
+
send(response, 200, { enabled: configured.enabled });
|
|
214
|
+
}
|
|
215
|
+
else
|
|
216
|
+
send(response, 200, await runtime.checkConfig(studioPlugin[1]));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
200
219
|
if (method === 'GET' && path === '/studio/sdk') {
|
|
201
220
|
send(response, 200, await sdkVersions.status());
|
|
202
221
|
return;
|
package/dist/protocol/index.d.ts
CHANGED
|
@@ -50,6 +50,23 @@ export type PluginMenuContribution = {
|
|
|
50
50
|
order?: number;
|
|
51
51
|
icon?: string;
|
|
52
52
|
};
|
|
53
|
+
export type NodePlacement = {
|
|
54
|
+
slot: 'canvas.add.node';
|
|
55
|
+
group?: 'basic' | 'storyboard' | 'tools';
|
|
56
|
+
order?: number;
|
|
57
|
+
icon?: string;
|
|
58
|
+
};
|
|
59
|
+
export type ResourcePlacement = {
|
|
60
|
+
slot: 'canvas.add.resource';
|
|
61
|
+
order?: number;
|
|
62
|
+
icon?: string;
|
|
63
|
+
};
|
|
64
|
+
export type PlacementOverride = {
|
|
65
|
+
visible?: boolean;
|
|
66
|
+
group?: 'basic' | 'storyboard' | 'tools';
|
|
67
|
+
order?: number;
|
|
68
|
+
};
|
|
69
|
+
export type PlacementOverrides = Record<string, PlacementOverride>;
|
|
53
70
|
/** Platform-derived identity and location, shared by backend node handlers and queries. Never accepts a login token. */
|
|
54
71
|
export type PluginRuntimeContext = {
|
|
55
72
|
user: {
|
|
@@ -100,6 +117,7 @@ export type NodeDefinition = {
|
|
|
100
117
|
defaults?: Record<string, unknown>;
|
|
101
118
|
timeoutMs?: number;
|
|
102
119
|
menu?: PluginMenuContribution;
|
|
120
|
+
placement?: NodePlacement;
|
|
103
121
|
capabilities?: PluginNodeCapabilities;
|
|
104
122
|
runtime?: PluginNodeRuntimePolicy;
|
|
105
123
|
};
|
|
@@ -128,7 +146,8 @@ export type PluginAction = {
|
|
|
128
146
|
url?: string;
|
|
129
147
|
origin?: string;
|
|
130
148
|
};
|
|
131
|
-
menu
|
|
149
|
+
menu?: PluginMenuContribution;
|
|
150
|
+
placement?: ResourcePlacement;
|
|
132
151
|
/** Set only on the normalized view returned by listPluginActions(). */
|
|
133
152
|
legacyAssetSource?: boolean;
|
|
134
153
|
};
|
|
@@ -252,6 +271,7 @@ export type HostSnapshot = {
|
|
|
252
271
|
serviceId: string;
|
|
253
272
|
entry: CatalogEntry;
|
|
254
273
|
enabled: boolean;
|
|
274
|
+
placementOverrides?: PlacementOverrides;
|
|
255
275
|
uiBundle?: {
|
|
256
276
|
url: string;
|
|
257
277
|
digest: string;
|
|
@@ -268,6 +288,8 @@ export declare class ContractError extends Error {
|
|
|
268
288
|
}
|
|
269
289
|
export declare function object(value: unknown, path?: string): Record<string, unknown>;
|
|
270
290
|
export declare function validatePluginRuntimeContext(value: unknown): PluginRuntimeContext;
|
|
291
|
+
export declare function resolveNodePlacement(node: NodeDefinition): NodePlacement | null;
|
|
292
|
+
export declare function resolveResourcePlacement(action: PluginAction): ResourcePlacement | null;
|
|
271
293
|
export declare function identifier(value: unknown, path: string): string;
|
|
272
294
|
export declare function validateParameters(fields: ConfigField[], input: Record<string, unknown>): Record<string, unknown>;
|
|
273
295
|
/** Returns the interaction-oriented view while preserving legacy asset source manifests. */
|
package/dist/protocol/index.js
CHANGED
|
@@ -62,6 +62,41 @@ function validateMenu(value, path) {
|
|
|
62
62
|
if (menu.icon !== undefined && typeof menu.icon !== 'string')
|
|
63
63
|
throw new ContractError(`${path} 菜单图标无效`);
|
|
64
64
|
}
|
|
65
|
+
function validatePlacement(value, path, slot) {
|
|
66
|
+
if (value === undefined)
|
|
67
|
+
return;
|
|
68
|
+
const placement = keys(value, ['slot', 'group', 'order', 'icon'], path);
|
|
69
|
+
if (placement.slot !== slot)
|
|
70
|
+
throw new ContractError(`${path} 位置与贡献类型不匹配`);
|
|
71
|
+
if (slot === 'canvas.add.node') {
|
|
72
|
+
if (placement.group !== undefined && !['basic', 'storyboard', 'tools'].includes(String(placement.group)))
|
|
73
|
+
throw new ContractError(`${path} 节点分组无效`);
|
|
74
|
+
}
|
|
75
|
+
else if (placement.group !== undefined)
|
|
76
|
+
throw new ContractError(`${path} 素材入口不能设置节点分组`);
|
|
77
|
+
if (placement.order !== undefined && (typeof placement.order !== 'number' || !Number.isFinite(placement.order)))
|
|
78
|
+
throw new ContractError(`${path} 顺序无效`);
|
|
79
|
+
if (placement.icon !== undefined && typeof placement.icon !== 'string')
|
|
80
|
+
throw new ContractError(`${path} 图标无效`);
|
|
81
|
+
}
|
|
82
|
+
export function resolveNodePlacement(node) {
|
|
83
|
+
if (node.placement)
|
|
84
|
+
return { ...node.placement, group: node.placement.group ?? 'tools' };
|
|
85
|
+
if (node.menu?.group === 'resources')
|
|
86
|
+
return null;
|
|
87
|
+
return { slot: 'canvas.add.node', group: node.menu?.group ?? 'tools',
|
|
88
|
+
...(node.menu?.order !== undefined ? { order: node.menu.order } : {}),
|
|
89
|
+
...(node.menu?.icon !== undefined ? { icon: node.menu.icon } : {}) };
|
|
90
|
+
}
|
|
91
|
+
export function resolveResourcePlacement(action) {
|
|
92
|
+
if (action.placement)
|
|
93
|
+
return action.placement;
|
|
94
|
+
if (action.menu && action.menu.group !== 'resources')
|
|
95
|
+
return null;
|
|
96
|
+
return { slot: 'canvas.add.resource',
|
|
97
|
+
...(action.menu?.order !== undefined ? { order: action.menu.order } : {}),
|
|
98
|
+
...(action.menu?.icon !== undefined ? { icon: action.menu.icon } : {}) };
|
|
99
|
+
}
|
|
65
100
|
function optionalBoolean(value, path) {
|
|
66
101
|
if (value !== undefined && typeof value !== 'boolean')
|
|
67
102
|
throw new ContractError(`${path} 必须是布尔值`);
|
|
@@ -182,7 +217,7 @@ export function validateManifest(value) {
|
|
|
182
217
|
throw new ContractError('nodes 必须为数组');
|
|
183
218
|
const names = new Set();
|
|
184
219
|
for (const raw of c.nodes) {
|
|
185
|
-
const node = keys(raw, ['type', 'name', 'description', 'inputs', 'outputs', 'parameters', 'execution', 'ui', 'defaults', 'timeoutMs', 'menu', 'capabilities', 'runtime'], 'node');
|
|
220
|
+
const node = keys(raw, ['type', 'name', 'description', 'inputs', 'outputs', 'parameters', 'execution', 'ui', 'defaults', 'timeoutMs', 'menu', 'placement', 'capabilities', 'runtime'], 'node');
|
|
186
221
|
const id = identifier(node.type, 'node.type');
|
|
187
222
|
if (names.has(id))
|
|
188
223
|
throw new ContractError('节点标识重复');
|
|
@@ -192,6 +227,7 @@ export function validateManifest(value) {
|
|
|
192
227
|
if (node.parameters !== undefined)
|
|
193
228
|
validateFields(node.parameters);
|
|
194
229
|
validateMenu(node.menu, 'node.menu');
|
|
230
|
+
validatePlacement(node.placement, 'node.placement', 'canvas.add.node');
|
|
195
231
|
validateNodeCapabilities(node.capabilities);
|
|
196
232
|
validateNodeRuntime(node.runtime);
|
|
197
233
|
if (node.timeoutMs !== undefined && (typeof node.timeoutMs !== 'number' || !Number.isInteger(node.timeoutMs) || node.timeoutMs < 100 || node.timeoutMs > 3_600_000))
|
|
@@ -216,7 +252,7 @@ export function validateManifest(value) {
|
|
|
216
252
|
throw new ContractError('actions 必须为数组');
|
|
217
253
|
const actionIds = new Set();
|
|
218
254
|
for (const raw of c.actions) {
|
|
219
|
-
const action = keys(raw, ['id', 'name', 'description', 'action', 'presentation', 'mediaTypes', 'maxCount', 'ui', 'menu'], 'action');
|
|
255
|
+
const action = keys(raw, ['id', 'name', 'description', 'action', 'presentation', 'mediaTypes', 'maxCount', 'ui', 'menu', 'placement'], 'action');
|
|
220
256
|
const actionId = identifier(action.id, 'action.id');
|
|
221
257
|
if (actionIds.has(actionId))
|
|
222
258
|
throw new ContractError('操作标识重复');
|
|
@@ -231,6 +267,7 @@ export function validateManifest(value) {
|
|
|
231
267
|
throw new ContractError('操作素材类型无效');
|
|
232
268
|
boundedInteger(action.maxCount, '操作素材数量', 1, 256);
|
|
233
269
|
validateMenu(action.menu, 'action.menu');
|
|
270
|
+
validatePlacement(action.placement, 'action.placement', 'canvas.add.resource');
|
|
234
271
|
const ui = keys(action.ui, ['component', 'url', 'origin'], 'action.ui');
|
|
235
272
|
const component = optionalString(ui.component, 'action.ui.component');
|
|
236
273
|
if (component)
|
package/dist/studio/index.html
CHANGED
|
@@ -148,6 +148,14 @@ dialog::backdrop { background: #30384266; }
|
|
|
148
148
|
.status-badge.error { color: var(--danger); background: #fff1ef; }
|
|
149
149
|
.plugin-list { display: grid; gap: 8px; max-height: 224px; overflow: auto; }
|
|
150
150
|
.plugin-list:empty::before { content: "暂无可调试的插件"; padding: 18px; border: 1px dashed var(--line-strong); border-radius: 12px; color: var(--quiet); font-size: 12px; text-align: center; }
|
|
151
|
+
.plugin-config { display: grid; gap: 10px; margin-top: 18px; padding: 16px; border: 1px solid var(--line); border-radius: 12px; background: var(--soft); }
|
|
152
|
+
.plugin-config[hidden] { display: none; }
|
|
153
|
+
.plugin-config h4 { margin: 0; font-size: 13px; }
|
|
154
|
+
.plugin-config label { display: grid; gap: 5px; color: var(--muted); font-size: 12px; }
|
|
155
|
+
.plugin-config input:not([type="checkbox"]), .plugin-config select { width: 100%; min-height: 36px; padding: 6px 9px; border: 1px solid var(--line-strong); border-radius: 8px; background: #fff; }
|
|
156
|
+
.plugin-config button { min-height: 34px; padding: 0 12px; border: 1px solid var(--line-strong); border-radius: 8px; background: #fff; }
|
|
157
|
+
.plugin-config .config-actions { display: flex; gap: 8px; }
|
|
158
|
+
.plugin-config small { color: var(--quiet); }
|
|
151
159
|
.plugin-choice { position: relative; display: grid; grid-template-columns: 18px minmax(0, 1fr) auto; align-items: center; gap: 10px; min-height: 68px; border: 1px solid var(--line); border-radius: 12px; padding: 10px 12px; background: #fff; cursor: pointer; }
|
|
152
160
|
.plugin-choice:hover { border-color: var(--line-strong); background: var(--soft); }
|
|
153
161
|
.plugin-choice:has(input:checked) { border-color: #aab6c1; background: #f9fafb; }
|
|
@@ -263,6 +271,7 @@ dialog::backdrop { background: #30384266; }
|
|
|
263
271
|
<h1 id="connection-title">连接画布平台</h1>
|
|
264
272
|
<p class="setup-intro">输入平台地址,在新窗口授权后进入私人调试画布。</p>
|
|
265
273
|
<div class="sdk-card" hidden><strong>画布插件 SDK</strong><p class="sdk-message" role="status" aria-live="polite"></p><button class="sdk-action" type="button" hidden>升级 SDK</button></div>
|
|
274
|
+
<div class="form-actions"><button id="open-local-config" type="button">配置本地插件</button></div>
|
|
266
275
|
<form id="connect-form"><label for="platform-address">画布平台地址</label><input id="platform-address" type="url" placeholder="https://canvas.example.com" autocomplete="url" aria-describedby="address-help" required><p id="address-help" class="field-help">线上平台使用 HTTPS,本机平台可使用 HTTP。</p><div class="form-actions"><button id="connect-button" type="submit" disabled><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 10h11m-4-4 4 4-4 4M15.5 4h2v12h-2"/></svg><span>登录并连接</span></button></div></form>
|
|
267
276
|
<p id="setup-status" role="status" aria-live="polite"></p>
|
|
268
277
|
</section>
|
|
@@ -287,7 +296,7 @@ dialog::backdrop { background: #30384266; }
|
|
|
287
296
|
<div id="notice" class="notice" role="status" hidden></div>
|
|
288
297
|
<dialog id="panel" aria-labelledby="panel-title">
|
|
289
298
|
<div class="dialog-head"><div><h2 id="panel-title">插件管理</h2><p>选择调试插件并下载可用的插件包</p></div><button id="close" type="button" aria-label="关闭管理面板"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M4.5 4.5 15.5 15.5M15.5 4.5 4.5 15.5"/></svg></button></div>
|
|
290
|
-
<div class="dialog-body"><div class="dialog-main"><section class="target-section" aria-labelledby="target-title"><div class="section-heading"><h3 id="target-title">插件</h3><span id="panel-status" class="status-badge" role="status">正在连接</span></div><div id="plugin-list" class="plugin-list" role="radiogroup" aria-labelledby="target-title"></div><div class="plugin-actions"><button id="open-plugin" type="button"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 10h11m-4-4 4 4-4 4M16 4h2v12h-2"/></svg><span>在画布中调试</span></button><button id="download-package" type="button" disabled><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 2.5v10m-3.5-3.5L10 12.5 13.5 9M3.5 14v2.5h13V14"/></svg><span id="download-label">下载插件包</span></button></div><p id="package-note" class="package-note" role="status" aria-live="polite"></p></section>
|
|
299
|
+
<div class="dialog-body"><div class="dialog-main"><section class="target-section" aria-labelledby="target-title"><div class="section-heading"><h3 id="target-title">插件</h3><span id="panel-status" class="status-badge" role="status">正在连接</span></div><div id="plugin-list" class="plugin-list" role="radiogroup" aria-labelledby="target-title"></div><div class="plugin-actions"><button id="open-plugin" type="button"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 10h11m-4-4 4 4-4 4M16 4h2v12h-2"/></svg><span>在画布中调试</span></button><button id="download-package" type="button" disabled><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 2.5v10m-3.5-3.5L10 12.5 13.5 9M3.5 14v2.5h13V14"/></svg><span id="download-label">下载插件包</span></button></div><p id="package-note" class="package-note" role="status" aria-live="polite"></p><form id="plugin-config" class="plugin-config" hidden><h4>本地插件配置</h4><p id="config-status" role="status"></p><div id="config-fields"></div><div class="config-actions"><button type="submit">保存并启用</button><button id="config-check" type="button">检查配置</button></div></form></section>
|
|
291
300
|
<section class="session-section" aria-labelledby="session-title"><div class="section-heading"><h3 id="session-title">当前会话</h3><span id="active-plugin" class="section-context"></span></div><dl class="session-summary"><div><dt>开发服务</dt><dd id="connection">—</dd></div><div><dt>当前构建</dt><dd id="build">—</dd></div><div><dt>调试画布</dt><dd id="project">—</dd></div></dl><div class="sdk-card" hidden><strong>画布插件 SDK</strong><p class="sdk-message" role="status" aria-live="polite"></p><button class="sdk-action" type="button" hidden>升级 SDK</button></div><details class="technical-details"><summary>查看连接详情</summary><dl><dt>连接 ID</dt><dd id="connection-id">—</dd><dt>构建摘要</dt><dd id="build-full">—</dd><dt>项目 ID</dt><dd id="project-id">—</dd></dl></details></section></div>
|
|
292
301
|
<aside class="activity-section" aria-labelledby="activity-title"><div class="section-heading"><h3 id="activity-title">最近活动</h3><span id="activity-count" class="activity-count">0 条</span></div><p>当前工作台会话的连接与构建记录</p><div id="log" class="activity-list" role="log" aria-label="调试活动" aria-live="polite"></div></aside></div>
|
|
293
302
|
<div class="dialog-actions"><button id="disconnect-panel" type="button">退出连接</button><div><button id="panel-cancel" type="button">关闭</button><button id="reconnect" class="primary" type="button"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16.6 8.5A6.7 6.7 0 1 0 17 12M16.5 3.5v5h-5"/></svg><span>重新连接</span></button></div></div>
|
|
@@ -498,9 +507,44 @@ dialog::backdrop { background: #30384266; }
|
|
|
498
507
|
$('dock-expand').addEventListener('click', () => { manuallyCollapsed = false; expandedOverDialog = canvasDialogOpen; renderDock(); });
|
|
499
508
|
const chosen = () => catalog?.plugins.find(item => item.id === selected);
|
|
500
509
|
const panelPlugin = () => catalog?.plugins.find(item => item.id === panelSelected);
|
|
510
|
+
let configFormKey = '';
|
|
511
|
+
function renderConfig() {
|
|
512
|
+
const plugin = panelPlugin();
|
|
513
|
+
const form = $('plugin-config');
|
|
514
|
+
const fields = plugin?.manifest.configFields || [];
|
|
515
|
+
form.hidden = !fields.length;
|
|
516
|
+
if (!fields.length) { configFormKey = ''; return; }
|
|
517
|
+
const key = JSON.stringify([plugin.id, plugin.buildKey, plugin.config, plugin.secretConfigured]);
|
|
518
|
+
if (key === configFormKey) return;
|
|
519
|
+
configFormKey = key;
|
|
520
|
+
$('config-fields').replaceChildren(); $('config-status').textContent = plugin.configurationRequired ? '待配置:填写必填项后才能启用。' : '配置仅保存在本地开发服务中。';
|
|
521
|
+
for (const field of fields) {
|
|
522
|
+
const label = document.createElement('label');
|
|
523
|
+
label.textContent = `${field.label}${field.required ? ' *' : ''}`;
|
|
524
|
+
let input;
|
|
525
|
+
if (field.type === 'select') {
|
|
526
|
+
input = document.createElement('select');
|
|
527
|
+
const empty = document.createElement('option'); empty.value = ''; empty.textContent = '请选择'; input.append(empty);
|
|
528
|
+
for (const choice of field.options || []) { const option = document.createElement('option'); option.value = choice.value; option.textContent = choice.label; input.append(option); }
|
|
529
|
+
input.value = String(plugin.config?.[field.key] ?? field.default ?? '');
|
|
530
|
+
} else {
|
|
531
|
+
input = document.createElement('input');
|
|
532
|
+
input.type = field.type === 'secret' ? 'password' : field.type === 'number' ? 'number' : field.type === 'boolean' ? 'checkbox' : 'text';
|
|
533
|
+
if (field.type === 'boolean') input.checked = Boolean(plugin.config?.[field.key] ?? field.default);
|
|
534
|
+
else if (field.type !== 'secret') input.value = String(plugin.config?.[field.key] ?? field.default ?? '');
|
|
535
|
+
if (field.type === 'number') input.step = 'any';
|
|
536
|
+
}
|
|
537
|
+
input.name = field.key;
|
|
538
|
+
input.required = Boolean(field.required && (field.type !== 'secret' || !plugin.secretConfigured?.[field.key]));
|
|
539
|
+
label.append(input);
|
|
540
|
+
if (field.type === 'secret') { const hint = document.createElement('small'); hint.textContent = plugin.secretConfigured?.[field.key] ? '已配置;留空保留原密钥' : '尚未配置'; label.append(hint); }
|
|
541
|
+
$('config-fields').append(label);
|
|
542
|
+
}
|
|
543
|
+
$('config-check').hidden = !plugin.configCheckAvailable;
|
|
544
|
+
}
|
|
501
545
|
function renderPluginList() {
|
|
502
546
|
const plugins = catalog?.plugins || [];
|
|
503
|
-
const key = JSON.stringify(plugins.map(item => [item.id, item.name, item.nodes, item.actions, item.enabled]));
|
|
547
|
+
const key = JSON.stringify(plugins.map(item => [item.id, item.name, item.nodes, item.actions, item.enabled, item.configurationRequired]));
|
|
504
548
|
if (key !== pluginListKey) {
|
|
505
549
|
pluginListKey = key;
|
|
506
550
|
$('plugin-list').replaceChildren();
|
|
@@ -513,7 +557,7 @@ dialog::backdrop { background: #30384266; }
|
|
|
513
557
|
const id = document.createElement('code'); id.textContent = item.id;
|
|
514
558
|
copy.append(name, id);
|
|
515
559
|
const meta = document.createElement('span'); meta.className = `plugin-choice-meta${item.enabled ? '' : ' disabled'}`;
|
|
516
|
-
meta.textContent = item.enabled ? `${item.nodes} 个节点 / ${item.actions} 个操作` : '已停用';
|
|
560
|
+
meta.textContent = item.configurationRequired ? '待配置' : item.enabled ? `${item.nodes} 个节点 / ${item.actions} 个操作` : '已停用';
|
|
517
561
|
row.append(input, mark, copy, meta); $('plugin-list').append(row);
|
|
518
562
|
}
|
|
519
563
|
}
|
|
@@ -533,8 +577,8 @@ dialog::backdrop { background: #30384266; }
|
|
|
533
577
|
const plugin = chosen();
|
|
534
578
|
$('plugin-name').textContent = plugin?.name || '插件调试';
|
|
535
579
|
const building = catalog?.devBuild?.state;
|
|
536
|
-
$('state').textContent = building === 'error' ? '构建失败' : building === 'building' ? '正在构建' : frameFailed ? '画布加载失败' : busy ? (session ? '正在刷新' : '正在打开画布') : session ? (!session.active ? '会话已失效' : plugin?.buildKey === session.fingerprint ? '已连接' : '发现新构建') : '等待画布';
|
|
537
|
-
$('dot').className = `dot ${building === 'error' || frameFailed ? 'error' : building === 'building' ? 'warning' : session ? !session.active ? 'error' : plugin?.buildKey === session.fingerprint ? 'ready' : 'warning' : ''}`;
|
|
580
|
+
$('state').textContent = building === 'error' ? '构建失败' : building === 'building' ? '正在构建' : plugin?.configurationRequired ? '待配置' : frameFailed ? '画布加载失败' : busy ? (session ? '正在刷新' : '正在打开画布') : session ? (!session.active ? '会话已失效' : plugin?.buildKey === session.fingerprint ? '已连接' : '发现新构建') : '等待画布';
|
|
581
|
+
$('dot').className = `dot ${building === 'error' || frameFailed ? 'error' : building === 'building' || plugin?.configurationRequired ? 'warning' : session ? !session.active ? 'error' : plugin?.buildKey === session.fingerprint ? 'ready' : 'warning' : ''}`;
|
|
538
582
|
$('refresh').disabled = !session?.active || busy || frameFailed || Boolean(building && building !== 'ready');
|
|
539
583
|
const stateText = $('state').textContent;
|
|
540
584
|
$('panel-status').textContent = stateText;
|
|
@@ -547,6 +591,7 @@ dialog::backdrop { background: #30384266; }
|
|
|
547
591
|
$('build-full').textContent = plugin?.buildKey || '—';
|
|
548
592
|
$('project-id').textContent = session?.projectId || '—';
|
|
549
593
|
renderPluginList();
|
|
594
|
+
renderConfig();
|
|
550
595
|
const inspected = panelPlugin();
|
|
551
596
|
$('open-plugin').disabled = !inspected?.enabled || busy || packageDownloading;
|
|
552
597
|
$('download-package').disabled = !inspected?.package || packageDownloading;
|
|
@@ -659,13 +704,41 @@ dialog::backdrop { background: #30384266; }
|
|
|
659
704
|
render();
|
|
660
705
|
});
|
|
661
706
|
$('manage').addEventListener('click', () => { panelSelected = selected; packageMessage = ''; packageError = false; render(); panel.showModal(); $('plugin-list').querySelector('input:checked')?.focus(); renderDock(); });
|
|
662
|
-
|
|
707
|
+
$('open-local-config').addEventListener('click', () => { panelSelected = selected; packageMessage = ''; packageError = false; render(); panel.showModal(); $('plugin-list').querySelector('input:checked')?.focus(); });
|
|
708
|
+
panel.addEventListener('close', () => { renderDock(); ($('setup').hidden ? $('manage') : $('open-local-config')).focus(); });
|
|
663
709
|
$('close').addEventListener('click', () => panel.close());
|
|
664
710
|
$('panel-cancel').addEventListener('click', () => panel.close());
|
|
665
711
|
$('plugin-list').addEventListener('change', event => {
|
|
666
712
|
if (!event.target.matches('input[name="studio-plugin"]')) return;
|
|
667
713
|
panelSelected = event.target.value; packageMessage = ''; packageError = false; render();
|
|
668
714
|
});
|
|
715
|
+
$('plugin-config').addEventListener('submit', async event => {
|
|
716
|
+
event.preventDefault();
|
|
717
|
+
const plugin = panelPlugin(); if (!plugin) return;
|
|
718
|
+
const config = {};
|
|
719
|
+
for (const field of plugin.manifest.configFields || []) {
|
|
720
|
+
const input = $('plugin-config').elements.namedItem(field.key);
|
|
721
|
+
if (!input) continue;
|
|
722
|
+
if (field.type === 'secret' && !input.value) continue;
|
|
723
|
+
config[field.key] = field.type === 'boolean' ? input.checked : field.type === 'number' ? Number(input.value) : input.value;
|
|
724
|
+
}
|
|
725
|
+
try {
|
|
726
|
+
const response = await fetch(`/studio/plugins/${encodeURIComponent(plugin.id)}/configure`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ config, enabled: true }) });
|
|
727
|
+
const result = await response.json();
|
|
728
|
+
if (!response.ok) throw new Error(result.error || '配置保存失败');
|
|
729
|
+
$('config-status').textContent = '配置已保存,插件已启用。';
|
|
730
|
+
await loadCatalog();
|
|
731
|
+
} catch (error) { $('config-status').textContent = error.message || '配置保存失败'; }
|
|
732
|
+
});
|
|
733
|
+
$('config-check').addEventListener('click', async () => {
|
|
734
|
+
const plugin = panelPlugin(); if (!plugin) return;
|
|
735
|
+
try {
|
|
736
|
+
const response = await fetch(`/studio/plugins/${encodeURIComponent(plugin.id)}/check`, { method: 'POST' });
|
|
737
|
+
const result = await response.json();
|
|
738
|
+
if (!response.ok) throw new Error(result.error || '配置检查失败');
|
|
739
|
+
$('config-status').textContent = result.message || result.result?.message || '配置检查通过。';
|
|
740
|
+
} catch (error) { $('config-status').textContent = error.message || '配置检查失败'; }
|
|
741
|
+
});
|
|
669
742
|
$('open-plugin').addEventListener('click', () => {
|
|
670
743
|
if (!panelPlugin()?.enabled || busy) return;
|
|
671
744
|
selected = panelSelected; show(''); panel.close(); connect();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@easyai-canvas/canvas-plugin-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "EasyAI 画布插件开发工具与运行时",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"LICENSE"
|
|
32
32
|
],
|
|
33
33
|
"publishConfig": {
|
|
34
|
-
"access": "public"
|
|
34
|
+
"access": "public",
|
|
35
|
+
"registry": "https://registry.npmjs.org/"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
38
|
"better-sqlite3": "12.4.1",
|
|
@@ -30,6 +30,7 @@ export async function buildPluginPackage({
|
|
|
30
30
|
const plugin = (
|
|
31
31
|
await import(pathToFileURL(resolve(directory, 'server/plugin.mjs')).href)
|
|
32
32
|
).default;
|
|
33
|
+
if (!plugin?.manifest) throw new Error(`插件入口 ${serverEntry} 必须默认导出 definePlugin(...)`);
|
|
33
34
|
await build({
|
|
34
35
|
configFile: false,
|
|
35
36
|
publicDir: false,
|