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

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
@@ -15,6 +15,8 @@ npm start
15
15
 
16
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
17
 
18
+ `init` 还会在项目根目录生成 `AGENTS.md`,供 Codex 等编程助手了解插件目录、SDK 边界与验证命令。项目约定变化时,请与代码一起维护该文件。
19
+
18
20
  开发工作台位于 `http://localhost:4310/studio`,按插件展示配置表单和待配置状态。`npm run build` 为新结构分别输出 `dist/plugins/<pluginId>.zip` 与摘要文件;管理员可分别导入,或用 `npm start` 运行同一服务下的多个插件。
19
21
 
20
22
  节点在 `contributes.nodes[].placement` 声明 `{ slot: 'canvas.add.node', group: 'tools', order: 100 }`,素材选择操作在 `contributes.actions[].placement` 声明 `{ slot: 'canvas.add.resource', order: 100 }`。节点分组还可选 `basic`、`storyboard`;素材入口不使用节点分组。插件的 Vue 组件负责内部样式与交互,画布负责添加入口及拖动、权限等边界。旧版 `menu` 声明继续可用;未知位置或贡献能力与位置不匹配会在打包校验时报错。
@@ -138,6 +138,7 @@ async function init() {
138
138
  };
139
139
  const files = {
140
140
  'package.json': `${JSON.stringify(project, null, 2)}\n`,
141
+ 'AGENTS.md': await readFile(join(packageRoot, 'templates/AGENTS.md'), 'utf8'),
141
142
  [`src/plugins/${slug}/plugin.ts`]: pluginSource(id, title, template),
142
143
  [`src/plugins/${slug}/ui.ts`]: uiSource(template),
143
144
  [`test/${slug}.test.ts`]: testSource(id, template, slug),
@@ -80,7 +80,8 @@ async function studioCatalog() {
80
80
  nodes: entry.manifest.contributes.nodes?.length ?? 0,
81
81
  actions: entry.manifest.contributes.actions?.length ?? 0,
82
82
  package: packaged.downloads[entry.manifest.id] ? { mode: 'download' } : null,
83
- buildKey: createHash('sha256').update(JSON.stringify({ revision: catalog.revision, entry })).digest('hex'),
83
+ buildKey: createHash('sha256').update(JSON.stringify({ instanceId: catalog.instanceId,
84
+ entry, bundleDigest: bundle?.digest, cssDigest: bundle?.cssDigest })).digest('hex'),
84
85
  };
85
86
  }),
86
87
  };
