@easyai-canvas/canvas-plugin-sdk 0.1.6 → 0.1.8

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 CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  SDK 为商业专有软件。npm 公开下载不自动授予使用或再分发许可,具体权限以 EasyAI 客户协议为准。
6
6
 
7
+ 开发、构建和运行插件需使用 Node 22.22.0 或更新的 Node 22。请先切换 Node 版本,再运行以下初始化命令;`init` 会自动安装项目依赖,无需先在空目录执行 `npm i @easyai-canvas/canvas-plugin-sdk`。若依赖已在其他 Node 主版本下安装,切换后在项目目录运行 `npm ci`,以重装原生依赖。
8
+
7
9
  ```bash
8
10
  npx @easyai-canvas/canvas-plugin-sdk init my-plugin
9
11
  cd my-plugin
@@ -18,7 +18,9 @@ const args = process.argv.slice(3);
18
18
  function fail(message) { throw new Error(message); }
19
19
  function nodeVersion() {
20
20
  const [major, minor] = process.versions.node.split('.').map(Number);
21
- if (major !== 22 || minor < 22) fail('请使用 Node 22.22.0 或更新的 Node 22 版本');
21
+ if (major !== 22 || minor < 22) {
22
+ fail(`当前 Node ${process.versions.node} 不支持 SDK(要求 ${metadata.engines.node})。请先切换 Node 版本,再安装依赖或运行命令。`);
23
+ }
22
24
  }
23
25
  function safeName(value) {
24
26
  const name = value.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
@@ -121,6 +123,7 @@ async function init() {
121
123
  if (!['node', 'action', 'combined'].includes(template)) fail('模板只支持 node、action 或 combined');
122
124
  const targetArg = args.find((arg, index) => !arg.startsWith('--') && args[index - 1] !== '--template');
123
125
  if (!targetArg) fail('用法:easyai-plugin init <目录> [--template node|action|combined] [--no-install]');
126
+ nodeVersion();
124
127
  const target = resolve(targetArg);
125
128
  if (existsSync(target) && (await readdir(target)).length) fail('目标目录非空,不会覆盖已有文件');
126
129
  const slug = safeName(basename(target));
@@ -131,6 +134,7 @@ async function init() {
131
134
  const spec = process.env.EASYAI_SDK_INSTALL_SPEC ?? metadata.version;
132
135
  const project = {
133
136
  name: slug, version: '0.1.0', private: true, type: 'module',
137
+ engines: { node: metadata.engines.node },
134
138
  scripts: { dev: 'easyai-plugin dev', 'test:contract': 'easyai-plugin test:contract',
135
139
  build: 'easyai-plugin build', start: 'easyai-plugin start', doctor: 'easyai-plugin doctor' },
136
140
  dependencies: { [metadata.name]: spec, vue: '3.5.21' },
@@ -281,7 +285,13 @@ async function contractTest() {
281
285
  async function doctor() {
282
286
  const checks = [];
283
287
  const [major, minor] = process.versions.node.split('.').map(Number);
284
- checks.push({ name: 'Node 22.22+', ok: major === 22 && minor >= 22 });
288
+ const nodeCheck = `Node ${metadata.engines.node}(当前 ${process.versions.node})`;
289
+ if (major !== 22 || minor < 22) {
290
+ console.log(`✗ ${nodeCheck}`);
291
+ process.exitCode = 1;
292
+ return;
293
+ }
294
+ checks.push({ name: nodeCheck, ok: true });
285
295
  const entries = await pluginEntries().catch(() => []);
286
296
  checks.push({ name: `插件入口(${entries.length} 个)`, ok: entries.length > 0 });
287
297
  checks.push({ name: '插件 ID 唯一', ok: entries.length > 0 && (await build().then(() => true).catch(() => false)) });
@@ -74,6 +74,7 @@ async function studioCatalog() {
74
74
  id: entry.manifest.id, name: entry.manifest.name, release: entry.manifest.release,
75
75
  enabled: entry.enabled, manifest: entry.manifest, uiBundle,
76
76
  config: entry.config, secretConfigured: entry.secretConfigured,
77
+ studioPlacementOverrides: entry.studioPlacementOverrides,
77
78
  configCheckAvailable: entry.configCheckAvailable,
78
79
  configurationRequired: (entry.manifest.configFields ?? []).some(field => field.required &&
79
80
  !(field.type === 'secret' ? entry.secretConfigured?.[field.key] : entry.config[field.key] !== undefined && entry.config[field.key] !== '')),
@@ -202,11 +203,16 @@ const server = createServer(async (request, response) => {
202
203
  send(response, 200, await studioCatalog());
203
204
  return;
204
205
  }
205
- const studioPlugin = /^\/studio\/plugins\/([A-Za-z0-9_.-]+)\/(configure|check)$/.exec(path);
206
+ const studioPlugin = /^\/studio\/plugins\/([A-Za-z0-9_.-]+)\/(configure|check|placements)$/.exec(path);
206
207
  if (method === 'POST' && studioPlugin) {
207
208
  if (request.headers.origin && ![`http://localhost:${port}`, `http://127.0.0.1:${port}`, `http://[::1]:${port}`].includes(request.headers.origin))
