@easyai-canvas/canvas-plugin-sdk 0.1.5 → 0.1.7

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.
@@ -74,13 +74,15 @@ 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] !== '')),
80
81
  nodes: entry.manifest.contributes.nodes?.length ?? 0,
81
82
  actions: entry.manifest.contributes.actions?.length ?? 0,
82
83
  package: packaged.downloads[entry.manifest.id] ? { mode: 'download' } : null,
83
- buildKey: createHash('sha256').update(JSON.stringify({ revision: catalog.revision, entry })).digest('hex'),
84
+ buildKey: createHash('sha256').update(JSON.stringify({ instanceId: catalog.instanceId,
85
+ entry, bundleDigest: bundle?.digest, cssDigest: bundle?.cssDigest })).digest('hex'),
84
86
  };
85
87
  }),
86
88
  };
@@ -201,11 +203,16 @@ const server = createServer(async (request, response) => {
201
203
  send(response, 200, await studioCatalog());
202
204
  return;
203
205
  }
204
- 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);
205
207
  if (method === 'POST' && studioPlugin) {
206
208
  if (request.headers.origin && ![`http://localhost:${port}`, `http://127.0.0.1:${port}`, `http://[::1]:${port}`].includes(request.headers.origin))
207
209
  throw new ContractError('本地配置请求来源无效', 403);
208
- 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') {
209
216
  const input = await body(request);
210
217
  const configured = runtime.configure(studioPlugin[1], {
211
218
  config: object(input.config, 'config'), enabled: input.enabled === true,
@@ -249,7 +256,7 @@ const server = createServer(async (request, response) => {
249
256
  }
250
257
  if (method === 'POST' && path === '/studio/open') {
251
258
  const input = await body(request);
252
- send(response, 200, await studio.openDebugSession(String(input.pluginId ?? '')));
259
+ send(response, 200, await studio.openDebugSession(typeof input.pluginId === 'string' && input.pluginId ? input.pluginId : undefined));
253
260
  return;
254
261
  }
255
262
  if (method === 'POST' && path === '/studio/embed-ticket') {
@@ -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; }
@@ -316,14 +329,15 @@ dialog::backdrop { background: #30384266; }
316
329
  </div>
317
330
  <div id="notice" class="notice" role="status" hidden></div>
318
331
  <dialog id="panel" aria-labelledby="panel-title">
319
- <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>
332
+ <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>
320
333
  <div class="dialog-body">
321
334
  <aside class="plugin-sidebar" aria-labelledby="target-title"><div class="section-heading"><h3 id="target-title">本地插件</h3><span id="plugin-count" class="activity-count">0 个</span></div><p>选择插件后在右侧管理</p><div id="plugin-list" class="plugin-list" role="radiogroup" aria-labelledby="target-title"></div></aside>
322
335
  <div class="plugin-detail"><p id="detail-empty" class="detail-empty" hidden>暂无插件。请在本地服务中注册插件。</p><div id="plugin-detail-content" class="plugin-detail-content">
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
- <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>
326
- <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>
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>
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>
329
343
  </div>
@@ -535,7 +549,54 @@ dialog::backdrop { background: #30384266; }
535
549
  $('dock-expand').addEventListener('click', () => { manuallyCollapsed = false; expandedOverDialog = canvasDialogOpen; renderDock(); });
536
550
  const chosen = () => catalog?.plugins.find(item => item.id === selected);
537
551
  const panelPlugin = () => catalog?.plugins.find(item => item.id === panelSelected);
538
- let configFormKey = '', configFeedback = null;
552
+ const activePluginId = () => session?.pluginId || preparedSession?.pluginId || selected;
553
+ const hasNewBuild = () => (session?.plugins || (session ? [{ pluginId: session.pluginId, fingerprint: session.fingerprint }] : []))
554
+ .some(item => item.active !== false && catalog?.plugins.find(plugin => plugin.id === item.pluginId)?.buildKey !== item.fingerprint) ||
555
+ Boolean(session && catalog?.plugins.some(plugin => plugin.enabled && !session.plugins?.some(item => item.pluginId === plugin.id && item.active)));
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
+ }
539
600
  function renderConfig() {
540
601
  const plugin = panelPlugin();
541
602
  const form = $('plugin-config');
@@ -599,29 +660,30 @@ dialog::backdrop { background: #30384266; }
599
660
  }
600
661
  const url = () => {
601
662
  const address = new URL('/developer/plugin-debug', catalog.platformOrigin);
602
- address.searchParams.set('pluginId', selected);
663
+ address.searchParams.set('pluginId', activePluginId());
603
664
  address.searchParams.set('connectionId', catalog.connectionId);
604
665
  if (preparedSession?.sessionId) address.searchParams.set('sessionId', preparedSession.sessionId);
605
666
  return address.toString();
606
667
  };
607
668
  function render() {
608
669
  const plugin = chosen();
609
- $('plugin-name').textContent = plugin?.name || '插件调试';
670
+ $('plugin-name').textContent = catalog?.plugins?.length ? `${catalog.plugins.filter(item => item.enabled).length} 个插件 · 调试画布` : '插件调试画布';
610
671
  const building = catalog?.devBuild?.state;
611
- $('state').textContent = building === 'error' ? '构建失败' : building === 'building' ? '正在构建' : plugin?.configurationRequired ? '待配置' : frameFailed ? '画布加载失败' : busy ? (session ? '正在刷新' : '正在打开画布') : session ? (!session.active ? '会话已失效' : plugin?.buildKey === session.fingerprint ? '已连接' : '发现新构建') : '等待画布';
612
- $('dot').className = `dot ${building === 'error' || frameFailed ? 'error' : building === 'building' || plugin?.configurationRequired ? 'warning' : session ? !session.active ? 'error' : plugin?.buildKey === session.fingerprint ? 'ready' : 'warning' : ''}`;
672
+ $('state').textContent = building === 'error' ? '构建失败' : building === 'building' ? '正在构建' : frameFailed ? '画布加载失败' : busy ? (session ? '正在刷新' : '正在打开画布') : session ? (!session.active ? '会话已失效' : hasNewBuild() ? '发现新构建' : '已连接') : '等待画布';
673
+ $('dot').className = `dot ${building === 'error' || frameFailed ? 'error' : building === 'building' ? 'warning' : session ? !session.active ? 'error' : hasNewBuild() ? 'warning' : 'ready' : ''}`;
613
674
  $('refresh').disabled = !session?.active || busy || frameFailed || Boolean(building && building !== 'ready');
614
675
  const stateText = $('state').textContent;
615
676
  $('panel-status').textContent = stateText;
616
677
  $('panel-status').className = `status-badge ${$('dot').classList.contains('error') ? 'error' : $('dot').classList.contains('ready') ? 'ready' : ''}`;
617
678
  $('connection').textContent = connectorStatus?.phase === 'connected' ? '已连接 · 本机' : '未连接';
618
- $('active-plugin').textContent = plugin?.name || '';
619
- $('build').textContent = building === 'error' ? '构建失败' : building === 'building' ? '构建中' : plugin?.buildKey ? plugin.buildKey.slice(0, 8) : '等待构建';
679
+ $('active-plugin').textContent = catalog?.plugins.filter(item => item.enabled).map(item => item.name).join('、') || '';
680
+ $('build').textContent = building === 'error' ? '构建失败' : building === 'building' ? '构建中' : `${catalog?.plugins.filter(item => item.enabled).length || 0} 个插件`;
620
681
  $('project').textContent = session?.active ? '私人调试画布' : session ? '会话已失效' : '等待打开';
621
682
  $('connection-id').textContent = catalog?.connectionId || '—';
622
- $('build-full').textContent = plugin?.buildKey || '—';
683
+ $('build-full').textContent = catalog?.plugins.filter(item => item.enabled).map(item => `${item.name}: ${item.buildKey}`).join('\n') || '—';
623
684
  $('project-id').textContent = session?.projectId || '—';
624
685
  renderPluginList();
686
+ renderPlacements();
625
687
  renderConfig();
626
688
  const inspected = panelPlugin();
627
689
  $('detail-empty').hidden = Boolean(inspected);
@@ -633,7 +695,7 @@ dialog::backdrop { background: #30384266; }
633
695
  $('detail-package').textContent = inspected?.package ? '插件包可下载' : '插件包不可用';
634
696
  $('detail-status').textContent = inspected?.configurationRequired ? '待配置' : inspected?.enabled ? '已启用' : '已停用';
635
697
  $('detail-status').className = `status-badge ${inspected?.configurationRequired ? '' : inspected?.enabled ? 'ready' : 'error'}`;
636
- $('open-plugin').disabled = !inspected?.enabled || busy || packageDownloading;
698
+ $('open-plugin').disabled = busy || packageDownloading;
637
699
  $('download-package').disabled = !inspected?.package || packageDownloading;
638
700
  $('download-package').setAttribute('aria-busy', String(packageDownloading));
639
701
  $('download-label').textContent = packageDownloading ? '正在准备…' : inspected?.package?.mode === 'build' ? '打包并下载' : '下载插件包';
@@ -649,13 +711,13 @@ dialog::backdrop { background: #30384266; }
649
711
  $('empty-text').textContent = '正在打开调试画布…';
650
712
  canvasDialogOpen = false; expandedOverDialog = false; renderDock(); render();
651
713
  try {
652
- const response = await fetch('/studio/open', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ pluginId: selected }) });
714
+ const response = await fetch('/studio/open', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' });
653
715
  const next = await response.json();
654
716
  if (!response.ok) throw new Error(next.message || next.error || '调试画布创建失败');
655
717
  preparedSession = next;
656
718
  } catch (error) { busy = false; frameLoading = false; frameFailed = true; $('empty-text').textContent = error.message; $('retry').hidden = false; show(error.message); log(error.message); render(); return; }
657
719
  frame.src = url();
658
- log(`打开真实画布:${selected}`);
720
+ log('打开全部插件的调试画布');
659
721
  readyTimer = setTimeout(() => {
660
722
  if (!session) {
661
723
  busy = false; frameLoading = false; frameFailed = true; $('retry').hidden = false;
@@ -695,7 +757,7 @@ dialog::backdrop { background: #30384266; }
695
757
  if (event.origin === window.location.origin && event.data?.type === 'plugin-debug-auth-complete') { void loadConnectionStatus(); return; }
696
758
  if (!catalog || event.origin !== catalog.platformOrigin || event.source !== frame.contentWindow) return;
697
759
  const data = event.data;
698
- if (!data || typeof data !== 'object' || data.protocol !== protocol || data.pluginId !== selected) return;
760
+ if (!data || typeof data !== 'object' || data.protocol !== protocol || data.pluginId !== activePluginId()) return;
699
761
  if (data.type === 'bootstrap.request' && preparedSession?.sessionId === data.sessionId &&
700
762
  typeof data.codeChallenge === 'string' && /^[A-Za-z0-9_-]{43,128}$/.test(data.codeChallenge)) {
701
763
  void (async () => {
@@ -704,7 +766,7 @@ dialog::backdrop { background: #30384266; }
704
766
  body: JSON.stringify({ debugSessionId: data.sessionId, codeChallenge: data.codeChallenge }) });
705
767
  const issued = await response.json();
706
768
  if (!response.ok) throw new Error(issued.message || issued.error || '画布登录失败');
707
- frame.contentWindow.postMessage({ protocol, type: 'bootstrap.ticket', pluginId: selected,
769
+ frame.contentWindow.postMessage({ protocol, type: 'bootstrap.ticket', pluginId: activePluginId(),
708
770
  sessionId: data.sessionId, ticket: issued.ticket }, catalog.platformOrigin);
709
771
  } catch (error) { show(error.message); log(error.message); }
710
772
  })();
@@ -726,6 +788,10 @@ dialog::backdrop { background: #30384266; }
726
788
  if (data.type === 'status' && data.status?.sessionId === session.sessionId) { session = data.status; if (!session.active) show('调试会话已失效,请在管理菜单中重新连接。'); render(); return; }
727
789
  if (data.type === 'refreshing' && data.requestId === waiting) { busy = true; log('正在保存画布并检查新构建'); render(); }
728
790
  if (data.type === 'refreshResult' && data.requestId === waiting) {
791
+ if (data.status?.sessionId !== session.sessionId) {
792
+ waiting = null; show('插件列表已更新,正在重新打开调试画布。', false);
793
+ void connect(); return;
794
+ }
729
795
  session = data.status; waiting = null; busy = Boolean(data.changed); frameLoading = Boolean(data.changed); show(data.changed ? '新构建已启用,正在重新加载画布…' : '当前已经是最新构建。', false); log(data.changed ? '构建已更新,画布正在重载' : '当前构建没有变化');
730
796
  if (data.changed) $('empty-text').textContent = '正在加载新构建…';
731
797
  if (data.changed) readyTimer = setTimeout(() => {
@@ -740,12 +806,12 @@ dialog::backdrop { background: #30384266; }
740
806
  $('refresh').addEventListener('click', () => {
741
807
  if (!session || busy) return;
742
808
  waiting = crypto.randomUUID(); busy = true;
743
- frame.contentWindow.postMessage({ protocol, type: 'refresh', sessionId: session.sessionId, pluginId: selected, requestId: waiting }, catalog.platformOrigin);
809
+ frame.contentWindow.postMessage({ protocol, type: 'refresh', sessionId: session.sessionId, pluginId: activePluginId(), requestId: waiting }, catalog.platformOrigin);
744
810
  render();
745
811
  });
746
812
  function openPanel(forConfiguration = false) {
747
813
  panelSelected = forConfiguration ? catalog?.plugins.find(item => item.configurationRequired)?.id || selected : selected;
748
- packageMessage = ''; packageError = false; configFeedback = null; configFormKey = ''; render(); panel.showModal();
814
+ packageMessage = ''; packageError = false; configFeedback = null; configFormKey = ''; placementFormKey = ''; render(); panel.showModal();
749
815
  const firstField = forConfiguration ? $('config-fields').querySelector('input, select') : null;
750
816
  (firstField || $('plugin-list').querySelector('input:checked') || $('close')).focus();
751
817
  renderDock();
@@ -759,6 +825,41 @@ dialog::backdrop { background: #30384266; }
759
825
  if (!event.target.matches('input[name="studio-plugin"]')) return;
760
826
  panelSelected = event.target.value; packageMessage = ''; packageError = false; configFeedback = null; render(); document.querySelector('.plugin-detail').scrollTop = 0;
761
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
+ });
762
863
  $('plugin-config').addEventListener('submit', async event => {
763
864
  event.preventDefault();
764
865
  const plugin = panelPlugin(); if (!plugin) return;
@@ -789,8 +890,7 @@ dialog::backdrop { background: #30384266; }
789
890
  } catch (error) { configFeedback = { pluginId: plugin.id, message: error.message || '配置检查失败' }; $('config-status').textContent = configFeedback.message; }
790
891
  });
791
892
  $('open-plugin').addEventListener('click', () => {
792
- if (!panelPlugin()?.enabled || busy) return;
793
- selected = panelSelected; show(''); panel.close(); connect();
893
+ panel.close();
794
894
  });
795
895
  $('download-package').addEventListener('click', async () => {
796
896
  const plugin = panelPlugin();
@@ -42,7 +42,7 @@ export declare class RemoteStudioConnector {
42
42
  }>;
43
43
  private post;
44
44
  private renewAccessToken;
45
- openDebugSession(pluginId: string): Promise<{
45
+ openDebugSession(pluginId?: string): Promise<{
46
46
  sessionId: string;
47
47
  projectId: string;
48
48
  connectionId: string;
@@ -119,7 +119,7 @@ export class RemoteStudioConnector {
119
119
  if (!this.ticket || this.phase !== 'connected')
120
120
  throw new Error('请先连接画布平台');
121
121
  await this.renewAccessToken();
122
- return this.post(`/canvas-workflow/plugin-debug-connectors/${this.ticket.connectionId}/open`, { pluginId }, true);
122
+ return this.post(`/canvas-workflow/plugin-debug-connectors/${this.ticket.connectionId}/open`, pluginId ? { pluginId } : {}, true);
123
123
  }
124
124
  async createEmbedTicket(debugSessionId, codeChallenge) {
125
125
  if (!this.ticket || this.phase !== 'connected')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easyai-canvas/canvas-plugin-sdk",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "EasyAI 画布插件开发工具与运行时",
5
5
  "type": "module",
6
6
  "engines": {
@@ -27,9 +27,11 @@ export async function buildPluginPackage({
27
27
  },
28
28
  },
29
29
  });
30
- const plugin = (
31
- await import(pathToFileURL(resolve(directory, 'server/plugin.mjs')).href)
32
- ).default;
30
+ const serverModule = resolve(directory, 'server/plugin.mjs');
31
+ const serverBytes = await readFile(serverModule);
32
+ const moduleUrl = new URL(pathToFileURL(serverModule).href);
33
+ moduleUrl.searchParams.set('build', createHash('sha256').update(serverBytes).digest('hex'));
34
+ const plugin = (await import(moduleUrl.href)).default;
33
35
  if (!plugin?.manifest) throw new Error(`插件入口 ${serverEntry} 必须默认导出 definePlugin(...)`);
34
36
  await build({
35
37
  configFile: false,