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

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),
@@ -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,19 @@ 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
+ let configFormKey = '', configFeedback = null;
511
539
  function renderConfig() {
512
540
  const plugin = panelPlugin();
513
541
  const form = $('plugin-config');
514
- const fields = plugin?.manifest.configFields || [];
542
+ const fields = plugin?.manifest?.configFields || [];
515
543
  form.hidden = !fields.length;
544
+ $('no-config').hidden = !plugin || Boolean(fields.length);
516
545
  if (!fields.length) { configFormKey = ''; return; }
517
546
  const key = JSON.stringify([plugin.id, plugin.buildKey, plugin.config, plugin.secretConfigured]);
518
547
  if (key === configFormKey) return;
519
548
  configFormKey = key;
520
- $('config-fields').replaceChildren(); $('config-status').textContent = plugin.configurationRequired ? '待配置:填写必填项后才能启用。' : '配置仅保存在本地开发服务中。';
549
+ $('config-fields').replaceChildren();
550
+ $('config-status').textContent = configFeedback?.pluginId === plugin.id ? configFeedback.message : plugin.configurationRequired ? '待配置:填写必填项后才能启用。' : '可以修改配置并保存。';
521
551
  for (const field of fields) {
522
552
  const label = document.createElement('label');
523
553
  label.textContent = `${field.label}${field.required ? ' *' : ''}`;
@@ -544,6 +574,7 @@ dialog::backdrop { background: #30384266; }
544
574
  }
545
575
  function renderPluginList() {
546
576
  const plugins = catalog?.plugins || [];
577
+ $('plugin-count').textContent = `${plugins.length} 个`;
547
578
  const key = JSON.stringify(plugins.map(item => [item.id, item.name, item.nodes, item.actions, item.enabled, item.configurationRequired]));
548
579
  if (key !== pluginListKey) {
549
580
  pluginListKey = key;
@@ -593,6 +624,15 @@ dialog::backdrop { background: #30384266; }
593
624
  renderPluginList();
594
625
  renderConfig();
595
626
  const inspected = panelPlugin();
627
+ $('detail-empty').hidden = Boolean(inspected);
628
+ $('plugin-detail-content').hidden = !inspected;
629
+ $('detail-name').textContent = inspected?.name || '插件';
630
+ $('detail-id').textContent = inspected?.id || '';
631
+ $('detail-nodes').textContent = `${inspected?.nodes || 0} 个节点`;
632
+ $('detail-actions').textContent = `${inspected?.actions || 0} 个操作`;
633
+ $('detail-package').textContent = inspected?.package ? '插件包可下载' : '插件包不可用';
634
+ $('detail-status').textContent = inspected?.configurationRequired ? '待配置' : inspected?.enabled ? '已启用' : '已停用';
635
+ $('detail-status').className = `status-badge ${inspected?.configurationRequired ? '' : inspected?.enabled ? 'ready' : 'error'}`;
596
636
  $('open-plugin').disabled = !inspected?.enabled || busy || packageDownloading;
597
637
  $('download-package').disabled = !inspected?.package || packageDownloading;
598
638
  $('download-package').setAttribute('aria-busy', String(packageDownloading));
@@ -703,14 +743,21 @@ dialog::backdrop { background: #30384266; }
703
743
  frame.contentWindow.postMessage({ protocol, type: 'refresh', sessionId: session.sessionId, pluginId: selected, requestId: waiting }, catalog.platformOrigin);
704
744
  render();
705
745
  });
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(); });
746
+ function openPanel(forConfiguration = false) {
747
+ panelSelected = forConfiguration ? catalog?.plugins.find(item => item.configurationRequired)?.id || selected : selected;
748
+ packageMessage = ''; packageError = false; configFeedback = null; configFormKey = ''; render(); panel.showModal();
749
+ const firstField = forConfiguration ? $('config-fields').querySelector('input, select') : null;
750
+ (firstField || $('plugin-list').querySelector('input:checked') || $('close')).focus();
751
+ renderDock();
752
+ }
753
+ $('manage').addEventListener('click', () => openPanel());
754
+ $('open-local-config').addEventListener('click', () => openPanel(true));
708
755
  panel.addEventListener('close', () => { renderDock(); ($('setup').hidden ? $('manage') : $('open-local-config')).focus(); });
709
756
  $('close').addEventListener('click', () => panel.close());
710
757
  $('panel-cancel').addEventListener('click', () => panel.close());
711
758
  $('plugin-list').addEventListener('change', event => {
712
759
  if (!event.target.matches('input[name="studio-plugin"]')) return;
713
- panelSelected = event.target.value; packageMessage = ''; packageError = false; render();
760
+ panelSelected = event.target.value; packageMessage = ''; packageError = false; configFeedback = null; render(); document.querySelector('.plugin-detail').scrollTop = 0;
714
761
  });
715
762
  $('plugin-config').addEventListener('submit', async event => {
716
763
  event.preventDefault();
@@ -726,9 +773,10 @@ dialog::backdrop { background: #30384266; }
726
773
  const response = await fetch(`/studio/plugins/${encodeURIComponent(plugin.id)}/configure`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ config, enabled: true }) });
727
774
  const result = await response.json();
728
775
  if (!response.ok) throw new Error(result.error || '配置保存失败');
729
- $('config-status').textContent = '配置已保存,插件已启用。';
776
+ configFeedback = { pluginId: plugin.id, message: '配置已保存,插件已启用。' };
777
+ $('config-status').textContent = configFeedback.message;
730
778
  await loadCatalog();
731
- } catch (error) { $('config-status').textContent = error.message || '配置保存失败'; }
779
+ } catch (error) { configFeedback = { pluginId: plugin.id, message: error.message || '配置保存失败' }; $('config-status').textContent = configFeedback.message; }
732
780
  });
733
781
  $('config-check').addEventListener('click', async () => {
734
782
  const plugin = panelPlugin(); if (!plugin) return;
@@ -736,8 +784,9 @@ dialog::backdrop { background: #30384266; }
736
784
  const response = await fetch(`/studio/plugins/${encodeURIComponent(plugin.id)}/check`, { method: 'POST' });
737
785
  const result = await response.json();
738
786
  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 || '配置检查失败'; }
787
+ configFeedback = { pluginId: plugin.id, message: result.message || result.result?.message || '配置检查通过。' };
788
+ $('config-status').textContent = configFeedback.message;
789
+ } catch (error) { configFeedback = { pluginId: plugin.id, message: error.message || '配置检查失败' }; $('config-status').textContent = configFeedback.message; }
741
790
  });
742
791
  $('open-plugin').addEventListener('click', () => {
743
792
  if (!panelPlugin()?.enabled || busy) return;
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.5",
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。