@easyai-canvas/canvas-plugin-sdk 0.1.1 → 0.1.3

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` 跳过依赖安装。开发工作台位于 `http://localhost:4310/studio`,由浏览器完成 EasyAI 平台授权。`npm run build` 输出 `dist/plugin.zip`,可由管理员导入平台托管,或用 `npm start` 运行独立插件服务。
17
17
 
18
+ 运行 `npm run dev` 后,工作台会显示当前 SDK 与 npm 最新版本。若有新版本,点击“一键升级”会在当前项目执行固定版本安装,更新 `package.json` 和 npm 锁文件;安装完成后重启 `npm run dev`,新运行时才会生效。版本检查失败时仍可继续开发。
19
+
18
20
  平台托管只接纳运行不超过 20 秒的节点任务;需要长任务、GPU 或自有基础设施时使用独立服务。构建还会生成 `dist/plugin.zip.sha256` 供管理员核对。独立服务生产环境必须提供 `PLUGIN_SERVICE_SIGNING_SECRET`、与平台连接 ID 一致的 `PLUGIN_SERVICE_AUDIENCE` 和持久化的 `PLUGIN_DATA_DIR`。业务密钥放在插件配置或部署环境,不写入前端组件。
19
21
 
20
22
  托管节点暂不支持文件和画布宿主能力。管理员导入 ZIP 后默认停用,审核插件代码、声明能力和版本摘要后启用。版本切换不会改变已提交任务使用的包摘要。
@@ -179,6 +179,7 @@ function serviceEnv(dev) {
179
179
  if (!dev && !process.env.PLUGIN_SERVICE_SIGNING_SECRET) fail('生产服务需要 PLUGIN_SERVICE_SIGNING_SECRET');
180
180
  return { ...process.env, NODE_ENV: dev ? 'development' : 'production',
181
181
  PLUGIN_BUILTINS: '0', PLUGIN_PACKAGES: JSON.stringify([zip]),
182
+ PLUGIN_SDK_VERSION: metadata.version,
182
183
  PLUGIN_DATA_DIR: process.env.PLUGIN_DATA_DIR ?? join(cwd, '.easyai-plugin'),
183
184
  ...(dev ? { PLUGIN_AUTH_MODE: 'local', PLUGIN_BIND: '127.0.0.1', PLUGIN_DEV_WATCH: '1' } : {}) };
184
185
  }
@@ -6,12 +6,14 @@ import { Runtime } from './runtime/engine.js';
6
6
  import { loadPackagedPlugins } from './runtime/packaged-plugins.js';
7
7
  import { authenticate } from './runtime/auth.js';
8
8
  import { RemoteStudioConnector } from './studio/remote-connector.js';
9
+ import { SdkVersionManager } from './studio/sdk-version.js';
9
10
  import { ContractError, object, validatePluginRuntimeContext } from './protocol/index.js';
10
11
  const dataDir = resolve(process.env.PLUGIN_DATA_DIR ?? './.easyai-plugin');
11
12
  const port = Number(process.env.PORT ?? 4310);
12
13
  const packaged = await loadPackagedPlugins(JSON.parse(process.env.PLUGIN_PACKAGES ?? '[]'), join(dataDir, 'packages'));
13
14
  const runtime = new Runtime(join(dataDir, 'runtime.sqlite'), packaged.plugins, packaged.presentations, packaged.executionEntries);
14
15
  const studio = new RemoteStudioConnector();