208
209
  throw new ContractError('本地配置请求来源无效', 403);
209
- if (studioPlugin[2] === 'configure') {
210
+ if (studioPlugin[2] === 'placements') {
211
+ const input = await body(request);
212
+ const configured = runtime.configureStudioPlacements(studioPlugin[1], input.placementOverrides);
213
+ send(response, 200, { placementOverrides: configured.studioPlacementOverrides });
214
+ }
215
+ else if (studioPlugin[2] === 'configure') {
210
216
  const input = await body(request);
211
217
  const configured = runtime.configure(studioPlugin[1], {
212
218
  config: object(input.config, 'config'), enabled: input.enabled === true,
@@ -181,6 +181,8 @@ export type CatalogEntry = {
181
181
  /** Secret values are never returned; only their configured state is exposed. */
182
182
  secretConfigured?: Record<string, boolean>;
183
183
  configCheckAvailable?: boolean;
184
+ /** Local Studio preview only; formal installations use their own placement overrides. */
185
+ studioPlacementOverrides?: PlacementOverrides;
184
186
  };
185
187
  export type Catalog = {
186
188
  protocol: typeof PROTOCOL;
@@ -23,6 +23,7 @@ export declare class Runtime {
23
23
  config?: Record<string, unknown>;
24
24
  expectedVersion?: number;
25
25
  }): import("../protocol/index.js").CatalogEntry;
26
+ configureStudioPlacements(id: string, value: unknown): import("../protocol/index.js").CatalogEntry;
26
27
  catalog(): Catalog;
27
28
  checkConfig(id: string): Promise<{
28
29
  supported: boolean;
@@ -4,7 +4,7 @@ import { fork } from 'node:child_process';
4
4
  import { RuntimeStore } from './store.js';
5
5
  import { acquireInstanceLock } from './instance-lock.js';
6
6
  import { fieldsConfig } from './plugin-config.js';
7
- import { PROTOCOL, ContractError, validateManifest, validateExecution, validateResult, validatePluginRuntimeContext } from '../protocol/index.js';
7
+ import { PROTOCOL, ContractError, object, validateManifest, validateExecution, validateResult, validatePluginRuntimeContext, resolveNodePlacement, resolveResourcePlacement, listPluginActions } from '../protocol/index.js';
8
8
  const defaultPlugins = process.env.PLUGIN_BUILTINS === '0'
9
9
  ? []
10
10
  : (await import('../plugins/catalog.js')).getPlugins();
@@ -60,6 +60,35 @@ export class Runtime {
60
60
  this.store.setPlugin(id, { enabled: patch.enabled, config, expectedVersion: patch.expectedVersion });
61
61
  return this.catalog().plugins.find(p => p.manifest.id === id);
62
62
  }
63
+ configureStudioPlacements(id, value) {
64
+ const manifest = this.catalog().plugins.find(entry => entry.manifest.id === id)?.manifest;
65
+ if (!manifest)
66
+ throw new ContractError('插件不存在', 404);
67
+ const overrides = object(value, '画布入口');
68
+ const allowed = new Map([
69
+ ...(manifest.contributes.nodes ?? []).filter(node => resolveNodePlacement(node)).map(node => [`node:${node.type}`, 'node']),
70
+ ...listPluginActions(manifest).filter(action => resolveResourcePlacement(action)).map(action => [`action:${action.id}`, 'resource']),
71
+ ]);
72
+ const cleaned = {};
73
+ for (const [key, raw] of Object.entries(overrides)) {
74
+ const kind = allowed.get(key);
75
+ if (!kind)
76
+ throw new ContractError(`未知画布入口 ${key}`);
77
+ const item = object(raw, key);
78
+ for (const field of Object.keys(item))
79
+ if (!['visible', 'group', 'order'].includes(field))
80
+ throw new ContractError(`${key}.${field} 不支持`);
81
+ if (item.visible !== undefined && typeof item.visible !== 'boolean')
82
+ throw new ContractError(`${key}.visible 无效`);
83
+ if (item.group !== undefined && (kind !== 'node' || !['basic', 'storyboard', 'tools'].includes(String(item.group))))
84
+ throw new ContractError(`${key}.group 无效`);
85
+ if (item.order !== undefined && (typeof item.order !== 'number' || !Number.isFinite(item.order)))
86
+ throw new ContractError(`${key}.order 无效`);
87
+ cleaned[key] = item;
88
+ }
89
+ this.store.setStudioPlacements(id, cleaned);
90
+ return this.catalog().plugins.find(entry => entry.manifest.id === id);
91
+ }
63
92
  catalog() {
64
93
  return { protocol: PROTOCOL, revision: this.store.revision, instanceId: this.store.instanceId, plugins: this.plugins.map(p => {
65
94
  const stored = this.store.getPlugin(p.manifest.id);
@@ -73,7 +102,8 @@ export class Runtime {
73
102
  for (const f of manifest.configFields ?? [])
74
103
  if (f.type === 'secret')
75
104
  delete config[f.key];
76
- return { ...this.presentations[p.manifest.id], manifest, enabled: stored.enabled, config, configVersion: stored.configVersion, secretConfigured, configCheckAvailable: Boolean(p.checkConfig) };
105
+ return { ...this.presentations[p.manifest.id], manifest, enabled: stored.enabled, config, configVersion: stored.configVersion, secretConfigured, configCheckAvailable: Boolean(p.checkConfig),
106
+ studioPlacementOverrides: this.store.getStudioPlacements(p.manifest.id) };
77
107
  }) };
78
108
  }
79
109
  async checkConfig(id) {
@@ -1,4 +1,4 @@
1
- import type { ExecutionRecord, ExecutionRequest } from '../protocol/index.js';
1
+ import type { ExecutionRecord, ExecutionRequest, PlacementOverrides } from '../protocol/index.js';
2
2
  export type ExecutionSnapshot = {
3
3
  config: Record<string, unknown>;
4
4
  release: string;
@@ -14,6 +14,8 @@ export declare class RuntimeStore {
14
14
  syncDeployment(digest: string): void;
15
15
  get revision(): number;
16
16
  get instanceId(): string;
17
+ getStudioPlacements(id: string): PlacementOverrides;
18
+ setStudioPlacements(id: string, overrides: PlacementOverrides): void;
17
19
  getPlugin(id: string): {
18
20
  enabled: boolean;
19
21
  config: Record<string, unknown>;
@@ -44,6 +44,17 @@ export class RuntimeStore {
44
44
  }
45
45
  get revision() { return this.db.prepare('SELECT revision FROM meta WHERE id = 1').get().revision; }
46
46
  get instanceId() { return this.db.prepare("SELECT value FROM settings WHERE key='instance_id'").get().value; }
47
+ getStudioPlacements(id) {
48
+ const row = this.db.prepare('SELECT value FROM settings WHERE key = ?').get(`studio_placements:${id}`);
49
+ return row ? JSON.parse(row.value) : {};
50
+ }
51
+ setStudioPlacements(id, overrides) {
52
+ this.db.transaction(() => {
53
+ this.db.prepare('INSERT INTO settings(key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value')
54
+ .run(`studio_placements:${id}`, JSON.stringify(overrides));
55
+ this.db.prepare('UPDATE meta SET revision=revision+1 WHERE id=1').run();
56
+ })();
57
+ }
47
58
  getPlugin(id) {
48
59
  const row = this.db.prepare('SELECT enabled, config, config_version FROM plugins WHERE id = ?').get(id);
49
60
  return row ? { enabled: Boolean(row.enabled), config: JSON.parse(row.config), configVersion: row.config_version } : { enabled: false, config: {}, configVersion: 0 };
@@ -171,6 +171,18 @@ dialog::backdrop { background: #30384266; }
171
171
  .plugin-config #config-status { margin: 0; color: var(--quiet); font-size: 12px; line-height: 1.5; }
172
172
  #config-fields { display: grid; gap: 14px; }
173
173
  .plugin-config small { color: var(--quiet); }
174
+ .placement-list { display: grid; gap: 10px; margin-bottom: 14px; }
175
+ .placement-item { display: grid; gap: 10px; padding: 14px; border: 1px solid var(--line); border-radius: 10px; background: var(--soft); }
176
+ .placement-item > div:first-child { display: flex; justify-content: space-between; gap: 12px; align-items: center; }
177
+ .placement-item strong { font-size: 12px; }
178
+ .placement-item label { color: var(--muted); font-size: 12px; }
179
+ .placement-item .placement-controls { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
180
+ .placement-item .placement-controls label { display: grid; gap: 5px; }
181
+ .placement-item select, .placement-item input[type="number"] { min-width: 0; min-height: 36px; padding: 5px 9px; border: 1px solid var(--line-strong); border-radius: 7px; background: #fff; }
182
+ .placement-actions { display: flex; gap: 9px; align-items: center; }
183
+ .placement-actions button { min-height: 36px; padding: 0 12px; border: 1px solid var(--line-strong); border-radius: 8px; background: #fff; font-size: 12px; }
184
+ .placement-actions button[type="submit"] { border-color: var(--accent); background: var(--accent); color: #fff; }
185
+ #placement-status { margin: 10px 0 0; color: var(--quiet); font-size: 12px; }
174
186
  .plugin-choice { position: relative; display: grid; grid-template-columns: 18px minmax(0, 1fr); align-items: center; gap: 10px; min-height: 78px; border: 1px solid var(--line); border-radius: 12px; padding: 11px 12px; background: #fff; cursor: pointer; }
175
187
  .plugin-choice:hover { border-color: var(--line-strong); background: var(--soft); }
176
188
  .plugin-choice:has(input:checked) { border-color: #aab6c1; background: #f9fafb; }
@@ -243,6 +255,7 @@ dialog::backdrop { background: #30384266; }
243
255
  }
244
256
  @media (max-width: 520px) {
245
257
  dialog { height: min(760px, calc(100dvh - 32px)); }
258
+ .placement-item .placement-controls { grid-template-columns: 1fr; }
246
259
  .setup-header { height: 58px; padding: 0 18px; }
247
260
  .brand strong { font-size: 18px; }
248
261
  .brand { font-size: 12px; }
@@ -323,6 +336,7 @@ dialog::backdrop { background: #30384266; }
323
336
  <div class="detail-head"><div><h3 id="detail-name">插件</h3><code id="detail-id"></code></div><span id="detail-status" class="status-badge" role="status">检查中</span></div>
324
337
  <div class="detail-meta"><span id="detail-nodes">0 个节点</span><span id="detail-actions">0 个操作</span><span id="detail-package">插件包不可用</span></div>
325
338
  <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>
339
+ <div class="configuration-section" aria-labelledby="placement-title"><h4 id="placement-title">画布入口</h4><p>可覆盖插件声明的默认位置,仅用于当前本地服务的调试画布。</p><form id="placement-form"><div id="placement-list" class="placement-list"></div><div class="placement-actions"><button type="submit">保存入口设置</button><button id="placement-reset" type="button">恢复默认位置</button></div><p id="placement-status" role="status" aria-live="polite"></p></form></div>
326
340
  <div class="configuration-section" aria-labelledby="configuration-title"><h4 id="configuration-title">插件配置</h4><p>配置仅保存在本地开发服务中。</p><div id="no-config" class="no-config" hidden>此插件无需配置,可以在画布中使用或下载插件包。</div><form id="plugin-config" class="plugin-config" hidden><p id="config-status" role="status" aria-live="polite"></p><div id="config-fields"></div><div class="config-actions"><button type="submit">保存并启用</button><button id="config-check" type="button">检查配置</button></div></form></div>
327
341
  <div class="secondary-panels"><details><summary>连接与构建 <span id="panel-status" class="status-badge" role="status">正在连接</span></summary><div class="panel-content"><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><span id="active-plugin" class="section-context" hidden></span><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></div></details><details><summary>最近活动 <span id="activity-count" class="activity-count">0 条</span></summary><div class="panel-content"><div id="log" class="activity-list" role="log" aria-label="调试活动" aria-live="polite"></div></div></details></div>
328
342
  </div></div>
@@ -539,7 +553,50 @@ dialog::backdrop { background: #30384266; }
539
553
  const hasNewBuild = () => (session?.plugins || (session ? [{ pluginId: session.pluginId, fingerprint: session.fingerprint }] : []))
540
554
  .some(item => item.active !== false && catalog?.plugins.find(plugin => plugin.id === item.pluginId)?.buildKey !== item.fingerprint) ||
541
555
  Boolean(session && catalog?.plugins.some(plugin => plugin.enabled && !session.plugins?.some(item => item.pluginId === plugin.id && item.active)));
542
- let configFormKey = '', configFeedback = null;
556
+ let configFormKey = '', configFeedback = null, placementFormKey = '';
557
+ const placementGroups = { basic: '基础节点', storyboard: '分镜与导演', tools: '工具与应用' };
558
+ function placementEntries(plugin) {
559
+ if (!plugin) return [];
560
+ const manifest = plugin.manifest;
561
+ const nodes = (manifest.contributes.nodes || []).filter(node => node.placement || node.menu?.group !== 'resources').map(node => ({
562
+ key: `node:${node.type}`, name: node.name, kind: 'node',
563
+ group: node.placement ? node.placement.group || 'tools' : node.menu?.group || 'tools',
564
+ order: node.placement ? node.placement.order ?? 1000 : node.menu?.order ?? 1000,
565
+ }));
566
+ const actions = [...(manifest.contributes.actions || [])];
567
+ for (const source of manifest.contributes.assetSources || []) if (!actions.some(action => action.id === source.id)) actions.push(source);
568
+ const resources = actions.filter(action => !action.menu || action.menu.group === 'resources' || action.placement).map(action => ({
569
+ key: `action:${action.id}`, name: action.name, kind: 'resource',
570
+ order: action.placement ? action.placement.order ?? 1000 : action.menu?.order ?? 1000,
571
+ }));
572
+ return [...nodes, ...resources].sort((a, b) => a.order - b.order || a.key.localeCompare(b.key));
573
+ }
574
+ function renderPlacements() {
575
+ const plugin = panelPlugin(), entries = placementEntries(plugin);
576
+ const form = $('placement-form'); form.hidden = !entries.length;
577
+ const key = JSON.stringify([plugin?.id, plugin?.manifest, plugin?.studioPlacementOverrides]);
578
+ if (key === placementFormKey) return;
579
+ placementFormKey = key;
580
+ $('placement-list').replaceChildren(); $('placement-status').textContent = '';
581
+ for (const entry of entries) {
582
+ const saved = plugin.studioPlacementOverrides?.[entry.key] || {};
583
+ const card = document.createElement('div'); card.className = 'placement-item'; card.dataset.key = entry.key;
584
+ card.dataset.group = entry.group || ''; card.dataset.order = String(entry.order);
585
+ const heading = document.createElement('div'), name = document.createElement('strong'); name.textContent = entry.name;
586
+ const visibility = document.createElement('label'), checkbox = document.createElement('input');
587
+ checkbox.type = 'checkbox'; checkbox.className = 'placement-visible'; checkbox.checked = saved.visible !== false;
588
+ visibility.append(checkbox, ' 显示入口'); heading.append(name, visibility); card.append(heading);
589
+ const controls = document.createElement('div'); controls.className = 'placement-controls';
590
+ if (entry.kind === 'node') {
591
+ const label = document.createElement('label'), select = document.createElement('select'); select.className = 'placement-group';
592
+ for (const [value, title] of Object.entries(placementGroups)) { const option = document.createElement('option'); option.value = value; option.textContent = title; select.append(option); }
593
+ select.value = saved.group ?? entry.group; label.append('显示分组', select); controls.append(label);
594
+ }
595
+ const label = document.createElement('label'), order = document.createElement('input');
596
+ order.type = 'number'; order.step = '1'; order.required = true; order.className = 'placement-order'; order.value = String(saved.order ?? entry.order);
597
+ label.append('排序值', order); controls.append(label); card.append(controls); $('placement-list').append(card);
598
+ }
599
+ }
543
600
  function renderConfig() {
544
601
  const plugin = panelPlugin();
545
602
  const form = $('plugin-config');
@@ -626,6 +683,7 @@ dialog::backdrop { background: #30384266; }
626
683
  $('build-full').textContent = catalog?.plugins.filter(item => item.enabled).map(item => `${item.name}: ${item.buildKey}`).join('\n') || '—';
627
684
  $('project-id').textContent = session?.projectId || '—';
628
685
  renderPluginList();
686
+ renderPlacements();
629
687
  renderConfig();
630
688
  const inspected = panelPlugin();
631
689
  $('detail-empty').hidden = Boolean(inspected);
@@ -753,7 +811,7 @@ dialog::backdrop { background: #30384266; }
753
811
  });
754
812
  function openPanel(forConfiguration = false) {
755
813
  panelSelected = forConfiguration ? catalog?.plugins.find(item => item.configurationRequired)?.id || selected : selected;
756
- packageMessage = ''; packageError = false; configFeedback = null; configFormKey = ''; render(); panel.showModal();
814
+ packageMessage = ''; packageError = false; configFeedback = null; configFormKey = ''; placementFormKey = ''; render(); panel.showModal();
757
815
  const firstField = forConfiguration ? $('config-fields').querySelector('input, select') : null;
758
816
  (firstField || $('plugin-list').querySelector('input:checked') || $('close')).focus();
759
817
  renderDock();
@@ -767,6 +825,41 @@ dialog::backdrop { background: #30384266; }
767
825
  if (!event.target.matches('input[name="studio-plugin"]')) return;
768
826
  panelSelected = event.target.value; packageMessage = ''; packageError = false; configFeedback = null; render(); document.querySelector('.plugin-detail').scrollTop = 0;
769
827
  });
828
+ $('placement-form').addEventListener('submit', async event => {
829
+ event.preventDefault();
830
+ const plugin = panelPlugin(); if (!plugin) return;
831
+ const placementOverrides = {};
832
+ for (const card of $('placement-list').children) {
833
+ const entry = {};
834
+ if (!card.querySelector('.placement-visible').checked) entry.visible = false;
835
+ const group = card.querySelector('.placement-group')?.value;
836
+ if (group && group !== card.dataset.group) entry.group = group;
837
+ const order = Number(card.querySelector('.placement-order').value);
838
+ if (!Number.isFinite(order)) { $('placement-status').textContent = '请填写有效的排序值。'; return; }
839
+ if (order !== Number(card.dataset.order)) entry.order = order;
840
+ if (Object.keys(entry).length) placementOverrides[card.dataset.key] = entry;
841
+ }
842
+ const changed = JSON.stringify(plugin.studioPlacementOverrides || {}) !== JSON.stringify(placementOverrides);
843
+ try {
844
+ const response = await fetch(`/studio/plugins/${encodeURIComponent(plugin.id)}/placements`, {
845
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ placementOverrides }),
846
+ });
847
+ const result = await response.json();
848
+ if (!response.ok) throw new Error(result.message || result.error || '入口设置保存失败');
849
+ await loadCatalog();
850
+ const activeHere = session?.active && session.plugins?.some(item => item.pluginId === plugin.id && item.active);
851
+ $('placement-status').textContent = changed && activeHere ? '已保存,正在刷新调试画布…' : '入口设置已保存。打开调试画布后生效。';
852
+ if (changed && activeHere) $('refresh').click();
853
+ } catch (error) { $('placement-status').textContent = error.message || '入口设置保存失败'; }
854
+ });
855
+ $('placement-reset').addEventListener('click', () => {
856
+ for (const card of $('placement-list').children) {
857
+ card.querySelector('.placement-visible').checked = true;
858
+ if (card.querySelector('.placement-group')) card.querySelector('.placement-group').value = card.dataset.group;
859
+ card.querySelector('.placement-order').value = card.dataset.order;
860
+ }
861
+ $('placement-form').requestSubmit();
862
+ });
770
863
  $('plugin-config').addEventListener('submit', async event => {
771
864
  event.preventDefault();
772
865
  const plugin = panelPlugin(); if (!plugin) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easyai-canvas/canvas-plugin-sdk",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "EasyAI 画布插件开发工具与运行时",
5
5
  "type": "module",
6
6
  "engines": {