@@ -249,7 +250,7 @@ const server = createServer(async (request, response) => {
249
250
  }
250
251
  if (method === 'POST' && path === '/studio/open') {
251
252
  const input = await body(request);
252
- send(response, 200, await studio.openDebugSession(String(input.pluginId ?? '')));
253
+ send(response, 200, await studio.openDebugSession(typeof input.pluginId === 'string' && input.pluginId ? input.pluginId : undefined));
253
254
  return;
254
255
  }
255
256
  if (method === 'POST' && path === '/studio/embed-ticket') {
@@ -127,36 +127,51 @@ button:focus-visible, select:focus-visible, input:focus-visible, summary:focus-v
127
127
  .empty button { justify-self: center; min-height: 40px; border: 0; border-radius: 10px; padding: 0 16px; font-size: 12px; font-weight: 600; }
128
128
 
129
129
  /* Plugin management dialog */
130
- dialog { width: min(820px, calc(100vw - 32px)); max-width: calc(100vw - 32px); max-height: min(720px, calc(100dvh - 32px)); margin: auto; overflow: hidden; border: 1px solid var(--line-strong); border-radius: 18px; padding: 0; color: var(--ink); background: #fff; box-shadow: var(--floating-shadow); }
130
+ dialog { width: min(1040px, calc(100vw - 32px)); max-width: calc(100vw - 32px); max-height: min(900px, calc(100dvh - 32px)); margin: auto; overflow: hidden; border: 1px solid var(--line-strong); border-radius: 18px; padding: 0; color: var(--ink); background: #fff; box-shadow: var(--floating-shadow); }
131
131
  dialog[open] { display: flex; flex-direction: column; }
132
132
  dialog::backdrop { background: #30384266; }
133
- .dialog-head { flex: none; display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 24px 28px 20px; border-bottom: 1px solid var(--line); }
134
- .dialog-head h2 { margin: 0 0 5px; font-size: 20px; line-height: 1.35; font-weight: 600; }
133
+ .dialog-head { flex: none; display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 24px 30px 20px; border-bottom: 1px solid var(--line); }
134
+ .dialog-head h2 { margin: 0 0 5px; font-size: 22px; line-height: 1.35; font-weight: 650; }
135
135
  .dialog-head p { margin: 0; color: var(--quiet); font-size: 12px; }
136
136
  .dialog-head button { display: grid; place-items: center; width: 36px; height: 36px; flex: none; border: 0; border-radius: 9px; color: var(--quiet); background: transparent; }
137
137
  .dialog-head button:hover { color: var(--ink); background: var(--page); }
138
138
  .dialog-head svg { width: 18px; height: 18px; }
139
- .dialog-body { flex: 1; min-height: 0; display: grid; grid-template-columns: minmax(0, 1fr) 290px; overflow: hidden; }
140
- .dialog-main { min-width: 0; overflow: auto; padding: 0 28px 26px; }
141
- .target-section, .session-section { padding: 24px 0; }
142
- .target-section { border-bottom: 1px solid var(--line); }
139
+ .dialog-body { flex: 1; min-height: 0; display: grid; grid-template-columns: minmax(248px, 30%) minmax(0, 1fr); overflow: hidden; }
140
+ .plugin-sidebar { display: flex; flex-direction: column; min-width: 0; min-height: 0; padding: 24px 16px 20px 24px; border-right: 1px solid var(--line); background: var(--soft); }
141
+ .plugin-sidebar > p { margin: -6px 0 18px; color: var(--quiet); font-size: 12px; line-height: 1.5; }
142
+ .plugin-detail { min-width: 0; overflow: auto; padding: 27px 32px 30px; }
143
+ .detail-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
144
+ .detail-head h3 { margin: 0 0 5px; font-size: 20px; font-weight: 650; line-height: 1.35; }
145
+ .detail-head code { display: block; color: var(--quiet); font: 11px/1.5 var(--font-code); overflow-wrap: anywhere; }
146
+ .detail-meta { display: flex; flex-wrap: wrap; gap: 8px; margin: 17px 0 0; }
147
+ .detail-meta span { padding: 6px 10px; border: 1px solid var(--line); border-radius: 7px; color: var(--muted); background: var(--soft); font-size: 11px; }
148
+ .detail-empty { color: var(--quiet); font-size: 13px; }
149
+ .detail-empty[hidden], .plugin-detail-content[hidden] { display: none; }
143
150
  .section-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 15px; }
144
151
  .section-heading h3 { margin: 0; font-size: 13px; font-weight: 700; }
145
152
  .status-badge { display: inline-flex; align-items: center; gap: 7px; padding: 6px 10px; border-radius: 999px; color: var(--warning); background: #faf1df; font-size: 11px; font-weight: 600; white-space: nowrap; }
146
153
  .status-badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
147
154
  .status-badge.ready { color: var(--success); background: #e9f4ef; }
148
155
  .status-badge.error { color: var(--danger); background: #fff1ef; }
149
- .plugin-list { display: grid; gap: 8px; max-height: 224px; overflow: auto; }
156
+ .plugin-list { display: grid; align-content: start; gap: 8px; min-height: 0; overflow: auto; padding-right: 7px; }
150
157
  .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); }
158
+ .configuration-section { margin-top: 28px; padding-top: 22px; border-top: 1px solid var(--line); }
159
+ .configuration-section h4 { margin: 0; font-size: 15px; font-weight: 650; }
160
+ .configuration-section > p { margin: 7px 0 16px; color: var(--quiet); font-size: 12px; line-height: 1.5; }
161
+ .no-config { padding: 16px; border: 1px solid var(--line); border-radius: 10px; color: var(--muted); background: var(--soft); font-size: 12px; }
162
+ .no-config[hidden] { display: none; }
163
+ .plugin-config { display: grid; gap: 14px; padding: 18px; border: 1px solid var(--line); border-radius: 12px; background: var(--soft); }
152
164
  .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; }
165
+ .plugin-config label { display: grid; gap: 7px; color: var(--muted); font-size: 12px; font-weight: 600; }
166
+ .plugin-config input:not([type="checkbox"]), .plugin-config select { width: 100%; min-height: 42px; padding: 8px 11px; border: 1px solid var(--line-strong); border-radius: 9px; color: var(--ink); background: #fff; font-weight: 400; }
167
+ .plugin-config input[type="checkbox"] { width: 18px; height: 18px; }
168
+ .plugin-config button { min-height: 40px; padding: 0 14px; border: 1px solid var(--line-strong); border-radius: 9px; color: var(--ink); background: #fff; font-size: 12px; font-weight: 600; }
169
+ .plugin-config .config-actions { display: flex; flex-wrap: wrap; gap: 8px; }
170
+ .plugin-config .config-actions button[type="submit"] { border-color: var(--accent); color: #fff; background: var(--accent); }
171
+ .plugin-config #config-status { margin: 0; color: var(--quiet); font-size: 12px; line-height: 1.5; }
172
+ #config-fields { display: grid; gap: 14px; }
158
173
  .plugin-config small { color: var(--quiet); }
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; }
174
+ .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; }
160
175
  .plugin-choice:hover { border-color: var(--line-strong); background: var(--soft); }
161
176
  .plugin-choice:has(input:checked) { border-color: #aab6c1; background: #f9fafb; }
162
177
  .plugin-choice:focus-within { outline: 2px solid #8e9ca9; outline-offset: -3px; }
@@ -167,16 +182,25 @@ dialog::backdrop { background: #30384266; }
167
182
  .plugin-choice-copy strong, .plugin-choice-copy code { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
168
183
  .plugin-choice-copy strong { color: var(--ink); font-size: 13px; font-weight: 600; }
169
184
  .plugin-choice-copy code { color: var(--quiet); font: 400 10px/1.4 var(--font-code); }
170
- .plugin-choice-meta { color: var(--quiet); font-size: 11px; white-space: nowrap; }
185
+ .plugin-choice-meta { grid-column: 2; color: var(--quiet); font-size: 11px; white-space: nowrap; }
171
186
  .plugin-choice-meta.disabled { color: var(--danger); }
172
- .plugin-actions { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
173
- .plugin-actions button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-height: 40px; border: 1px solid var(--line-strong); border-radius: 10px; padding: 0 12px; color: var(--ink); background: #fff; box-shadow: var(--control-shadow); font-size: 12px; font-weight: 600; }
187
+ .plugin-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 22px; }
188
+ .plugin-actions button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-height: 42px; border: 1px solid var(--line-strong); border-radius: 10px; padding: 0 14px; color: var(--ink); background: #fff; box-shadow: var(--control-shadow); font-size: 12px; font-weight: 600; }
174
189
  .plugin-actions button:hover:not(:disabled) { background: var(--page); }
175
- .plugin-actions #open-plugin { background: #f2f4f6; }
190
+ .plugin-actions #open-plugin { border-color: var(--accent); color: #fff; background: var(--accent); }
191
+ .plugin-actions #open-plugin:hover:not(:disabled) { background: #252b31; }
176
192
  .plugin-actions svg { width: 16px; height: 16px; flex: none; }
177
193
  .package-note { margin: 10px 0 0; color: var(--quiet); font-size: 11px; line-height: 1.5; }
178
194
  .package-note:empty { display: none; }
179
195
  .package-note.error { color: var(--danger); }
196
+ .secondary-panels { display: grid; gap: 12px; margin-top: 26px; }
197
+ .secondary-panels > details { border: 1px solid var(--line); border-radius: 11px; background: #fff; }
198
+ .secondary-panels > details > summary { display: flex; align-items: center; gap: 8px; padding: 14px 16px; font-size: 12px; font-weight: 650; cursor: pointer; list-style: none; }
199
+ .secondary-panels > details > summary::-webkit-details-marker { display: none; }
200
+ .secondary-panels > details > summary::after { content: "+"; margin-left: auto; color: var(--quiet); font-size: 18px; font-weight: 400; line-height: 1; }
201
+ .secondary-panels > details[open] > summary::after { content: "−"; }
202
+ .secondary-panels .panel-content { padding: 0 16px 16px; }
203
+ .secondary-panels .activity-list { max-height: 220px; }
180
204
  .section-context { overflow: hidden; color: var(--quiet); font-size: 11px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
181
205
  .session-summary { margin: 0; }
182
206
  .session-summary > div { display: flex; align-items: center; justify-content: space-between; gap: 14px; min-height: 51px; border-top: 1px solid #e9ebea; }
@@ -188,10 +212,8 @@ dialog::backdrop { background: #30384266; }
188
212
  .technical-details dl { display: grid; grid-template-columns: 84px minmax(0, 1fr); gap: 10px 12px; margin: 14px 0 0; padding: 14px; border: 1px solid var(--line); border-radius: 12px; background: var(--soft); font-size: 11px; }
189
213
  .technical-details dt { color: var(--quiet); }
190
214
  .technical-details dd { margin: 0; overflow-wrap: anywhere; font-family: var(--font-code); }
191
- .activity-section { display: flex; flex-direction: column; min-width: 0; overflow: hidden; padding: 24px 18px; border-left: 1px solid var(--line); background: var(--soft); }
192
- .activity-section > p { margin: -6px 0 16px; color: var(--quiet); font-size: 12px; line-height: 1.5; }
193
215
  .activity-count { color: var(--quiet); font-size: 11px; }
194
- .activity-list { flex: 1; min-height: 120px; max-height: 350px; overflow: auto; border-top: 1px solid var(--line); }
216
+ .activity-list { min-height: 80px; overflow: auto; border-top: 1px solid var(--line); }
195
217
  .activity-list:empty::before { content: "暂无活动"; display: block; padding: 25px; color: var(--quiet); font-size: 12px; text-align: center; }
196
218
  .activity-item { display: grid; grid-template-columns: 58px minmax(0, 1fr); gap: 10px; padding: 11px 2px; color: var(--muted); font-size: 12px; line-height: 1.5; }
197
219
  .activity-item + .activity-item { border-top: 1px solid #eceeed; }
@@ -211,10 +233,13 @@ dialog::backdrop { background: #30384266; }
211
233
  .diagnostics { padding: 32px 46px 38px; border-top: 1px solid var(--line); border-left: 0; }
212
234
  }
213
235
  @media (max-width: 760px) {
214
- .dialog-body { grid-template-columns: 1fr; overflow: auto; }
215
- .dialog-main { overflow: visible; }
216
- .activity-section { min-height: 180px; border-top: 1px solid var(--line); border-left: 0; }
217
- .activity-list { max-height: 180px; }
236
+ .dialog-body { display: block; overflow: auto; }
237
+ .plugin-sidebar { padding: 18px 20px; border-right: 0; border-bottom: 1px solid var(--line); }
238
+ .plugin-sidebar > p { margin-bottom: 12px; }
239
+ .plugin-list { display: flex; flex: none; overflow-x: auto; padding: 2px 0 8px; }
240
+ .plugin-choice { flex: 0 0 min(245px, 75vw); }
241
+ .plugin-detail { overflow: visible; padding: 24px 20px; }
242
+ .detail-head { position: sticky; top: 0; z-index: 1; margin: -24px -20px 0; padding: 14px 20px; border-bottom: 1px solid var(--line); background: #fff; }
218
243
  }
219
244
  @media (max-width: 520px) {
220
245
  dialog { height: min(760px, calc(100dvh - 32px)); }
@@ -238,21 +263,17 @@ dialog::backdrop { background: #30384266; }
238
263
  .controls .button-label { display: none; }
239
264
  .dialog-head { padding: 18px 20px; }
240
265
  .dialog-head button { width: 44px; height: 44px; margin: -6px -6px 0 0; }
241
- .dialog-main { padding-inline: 20px; }
242
- .target-section, .session-section { padding-block: 18px; }
266
+ .detail-head h3 { font-size: 18px; }
243
267
  .section-heading { margin-bottom: 12px; }
244
268
  .session-summary > div { min-height: 42px; }
245
269
  .technical-details { padding-top: 12px; }
246
- .activity-section { padding: 18px 20px; }
247
270
  .dialog-actions { padding: 12px 20px; }
248
271
  .dialog-actions button { min-height: 44px; padding-inline: 9px; }
249
272
  .dock button { min-height: 44px; padding-inline: 0; }
250
273
  .dock-grip, .dock-collapse { width: 44px; }
251
274
  }
252
275
  @media (max-width: 375px) {
253
- .plugin-choice { grid-template-columns: 18px minmax(0, 1fr); }
254
276
  .plugin-choice-copy code { white-space: normal; overflow-wrap: anywhere; }
255
- .plugin-choice-meta { grid-column: 2; }
256
277
  .dialog-actions { flex-wrap: wrap; }
257
278
  .dialog-actions > div { margin-left: auto; }
258
279
  .dialog-actions button { padding-inline: 8px; }
@@ -295,10 +316,17 @@ dialog::backdrop { background: #30384266; }
295
316
  </div>
296
317
  <div id="notice" class="notice" role="status" hidden></div>
297
318
  <dialog id="panel" aria-labelledby="panel-title">
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>
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>
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>
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>
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>
320
+ <div class="dialog-body">
321
+ <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
+ <div class="plugin-detail"><p id="detail-empty" class="detail-empty" hidden>暂无插件。请在本地服务中注册插件。</p><div id="plugin-detail-content" class="plugin-detail-content">
323
+ <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
+ <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>
327
+ <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
+ </div></div>
329
+ </div>
302
330
  <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>
303
331
  </dialog>
304
332
  <script>
@@ -507,17 +535,23 @@ dialog::backdrop { background: #30384266; }
507
535
  $('dock-expand').addEventListener('click', () => { manuallyCollapsed = false; expandedOverDialog = canvasDialogOpen; renderDock(); });
508
536
  const chosen = () => catalog?.plugins.find(item => item.id === selected);
509
537
  const panelPlugin = () => catalog?.plugins.find(item => item.id === panelSelected);
510
- let configFormKey = '';
538
+ const activePluginId = () => session?.pluginId || preparedSession?.pluginId || selected;
539
+ const hasNewBuild = () => (session?.plugins || (session ? [{ pluginId: session.pluginId, fingerprint: session.fingerprint }] : []))
540
+ .some(item => item.active !== false && catalog?.plugins.find(plugin => plugin.id === item.pluginId)?.buildKey !== item.fingerprint) ||
541
+ Boolean(session && catalog?.plugins.some(plugin => plugin.enabled && !session.plugins?.some(item => item.pluginId === plugin.id && item.active)));
542
+ let configFormKey = '', configFeedback = null;
511
543
  function renderConfig() {
512
544
  const plugin = panelPlugin();
513
545
  const form = $('plugin-config');
514
- const fields = plugin?.manifest.configFields || [];
546
+ const fields = plugin?.manifest?.configFields || [];
515
547
  form.hidden = !fields.length;
548
+ $('no-config').hidden = !plugin || Boolean(fields.length);
516
549
  if (!fields.length) { configFormKey = ''; return; }
517
550
  const key = JSON.stringify([plugin.id, plugin.buildKey, plugin.config, plugin.secretConfigured]);
518
551
  if (key === configFormKey) return;
519
552
  configFormKey = key;
520
- $('config-fields').replaceChildren(); $('config-status').textContent = plugin.configurationRequired ? '待配置:填写必填项后才能启用。' : '配置仅保存在本地开发服务中。';
553
+ $('config-fields').replaceChildren();
554
+ $('config-status').textContent = configFeedback?.pluginId === plugin.id ? configFeedback.message : plugin.configurationRequired ? '待配置:填写必填项后才能启用。' : '可以修改配置并保存。';
521
555
  for (const field of fields) {
522
556
  const label = document.createElement('label');
523
557
  label.textContent = `${field.label}${field.required ? ' *' : ''}`;
@@ -544,6 +578,7 @@ dialog::backdrop { background: #30384266; }
544
578
  }
545
579
  function renderPluginList() {
546
580
  const plugins = catalog?.plugins || [];
581
+ $('plugin-count').textContent = `${plugins.length} 个`;
547
582
  const key = JSON.stringify(plugins.map(item => [item.id, item.name, item.nodes, item.actions, item.enabled, item.configurationRequired]));
548
583
  if (key !== pluginListKey) {
549
584
  pluginListKey = key;
@@ -568,32 +603,41 @@ dialog::backdrop { background: #30384266; }
568
603
  }
569
604
  const url = () => {
570
605
  const address = new URL('/developer/plugin-debug', catalog.platformOrigin);
571
- address.searchParams.set('pluginId', selected);
606
+ address.searchParams.set('pluginId', activePluginId());
572
607
  address.searchParams.set('connectionId', catalog.connectionId);
573
608
  if (preparedSession?.sessionId) address.searchParams.set('sessionId', preparedSession.sessionId);
574
609
  return address.toString();
575
610
  };
576
611
  function render() {
577
612
  const plugin = chosen();
578
- $('plugin-name').textContent = plugin?.name || '插件调试';
613
+ $('plugin-name').textContent = catalog?.plugins?.length ? `${catalog.plugins.filter(item => item.enabled).length} 个插件 · 调试画布` : '插件调试画布';
579
614
  const building = catalog?.devBuild?.state;
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' : ''}`;
615
+ $('state').textContent = building === 'error' ? '构建失败' : building === 'building' ? '正在构建' : frameFailed ? '画布加载失败' : busy ? (session ? '正在刷新' : '正在打开画布') : session ? (!session.active ? '会话已失效' : hasNewBuild() ? '发现新构建' : '已连接') : '等待画布';
616
+ $('dot').className = `dot ${building === 'error' || frameFailed ? 'error' : building === 'building' ? 'warning' : session ? !session.active ? 'error' : hasNewBuild() ? 'warning' : 'ready' : ''}`;
582
617
  $('refresh').disabled = !session?.active || busy || frameFailed || Boolean(building && building !== 'ready');
583
618
  const stateText = $('state').textContent;
584
619
  $('panel-status').textContent = stateText;
585
620
  $('panel-status').className = `status-badge ${$('dot').classList.contains('error') ? 'error' : $('dot').classList.contains('ready') ? 'ready' : ''}`;
586
621
  $('connection').textContent = connectorStatus?.phase === 'connected' ? '已连接 · 本机' : '未连接';
587
- $('active-plugin').textContent = plugin?.name || '';
588
- $('build').textContent = building === 'error' ? '构建失败' : building === 'building' ? '构建中' : plugin?.buildKey ? plugin.buildKey.slice(0, 8) : '等待构建';
622
+ $('active-plugin').textContent = catalog?.plugins.filter(item => item.enabled).map(item => item.name).join('、') || '';
623
+ $('build').textContent = building === 'error' ? '构建失败' : building === 'building' ? '构建中' : `${catalog?.plugins.filter(item => item.enabled).length || 0} 个插件`;
589
624
  $('project').textContent = session?.active ? '私人调试画布' : session ? '会话已失效' : '等待打开';
590
625
  $('connection-id').textContent = catalog?.connectionId || '—';
591
- $('build-full').textContent = plugin?.buildKey || '—';
626
+ $('build-full').textContent = catalog?.plugins.filter(item => item.enabled).map(item => `${item.name}: ${item.buildKey}`).join('\n') || '—';
592
627
  $('project-id').textContent = session?.projectId || '—';
593
628
  renderPluginList();
594
629
  renderConfig();
595
630
  const inspected = panelPlugin();
596
- $('open-plugin').disabled = !inspected?.enabled || busy || packageDownloading;
631
+ $('detail-empty').hidden = Boolean(inspected);
632
+ $('plugin-detail-content').hidden = !inspected;
633
+ $('detail-name').textContent = inspected?.name || '插件';
634
+ $('detail-id').textContent = inspected?.id || '';
635
+ $('detail-nodes').textContent = `${inspected?.nodes || 0} 个节点`;
636
+ $('detail-actions').textContent = `${inspected?.actions || 0} 个操作`;
637
+ $('detail-package').textContent = inspected?.package ? '插件包可下载' : '插件包不可用';
638
+ $('detail-status').textContent = inspected?.configurationRequired ? '待配置' : inspected?.enabled ? '已启用' : '已停用';
639
+ $('detail-status').className = `status-badge ${inspected?.configurationRequired ? '' : inspected?.enabled ? 'ready' : 'error'}`;
640
+ $('open-plugin').disabled = busy || packageDownloading;
597
641
  $('download-package').disabled = !inspected?.package || packageDownloading;
598
642
  $('download-package').setAttribute('aria-busy', String(packageDownloading));
599
643
  $('download-label').textContent = packageDownloading ? '正在准备…' : inspected?.package?.mode === 'build' ? '打包并下载' : '下载插件包';
@@ -609,13 +653,13 @@ dialog::backdrop { background: #30384266; }
609
653
  $('empty-text').textContent = '正在打开调试画布…';
610
654
  canvasDialogOpen = false; expandedOverDialog = false; renderDock(); render();
611
655
  try {
612
- const response = await fetch('/studio/open', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ pluginId: selected }) });
656
+ const response = await fetch('/studio/open', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' });
613
657
  const next = await response.json();
614
658
  if (!response.ok) throw new Error(next.message || next.error || '调试画布创建失败');
615
659
  preparedSession = next;
616
660
  } catch (error) { busy = false; frameLoading = false; frameFailed = true; $('empty-text').textContent = error.message; $('retry').hidden = false; show(error.message); log(error.message); render(); return; }
617
661
  frame.src = url();
618
- log(`打开真实画布:${selected}`);
662
+ log('打开全部插件的调试画布');
619
663
  readyTimer = setTimeout(() => {
620
664
  if (!session) {
621
665
  busy = false; frameLoading = false; frameFailed = true; $('retry').hidden = false;
@@ -655,7 +699,7 @@ dialog::backdrop { background: #30384266; }
655
699
  if (event.origin === window.location.origin && event.data?.type === 'plugin-debug-auth-complete') { void loadConnectionStatus(); return; }
656
700
  if (!catalog || event.origin !== catalog.platformOrigin || event.source !== frame.contentWindow) return;
657
701
  const data = event.data;
658
- if (!data || typeof data !== 'object' || data.protocol !== protocol || data.pluginId !== selected) return;
702
+ if (!data || typeof data !== 'object' || data.protocol !== protocol || data.pluginId !== activePluginId()) return;
659
703
  if (data.type === 'bootstrap.request' && preparedSession?.sessionId === data.sessionId &&
660
704
  typeof data.codeChallenge === 'string' && /^[A-Za-z0-9_-]{43,128}$/.test(data.codeChallenge)) {
661
705
  void (async () => {
@@ -664,7 +708,7 @@ dialog::backdrop { background: #30384266; }
664
708
  body: JSON.stringify({ debugSessionId: data.sessionId, codeChallenge: data.codeChallenge }) });
665
709
  const issued = await response.json();
666
710
  if (!response.ok) throw new Error(issued.message || issued.error || '画布登录失败');
667
- frame.contentWindow.postMessage({ protocol, type: 'bootstrap.ticket', pluginId: selected,
711
+ frame.contentWindow.postMessage({ protocol, type: 'bootstrap.ticket', pluginId: activePluginId(),
668
712
  sessionId: data.sessionId, ticket: issued.ticket }, catalog.platformOrigin);
669
713
  } catch (error) { show(error.message); log(error.message); }
670
714
  })();
@@ -686,6 +730,10 @@ dialog::backdrop { background: #30384266; }
686
730
  if (data.type === 'status' && data.status?.sessionId === session.sessionId) { session = data.status; if (!session.active) show('调试会话已失效,请在管理菜单中重新连接。'); render(); return; }
687
731
  if (data.type === 'refreshing' && data.requestId === waiting) { busy = true; log('正在保存画布并检查新构建'); render(); }
688
732
  if (data.type === 'refreshResult' && data.requestId === waiting) {
733
+ if (data.status?.sessionId !== session.sessionId) {
734
+ waiting = null; show('插件列表已更新,正在重新打开调试画布。', false);
735
+ void connect(); return;
736
+ }
689
737
  session = data.status; waiting = null; busy = Boolean(data.changed); frameLoading = Boolean(data.changed); show(data.changed ? '新构建已启用,正在重新加载画布…' : '当前已经是最新构建。', false); log(data.changed ? '构建已更新,画布正在重载' : '当前构建没有变化');
690
738
  if (data.changed) $('empty-text').textContent = '正在加载新构建…';
691
739
  if (data.changed) readyTimer = setTimeout(() => {
@@ -700,17 +748,24 @@ dialog::backdrop { background: #30384266; }
700
748
  $('refresh').addEventListener('click', () => {
701
749
  if (!session || busy) return;
702
750
  waiting = crypto.randomUUID(); busy = true;
703
- frame.contentWindow.postMessage({ protocol, type: 'refresh', sessionId: session.sessionId, pluginId: selected, requestId: waiting }, catalog.platformOrigin);
751
+ frame.contentWindow.postMessage({ protocol, type: 'refresh', sessionId: session.sessionId, pluginId: activePluginId(), requestId: waiting }, catalog.platformOrigin);
704
752
  render();
705
753
  });
706
- $('manage').addEventListener('click', () => { panelSelected = selected; packageMessage = ''; packageError = false; render(); panel.showModal(); $('plugin-list').querySelector('input:checked')?.focus(); renderDock(); });
707
- $('open-local-config').addEventListener('click', () => { panelSelected = selected; packageMessage = ''; packageError = false; render(); panel.showModal(); $('plugin-list').querySelector('input:checked')?.focus(); });
754
+ function openPanel(forConfiguration = false) {
755
+ panelSelected = forConfiguration ? catalog?.plugins.find(item => item.configurationRequired)?.id || selected : selected;
756
+ packageMessage = ''; packageError = false; configFeedback = null; configFormKey = ''; render(); panel.showModal();
757
+ const firstField = forConfiguration ? $('config-fields').querySelector('input, select') : null;
758
+ (firstField || $('plugin-list').querySelector('input:checked') || $('close')).focus();
759
+ renderDock();
760
+ }
761
+ $('manage').addEventListener('click', () => openPanel());
762
+ $('open-local-config').addEventListener('click', () => openPanel(true));
708
763
  panel.addEventListener('close', () => { renderDock(); ($('setup').hidden ? $('manage') : $('open-local-config')).focus(); });
709
764
  $('close').addEventListener('click', () => panel.close());
710
765
  $('panel-cancel').addEventListener('click', () => panel.close());
711
766
  $('plugin-list').addEventListener('change', event => {
712
767
  if (!event.target.matches('input[name="studio-plugin"]')) return;
713
- panelSelected = event.target.value; packageMessage = ''; packageError = false; render();
768
+ panelSelected = event.target.value; packageMessage = ''; packageError = false; configFeedback = null; render(); document.querySelector('.plugin-detail').scrollTop = 0;
714
769
  });
715
770
  $('plugin-config').addEventListener('submit', async event => {
716
771
  event.preventDefault();
@@ -726,9 +781,10 @@ dialog::backdrop { background: #30384266; }
726
781
  const response = await fetch(`/studio/plugins/${encodeURIComponent(plugin.id)}/configure`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ config, enabled: true }) });
727
782
  const result = await response.json();
728
783
  if (!response.ok) throw new Error(result.error || '配置保存失败');
729
- $('config-status').textContent = '配置已保存,插件已启用。';
784
+ configFeedback = { pluginId: plugin.id, message: '配置已保存,插件已启用。' };
785
+ $('config-status').textContent = configFeedback.message;
730
786
  await loadCatalog();
731
- } catch (error) { $('config-status').textContent = error.message || '配置保存失败'; }
787
+ } catch (error) { configFeedback = { pluginId: plugin.id, message: error.message || '配置保存失败' }; $('config-status').textContent = configFeedback.message; }
732
788
  });
733
789
  $('config-check').addEventListener('click', async () => {
734
790
  const plugin = panelPlugin(); if (!plugin) return;
@@ -736,12 +792,12 @@ dialog::backdrop { background: #30384266; }
736
792
  const response = await fetch(`/studio/plugins/${encodeURIComponent(plugin.id)}/check`, { method: 'POST' });
737
793
  const result = await response.json();
738
794
  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 || '配置检查失败'; }
795
+ configFeedback = { pluginId: plugin.id, message: result.message || result.result?.message || '配置检查通过。' };
796
+ $('config-status').textContent = configFeedback.message;
797
+ } catch (error) { configFeedback = { pluginId: plugin.id, message: error.message || '配置检查失败' }; $('config-status').textContent = configFeedback.message; }
741
798
  });
742
799
  $('open-plugin').addEventListener('click', () => {
743
- if (!panelPlugin()?.enabled || busy) return;
744
- selected = panelSelected; show(''); panel.close(); connect();
800
+ panel.close();
745
801
  });
746
802
  $('download-package').addEventListener('click', async () => {
747
803
  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.4",
3
+ "version": "0.1.6",
4
4
  "description": "EasyAI 画布插件开发工具与运行时",
5
5
  "type": "module",
6
6
  "engines": {
@@ -27,6 +27,7 @@
27
27
  "bin",
28
28
  "dist",
29
29
  "tools",
30
+ "templates",
30
31
  "README.md",
31
32
  "LICENSE"
32
33
  ],
@@ -0,0 +1,21 @@
1
+ # 项目开发指引
2
+
3
+ 本项目使用 `@easyai-canvas/canvas-plugin-sdk` 开发 EasyAI 画布插件。请以当前安装版本的类型定义和项目代码为准;项目使用 Node 22.22.0 或更新的 Node 22 版本。
4
+
5
+ ## 代码位置
6
+
7
+ - 每个独立插件放在 `src/plugins/<目录>/`,其中 `plugin.ts` 声明 manifest、节点或操作及服务端处理器,`ui.ts` 实现 Vue 界面。对应契约测试放在 `test/`。
8
+ - 新增独立插件时运行 `npx easyai-plugin add <目录> --template node|action|combined`。同一插件的多个节点继续放在同一个目录,并保持插件 ID 与节点类型唯一。
9
+ - 只通过 SDK 的公开导入路径使用能力:`@easyai-canvas/canvas-plugin-sdk`、`/protocol`、`/ui`。不要依赖 EasyAI 平台源码或 SDK 内部文件。
10
+
11
+ ## 开发约定
12
+
13
+ - 修改 manifest、参数、输入输出或处理器时,同步更新对应契约测试。服务端从 `request.parameters` 和 `request.inputsByPort` 读取业务输入,并返回符合协议的输出。
14
+ - 前端通过 `usePluginNode()` 或 `usePluginAction()` 与画布交互。业务密钥放在插件配置或部署环境中,不写入 `ui.ts` 或前端产物。
15
+ - 有外部副作用的处理器使用 `executionId` 保证业务幂等;只有确认可安全重试时才声明 `safeRetry`。
16
+
17
+ ## 验证与交付
18
+
19
+ - 开发预览:`npm run dev`,工作台位于 `http://localhost:4310/studio`。
20
+ - 修改插件后运行 `npm run test:contract`;交付前运行 `npm run build`,检查 `dist/plugins/<pluginId>.zip` 与 `.sha256`。
21
+ - ZIP 由平台管理员审核、导入并启用;独立服务部署的配置参见 SDK README 和项目 README。
@@ -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,