16
+ const sdkVersions = new SdkVersionManager(process.cwd(), process.env.PLUGIN_SDK_VERSION ?? '');
15
17
  if (process.env.PLUGIN_DEV_WATCH === '1') {
16
18
  for (const plugin of packaged.plugins) {
17
19
  const entry = runtime.catalog().plugins.find(item => item.manifest.id === plugin.manifest.id);
@@ -82,7 +84,8 @@ async function studioCatalog() {
82
84
  const server = createServer(async (request, response) => {
83
85
  try {
84
86
  const method = request.method ?? 'GET';
85
- const path = new URL(request.url ?? '/', `http://localhost:${port}`).pathname;
87
+ const requestUrl = new URL(request.url ?? '/', `http://localhost:${port}`);
88
+ const path = requestUrl.pathname;
86
89
  const allowedOrigins = (process.env.PLUGIN_ALLOWED_ORIGINS ?? (process.env.NODE_ENV === 'production'
87
90
  ? '' : 'http://localhost:3000,http://127.0.0.1:3000')).split(',').map(s => s.trim()).filter(Boolean);
88
91
  if (request.headers.origin && allowedOrigins.includes(request.headers.origin)) {
@@ -177,6 +180,10 @@ const server = createServer(async (request, response) => {
177
180
  }
178
181
  if (path.startsWith('/studio')) {
179
182
  localStudio(request);
183
+ if (method === 'GET' && path === '/studio/') {
184
+ redirect(response, `/studio${requestUrl.search}`);
185
+ return;
186
+ }
180
187
  if (method === 'GET' && path === '/studio') {
181
188
  if (request.headers.host?.startsWith('127.0.0.1:')) {
182
189
  redirect(response, `http://localhost:${port}/studio`);
@@ -190,6 +197,23 @@ const server = createServer(async (request, response) => {
190
197
  send(response, 200, await studioCatalog());
191
198
  return;
192
199
  }
200
+ if (method === 'GET' && path === '/studio/sdk') {
201
+ send(response, 200, await sdkVersions.status());
202
+ return;
203
+ }
204
+ if (method === 'POST' && path === '/studio/sdk/upgrade') {
205
+ if (process.env.PLUGIN_DEV_WATCH !== '1' || request.headers.origin !== `http://localhost:${port}`)
206
+ throw new ContractError('仅允许从本机开发工作台升级 SDK', 403);
207
+ if (studio.status().phase === 'connected')
208
+ throw new ContractError('请先断开画布连接再升级 SDK', 409);
209
+ try {
210
+ send(response, 200, await sdkVersions.upgrade());
211
+ }
212
+ catch {
213
+ throw new ContractError('SDK 安装失败,请在项目终端手动安装并查看错误', 502);
214
+ }
215
+ return;
216
+ }
193
217
  if (method === 'GET' && path === '/studio/status') {
194
218
  send(response, 200, studio.status());
195
219
  return;
@@ -85,6 +85,13 @@ button:focus-visible, select:focus-visible, input:focus-visible, summary:focus-v
85
85
  .diagnostics-list .diag-error { color: var(--danger); }
86
86
  .diagnostics-list .diag-ready { color: var(--success); }
87
87
  .diagnostics-footnote { margin: 18px 0 0; color: var(--quiet); font-size: 11px; }
88
+ .sdk-card { margin-top: 24px; padding: 14px 16px; border: 1px solid var(--line); border-radius: 12px; background: var(--soft); }
89
+ .sdk-card[hidden] { display: none; }
90
+ .sdk-card strong { display: block; margin-bottom: 4px; font-size: 12px; }
91
+ .sdk-card p { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.6; }
92
+ .sdk-card button { margin-top: 10px; min-height: 34px; padding: 0 12px; border: 1px solid var(--line-strong); border-radius: 8px; color: var(--ink); background: #fff; font-size: 12px; font-weight: 600; }
93
+ .sdk-card button[hidden] { display: none; }
94
+ .sdk-card button:hover:not(:disabled) { background: var(--page); }
88
95
 
89
96
  /* Canvas controls */
90
97
  .dock { position: fixed; z-index: 3; left: 50%; bottom: 16px; transform: translateX(-50%); display: flex; align-items: center; gap: 3px; max-width: calc(100vw - 16px); padding: 5px; border: 1px solid var(--line-strong); border-radius: 13px; background: #fff; box-shadow: var(--floating-shadow); }
@@ -255,6 +262,7 @@ dialog::backdrop { background: #30384266; }
255
262
  <section class="setup-content" aria-labelledby="connection-title">
256
263
  <h1 id="connection-title">连接画布平台</h1>
257
264
  <p class="setup-intro">输入平台地址,在新窗口授权后进入私人调试画布。</p>
265
+ <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>
258
266
  <form id="connect-form"><label for="platform-address">画布平台地址</label><input id="platform-address" type="url" placeholder="https://canvas.example.com" autocomplete="url" aria-describedby="address-help" required><p id="address-help" class="field-help">线上平台使用 HTTPS,本机平台可使用 HTTP。</p><div class="form-actions"><button id="connect-button" type="submit" 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="M2.5 10h11m-4-4 4 4-4 4M15.5 4h2v12h-2"/></svg><span>登录并连接</span></button></div></form>
259
267
  <p id="setup-status" role="status" aria-live="polite"></p>
260
268
  </section>
@@ -280,7 +288,7 @@ dialog::backdrop { background: #30384266; }
280
288
  <dialog id="panel" aria-labelledby="panel-title">
281
289
  <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>
282
290
  <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></section>
283
- <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><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>
291
+ <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>
284
292
  <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>
285
293
  <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>
286
294
  </dialog>
@@ -290,6 +298,47 @@ dialog::backdrop { background: #30384266; }
290
298
  const $ = id => document.getElementById(id);
291
299
  const frame = $('canvas'), panel = $('panel'), dock = $('dock');
292
300
  let connectorStatus = null, connectAttemptError = '', localServiceAvailable = false, connectInProgress = false;
301
+ let sdkStatus = null, sdkBusy = false, sdkError = '';
302
+ function renderSdkStatus() {
303
+ const state = sdkStatus?.state;
304
+ const version = sdkStatus?.runningVersion || '未知';
305
+ const connected = connectorStatus?.phase === 'connected';
306
+ const message = sdkBusy ? '正在安装新版本并更新项目依赖,请稍候…'
307
+ : sdkError || (state === 'update' ? `当前运行 ${version},npm 最新 ${sdkStatus.latestVersion}。${connected ? '请先断开画布连接再升级。' : '可以升级。'}`
308
+ : state === 'restart' ? `已安装 ${sdkStatus.installedVersion},重启 npm run dev 后生效。`
309
+ : state === 'current' ? `当前运行 ${version},已是最新版本。`
310
+ : state === 'error' ? sdkStatus.message : '');
311
+ for (const card of document.querySelectorAll('.sdk-card')) {
312
+ card.hidden = !state || state === 'unavailable';
313
+ card.querySelector('.sdk-message').textContent = message;
314
+ const button = card.querySelector('.sdk-action');
315
+ button.hidden = state !== 'update' && state !== 'error';
316
+ button.textContent = state === 'error' ? '重新检查' : connected ? '断开连接后升级' : '一键升级';
317
+ button.disabled = sdkBusy || connected && state === 'update';
318
+ }
319
+ }
320
+ async function loadSdkStatus() {
321
+ if (sdkBusy) return;
322
+ try {
323
+ const response = await fetch('/studio/sdk', { cache: 'no-store' });
324
+ if (!response.ok) throw new Error('无法读取 SDK 版本');
325
+ sdkStatus = await response.json(); sdkError = '';
326
+ } catch { sdkStatus = { state: 'error', message: '无法读取 SDK 版本,请检查本地服务。' }; }
327
+ renderSdkStatus();
328
+ }
329
+ for (const button of document.querySelectorAll('.sdk-action')) button.addEventListener('click', async () => {
330
+ if (sdkStatus?.state === 'error') { await loadSdkStatus(); return; }
331
+ if (sdkStatus?.state !== 'update' || sdkBusy) return;
332
+ sdkBusy = true; sdkError = ''; renderSdkStatus();
333
+ try {
334
+ const response = await fetch('/studio/sdk/upgrade', { method: 'POST' });
335
+ const result = await response.json();
336
+ if (!response.ok) throw new Error(result.error || '升级失败');
337
+ sdkStatus = result;
338
+ } catch (error) { sdkError = `${error.message}。可在终端手动运行 npm install --save-exact @easyai-canvas/canvas-plugin-sdk@latest。`; }
339
+ finally { sdkBusy = false; renderSdkStatus(); }
340
+ });
341
+ void loadSdkStatus(); setInterval(loadSdkStatus, 10 * 60_000);
293
342
  try { $('platform-address').value = localStorage.getItem('plugin-debug-platform-url') || ''; } catch {}
294
343
  $('diag-local').textContent = window.location.host;
295
344
  function renderTargetAddress(raw = $('platform-address').value) {
@@ -326,6 +375,7 @@ dialog::backdrop { background: #30384266; }
326
375
  const response = await fetch('/studio/status', { cache: 'no-store' });
327
376
  if (!response.ok) throw new Error('本地服务状态读取失败');
328
377
  connectorStatus = await response.json();
378
+ renderSdkStatus();
329
379
  setLocalServiceState('ready');
330
380
  const targetAddress = connectorStatus.phase === 'idle' ? $('platform-address').value : connectorStatus.platformOrigin || $('platform-address').value;
331
381
  renderTargetAddress(targetAddress);
@@ -0,0 +1,74 @@
1
+ export declare const SDK_NAME = "@easyai-canvas/canvas-plugin-sdk";
2
+ type Runner = (args: string[], cwd: string, timeout: number) => Promise<string>;
3
+ export declare function compareVersions(left: string, right: string): number | null;
4
+ export declare class SdkVersionManager {
5
+ private readonly projectDir;
6
+ private readonly runningVersion;
7
+ private readonly run;
8
+ private latest;
9
+ private checkedAt;
10
+ private checking;
11
+ private upgrading;
12
+ constructor(projectDir: string, runningVersion: string, run?: Runner);
13
+ private packageVersion;
14
+ private latestVersion;
15
+ status(): Promise<{
16
+ state: "unavailable";
17
+ runningVersion: string;
18
+ installedVersion?: undefined;
19
+ declared?: undefined;
20
+ latestVersion?: undefined;
21
+ message?: undefined;
22
+ } | {
23
+ state: "restart";
24
+ runningVersion: string;
25
+ installedVersion: string;
26
+ declared: string;
27
+ latestVersion?: undefined;
28
+ message?: undefined;
29
+ } | {
30
+ state: "update" | "current";
31
+ runningVersion: string;
32
+ installedVersion: string;
33
+ latestVersion: string;
34
+ declared: string;
35
+ message?: undefined;
36
+ } | {
37
+ state: "error";
38
+ runningVersion: string;
39
+ installedVersion: string;
40
+ declared: string;
41
+ message: string;
42
+ latestVersion?: undefined;
43
+ }>;
44
+ upgrade(): Promise<{
45
+ state: "unavailable";
46
+ runningVersion: string;
47
+ installedVersion?: undefined;
48
+ declared?: undefined;
49
+ latestVersion?: undefined;
50
+ message?: undefined;
51
+ } | {
52
+ state: "restart";
53
+ runningVersion: string;
54
+ installedVersion: string;
55
+ declared: string;
56
+ latestVersion?: undefined;
57
+ message?: undefined;
58
+ } | {
59
+ state: "update" | "current";
60
+ runningVersion: string;
61
+ installedVersion: string;
62
+ latestVersion: string;
63
+ declared: string;
64
+ message?: undefined;
65
+ } | {
66
+ state: "error";
67
+ runningVersion: string;
68
+ installedVersion: string;
69
+ declared: string;
70
+ message: string;
71
+ latestVersion?: undefined;
72
+ }>;
73
+ }
74
+ export {};
@@ -0,0 +1,101 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ const execute = promisify(execFile);
6
+ export const SDK_NAME = '@easyai-canvas/canvas-plugin-sdk';
7
+ async function npm(args, cwd, timeout) {
8
+ const { stdout } = await execute('npm', args, { cwd, timeout, maxBuffer: 1024 * 1024 });
9
+ return stdout.trim();
10
+ }
11
+ function versionParts(value) {
12
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value);
13
+ return match ? match.slice(1).map(Number) : null;
14
+ }
15
+ export function compareVersions(left, right) {
16
+ const a = versionParts(left);
17
+ const b = versionParts(right);
18
+ if (!a || !b)
19
+ return null;
20
+ for (let index = 0; index < 3; index++) {
21
+ if (a[index] !== b[index])
22
+ return Math.sign(a[index] - b[index]);
23
+ }
24
+ return 0;
25
+ }
26
+ export class SdkVersionManager {
27
+ projectDir;
28
+ runningVersion;
29
+ run;
30
+ latest = null;
31
+ checkedAt = 0;
32
+ checking = null;
33
+ upgrading = false;
34
+ constructor(projectDir, runningVersion, run = npm) {
35
+ this.projectDir = projectDir;
36
+ this.runningVersion = runningVersion;
37
+ this.run = run;
38
+ }
39
+ async packageVersion(path) {
40
+ try {
41
+ return JSON.parse(await readFile(path, 'utf8'));
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ async latestVersion() {
48
+ if (this.latest && Date.now() - this.checkedAt < 10 * 60_000)
49
+ return this.latest;
50
+ if (!this.checking) {
51
+ this.checking = this.run(['view', `${SDK_NAME}@latest`, 'version', '--json', '--fetch-retries=0'], this.projectDir, 8000)
52
+ .then(output => {
53
+ const version = JSON.parse(output);
54
+ if (typeof version !== 'string' || !versionParts(version))
55
+ throw new Error('npm 返回了无效的 SDK 版本');
56
+ this.latest = version;
57
+ this.checkedAt = Date.now();
58
+ return version;
59
+ }).finally(() => { this.checking = null; });
60
+ }
61
+ return this.checking;
62
+ }
63
+ async status() {
64
+ const project = await this.packageVersion(join(this.projectDir, 'package.json'));
65
+ const declared = project?.dependencies?.[SDK_NAME] ?? project?.devDependencies?.[SDK_NAME];
66
+ if (!declared)
67
+ return { state: 'unavailable', runningVersion: this.runningVersion };
68
+ const installed = await this.packageVersion(join(this.projectDir, 'node_modules', SDK_NAME, 'package.json'));
69
+ const installedVersion = installed?.version ?? this.runningVersion;
70
+ if (compareVersions(installedVersion, this.runningVersion) === 1)
71
+ return { state: 'restart', runningVersion: this.runningVersion, installedVersion, declared };
72
+ try {
73
+ const latestVersion = await this.latestVersion();
74
+ return { state: compareVersions(installedVersion, latestVersion) === -1 ? 'update' : 'current',
75
+ runningVersion: this.runningVersion, installedVersion, latestVersion, declared };
76
+ }
77
+ catch {
78
+ return { state: 'error', runningVersion: this.runningVersion, installedVersion, declared,
79
+ message: '暂时无法查询 npm 最新版本,请检查网络或 npm 仓库配置。' };
80
+ }
81
+ }
82
+ async upgrade() {
83
+ if (this.upgrading)
84
+ throw new Error('SDK 正在升级,请稍后再试');
85
+ this.upgrading = true;
86
+ try {
87
+ const status = await this.status();
88
+ if (status.state === 'unavailable')
89
+ throw new Error('当前项目没有声明 SDK 依赖');
90
+ if (status.state === 'error')
91
+ throw new Error(status.message);
92
+ if (status.state !== 'update')
93
+ return status;
94
+ await this.run(['install', '--save-exact', `${SDK_NAME}@${status.latestVersion}`], this.projectDir, 120_000);
95
+ return this.status();
96
+ }
97
+ finally {
98
+ this.upgrading = false;
99
+ }
100
+ }
101
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easyai-canvas/canvas-plugin-sdk",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "EasyAI 画布插件开发工具与运行时",
5
5
  "type": "module",
6
6
  "engines": {