@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 +2 -0
- package/bin/easyai-plugin.mjs +1 -0
- package/dist/customer-main.js +3 -2
- package/dist/studio/index.html +116 -60
- package/dist/studio/remote-connector.d.ts +1 -1
- package/dist/studio/remote-connector.js +1 -1
- package/package.json +2 -1
- package/templates/AGENTS.md +21 -0
- package/tools/build-plugin-package.mjs +5 -3
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` 声明继续可用;未知位置或贡献能力与位置不匹配会在打包校验时报错。
|
package/bin/easyai-plugin.mjs
CHANGED
|
@@ -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),
|
package/dist/customer-main.js
CHANGED
|
@@ -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({
|
|
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(
|
|
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') {
|
package/dist/studio/index.html
CHANGED
|
@@ -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(
|
|
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
|
|
134
|
-
.dialog-head h2 { margin: 0 0 5px; font-size:
|
|
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)
|
|
140
|
-
.
|
|
141
|
-
.
|
|
142
|
-
.
|
|
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;
|
|
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
|
-
.
|
|
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
|
|
154
|
-
.plugin-config
|
|
155
|
-
.plugin-config input
|
|
156
|
-
.plugin-config button { min-height:
|
|
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)
|
|
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:
|
|
173
|
-
.plugin-actions button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-height:
|
|
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 {
|
|
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 {
|
|
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 {
|
|
215
|
-
.
|
|
216
|
-
.
|
|
217
|
-
.
|
|
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
|
-
.
|
|
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
|
|
299
|
-
<div class="dialog-body"
|
|
300
|
-
<
|
|
301
|
-
<
|
|
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
|
-
|
|
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
|
|
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();
|
|
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',
|
|
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 =
|
|
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' ? '正在构建' :
|
|
581
|
-
$('dot').className = `dot ${building === 'error' || frameFailed ? 'error' : building === 'building'
|
|
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 =
|
|
588
|
-
$('build').textContent = building === 'error' ? '构建失败' : building === 'building' ? '构建中' :
|
|
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 =
|
|
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
|
-
$('
|
|
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:
|
|
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(
|
|
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 !==
|
|
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:
|
|
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:
|
|
751
|
+
frame.contentWindow.postMessage({ protocol, type: 'refresh', sessionId: session.sessionId, pluginId: activePluginId(), requestId: waiting }, catalog.platformOrigin);
|
|
704
752
|
render();
|
|
705
753
|
});
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
784
|
+
configFeedback = { pluginId: plugin.id, message: '配置已保存,插件已启用。' };
|
|
785
|
+
$('config-status').textContent = configFeedback.message;
|
|
730
786
|
await loadCatalog();
|
|
731
|
-
} catch (error) { $('config-status').textContent =
|
|
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
|
-
|
|
740
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
31
|
-
|
|
32
|
-
).
|
|
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,
|