batter-pixso-mcp 0.4.0 → 0.6.0

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/dist/index.js CHANGED
@@ -1,21 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/plugin/assets.ts
4
- import { createHash } from "node:crypto";
5
- import { mkdir, readdir, stat, unlink, writeFile } from "node:fs/promises";
6
- import { join as join2 } from "node:path";
7
-
8
3
  // src/config.ts
9
4
  import { tmpdir } from "node:os";
10
5
  import { join } from "node:path";
11
- var VERSION = true ? "0.4.0" : "0.0.0-dev";
6
+ var VERSION = true ? "0.6.0" : "0.0.0-dev";
12
7
  var HUB_NAME = "batter-pixso-mcp";
13
8
  var ASSETS_DIR = join(tmpdir(), "batter-pixso-assets");
14
9
  var config = {
15
10
  /** 插件通信 HTTP 端口,仅监听 127.0.0.1。与插件 UI 的 DEFAULT_PORT 互为默认对端,改默认需同步 */
16
11
  httpPort: Number(process.env.HTTP_PORT ?? 3210),
17
- /** hub 空闲自毁阈值(无插件、无 agent 请求),默认 30 分钟 */
18
- hubIdleMs: Number(process.env.HUB_IDLE_MS ?? 30 * 60 * 1e3),
12
+ // hub 常驻(不自毁):升级由 proxy 版本比对自动换血,手动停止用 --stop
13
+ /** MCP HTTP 会话空闲 TTL:超时清扫防泄漏(proxy 对失效会话有自动重建兜底),默认 30 分钟 */
14
+ mcpSessionTtlMs: Number(process.env.MCP_SESSION_TTL_MS ?? 30 * 60 * 1e3),
19
15
  /** 产物图片保留时长,默认 7 天 */
20
16
  assetTtlMs: Number(process.env.ASSET_TTL_DAYS ?? 7) * 24 * 60 * 60 * 1e3
21
17
  };
@@ -36,6 +32,9 @@ function withResolvers() {
36
32
  }
37
33
 
38
34
  // src/plugin/assets.ts
35
+ import { createHash } from "node:crypto";
36
+ import { mkdir, readdir, stat, unlink, writeFile } from "node:fs/promises";
37
+ import { join as join2 } from "node:path";
39
38
  var EXT_BY_FORMAT = {
40
39
  PNG: ".png",
41
40
  SVG: ".svg",
@@ -89,114 +88,138 @@ async function cleanOldAssets(ttlMs) {
89
88
  return removed;
90
89
  }
91
90
 
92
- // src/plugin/sessions.ts
93
- import { randomUUID } from "node:crypto";
94
- var ONLINE_WINDOW_MS = 45e3;
95
- function unknownTargetError(target) {
96
- return new Error(`\u627E\u4E0D\u5230\u76EE\u6807\u63D2\u4EF6\u4F1A\u8BDD\uFF1A${target}\u3002\u53EF\u7528 list_sessions \u67E5\u770B\u5F53\u524D\u8FDE\u63A5\u7684\u4F1A\u8BDD`);
97
- }
98
- var SessionRegistry = class {
99
- sessions = /* @__PURE__ */ new Map();
100
- lastActiveId = null;
101
- /** 会话亲和:fileKey → 上一次路由使用的 sessionId(仍在线时优先,避免同文件多实例间漂移) */
102
- lastUsedByFileKey = /* @__PURE__ */ new Map();
103
- isOnline(s) {
104
- return s.connected || Date.now() - s.lastSeen < ONLINE_WINDOW_MS;
105
- }
106
- /** fileKey 寻址候选:该文件的在线会话(谁先 pong 由 ping 竞速决定,此处不排序) */
107
- candidatesForFileKey(fileKey) {
108
- return [...this.sessions.values()].filter((s) => s.meta.fileKey === fileKey && this.isOnline(s));
109
- }
110
- /** 亲和查询:上一次为该 fileKey 服务的 sessionId */
111
- affinityIdOf(fileKey) {
112
- return this.lastUsedByFileKey.get(fileKey);
113
- }
114
- /** 亲和钉住:fileKey 寻址探活成功后记录(下次优先) */
115
- pinAffinity(fileKey, sessionId) {
116
- this.lastUsedByFileKey.set(fileKey, sessionId);
91
+ // src/process/hub-process.ts
92
+ import { spawn } from "node:child_process";
93
+ import { closeSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
94
+ import { tmpdir as tmpdir2 } from "node:os";
95
+ import { join as join3 } from "node:path";
96
+ import { fileURLToPath } from "node:url";
97
+ async function probeHub() {
98
+ try {
99
+ const res = await fetch(`${hubBaseUrl()}/health`, { signal: AbortSignal.timeout(1500) });
100
+ if (!res.ok) return { state: "foreign" };
101
+ const body = await res.json();
102
+ if (body.ok === true && body.name === HUB_NAME) return { state: "alive", health: body };
103
+ return { state: "foreign" };
104
+ } catch (err) {
105
+ const hung = err instanceof Error && err.name === "TimeoutError";
106
+ return { state: hung ? "hung" : "down" };
117
107
  }
118
- /** 剔除会话(幽灵剔除/ping 判死共用):清理亲和引用;存活但繁忙被误剔的插件会在 SSE 断开或上报 404 后自重注册 */
119
- evict(id) {
120
- if (this.sessions.delete(id)) {
121
- for (const [k, v] of this.lastUsedByFileKey) {
122
- if (v === id) this.lastUsedByFileKey.delete(k);
123
- }
124
- log(`session evicted: ${id.slice(0, 8)}`);
125
- }
108
+ }
109
+ var PORT_FIX_HINT = `\u7528 netstat -ano | findstr :${config.httpPort} \u67E5\u5230 PID \u540E taskkill /PID <pid> /F \u7ED3\u675F\uFF0C\u518D\u91CD\u8BD5`;
110
+ function hubStateError(state) {
111
+ if (state === "hung") {
112
+ return new Error(`\u7AEF\u53E3 ${config.httpPort} \u4E0A\u7684\u670D\u52A1\u65E0\u54CD\u5E94\uFF08\u53EF\u80FD\u6302\u6B7B\uFF09\u4E14\u65E0\u6CD5\u5B9A\u4F4D pid\u3002${PORT_FIX_HINT}`);
126
113
  }
127
- register(meta = {}) {
128
- if (meta.fileKey) {
129
- for (const s of [...this.sessions.values()]) {
130
- if (s.meta.fileKey === meta.fileKey && !this.isOnline(s)) {
131
- this.evict(s.id);
132
- }
114
+ return new Error(`\u7AEF\u53E3 ${config.httpPort} \u88AB\u5176\u4ED6\u7A0B\u5E8F\u5360\u7528\u3002${PORT_FIX_HINT}\uFF0C\u6216\u4FEE\u6539 HTTP_PORT \u6362\u7AEF\u53E3`);
115
+ }
116
+ var HUB_LOG = join3(tmpdir2(), "batter-pixso-mcp-hub.log");
117
+ var PID_FILE = join3(tmpdir2(), "batter-pixso-mcp-hub.pid");
118
+ function writePidFile(pid) {
119
+ try {
120
+ writeFileSync(PID_FILE, String(pid), "utf8");
121
+ process.on("exit", () => {
122
+ try {
123
+ rmSync(PID_FILE);
124
+ } catch {
133
125
  }
134
- }
135
- const session = { id: randomUUID(), meta, lastSeen: Date.now(), connected: false };
136
- this.sessions.set(session.id, session);
137
- this.lastActiveId = session.id;
138
- log(`session registered: ${session.id} (${meta.fileName ?? "unknown file"})`);
139
- return this.toInfo(session);
126
+ });
127
+ } catch {
140
128
  }
141
- /** 心跳/连接状态用:只刷 lastSeen,不影响 lastActiveId("最近活跃"= 用户行为,心跳不算) */
142
- touchSeen(id) {
143
- const s = this.sessions.get(id);
144
- if (!s) return null;
145
- s.lastSeen = Date.now();
146
- return s;
129
+ }
130
+ function readPidFile() {
131
+ try {
132
+ const pid = Number(readFileSync(PID_FILE, "utf8").trim());
133
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
134
+ } catch {
135
+ return null;
147
136
  }
148
- /** 用户行为(选区上报/命令服务/注册):刷 lastSeen 并标记最近活跃 */
149
- touch(id) {
150
- const s = this.touchSeen(id);
151
- if (!s) return null;
152
- this.lastActiveId = id;
153
- return s;
137
+ }
138
+ function removePidFile() {
139
+ try {
140
+ rmSync(PID_FILE);
141
+ } catch {
154
142
  }
155
- setConnected(id, connected) {
156
- const s = this.touchSeen(id);
157
- if (!s) return false;
158
- s.connected = connected;
143
+ }
144
+ function killHubByPid(pid) {
145
+ try {
146
+ process.kill(pid);
159
147
  return true;
148
+ } catch {
149
+ return false;
160
150
  }
161
- updateContext(id, meta) {
162
- const s = this.touch(id);
163
- if (!s) return false;
164
- s.meta = { ...s.meta, ...meta };
165
- log(
166
- `context updated [${id.slice(0, 8)}]: ${s.meta.fileName ?? "?"} / ${s.meta.pageName ?? "?"} / ${s.meta.selection?.length ?? 0} selected`
167
- );
168
- return true;
151
+ }
152
+ function killHubByPidFile() {
153
+ const pid = readPidFile();
154
+ if (!pid) return false;
155
+ if (!killHubByPid(pid)) return false;
156
+ removePidFile();
157
+ return true;
158
+ }
159
+ function spawnHub() {
160
+ const out = openSync(HUB_LOG, "a");
161
+ const entry = process.argv[1] ?? fileURLToPath(import.meta.url);
162
+ const SKIP_FLAGS = /* @__PURE__ */ new Set(["--eval", "-e", "--print", "-p", "--watch", "--watch-preserve-output"]);
163
+ const SKIP_PREFIXES = [...SKIP_FLAGS].map((f) => f + "=");
164
+ const execArgv = process.execArgv.filter(
165
+ (a, i, arr) => !SKIP_FLAGS.has(a) && !SKIP_PREFIXES.some((p) => a.startsWith(p)) && !(i > 0 && SKIP_FLAGS.has(arr[i - 1]))
166
+ );
167
+ const child = spawn(process.execPath, [...execArgv, entry, "--hub"], {
168
+ detached: true,
169
+ stdio: ["ignore", out, out],
170
+ windowsHide: true,
171
+ env: { ...process.env }
172
+ });
173
+ child.on("error", (err) => log("hub spawn \u5931\u8D25:", err.message));
174
+ child.unref();
175
+ closeSync(out);
176
+ log(`hub spawned (pid ${child.pid}), log: ${HUB_LOG}`);
177
+ }
178
+ async function waitForHub(timeoutMs = 15e3) {
179
+ const deadline = Date.now() + timeoutMs;
180
+ for (; ; ) {
181
+ const { state } = await probeHub();
182
+ if (state === "alive") return;
183
+ if (state !== "down") throw hubStateError(state);
184
+ if (Date.now() > deadline) throw new Error(`\u7B49\u5F85 hub \u5C31\u7EEA\u8D85\u65F6\uFF08${timeoutMs / 1e3}s\uFF09`);
185
+ await new Promise((r) => setTimeout(r, 200));
169
186
  }
170
- list() {
171
- return [...this.sessions.values()].map((s) => this.toInfo(s));
187
+ }
188
+ async function waitForHubDown(timeoutMs = 5e3) {
189
+ const deadline = Date.now() + timeoutMs;
190
+ for (; ; ) {
191
+ const { state } = await probeHub();
192
+ if (state === "down") return;
193
+ if (Date.now() > deadline) throw new Error(`\u7B49\u5F85 hub \u9000\u51FA\u8D85\u65F6\uFF08${timeoutMs / 1e3}s\uFF09`);
194
+ await new Promise((r) => setTimeout(r, 100));
172
195
  }
173
- /** 纯查找(不判活):工具层寻址用——离线会话也可取到 fileKey 用于候选扩展与故障转移 */
174
- get(id) {
175
- return this.sessions.get(id);
196
+ }
197
+ async function stopHub() {
198
+ const { state, health } = await probeHub();
199
+ if (state === "down") {
200
+ log(`hub \u672A\u8FD0\u884C\uFF08\u7AEF\u53E3 ${config.httpPort} \u65E0\u670D\u52A1\uFF09`);
201
+ return;
176
202
  }
177
- /** 缺省路由:最近活跃在线会话 > 任意在线会话(显式 target 由工具层 get/candidatesForFileKey + ping 完成) */
178
- resolve() {
179
- if (this.lastActiveId) {
180
- const s = this.sessions.get(this.lastActiveId);
181
- if (s && this.isOnline(s)) return s;
203
+ if (state === "foreign") {
204
+ log(`\u7AEF\u53E3 ${config.httpPort} \u4E0A\u4E0D\u662F batter-pixso-mcp\uFF0C\u672A\u5904\u7406`);
205
+ return;
206
+ }
207
+ if (state === "hung") {
208
+ if (killHubByPidFile()) {
209
+ log("hub \u65E0\u54CD\u5E94\uFF08\u50F5\u5C38\u8FDB\u7A0B\uFF09\uFF0C\u5DF2\u6309 pid \u6587\u4EF6\u5F3A\u6740\u3002\u4E0B\u6B21 agent \u8BF7\u6C42\u4F1A\u81EA\u52A8\u62C9\u8D77\u65B0\u7248\u672C");
210
+ return;
182
211
  }
183
- const fallback = [...this.sessions.values()].find((s) => this.isOnline(s));
184
- if (fallback) return fallback;
185
- throw new Error("Pixso \u63D2\u4EF6\u672A\u8FDE\u63A5\uFF1A\u8BF7\u5728 Pixso \u5BA2\u6237\u7AEF\u4E2D\u8FD0\u884C Batter Pixso Bridge \u63D2\u4EF6");
212
+ log(`\u7AEF\u53E3 ${config.httpPort} \u4E0A\u7684\u670D\u52A1\u65E0\u54CD\u5E94\u4E14\u65E0\u6CD5\u5B9A\u4F4D pid\u3002${PORT_FIX_HINT}`);
213
+ return;
186
214
  }
187
- toInfo(s) {
188
- return {
189
- sessionId: s.id,
190
- fileKey: s.meta.fileKey,
191
- fileName: s.meta.fileName,
192
- pageName: s.meta.pageName,
193
- selectionCount: s.meta.selection?.length,
194
- online: this.isOnline(s),
195
- lastSeen: s.lastSeen
196
- };
215
+ const pid = health.pid;
216
+ if (killHubByPid(pid)) {
217
+ removePidFile();
218
+ log(`hub \u5DF2\u505C\u6B62\uFF08pid ${pid}\uFF09\u3002\u4E0B\u6B21 agent \u8BF7\u6C42\u4F1A\u81EA\u52A8\u62C9\u8D77\u65B0\u7248\u672C`);
219
+ } else {
220
+ log(`\u505C\u6B62 pid ${pid} \u5931\u8D25\u3002${PORT_FIX_HINT}`);
197
221
  }
198
- };
199
- var sessions = new SessionRegistry();
222
+ }
200
223
 
201
224
  // src/http.ts
202
225
  import { createServer } from "node:http";
@@ -302,13 +325,13 @@ var INTRO = `# batter-pixso-mcp \u4F7F\u7528\u6307\u5357
302
325
 
303
326
  ## \u591A\u6587\u4EF6\u573A\u666F
304
327
 
305
- \u6BCF\u4E2A\u8FD0\u884C\u63D2\u4EF6\u7684\u8BBE\u8BA1\u6587\u4EF6\u662F\u4E00\u4E2A\u4F1A\u8BDD\u3002\u591A\u4E2A\u6587\u4EF6\u540C\u65F6\u6253\u5F00\u65F6\uFF1A
306
- \u5148\u7528 \`list_sessions\` \u786E\u8BA4\u76EE\u6807\u6587\u4EF6\uFF0C\u518D\u628A\u8FD4\u56DE\u7684 \`sessionId\` \u6216 \`fileKey\`
307
- \u4F5C\u4E3A\u5176\u4ED6\u5DE5\u5177\u7684 \`target\` \u53C2\u6570\u4F20\u5165\uFF1B\u4E0D\u4F20 \`target\` \u65F6\u8DEF\u7531\u5230\u6700\u8FD1\u6D3B\u8DC3\u7684\u6587\u4EF6\u3002
328
+ \u6BCF\u4E2A\u8FD0\u884C\u63D2\u4EF6\u7684\u8BBE\u8BA1\u6587\u4EF6\u662F\u4E00\u4E2A\u4F1A\u8BDD\u3002\u8282\u70B9\u7C7B\u5DE5\u5177\uFF08\`get_node_dsl\`/\`get_metadata\`/\`get_screenshot\`/\`export_image\`\uFF09
329
+ \u7684 \`guid\` \u5FC5\u4F20\uFF0C\u4E14 \`fileKey\`/\`sessionId\` \u4E8C\u9009\u4E00\u5FC5\u4F20\uFF1A\u5148\u7528 \`list_sessions\` \u786E\u8BA4\u76EE\u6807\u6587\u4EF6\uFF0C\u628A\u8FD4\u56DE\u7684 \`fileKey\`
330
+ \u4F5C\u4E3A\u8DEF\u7531\u53C2\u6570\u4F20\u5165\uFF08\u8DE8\u91CD\u5F00\u7A33\u5B9A\uFF0C\u4F18\u5148\u4E8E \`sessionId\`\uFF09\uFF1B\u4EC5 \`get_selected\`/\`list_sessions\` \u53EF\u7F3A\u7701\u8DEF\u7531\u53C2\u6570\u505A\u63A2\u67E5\u3002
308
331
 
309
- **\u4F1A\u8BDD\u89E3\u6790\u4E0E\u4FE1\u5C01**\uFF1A\u63D2\u4EF6\u7C7B\u5DE5\u5177\u7684\u8FD4\u56DE\u7EDF\u4E00\u4E3A \`{sessionId, data}\`\u2014\u2014\`sessionId\` \u662F\u672C\u6B21\u5B9E\u9645\u670D\u52A1\u7684\u4F1A\u8BDD
310
- \uFF08\u6BCF\u6B21\u8C03\u7528\u7ECF ping/pong \u63A2\u6D3B\u91CD\u65B0\u786E\u8BA4\uFF0C\u4E0D\u662F\u53EF\u957F\u671F\u9489\u4F4F\u7684\u56FA\u5B9A\u6807\u8BC6\uFF09\u3002\u63A8\u8350 \`target\` \u7528 \`fileKey\`\uFF1A
311
- \u6BCF\u6B21\u8C03\u7528\u6309 fileKey \u91CD\u65B0\u5BFB\u5740\u2014\u2014\u540C fileKey \u591A\u4F1A\u8BDD\u65F6\u4F18\u5148\u4E0A\u4E00\u6B21\u4F7F\u7528\u7684\uFF08\u4EB2\u548C\uFF09\uFF0C\u6587\u4EF6\u5173\u95ED\u91CD\u5F00\u3001\u63D2\u4EF6\u91CD\u8DD1\u540E
332
+ **\u4F1A\u8BDD\u89E3\u6790\u4E0E\u4FE1\u5C01**\uFF1A\u63D2\u4EF6\u7C7B\u5DE5\u5177\u7684\u8FD4\u56DE\u7EDF\u4E00\u4E3A \`{sessionId, fileKey, data}\`\u2014\u2014\`sessionId\` \u662F\u672C\u6B21\u5B9E\u9645\u670D\u52A1\u7684\u4F1A\u8BDD
333
+ \uFF08\u6BCF\u6B21\u8C03\u7528\u7ECF ping/pong \u63A2\u6D3B\u91CD\u65B0\u786E\u8BA4\uFF0C\u4E0D\u662F\u53EF\u957F\u671F\u9489\u4F4F\u7684\u56FA\u5B9A\u6807\u8BC6\uFF09\uFF1B\`fileKey\` \u662F\u8DE8\u91CD\u5F00\u7A33\u5B9A\u7684\u53E5\u67C4\uFF0C\u540E\u7EED\u8C03\u7528
334
+ \u7528\u5B83\u505A\u8DEF\u7531\u53C2\u6570\uFF1A\u6BCF\u6B21\u8C03\u7528\u6309 fileKey \u91CD\u65B0\u5BFB\u5740\u2014\u2014\u540C fileKey \u591A\u4F1A\u8BDD\u65F6\u4F18\u5148\u4E0A\u4E00\u6B21\u4F7F\u7528\u7684\uFF08\u4EB2\u548C\uFF09\uFF0C\u6587\u4EF6\u5173\u95ED\u91CD\u5F00\u3001\u63D2\u4EF6\u91CD\u8DD1\u540E
312
335
  \u81EA\u52A8\u89E3\u6790\u5230\u5F53\u524D\u5B58\u6D3B\u5B9E\u4F8B\uFF0Cagent \u65E0\u9700\u5173\u5FC3 sessionId \u7684\u5931\u6548\u3002
313
336
 
314
337
  \u8BE6\u7EC6\u5DE5\u4F5C\u6D41\u89C1\u8D44\u6E90 \`batter://guides/design-to-code\`\u3002
@@ -318,7 +341,7 @@ var DESIGN_TO_CODE = `# \u8BBE\u8BA1\u8F6C\u4EE3\u7801\u5DE5\u4F5C\u6D41
318
341
  ## \u3010\u786C\u6027\u89C4\u5219 \xB7 \u8FDD\u53CD\u5373\u8FD4\u5DE5\u3011
319
342
 
320
343
  1. **\u8D44\u6E90\u662F\u672C\u673A\u6587\u4EF6\uFF0C\u6309\u9700\u53D6\u7528**\uFF1ADSL \u4E0E\u622A\u56FE\u7ED3\u679C\u4E2D\u7684 \`img\` \u5B57\u6BB5\u662F\u4EA7\u7269**\u6587\u4EF6\u540D**
321
- \uFF08\u5982 \`"ab12cd34.svg"\`\uFF09\uFF0C\u5B8C\u6574\u8DEF\u5F84 = \u4EA7\u7269\u76EE\u5F55 + \u6587\u4EF6\u540D\uFF08\u901A\u5E38\u4E3A \`%TEMP%\\batter-pixso-assets\`\uFF09\u3002\u4EA7\u7269\u4E0E\u670D\u52A1\u540C\u673A\uFF0C\u76F4\u63A5\u8BFB\u6587\u4EF6\u7CFB\u7EDF\uFF1A
344
+ \uFF08\u5982 \`"ab12cd34.svg"\`\uFF09\uFF0C\u5B8C\u6574\u8DEF\u5F84 = \u4EA7\u7269\u76EE\u5F55 + \u6587\u4EF6\u540D\uFF08\u672C\u673A\u4EA7\u7269\u76EE\u5F55\uFF1A\`${ASSETS_DIR}\`\uFF09\u3002\u4EA7\u7269\u4E0E\u670D\u52A1\u540C\u673A\uFF0C\u76F4\u63A5\u8BFB\u6587\u4EF6\u7CFB\u7EDF\uFF1A
322
345
  - **\u622A\u56FE\u7B49\u89C6\u89C9\u53C2\u7167**\uFF1A\u7528 Read \u5DE5\u5177\u76F4\u63A5\u8BFB\u8BE5\u8DEF\u5F84\u67E5\u770B\uFF0C\u65E0\u9700\u590D\u5236\u3002
323
346
  - **\u8981\u5199\u8FDB\u4EA4\u4ED8\u4EE3\u7801\u7684\u56FE\u6807/\u56FE\u7247**\uFF1A**\u5FC5\u987B**\u5148\u590D\u5236\u5230\u9879\u76EE\u8D44\u6E90\u76EE\u5F55
324
347
  \uFF08PowerShell \`Copy-Item\` / \u7C7B Unix \`cp\`\uFF09\uFF0C\u518D\u4EE5\u672C\u5730\u76F8\u5BF9\u8DEF\u5F84\u5F15\u7528
@@ -336,12 +359,12 @@ var DESIGN_TO_CODE = `# \u8BBE\u8BA1\u8F6C\u4EE3\u7801\u5DE5\u4F5C\u6D41
336
359
 
337
360
  1. **\u786E\u8BA4\u76EE\u6807\u8282\u70B9**
338
361
  - \u7528\u6237\u7ED9\u4E86 Pixso \u94FE\u63A5\uFF1A\u6309\u4E0A\u65B9\u786C\u6027\u89C4\u5219\u7B2C 2 \u6761\u63D0\u53D6 guid\uFF0C\u76F4\u63A5\u7EE7\u7EED\u3002
339
- - \u7528\u6237\u660E\u786E\u8BF4\u4E86"\u8FD9\u4E2A/\u6211\u9009\u4E2D\u7684"\uFF1A\u8C03 \`get_selected\` \u62FF guid\uFF0C\u76F4\u63A5\u7EE7\u7EED\u3002
362
+ - \u7528\u6237\u660E\u786E\u8BF4\u4E86"\u8FD9\u4E2A/\u6211\u9009\u4E2D\u7684"\uFF1A\u8C03 \`get_selected\` \u62FF guid \u4E0E fileKey\uFF0C\u76F4\u63A5\u7EE7\u7EED\u3002
340
363
  - \u5176\u4F59\u4F9D\u8D56\u5F53\u524D\u9009\u533A\u7684\u573A\u666F\uFF1A**\u4E25\u7981\u9690\u5F0F\u4F7F\u7528\u9009\u533A guid**\u2014\u2014\u8C03 \`get_selected\` \u540E
341
364
  \u5FC5\u987B\u5148\u628A\u9009\u4E2D\u56FE\u5C42\u540D\u79F0\u62A5\u7ED9\u7528\u6237\u786E\u8BA4\uFF08"\u5F53\u524D\u9009\u4E2D\u7684\u662F\u300CXX\u300D\uFF0C\u786E\u8BA4\u5904\u7406\u5B83\u5417\uFF1F"\uFF09\uFF0C\u786E\u8BA4\u540E\u518D\u7EE7\u7EED\u3002
342
- \`get_node_dsl\` / \`get_screenshot\` \u7684 guid \u7F3A\u7701\u56DE\u9000\u540C\u6837\u9002\u7528\u672C\u89C4\u5219\u3002
343
- 2. **\u53D6\u7ED3\u6784**\uFF1A\u63D2\u4EF6\u7C7B\u5DE5\u5177\u8FD4\u56DE\u7EDF\u4E00\u4E3A \`{sessionId, data}\` \u4FE1\u5C01\u2014\u2014DSL \u5728 \`data\` \u5B57\u6BB5\u91CC\u3002
344
- \`get_node_dsl(guid)\` \u62FF\u7CBE\u7B80 DSL\u3002
365
+ \u786E\u8BA4\u540E\u7684\u8C03\u7528\u91CC guid \u7528\u8FD4\u56DE\u7684\u9009\u4E2D\u56FE\u5C42 id\u3001\u8DEF\u7531\u53C2\u6570\u7528\u8FD4\u56DE\u7684 fileKey\uFF08guid \u5FC5\u4F20\u3001fileKey/sessionId \u5FC5\u4F20\u5176\u4E00\uFF09\u3002
366
+ 2. **\u53D6\u7ED3\u6784**\uFF1A\u63D2\u4EF6\u7C7B\u5DE5\u5177\u8FD4\u56DE\u7EDF\u4E00\u4E3A \`{sessionId, fileKey, data}\` \u4FE1\u5C01\u2014\u2014DSL \u5728 \`data\` \u5B57\u6BB5\u91CC\u3002
367
+ \`get_node_dsl(guid, fileKey)\` \u62FF\u7CBE\u7B80 DSL\uFF08guid \u5FC5\u4F20\uFF0CfileKey/sessionId \u5FC5\u4F20\u5176\u4E00\uFF09\u3002
345
368
  **\u9996\u6B21\u63A5\u89E6\u4E00\u4E2A\u8BBE\u8BA1\u7A3F/\u9875\u9762**\uFF08\u8FD8\u6CA1\u6709\u76EE\u6807\u8282\u70B9\u7684\u4E0A\u4E0B\u6587\u65F6\uFF09\uFF1A\u5148 \`get_metadata(guid)\`
346
369
  \u62FF\u5168\u6811\u9AA8\u67B6\uFF08\u4EC5 id/name/type/kids\uFF0C\u65E0\u6837\u5F0F/\u5750\u6807/\u8D44\u6E90\uFF0C\u4F53\u79EF\u6781\u5C0F\uFF09+ \`get_screenshot(guid)\` \u62FF\u89C6\u89C9\u5168\u8C8C\uFF0C
347
370
  \u636E\u6B64\u5B9A\u4F4D\u76EE\u6807\u8282\u70B9\uFF0C\u518D\u5BF9\u76EE\u6807\u6B63\u5E38\u53D6 DSL\u3002\u9AA8\u67B6\u4E2D ICON/IMAGE \u53F6\u5B50\u65E0 \`img\`\u2014\u2014\u9700\u8D44\u6E90\u65F6\u6309\u5176 \`id\` \u4E0B\u94BB\u5168\u91CF DSL\u3002
@@ -388,6 +411,7 @@ DSL \u662F\u5355\u884C\u7D27\u51D1 JSON\uFF1A\u77ED key\u3001\u4F4D\u7F6E\u6570\
388
411
  "palette": ["#1a73e8", "#f5222d"], // \u8C03\u8272\u677F\uFF08\u4EC5\u6839\u8282\u70B9\uFF09\uFF0C"p0"/"p1" \u4E3A\u4E0B\u6807\u5F15\u7528
389
412
  "depth": 6, // \u6574\u68F5\u6811\u5B9E\u9645\u6700\u5927\u6DF1\u5EA6\uFF08\u4EC5\u6839\u8282\u70B9\uFF0C\u603B\u662F\u8F93\u51FA\uFF0C\u4E0E\u622A\u65AD\u65E0\u5173\uFF09
390
413
  "more": 3, // \u4EC5\u56E0 maxDepth \u622A\u65AD\u65F6\u51FA\u73B0\uFF1A\u8BE5\u8282\u70B9\u4E0B\u8FD8\u6709 3 \u7EA7\u672A\u5C55\u5F00\uFF0C\u62FF id \u56DE\u4F20 guid \u7EE7\u7EED\u4E0B\u94BB
414
+ "kidsSummary": [{"id": "...", "name": "...", "type": "FRAME"}], // \u622A\u65AD\u5904\u6216\u584C\u7F29\u53F6\u5B50\uFF08img \u8282\u70B9\uFF09\u7684\u76F4\u63A5\u5B50\u8282\u70B9\u6D45\u9884\u89C8
391
415
  "kids": [] // \u5B50\u8282\u70B9\u9012\u5F52
392
416
  }
393
417
  \`\`\`
@@ -406,7 +430,7 @@ DSL \u662F\u5355\u884C\u7D27\u51D1 JSON\uFF1A\u77ED key\u3001\u4F4D\u7F6E\u6570\
406
430
  - **\u7F3A guid**\uFF1A\u5411\u7528\u6237\u8981\u5B8C\u6574\u7684 Pixso \u94FE\u63A5\uFF08\u542B item-id \u67E5\u8BE2\u53C2\u6570\uFF09\uFF0C\u4E0D\u8981\u731C\u6D4B\u8282\u70B9 id\u3002
407
431
  - **\u547D\u4EE4\u8D85\u65F6/\u7ED3\u679C\u8FC7\u5927**\uFF1A\u5BF9\u76EE\u6807\u8282\u70B9\u7684\u5B50\u8282\u70B9\u7528 \`get_node_dsl\` + \u8F83\u4F4E \`maxDepth\` \u5206\u6BB5\u53D6\uFF0C\u4E0D\u8981\u6574\u6811\u4E00\u6B21\u62C9\u3002
408
432
  - **\u7ED3\u679C\u7591\u4F3C\u8FC7\u671F**\uFF08\u8BBE\u8BA1\u7A3F\u521A\u6539\u8FC7\uFF09\uFF1A\u7ED9\u5BFC\u51FA\u7C7B\u5DE5\u5177\u52A0 \`cache: "refresh"\` \u5F3A\u5236\u5237\u65B0\u3002
409
- - **\u63D2\u4EF6\u65E0\u54CD\u5E94**\uFF08ping \u63A2\u6D4B\u8D85\u65F6\uFF09\uFF1A\u63D2\u4EF6\u672A\u8FD0\u884C\u6216\u6B63\u5728\u6267\u884C\u957F\u547D\u4EE4\u2014\u2014\u7A0D\u540E\u91CD\u8BD5\uFF1B\u6587\u4EF6\u91CD\u5F00\u573A\u666F \`target\` \u7528 fileKey \u4F1A\u81EA\u52A8\u89E3\u6790\u5230\u65B0\u5B9E\u4F8B\u3002
433
+ - **\u63D2\u4EF6\u65E0\u54CD\u5E94**\uFF08ping \u63A2\u6D4B\u8D85\u65F6\uFF09\uFF1A\u63D2\u4EF6\u672A\u8FD0\u884C\u6216\u6B63\u5728\u6267\u884C\u957F\u547D\u4EE4\u2014\u2014\u7A0D\u540E\u91CD\u8BD5\uFF1B\u6587\u4EF6\u91CD\u5F00\u573A\u666F\u7528 fileKey \u5BFB\u5740\u4F1A\u81EA\u52A8\u89E3\u6790\u5230\u65B0\u5B9E\u4F8B\u3002
410
434
  - **\u7981\u6B62\u4EC5\u51ED\u622A\u56FE\u786C\u7F16\u7801**\uFF1A\u622A\u56FE\u662F\u6821\u9A8C\u4E0E\u610F\u56FE\u53C2\u7167\uFF0C\u4E0D\u662F\u7ED3\u6784\u6765\u6E90\u2014\u2014\u7ED3\u6784\u4E00\u5F8B\u8D70 DSL\u3002
411
435
 
412
436
  ## \u7EC4\u4EF6\u5173\u8054\uFF08comp \u5B57\u6BB5\uFF09
@@ -424,6 +448,8 @@ INSTANCE / COMPONENT / COMPONENT_SET \u8282\u70B9\u5E26 \`comp\` \u5B57\u6BB5\uF
424
448
  - \`type: "IMAGE"\`\uFF1A\u56FE\u7247\u586B\u5145\u8282\u70B9\uFF0C\`img\` \u4E3A **PNG** \u6587\u4EF6\u540D\u3002
425
449
  - \u6587\u4EF6\u540D\u62FC\u8DEF\u5F84\u4E0E\u53D6\u7528\u8981\u6C42\u89C1\u5F00\u5934\u786C\u6027\u89C4\u5219\u7B2C 1 \u6761\u3002
426
450
  - \u5BB9\u5668\u81EA\u8EAB\u6709\u5BFC\u51FA\u8BBE\u7F6E\u6807\u8BB0\u65F6\u4E5F\u4F1A\u584C\u7F29\u4E3A\u53F6\u5B50\uFF08\u5C0A\u91CD\u8BBE\u8BA1\u5E08\u7684\u5BFC\u51FA\u610F\u56FE\uFF09\u3002
451
+ - **\u584C\u7F29\u53F6\u5B50\u5E26 \`kidsSummary\`**\uFF08\u76F4\u63A5\u5B50\u8282\u70B9\u6D45\u9884\u89C8\uFF09\uFF1A\u5185\u90E8\u7ED3\u6784\u9ED8\u8BA4\u584C\u7F29\u9690\u85CF\uFF0C\u9700\u8981\u4E0B\u63A2\u65F6
452
+ \uFF08\u5982\u5B9E\u4F8B\u5185\u8FD8\u6709\u6587\u672C/\u5FBD\u6807/\u5355\u72EC\u6807\u8BB0\u5BFC\u51FA\u7684\u5B50\u8282\u70B9\uFF09\u62FF\u5B50\u8282\u70B9 \`id\` \u56DE\u4F20 \`guid\` \u53D6\u5168\u91CF DSL\u3002
427
453
  - **\u5DF2\u5E26 \`img\` \u7684\u8282\u70B9\u5728\u53D6 DSL \u65F6\u5DF2\u5B8C\u6210\u5BFC\u51FA**\uFF1A\u76F4\u63A5\u4F7F\u7528\u5176\u6587\u4EF6\u540D\uFF0C\u4E25\u7981\u5BF9\u5B83\u4EEC\u518D\u8C03 \`export_image\`\uFF08\u91CD\u590D\u5BFC\u51FA\u540C\u4E00\u4EFD\u5185\u5BB9\uFF09\u3002
428
454
  \`export_image\` \u4EC5\u7528\u4E8E DSL \u4E2D\u672A\u584C\u7F29\u7684\u8282\u70B9\uFF08\u4E3B\u52A8\u5207\u56FE\uFF09\u3002
429
455
 
@@ -432,7 +458,7 @@ INSTANCE / COMPONENT / COMPONENT_SET \u8282\u70B9\u5E26 \`comp\` \u5B57\u6BB5\uF
432
458
  - DSL \u662F**\u6709\u635F\u7CBE\u7B80**\uFF1A\u7701\u7565\u4E86\u5E03\u5C14\u8FD0\u7B97\u8DEF\u5F84\u3001\u8499\u7248\u7B49\u7EAF\u8BBE\u8BA1\u7EC6\u8282\uFF0C\u89C6\u89C9\u590D\u6742\u8282\u70B9\u4EE5\u622A\u56FE\u4E3A\u51C6\u3002
433
459
  - \u5BFC\u51FA\u7C7B\u5DE5\u5177\uFF08\`get_node_dsl\`/\`get_screenshot\`/\`export_image\`\uFF09\u5E26 15 \u5206\u949F\u7ED3\u679C\u7F13\u5B58\uFF08\u63D2\u4EF6\u4FA7 TTL\uFF09\uFF1B
434
460
  \u8BBE\u8BA1\u7A3F\u521A\u6539\u8FC7\u4F46\u7ED3\u679C\u5BF9\u4E0D\u4E0A\u65F6\uFF0C\u52A0 \`cache: "refresh"\` \u5F3A\u5236\u5237\u65B0\u3002
435
- - \u591A\u6587\u4EF6\u573A\u666F\u6240\u6709\u5DE5\u5177\u90FD\u652F\u6301 \`target\` \u53C2\u6570\uFF08sessionId/fileKey\uFF09\uFF0C\u89C1 \`batter://guides/intro\`\u3002
461
+ - \u591A\u6587\u4EF6\u573A\u666F\u8282\u70B9\u7C7B\u5DE5\u5177\u7528 \`fileKey\`/\`sessionId\` \u53C2\u6570\u8DEF\u7531\uFF08\u4E8C\u9009\u4E00\u5FC5\u4F20\uFF09\uFF0C\u89C1 \`batter://guides/intro\`\u3002
436
462
  `;
437
463
  var GUIDES = [
438
464
  {
@@ -477,7 +503,7 @@ var DESIGN_TO_CODE2 = {
477
503
  `## \u6267\u884C\u6D41\u7A0B`,
478
504
  ``,
479
505
  `1. \u786E\u8BA4\u76EE\u6807\u8282\u70B9\uFF1A${targetLine}`,
480
- `2. \u53D6\u6570\u636E\uFF1A\u63D2\u4EF6\u7C7B\u5DE5\u5177\u8FD4\u56DE\u7EDF\u4E00\u4E3A \`{sessionId, data}\` \u4FE1\u5C01\uFF0C\u53D6 \`data\` \u5B57\u6BB5\u3002\u9996\u6B21\u63A5\u89E6\u8BBE\u8BA1\u7A3F\uFF1A\u5148 \`get_metadata(guid)\` \u62FF\u5168\u6811\u9AA8\u67B6\u5B9A\u4F4D\u76EE\u6807 + \`get_screenshot(guid)\` \u62FF\u89C6\u89C9\u5168\u8C8C\uFF1B\u968F\u540E \`get_node_dsl(guid)\` \u62FF\u5168\u91CF DSL\uFF08\u7ED3\u6784\u5F88\u5927\u65F6\u7528 \`maxDepth\` \u5206\u6BB5\u4E0B\u63A2\u2014\u2014\u622A\u65AD\u5904\u8282\u70B9\u5E26 \`more\` \u6807\u8BB0\u4E0E \`kidsSummary\` \u6D45\u9884\u89C8\uFF0C\u62FF\u5176 \`id\` \u56DE\u4F20 \`guid\` \u4E0B\u94BB\uFF09\u3002DSL \u4E2D\u5DF2\u5E26 \`img\` \u7684\u8282\u70B9\uFF08ICON/IMAGE \u53F6\u5B50\uFF09\u5DF2\u5B8C\u6210\u5BFC\u51FA\uFF0C\u76F4\u63A5\u7528\u5176\u6587\u4EF6\u540D\uFF0C\u4E0D\u8981\u518D\u8C03 \`export_image\`\u3002`,
506
+ `2. \u53D6\u6570\u636E\uFF1A\u63D2\u4EF6\u7C7B\u5DE5\u5177\u8FD4\u56DE\u7EDF\u4E00\u4E3A \`{sessionId, fileKey, data}\` \u4FE1\u5C01\uFF0C\u53D6 \`data\` \u5B57\u6BB5\u3002\u8282\u70B9\u7C7B\u5DE5\u5177\u7684 guid \u5FC5\u4F20\uFF0CfileKey/sessionId \u5FC5\u4F20\u5176\u4E00\uFF08\u4F18\u5148 fileKey\uFF1B\u9009\u533A\u573A\u666F\u7528 \`get_selected\` \u8FD4\u56DE\u7684 fileKey\uFF09\u3002\u9996\u6B21\u63A5\u89E6\u8BBE\u8BA1\u7A3F\uFF1A\u5148 \`get_metadata(guid, fileKey)\` \u62FF\u5168\u6811\u9AA8\u67B6\u5B9A\u4F4D\u76EE\u6807 + \`get_screenshot(guid, fileKey)\` \u62FF\u89C6\u89C9\u5168\u8C8C\uFF1B\u968F\u540E \`get_node_dsl(guid, fileKey)\` \u62FF\u5168\u91CF DSL\uFF08\u7ED3\u6784\u5F88\u5927\u65F6\u7528 \`maxDepth\` \u5206\u6BB5\u4E0B\u63A2\u2014\u2014\u622A\u65AD\u5904\u8282\u70B9\u5E26 \`more\` \u6807\u8BB0\u4E0E \`kidsSummary\` \u6D45\u9884\u89C8\uFF0C\u62FF\u5176 \`id\` \u56DE\u4F20 \`guid\` \u4E0B\u94BB\uFF09\u3002DSL \u4E2D\u5DF2\u5E26 \`img\` \u7684\u8282\u70B9\uFF08ICON/IMAGE \u53F6\u5B50\uFF09\u5DF2\u5B8C\u6210\u5BFC\u51FA\uFF0C\u76F4\u63A5\u7528\u5176\u6587\u4EF6\u540D\uFF0C\u4E0D\u8981\u518D\u8C03 \`export_image\`\u3002`,
481
507
  `3. \u751F\u6210\u4EE3\u7801\uFF1A\u6309\u6307\u5357\u4E2D\u7684 DSL \u5B57\u6BB5\u8BED\u4E49\u6620\u5C04\u5230\u76EE\u6807\u6846\u67B6\u3002`,
482
508
  `4. \u81EA\u68C0\uFF1A\u518D\u6B21\u8C03 \`get_screenshot(guid)\`\uFF0C\u4E0E\u4EE3\u7801\u6E32\u67D3\u7ED3\u679C\u5BF9\u6BD4\uFF0C\u6709\u504F\u5DEE\u5C31\u4FEE\u6B63\u540E\u518D\u6BD4\u4E00\u8F6E\uFF08\u5BFC\u51FA\u7C7B\u5DE5\u5177\u5E26 15 \u5206\u949F\u7F13\u5B58\uFF1B\u7ED3\u679C\u7591\u4F3C\u8FC7\u671F\u65F6\u52A0 \`cache: "refresh"\` \u5F3A\u5236\u5237\u65B0\uFF09\u3002`,
483
509
  ``,
@@ -565,15 +591,131 @@ var CommandBus = class {
565
591
  };
566
592
  var commandBus = new CommandBus();
567
593
 
594
+ // src/plugin/sessions.ts
595
+ import { randomUUID } from "node:crypto";
596
+ var ONLINE_WINDOW_MS = 45e3;
597
+ function unknownRouteError(route) {
598
+ if (route.fileKey) {
599
+ return new Error(`\u627E\u4E0D\u5230 fileKey \u4E3A ${route.fileKey} \u7684\u5B58\u6D3B\u63D2\u4EF6\u4F1A\u8BDD\u3002\u53EF\u7528 list_sessions \u67E5\u770B\u5F53\u524D\u8FDE\u63A5\u7684\u4F1A\u8BDD`);
600
+ }
601
+ return new Error(`\u627E\u4E0D\u5230 sessionId \u4E3A ${route.sessionId} \u7684\u5B58\u6D3B\u63D2\u4EF6\u4F1A\u8BDD\uFF08\u53EF\u80FD\u5DF2\u88AB\u5254\u9664\uFF09\u3002\u53EF\u7528 list_sessions \u67E5\u770B\u5F53\u524D\u8FDE\u63A5\u7684\u4F1A\u8BDD`);
602
+ }
603
+ var SessionRegistry = class {
604
+ sessions = /* @__PURE__ */ new Map();
605
+ lastActiveId = null;
606
+ /** 会话亲和:fileKey → 上一次路由使用的 sessionId(仍在线时优先,避免同文件多实例间漂移) */
607
+ lastUsedByFileKey = /* @__PURE__ */ new Map();
608
+ isOnline(s) {
609
+ return s.connected || Date.now() - s.lastSeen < ONLINE_WINDOW_MS;
610
+ }
611
+ /** fileKey 寻址候选:该文件的在线会话(谁先 pong 由 ping 竞速决定,此处不排序) */
612
+ candidatesForFileKey(fileKey) {
613
+ return [...this.sessions.values()].filter((s) => s.meta.fileKey === fileKey && this.isOnline(s));
614
+ }
615
+ /** 亲和查询:上一次为该 fileKey 服务的 sessionId */
616
+ affinityIdOf(fileKey) {
617
+ return this.lastUsedByFileKey.get(fileKey);
618
+ }
619
+ /** 亲和钉住:fileKey 寻址探活成功后记录(下次优先) */
620
+ pinAffinity(fileKey, sessionId) {
621
+ this.lastUsedByFileKey.set(fileKey, sessionId);
622
+ }
623
+ /** 剔除会话(幽灵剔除/ping 判死共用):清理亲和引用;存活但繁忙被误剔的插件会在 SSE 断开或上报 404 后自重注册;
624
+ * 失效 sessionId 的 failover 不靠墓碑——工具结果信封带 fileKey,agent 手里的稳定句柄一直是 fileKey */
625
+ evict(id) {
626
+ if (this.sessions.delete(id)) {
627
+ for (const [k, v] of this.lastUsedByFileKey) {
628
+ if (v === id) this.lastUsedByFileKey.delete(k);
629
+ }
630
+ log(`session evicted: ${id.slice(0, 8)}`);
631
+ }
632
+ }
633
+ register(meta = {}) {
634
+ if (meta.fileKey) {
635
+ for (const s of [...this.sessions.values()]) {
636
+ if (s.meta.fileKey === meta.fileKey && !this.isOnline(s)) {
637
+ this.evict(s.id);
638
+ }
639
+ }
640
+ }
641
+ const session = { id: randomUUID(), meta, lastSeen: Date.now(), connected: false };
642
+ this.sessions.set(session.id, session);
643
+ this.lastActiveId = session.id;
644
+ log(`session registered: ${session.id} (${meta.fileName ?? "unknown file"})`);
645
+ return this.toInfo(session);
646
+ }
647
+ /** 心跳/连接状态用:只刷 lastSeen,不影响 lastActiveId("最近活跃"= 用户行为,心跳不算) */
648
+ touchSeen(id) {
649
+ const s = this.sessions.get(id);
650
+ if (!s) return null;
651
+ s.lastSeen = Date.now();
652
+ return s;
653
+ }
654
+ /** 用户行为(选区上报/命令服务/注册):刷 lastSeen 并标记最近活跃 */
655
+ touch(id) {
656
+ const s = this.touchSeen(id);
657
+ if (!s) return null;
658
+ this.lastActiveId = id;
659
+ return s;
660
+ }
661
+ setConnected(id, connected) {
662
+ const s = this.touchSeen(id);
663
+ if (!s) return false;
664
+ s.connected = connected;
665
+ return true;
666
+ }
667
+ updateContext(id, meta) {
668
+ const s = this.touch(id);
669
+ if (!s) return false;
670
+ s.meta = { ...s.meta, ...meta };
671
+ log(
672
+ `context updated [${id.slice(0, 8)}]: ${s.meta.fileName ?? "?"} / ${s.meta.pageName ?? "?"} / ${s.meta.selection?.length ?? 0} selected`
673
+ );
674
+ return true;
675
+ }
676
+ list() {
677
+ return [...this.sessions.values()].map((s) => this.toInfo(s));
678
+ }
679
+ /** 纯查找(不判活):工具层寻址用——离线会话也可取到 fileKey 用于候选扩展与故障转移 */
680
+ get(id) {
681
+ return this.sessions.get(id);
682
+ }
683
+ /** 缺省路由:最近活跃在线会话 > 任意在线会话(显式 fileKey/sessionId 由工具层 get/candidatesForFileKey + ping 完成) */
684
+ resolve() {
685
+ if (this.lastActiveId) {
686
+ const s = this.sessions.get(this.lastActiveId);
687
+ if (s && this.isOnline(s)) return s;
688
+ }
689
+ const fallback = [...this.sessions.values()].find((s) => this.isOnline(s));
690
+ if (fallback) return fallback;
691
+ throw new Error("Pixso \u63D2\u4EF6\u672A\u8FDE\u63A5\uFF1A\u8BF7\u5728 Pixso \u5BA2\u6237\u7AEF\u4E2D\u8FD0\u884C Batter Pixso Bridge \u63D2\u4EF6");
692
+ }
693
+ toInfo(s) {
694
+ return {
695
+ sessionId: s.id,
696
+ fileKey: s.meta.fileKey,
697
+ fileName: s.meta.fileName,
698
+ pageName: s.meta.pageName,
699
+ selectionCount: s.meta.selection?.length,
700
+ online: this.isOnline(s),
701
+ lastSeen: s.lastSeen
702
+ };
703
+ }
704
+ };
705
+ var sessions = new SessionRegistry();
706
+
568
707
  // src/agent/tools/shared.ts
569
708
  var PING_TIMEOUT_MS = 800;
570
709
  async function run(type, args, signal) {
571
- const { target, ...rest } = args;
710
+ const { fileKey, sessionId, ...rest } = args;
572
711
  try {
573
- const session = await resolveAndPing(target, signal);
712
+ const session = await resolveAndPing(
713
+ { fileKey, sessionId },
714
+ signal
715
+ );
574
716
  const data = await commandBus.execute(type, rest, { sessionId: session.id, signal });
575
717
  sessions.touch(session.id);
576
- return jsonResult({ sessionId: session.id, data });
718
+ return jsonResult({ sessionId: session.id, fileKey: session.meta.fileKey, data });
577
719
  } catch (err) {
578
720
  return errorResult(err);
579
721
  }
@@ -602,29 +744,36 @@ async function pingCandidates(candidates, affinityId, signal) {
602
744
  return null;
603
745
  }
604
746
  }
605
- async function resolveAndPing(target, signal) {
606
- const s = target ? sessions.get(target) : sessions.resolve();
607
- const fileKey = s ? s.meta.fileKey : target;
747
+ async function resolveAndPing(route, signal) {
748
+ const s = route.sessionId ? sessions.get(route.sessionId) : route.fileKey ? void 0 : sessions.resolve();
749
+ if (route.sessionId && !s) throw unknownRouteError(route);
750
+ const fileKey = s ? s.meta.fileKey : route.fileKey;
608
751
  const candidates = s && !fileKey ? [s] : sessions.candidatesForFileKey(fileKey);
609
752
  if (candidates.length === 0) {
610
753
  if (s) throw new Error(`\u76EE\u6807\u63D2\u4EF6\u5DF2\u79BB\u7EBF\uFF1A${s.meta.fileName ?? s.id}\uFF08\u91CD\u65B0\u6253\u5F00\u6587\u4EF6\u5E76\u8FD0\u884C\u63D2\u4EF6\u540E\u91CD\u8BD5\uFF09`);
611
- throw unknownTargetError(target);
754
+ throw unknownRouteError(route);
612
755
  }
613
756
  const winner = await pingCandidates(candidates, fileKey ? sessions.affinityIdOf(fileKey) : void 0, signal);
614
757
  if (!winner) {
615
758
  for (const c of candidates) sessions.evict(c.id);
616
759
  throw new Error(
617
- `\u63D2\u4EF6\u65E0\u54CD\u5E94\uFF08${PING_TIMEOUT_MS}ms \u672A\u56DE pong\uFF09\uFF1A${fileKey ?? target}\u2014\u2014\u63D2\u4EF6\u672A\u8FD0\u884C\uFF0C\u6216\u6B63\u5728\u6267\u884C\u957F\u547D\u4EE4\uFF08\u7A0D\u540E\u91CD\u8BD5\uFF09`
760
+ `\u63D2\u4EF6\u65E0\u54CD\u5E94\uFF08${PING_TIMEOUT_MS}ms \u672A\u56DE pong\uFF09\uFF1A${fileKey ?? route.sessionId}\u2014\u2014\u63D2\u4EF6\u672A\u8FD0\u884C\uFF0C\u6216\u6B63\u5728\u6267\u884C\u957F\u547D\u4EE4\uFF08\u7A0D\u540E\u91CD\u8BD5\uFF09`
618
761
  );
619
762
  }
620
763
  if (fileKey) sessions.pinAffinity(fileKey, winner.id);
621
764
  return winner;
622
765
  }
623
- var ENVELOPE_NOTE = "\u8FD4\u56DE\u5C01\u88C5\u4E3A {sessionId, data}\uFF08\u9519\u8BEF\u4E3A isError \u6587\u672C\uFF0C\u4E0D\u542B\u4FE1\u5C01\uFF09\uFF1AsessionId \u662F ping/pong \u786E\u8BA4\u7684\u5B9E\u9645\u670D\u52A1\u4F1A\u8BDD\uFF1Btarget \u7528 fileKey \u6700\u7A33\u2014\u2014\u6587\u4EF6\u91CD\u5F00\u540E\u81EA\u52A8\u89E3\u6790\u5230\u5B58\u6D3B\u5B9E\u4F8B\uFF08\u591A\u5B9E\u4F8B\u4F18\u5148\u4E0A\u4E00\u6B21\u4F7F\u7528\u7684\uFF09\u3002";
766
+ var ENVELOPE_NOTE = "\u8FD4\u56DE\u5C01\u88C5\u4E3A {sessionId, fileKey, data}\uFF08\u9519\u8BEF\u4E3A isError \u6587\u672C\uFF0C\u4E0D\u542B\u4FE1\u5C01\uFF09\uFF1AsessionId \u662F ping/pong \u786E\u8BA4\u7684\u5B9E\u9645\u670D\u52A1\u4F1A\u8BDD\uFF0CfileKey \u662F\u8DE8\u91CD\u5F00\u7A33\u5B9A\u7684\u53E5\u67C4\u2014\u2014\u540E\u7EED\u8C03\u7528\u7528 fileKey \u5BFB\u5740\u3002";
624
767
  var GUIDES_PTR = "\u5B8C\u6574\u89C4\u5219\uFF08\u8D44\u6E90\u53D6\u7528/\u7EC4\u4EF6\u6620\u5C04/\u9519\u8BEF\u6062\u590D/\u7981\u6B62\u622A\u56FE\u786C\u7F16\u7801\uFF09\u89C1\u8D44\u6E90 batter://guides/design-to-code\u3002";
625
- var TARGET_PROP = {
768
+ var FILE_KEY_PROP = {
626
769
  type: "string",
627
- description: "\u76EE\u6807\u63D2\u4EF6\u4F1A\u8BDD\uFF08sessionId \u6216 fileKey\uFF09\u3002\u591A\u4E2A Pixso \u6587\u4EF6\u540C\u65F6\u5F00\u63D2\u4EF6\u65F6\u624D\u9700\u8981\uFF1B\u7F3A\u7701\u8DEF\u7531\u5230\u6700\u8FD1\u6D3B\u8DC3\u6587\u4EF6\u3002\u53EF\u7528 list_sessions \u67E5\u770B\u5F53\u524D\u4F1A\u8BDD\u3002"
770
+ minLength: 1,
771
+ description: "\u76EE\u6807\u8BBE\u8BA1\u6587\u4EF6\u7684 fileKey\uFF08\u8DE8\u91CD\u5F00\u7A33\u5B9A\uFF0C\u4F18\u5148\uFF09\u3002\u4E0E sessionId \u4E8C\u9009\u4E00\uFF1B\u7528 list_sessions \u67E5\u770B\u5F53\u524D\u4F1A\u8BDD\u3002"
772
+ };
773
+ var SESSION_ID_PROP = {
774
+ type: "string",
775
+ minLength: 1,
776
+ description: "\u76EE\u6807\u63D2\u4EF6\u4F1A\u8BDD\u7684 sessionId\uFF08ping/pong \u786E\u8BA4\u7684\u5B58\u6D3B\u5B9E\u4F8B\uFF09\u3002\u4E0E fileKey \u4E8C\u9009\u4E00\u3002"
628
777
  };
629
778
  var CACHE_PROP = {
630
779
  type: "string",
@@ -633,7 +782,8 @@ var CACHE_PROP = {
633
782
  };
634
783
  var GUID_PROP = {
635
784
  type: "string",
636
- description: '\u8282\u70B9 GUID\uFF08\u5982 "297:133032"\uFF09\u3002\u94FE\u63A5\u8F93\u5165\uFF1A\u4ECE item-id \u63D0\u53D6\u5E76 URL \u89E3\u7801\u540E\u4F20\u5165\uFF1B\u7F3A\u7701\u56DE\u9000\u5F53\u524D\u9009\u4E2D\u56FE\u5C42\uFF08\u7981\u6B62\u9690\u5F0F\u4F7F\u7528\u2014\u2014\u9009\u4E2D\u786E\u8BA4\u89C4\u5219\u89C1 get_selected\uFF09\u3002'
785
+ minLength: 1,
786
+ description: '\u8282\u70B9 GUID\uFF08\u5982 "297:133032"\uFF09\uFF0C\u5FC5\u4F20\u3002\u94FE\u63A5\u8F93\u5165\uFF1A\u4ECE item-id \u63D0\u53D6\u5E76 URL \u89E3\u7801\u540E\u4F20\u5165\uFF1B\u8981\u64CD\u4F5C\u5F53\u524D\u9009\u4E2D\u56FE\u5C42\u65F6\uFF0C\u5148\u7528 get_selected \u53D6\u5176 guid \u518D\u4F20\u5165\u3002'
637
787
  };
638
788
  function jsonResult(data) {
639
789
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
@@ -648,94 +798,94 @@ function errorResult(err) {
648
798
  // src/agent/tools/export_image.ts
649
799
  var exportImage = {
650
800
  description: '\u3010\u7528\u9014\u3011\u5BFC\u51FA\u8282\u70B9\u7684\u56FE\u7247\u7D20\u6750\uFF08\u5207\u56FE\uFF09\uFF0C\u4F9B\u4EA4\u4ED8\u4EE3\u7801\u4F7F\u7528\u3002\u5355\u8282\u70B9\u4F20 guid\uFF1B\u591A\u8282\u70B9\u4F20 guids \u6570\u7EC4\u6279\u91CF\u3002DSL \u4E2D\u5DF2\u5E26 img \u7684\u8282\u70B9\uFF08ICON/IMAGE \u53F6\u5B50\uFF09\u5728\u53D6 DSL \u65F6\u5DF2\u5BFC\u51FA\uFF0C\u76F4\u63A5\u7528\u5176\u6587\u4EF6\u540D\uFF0C\u4E25\u7981\u5BF9\u672C\u7C7B\u8282\u70B9\u91CD\u590D\u8C03\u7528\u3002\u3010\u547D\u4E2D\u4FE1\u53F7\u3011"\u5207\u56FE/\u5BFC\u51FA\u56FE\u6807/\u5BFC\u51FA\u56FE\u7247\u7D20\u6750/\u628A\u67D0\u4E2A\u8282\u70B9\u5B58\u6210\u56FE\u7247"\u3002\u8981\u89C6\u89C9\u53C2\u7167\u7528 get_screenshot\u3002\u3010\u8FD4\u56DE\u3011\u5355\u8282\u70B9 {width, height, exports: [{img, format, scale?, suffix?}]}\uFF1B\u6279\u91CF {items: [{guid, width, height, exports}]}\u3002\u8282\u70B9\u5E26\u8BBE\u8BA1\u5E08\u5BFC\u51FA\u8BBE\u7F6E\u65F6\u6309\u5176\u8BBE\u7F6E\u9010\u4EFD\u5BFC\u51FA\uFF08format/scale \u53C2\u6570\u5931\u6548\uFF09\uFF1B\u65E0\u6807\u8BB0\u6309\u53C2\u6570\u5BFC\u51FA\u5355\u4EFD\uFF08\u9ED8\u8BA4 PNG\u30012x\uFF09\u3002suffix\uFF08\u5982 "@2x"\uFF09\u590D\u5236\u8FDB\u9879\u76EE\u65F6\u4FDD\u7559\u547D\u540D\u3002\u3010\u786C\u6027\u89C4\u5219\u3011' + GUIDES_PTR + " " + ENVELOPE_NOTE,
651
- inputSchema: {
652
- type: "object",
653
- properties: {
654
- guid: GUID_PROP,
655
- guids: {
656
- type: "array",
657
- items: { type: "string" },
658
- description: "\u6279\u91CF\u5BFC\u51FA\uFF1A\u8282\u70B9 GUID \u6570\u7EC4\uFF08\u4E0E guid \u4E8C\u9009\u4E00\uFF09\u3002\u591A\u8282\u70B9\u573A\u666F\u4F18\u5148\u7528\u6279\u91CF\uFF0C\u907F\u514D\u9010\u4E2A\u8C03\u7528\u3002"
659
- },
660
- format: {
661
- type: "string",
662
- enum: ["PNG", "SVG", "JPG"],
663
- description: "\u65E0\u5BFC\u51FA\u6807\u8BB0\u65F6\u7684\u515C\u5E95\u683C\u5F0F\uFF0C\u9ED8\u8BA4 PNG\u3002\u8282\u70B9\u5E26\u5BFC\u51FA\u6807\u8BB0\u65F6\u6B64\u53C2\u6570\u65E0\u6548\u3002"
664
- },
665
- scale: {
666
- type: "number",
667
- description: "\u65E0\u5BFC\u51FA\u6807\u8BB0\u65F6\u7684\u515C\u5E95\u500D\u7387\uFF0C\u9ED8\u8BA4 2\u3002\u8282\u70B9\u5E26\u5BFC\u51FA\u6807\u8BB0\u65F6\u6B64\u53C2\u6570\u65E0\u6548\u3002"
668
- },
669
- cache: CACHE_PROP,
670
- target: TARGET_PROP
671
- }
801
+ // 两组互斥各取其一:guid|guids(单节点|批量)× fileKey|sessionId(路由)
802
+ requires: [
803
+ ["guid", "guids"],
804
+ ["fileKey", "sessionId"]
805
+ ],
806
+ properties: {
807
+ guid: GUID_PROP,
808
+ guids: {
809
+ type: "array",
810
+ items: { type: "string" },
811
+ description: "\u6279\u91CF\u5BFC\u51FA\uFF1A\u8282\u70B9 GUID \u6570\u7EC4\uFF08\u4E0E guid \u4E8C\u9009\u4E00\uFF09\u3002\u591A\u8282\u70B9\u573A\u666F\u4F18\u5148\u7528\u6279\u91CF\uFF0C\u907F\u514D\u9010\u4E2A\u8C03\u7528\u3002"
812
+ },
813
+ format: {
814
+ type: "string",
815
+ enum: ["PNG", "SVG", "JPG"],
816
+ description: "\u65E0\u5BFC\u51FA\u6807\u8BB0\u65F6\u7684\u515C\u5E95\u683C\u5F0F\uFF0C\u9ED8\u8BA4 PNG\u3002\u8282\u70B9\u5E26\u5BFC\u51FA\u6807\u8BB0\u65F6\u6B64\u53C2\u6570\u65E0\u6548\u3002"
817
+ },
818
+ scale: {
819
+ type: "number",
820
+ description: "\u65E0\u5BFC\u51FA\u6807\u8BB0\u65F6\u7684\u515C\u5E95\u500D\u7387\uFF0C\u9ED8\u8BA4 2\u3002\u8282\u70B9\u5E26\u5BFC\u51FA\u6807\u8BB0\u65F6\u6B64\u53C2\u6570\u65E0\u6548\u3002"
821
+ },
822
+ cache: CACHE_PROP,
823
+ fileKey: FILE_KEY_PROP,
824
+ sessionId: SESSION_ID_PROP
672
825
  }
673
826
  };
674
827
 
675
828
  // src/agent/tools/get_metadata.ts
676
829
  var getMetadata = {
677
830
  description: '\u3010\u7528\u9014\u3011\u5168\u6811\u7ED3\u6784\u9AA8\u67B6\uFF08\u4EC5 id/name/type/kids\uFF0C\u65E0\u6837\u5F0F/\u5750\u6807/\u8D44\u6E90\uFF0C\u4F53\u79EF\u6781\u5C0F\uFF09\u3002\u9996\u6B21\u63A5\u89E6\u8BBE\u8BA1\u7A3F\u65F6\uFF1A\u672C\u5DE5\u5177 + get_screenshot \u5B9A\u4F4D\u76EE\u6807\u8282\u70B9\uFF0C\u518D\u5BF9\u76EE\u6807\u8C03 get_node_dsl \u53D6\u5168\u91CF\u3002\u3010\u547D\u4E2D\u4FE1\u53F7\u3011\u9996\u6B21\u63A5\u89E6\u8BBE\u8BA1\u7A3F\uFF1B"\u9875\u9762\u91CC\u6709\u6CA1\u6709 XX"\uFF1B\u6309\u540D\u5B57\u627E\u8282\u70B9\uFF1B\u53EA\u8981\u7ED3\u6784\u6982\u89C8\u4E0D\u8981\u6837\u5F0F\u3002\u3010\u8FD4\u56DE\u3011\u7D27\u51D1 JSON\uFF1A{id, name?, type, kids: \u9012\u5F52}\uFF1BICON/IMAGE \u53F6\u5B50\u65E0 img\uFF08\u9700\u8D44\u6E90\u65F6\u6309 id \u8C03 get_node_dsl\uFF09\u3002\u3010\u786C\u6027\u89C4\u5219\u3011' + GUIDES_PTR + " " + ENVELOPE_NOTE,
678
- inputSchema: {
679
- type: "object",
680
- properties: {
681
- guid: GUID_PROP,
682
- cache: CACHE_PROP,
683
- target: TARGET_PROP
684
- }
831
+ requires: ["guid", ["fileKey", "sessionId"]],
832
+ properties: {
833
+ guid: GUID_PROP,
834
+ cache: CACHE_PROP,
835
+ fileKey: FILE_KEY_PROP,
836
+ sessionId: SESSION_ID_PROP
685
837
  }
686
838
  };
687
839
 
688
840
  // src/agent/tools/get_node_dsl.ts
689
841
  var URL_RULE = '1. \u94FE\u63A5\u8F93\u5165\uFF1A\u5FC5\u987B\u4ECE item-id \u67E5\u8BE2\u53C2\u6570\u63D0\u53D6\u8282\u70B9 guid \u5E76 URL \u89E3\u7801\uFF081%3A2 \u2192 "1:2"\uFF09\uFF0C\u628A\u89E3\u7801\u503C\u4F5C\u4E3A guid \u4F20\u5165\uFF1B\u4E25\u7981\u628A\u6574\u4E2A URL \u5F53 guid \u4F20\u3002\u4EFB\u610F\u57DF\u540D\u90FD\u6709\u6548\uFF1A\u5B98\u65B9 pixso.cn \u6216\u79C1\u6709\u5316\u90E8\u7F72\uFF08\u5982 pixso.ky-tech.com.cn\uFF09\uFF0C\u683C\u5F0F\u5F62\u5982 https://<\u57DF\u540D>/app/design/<\u6587\u4EF6>?item-id=1%3A2\uFF08\u4E5F\u53EF\u80FD\u662F /app/editor/ \u8DEF\u5F84\uFF09\u3002';
690
- var DSL_LEGEND = '\u5B57\u6BB5\u56FE\u4F8B\uFF1Aid=\u8282\u70B9 guid\uFF08\u53EF\u56DE\u4F20 guid \u4E0B\u94BB\uFF09\uFF1Bname=\u56FE\u5C42\u540D\uFF08\u81EA\u52A8\u547D\u540D\u5DF2\u7701\u7565\uFF09\uFF1Btype=\u8282\u70B9\u7C7B\u578B\uFF08ICON/IMAGE \u4E3A\u584C\u7F29\u53F6\u5B50\uFF09\uFF1Brect=[x,y,w,h] \u7EDD\u5BF9\u5B9A\u4F4D\uFF1Blayout={dir:row|column, gap, pad:[\u4E0A,\u53F3,\u4E0B,\u5DE6], align:[\u4E3B\u8F74,\u4EA4\u53C9\u8F74], sizing:[hug|fixed|fill]\xD72}\uFF1Bconstraints={h,v}\uFF08\u9ED8\u8BA4 left/top \u7701\u7565\uFF09\uFF1Btext={content, size, font, weight, lh, align, color, deco, ls, link, segs=\u6DF7\u6392\u5206\u6BB5\uFF08\u4EC5\u5DEE\u5F02\u5B57\u6BB5\uFF0C\u987A\u5E8F\u5373\u5B57\u7B26\u504F\u79FB\uFF09}\uFF1Bimg=\u8D44\u6E90\u6587\u4EF6\u540D\uFF1Bkids=\u5B50\u8282\u70B9\u9012\u5F52\uFF1Bpalette=\u8C03\u8272\u677F\uFF08\u4EC5\u6839\u8282\u70B9\uFF09\uFF1A\u22652 \u6B21\u7684\u5355\u8272\u63D0\u53D6\u4E3A\u6570\u7EC4\uFF0Cfills/text.color/stroke \u4E2D "p0""p1" \u4E3A\u4E0B\u6807\u5F15\u7528\uFF08\u590D\u5408 CSS \u4E32\u5185\u989C\u8272\u4E0D\u67E5\u8868\uFF09\uFF1Bnotes=\u8BBE\u8BA1\u6807\u6CE8\u6587\u672C\uFF08\u4EC5\u5B58\u5728\u65F6\uFF09\uFF1Bcomp=\u7EC4\u4EF6\u5173\u8054\uFF08INSTANCE/COMPONENT/COMPONENT_SET\uFF09\uFF1Akey=\u8DE8\u6587\u4EF6\u7A33\u5B9A\u6807\u8BC6\uFF0Cname=\u7EC4\u4EF6\u540D\uFF08\u6620\u5C04\u6743\u91CD\u6700\u9AD8\u7684\u4F9D\u636E\u2014\u2014\u5728\u9879\u76EE\u7EC4\u4EF6\u5E93\u627E\u5BF9\u5E94\uFF0C\u4E0D\u9010\u5B57\u76F4\u7528\uFF09\uFF0Cprops=variant \u5C5E\u6027\uFF0Cset=variant \u7EC4\uFF1Bdepth=\u6574\u6811\u5B9E\u9645\u6DF1\u5EA6\uFF08\u4EC5\u6839\u8282\u70B9\uFF0C\u603B\u662F\u8F93\u51FA\uFF09\uFF1Bmore=\u622A\u65AD\u5904\u5269\u4F59\u5C42\u7EA7\u6570\uFF08\u4EC5 maxDepth \u622A\u65AD\u65F6\uFF0C\u62FF id \u56DE\u4F20 guid \u4E0B\u94BB\uFF09\uFF1BkidsSummary=\u622A\u65AD\u5904\u76F4\u63A5\u5B50\u8282\u70B9\u6D45\u9884\u89C8 [{id, name?, type}]\uFF08\u8F85\u52A9\u9009\u62E9\u4E0B\u94BB\u5206\u652F\uFF09\u3002\u7701\u7565\u89C4\u5219\uFF1A\u9ED8\u8BA4\u503C\u4E0D\u8F93\u51FA\uFF08text.align=left\u3001lh=normal\u3001weight=Regular\u3001gap=0\u3001pad \u5168 0\uFF09\u3002';
842
+ var DSL_LEGEND = '\u5B57\u6BB5\u56FE\u4F8B\uFF1Aid=\u8282\u70B9 guid\uFF08\u53EF\u56DE\u4F20 guid \u4E0B\u94BB\uFF09\uFF1Bname=\u56FE\u5C42\u540D\uFF08\u81EA\u52A8\u547D\u540D\u5DF2\u7701\u7565\uFF09\uFF1Btype=\u8282\u70B9\u7C7B\u578B\uFF08ICON/IMAGE \u4E3A\u584C\u7F29\u53F6\u5B50\uFF09\uFF1Brect=[x,y,w,h] \u7EDD\u5BF9\u5B9A\u4F4D\uFF1Blayout={dir:row|column, gap, pad:[\u4E0A,\u53F3,\u4E0B,\u5DE6], align:[\u4E3B\u8F74,\u4EA4\u53C9\u8F74], sizing:[hug|fixed|fill]\xD72}\uFF1Bconstraints={h,v}\uFF08\u9ED8\u8BA4 left/top \u7701\u7565\uFF09\uFF1Btext={content, size, font, weight, lh, align, color, deco, ls, link, segs=\u6DF7\u6392\u5206\u6BB5\uFF08\u4EC5\u5DEE\u5F02\u5B57\u6BB5\uFF0C\u987A\u5E8F\u5373\u5B57\u7B26\u504F\u79FB\uFF09}\uFF1Bimg=\u8D44\u6E90\u6587\u4EF6\u540D\uFF1Bkids=\u5B50\u8282\u70B9\u9012\u5F52\uFF1Bpalette=\u8C03\u8272\u677F\uFF08\u4EC5\u6839\u8282\u70B9\uFF09\uFF1A\u22652 \u6B21\u7684\u5355\u8272\u63D0\u53D6\u4E3A\u6570\u7EC4\uFF0Cfills/text.color/stroke \u4E2D "p0""p1" \u4E3A\u4E0B\u6807\u5F15\u7528\uFF08\u590D\u5408 CSS \u4E32\u5185\u989C\u8272\u4E0D\u67E5\u8868\uFF09\uFF1Bnotes=\u8BBE\u8BA1\u6807\u6CE8\u6587\u672C\uFF08\u4EC5\u5B58\u5728\u65F6\uFF09\uFF1Bcomp=\u7EC4\u4EF6\u5173\u8054\uFF08INSTANCE/COMPONENT/COMPONENT_SET\uFF09\uFF1Akey=\u8DE8\u6587\u4EF6\u7A33\u5B9A\u6807\u8BC6\uFF0Cname=\u7EC4\u4EF6\u540D\uFF08\u6620\u5C04\u6743\u91CD\u6700\u9AD8\u7684\u4F9D\u636E\u2014\u2014\u5728\u9879\u76EE\u7EC4\u4EF6\u5E93\u627E\u5BF9\u5E94\uFF0C\u4E0D\u9010\u5B57\u76F4\u7528\uFF09\uFF0Cprops=variant \u5C5E\u6027\uFF0Cset=variant \u7EC4\uFF1Bdepth=\u6574\u6811\u5B9E\u9645\u6DF1\u5EA6\uFF08\u4EC5\u6839\u8282\u70B9\uFF0C\u603B\u662F\u8F93\u51FA\uFF09\uFF1Bmore=\u622A\u65AD\u5904\u5269\u4F59\u5C42\u7EA7\u6570\uFF08\u4EC5 maxDepth \u622A\u65AD\u65F6\uFF0C\u62FF id \u56DE\u4F20 guid \u4E0B\u94BB\uFF09\uFF1BkidsSummary=\u622A\u65AD\u5904\u6216\u584C\u7F29\u53F6\u5B50\uFF08img \u8282\u70B9\uFF09\u7684\u76F4\u63A5\u5B50\u8282\u70B9\u6D45\u9884\u89C8 [{id, name?, type}]\uFF08\u8F85\u52A9\u9009\u62E9\u4E0B\u94BB\u5206\u652F\uFF09\u3002\u7701\u7565\u89C4\u5219\uFF1A\u9ED8\u8BA4\u503C\u4E0D\u8F93\u51FA\uFF08text.align=left\u3001lh=normal\u3001weight=Regular\u3001gap=0\u3001pad \u5168 0\uFF09\u3002';
691
843
  var getNodeDsl = {
692
844
  description: '\u3010\u7528\u9014\u3011\u751F\u6210\u8282\u70B9\u7684\u5F00\u53D1\u5411\u7D27\u51D1 DSL\uFF08\u7ED3\u6784+\u6837\u5F0F+\u8D44\u6E90\u6587\u4EF6\u540D\uFF09\uFF0C\u8BBE\u8BA1\u8F6C\u4EE3\u7801\u7684\u6838\u5FC3\u5165\u53E3\u3002\u3010\u547D\u4E2D\u4FE1\u53F7\u3011"\u6837\u5F0F\u8FD8\u539F/\u8BBE\u8BA1\u8FD8\u539F/UI \u8FD8\u539F/\u8BBE\u8BA1\u8F6C\u4EE3\u7801/\u6839\u636E\u8BBE\u8BA1\u7A3F\u5199\u9875\u9762\u3001\u7EC4\u4EF6"\uFF0C\u6216\u7ED9\u51FA Pixso \u94FE\u63A5\uFF08\u4EFB\u610F\u57DF\u540D\uFF09\u65F6\uFF0C\u9ED8\u8BA4\u4ECE\u672C\u5DE5\u5177\u5F00\u59CB\u3002\u9996\u6B21\u63A5\u89E6\u8BBE\u8BA1\u7A3F\uFF1A\u5148 get_metadata \u9AA8\u67B6 + get_screenshot \u89C6\u89C9\uFF0C\u5B9A\u4F4D\u76EE\u6807\u540E\u518D\u7528\u672C\u5DE5\u5177\u3002\u7ED3\u6784\u590D\u6742\uFF1AmaxDepth \u4F20 0 \u9884\u5224\u89C4\u6A21\uFF08\u6839\u8282\u70B9 depth=\u6574\u6811\u6DF1\u5EA6\uFF09\uFF0C\u518D\u5BF9\u5E26 more \u6807\u8BB0\u7684\u5B50\u6811\u6309 id \u4E0B\u94BB\uFF08kidsSummary \u8F85\u52A9\u9009\u5206\u652F\uFF09\u3002\u3010\u8FD4\u56DE\u3011\u5355\u884C\u7D27\u51D1 JSON\u3002' + DSL_LEGEND + "\u584C\u7F29\u53F6\u5B50\uFF1A\u7EAF\u77E2\u91CF\u7EC4\u4EF6\u5B9E\u4F8B\u2192SVG\u3001\u56FE\u7247\u586B\u5145\u2192PNG\u3001\u8BBE\u8BA1\u5E08\u6807\u8BB0\u6309\u5176\u8BBE\u7F6E\uFF1B\u5DF2\u5E26 img \u7684\u8282\u70B9\u5DF2\u5B8C\u6210\u5BFC\u51FA\uFF0C\u76F4\u63A5\u7528\u5176\u6587\u4EF6\u540D\uFF0C\u52FF\u518D\u8C03 export_image\u3002DSL \u6709\u635F\u7CBE\u7B80\uFF08\u7701\u7565\u8499\u7248/\u5E03\u5C14\u8DEF\u5F84\u7B49\uFF09\uFF0C\u89C6\u89C9\u590D\u6742\u8282\u70B9\u914D\u5408 get_screenshot \u5BF9\u7167\u3002\u3010\u786C\u6027\u89C4\u5219\u3011" + URL_RULE + " " + GUIDES_PTR + " " + ENVELOPE_NOTE,
693
- inputSchema: {
694
- type: "object",
695
- properties: {
696
- guid: GUID_PROP,
697
- maxDepth: {
698
- type: "number",
699
- description: "\u4E0B\u63A2\u5B50\u8282\u70B9\u7684\u6700\u5927\u6DF1\u5EA6\uFF08\u6839\u8282\u70B9\u4E3A\u7B2C 0 \u5C42\uFF09\uFF0C\u7F3A\u7701\u4E0D\u9650\u3002\u622A\u65AD\u8282\u70B9\u5E26 more\uFF08\u5269\u4F59\u5C42\u7EA7\u6570\uFF09+ kidsSummary\uFF08\u5B50\u8282\u70B9\u6D45\u9884\u89C8\uFF09\uFF0C\u62FF\u5176 id \u56DE\u4F20 guid \u7EE7\u7EED\u4E0B\u94BB\uFF1B\u4F20 0 \u53EA\u8FD4\u56DE\u6839\u8282\u70B9\uFF08\u914D\u5408 depth \u5B57\u6BB5\u9884\u5224\u89C4\u6A21\uFF09\u3002"
700
- },
701
- cache: CACHE_PROP,
702
- target: TARGET_PROP
703
- }
845
+ requires: ["guid", ["fileKey", "sessionId"]],
846
+ properties: {
847
+ guid: GUID_PROP,
848
+ maxDepth: {
849
+ type: "number",
850
+ description: "\u4E0B\u63A2\u5B50\u8282\u70B9\u7684\u6700\u5927\u6DF1\u5EA6\uFF08\u6839\u8282\u70B9\u4E3A\u7B2C 0 \u5C42\uFF09\uFF0C\u7F3A\u7701\u4E0D\u9650\u3002\u622A\u65AD\u8282\u70B9\u5E26 more\uFF08\u5269\u4F59\u5C42\u7EA7\u6570\uFF09+ kidsSummary\uFF08\u5B50\u8282\u70B9\u6D45\u9884\u89C8\uFF09\uFF0C\u62FF\u5176 id \u56DE\u4F20 guid \u7EE7\u7EED\u4E0B\u94BB\uFF1B\u4F20 0 \u53EA\u8FD4\u56DE\u6839\u8282\u70B9\uFF08\u914D\u5408 depth \u5B57\u6BB5\u9884\u5224\u89C4\u6A21\uFF09\u3002"
851
+ },
852
+ cache: CACHE_PROP,
853
+ fileKey: FILE_KEY_PROP,
854
+ sessionId: SESSION_ID_PROP
704
855
  }
705
856
  };
706
857
 
707
858
  // src/agent/tools/get_screenshot.ts
708
859
  var getScreenshot = {
709
860
  description: '\u3010\u7528\u9014\u3011\u5BFC\u51FA\u5355\u4E2A\u8282\u70B9\u7684\u622A\u56FE\uFF0C\u4F5C\u89C6\u89C9\u53C2\u7167\u3002\u3010\u547D\u4E2D\u4FE1\u53F7\u3011"\u770B\u770B\u8FD9\u4E2A\u8BBE\u8BA1\u7A3F\u957F\u4EC0\u4E48\u6837"/"\u7ED9\u6211\u622A\u56FE"\uFF1B\u8FD8\u539F\u4EFB\u52A1\u9700\u8981\u89C6\u89C9\u53C2\u7167\u6216\u8FD8\u539F\u5EA6\u6821\u9A8C\u3002\u8981\u5207\u56FE\u7D20\u6750\u7528 export_image\u3002\u751F\u6210\u4EE3\u7801\u540E\u53EF\u518D\u6B21\u8C03\u7528\u672C\u5DE5\u5177\u5BF9\u6BD4\u8FD8\u539F\u5EA6\uFF1B\u5EFA\u8BAE\u4E0E get_node_dsl \u914D\u5408\uFF1ADSL \u5B9A\u7ED3\u6784\uFF0C\u622A\u56FE\u6821\u9A8C\u89C6\u89C9\u3002\u3010\u8FD4\u56DE\u3011{img: \u4EA7\u7269\u6587\u4EF6\u540D, width, height, format}\u3002\u3010\u786C\u6027\u89C4\u5219\u3011' + GUIDES_PTR + " " + ENVELOPE_NOTE,
710
- inputSchema: {
711
- type: "object",
712
- properties: {
713
- guid: GUID_PROP,
714
- format: {
715
- type: "string",
716
- enum: ["PNG", "SVG"],
717
- description: "\u5BFC\u51FA\u683C\u5F0F\uFF0C\u9ED8\u8BA4 PNG\u3002\u622A\u56FE\u7528\u9014\u7528 PNG\uFF1B\u7EAF\u56FE\u5F62\u60F3\u62FF\u77E2\u91CF\u518D\u9009 SVG\u3002"
718
- },
719
- scale: {
720
- type: "number",
721
- description: "\u5BFC\u51FA\u500D\u7387\uFF0C\u9ED8\u8BA4 2\u3002"
722
- },
723
- cache: CACHE_PROP,
724
- target: TARGET_PROP
725
- }
861
+ requires: ["guid", ["fileKey", "sessionId"]],
862
+ properties: {
863
+ guid: GUID_PROP,
864
+ format: {
865
+ type: "string",
866
+ enum: ["PNG", "SVG"],
867
+ description: "\u5BFC\u51FA\u683C\u5F0F\uFF0C\u9ED8\u8BA4 PNG\u3002\u622A\u56FE\u7528\u9014\u7528 PNG\uFF1B\u7EAF\u56FE\u5F62\u60F3\u62FF\u77E2\u91CF\u518D\u9009 SVG\u3002"
868
+ },
869
+ scale: {
870
+ type: "number",
871
+ description: "\u5BFC\u51FA\u500D\u7387\uFF0C\u9ED8\u8BA4 2\u3002"
872
+ },
873
+ cache: CACHE_PROP,
874
+ fileKey: FILE_KEY_PROP,
875
+ sessionId: SESSION_ID_PROP
726
876
  }
727
877
  };
728
878
 
729
879
  // src/agent/tools/get_selected.ts
730
880
  var getSelected = {
731
- description: '\u3010\u7528\u9014\u3011\u5B9E\u65F6\u8BFB\u53D6 Pixso \u753B\u5E03\u5F53\u524D\u9009\u4E2D\u7684\u56FE\u5C42\uFF08guid\u3001\u540D\u79F0\u3001\u7C7B\u578B\uFF09\u3002\u3010\u547D\u4E2D\u4FE1\u53F7\u3011\u7528\u6237\u8BF4"\u8FD9\u4E2A/\u5F53\u524D\u9009\u4E2D\u7684"\u56FE\u5C42\u3001\u7EC4\u4EF6\u3002\u5DF2\u7ED9 Pixso \u94FE\u63A5\u65F6\u4E0D\u8981\u7528\u672C\u5DE5\u5177\u2014\u2014\u76F4\u63A5\u4ECE\u94FE\u63A5\u63D0\u53D6 guid\uFF08\u89C1 get_node_dsl\uFF09\u3002\u3010\u8FD4\u56DE\u3011{fileKey, fileName, pageName, selection: [{id, name, type}]}\u3002\u3010\u786C\u6027\u89C4\u5219\u3011\u4E25\u7981\u9690\u5F0F\u4F7F\u7528\u8FD4\u56DE\u7684 guid\uFF1A\u9664\u975E\u7528\u6237\u660E\u786E\u8BF4\u4E86"\u6211\u9009\u4E2D\u7684/\u5F53\u524D\u9009\u4E2D\u7684"\uFF0C\u5FC5\u987B\u5148\u628A\u9009\u4E2D\u56FE\u5C42\u540D\u79F0\u62A5\u7ED9\u7528\u6237\u786E\u8BA4\uFF08"\u5F53\u524D\u9009\u4E2D\u7684\u662F\u300CXX\u300D\uFF0C\u786E\u8BA4\u5904\u7406\u5B83\u5417\uFF1F"\uFF09\uFF0C\u786E\u8BA4\u540E\u624D\u80FD\u4F20\u7ED9\u5176\u4ED6\u5DE5\u5177\u3002 ' + ENVELOPE_NOTE,
732
- inputSchema: { type: "object", properties: { target: TARGET_PROP } }
881
+ description: '\u3010\u7528\u9014\u3011\u5B9E\u65F6\u8BFB\u53D6 Pixso \u753B\u5E03\u5F53\u524D\u9009\u4E2D\u7684\u56FE\u5C42\uFF08guid\u3001\u540D\u79F0\u3001\u7C7B\u578B\uFF09\u3002\u3010\u547D\u4E2D\u4FE1\u53F7\u3011\u7528\u6237\u8BF4"\u8FD9\u4E2A/\u5F53\u524D\u9009\u4E2D\u7684"\u56FE\u5C42\u3001\u7EC4\u4EF6\u3002\u5DF2\u7ED9 Pixso \u94FE\u63A5\u65F6\u4E0D\u8981\u7528\u672C\u5DE5\u5177\u2014\u2014\u76F4\u63A5\u4ECE\u94FE\u63A5\u63D0\u53D6 guid\uFF08\u89C1 get_node_dsl\uFF09\u3002\u3010\u8FD4\u56DE\u3011{fileKey, fileName, pageName, selection: [{id, name, type}]}\u3002\u3010\u786C\u6027\u89C4\u5219\u3011\u4E25\u7981\u9690\u5F0F\u4F7F\u7528\u8FD4\u56DE\u7684 guid\uFF1A\u9664\u975E\u7528\u6237\u660E\u786E\u8BF4\u4E86"\u6211\u9009\u4E2D\u7684/\u5F53\u524D\u9009\u4E2D\u7684"\uFF0C\u5FC5\u987B\u5148\u628A\u9009\u4E2D\u56FE\u5C42\u540D\u79F0\u62A5\u7ED9\u7528\u6237\u786E\u8BA4\uFF08"\u5F53\u524D\u9009\u4E2D\u7684\u662F\u300CXX\u300D\uFF0C\u786E\u8BA4\u5904\u7406\u5B83\u5417\uFF1F"\uFF09\uFF0C\u786E\u8BA4\u540E\u624D\u80FD\u4F20\u7ED9\u5176\u4ED6\u5DE5\u5177\u3002\u672C\u5DE5\u5177\u7684 fileKey/sessionId \u53EF\u7F3A\u7701\uFF08\u8DEF\u7531\u5230\u6700\u8FD1\u6D3B\u8DC3\u6587\u4EF6\uFF09\uFF1B\u5176\u4ED6\u5DE5\u5177\u4E24\u8005\u5FC5\u4F20\u5176\u4E00\u3002 ' + ENVELOPE_NOTE,
882
+ properties: { fileKey: FILE_KEY_PROP, sessionId: SESSION_ID_PROP }
733
883
  };
734
884
 
735
885
  // src/agent/tools/list_sessions.ts
736
886
  var listSessions = {
737
- description: "\u3010\u7528\u9014\u3011\u5217\u51FA\u5B58\u6D3B\u7684\u8BBE\u8BA1\u7A3F\u63D2\u4EF6\u4F1A\u8BDD\uFF08sessionId\u3001\u6587\u4EF6\u3001\u9875\u9762\u3001\u9009\u4E2D\u6570\uFF09\u2014\u2014\u6BCF\u6B21\u8C03\u7528\u7ECF ping/pong \u5B9E\u6D4B\uFF0C\u65E0\u54CD\u5E94\u7684\u4E0D\u51FA\u73B0\u3002\u6BCF\u4E2A\u8FD0\u884C\u63D2\u4EF6\u7684\u8BBE\u8BA1\u6587\u4EF6\u5BF9\u5E94\u4E00\u4E2A\u4F1A\u8BDD\u3002\u3010\u547D\u4E2D\u4FE1\u53F7\u3011\u591A\u4E2A Pixso \u6587\u4EF6\u540C\u65F6\u5F00\u63D2\u4EF6\u65F6\uFF0C\u5148\u7528\u672C\u5DE5\u5177\u786E\u8BA4\u76EE\u6807\u6587\u4EF6\uFF1B\u53EA\u6709\u4E00\u4E2A\u4F1A\u8BDD\u65F6\u65E0\u9700\u5173\u6CE8\u672C\u5DE5\u5177\u3002\u3010\u8FD4\u56DE\u3011\u4F1A\u8BDD\u5217\u8868 JSON\u3002\u628A sessionId \u6216 fileKey \u4F5C\u4E3A\u5176\u4ED6\u5DE5\u5177\u7684 target \u53C2\u6570\u4F20\u5165\u5373\u8DEF\u7531\u5230\u5BF9\u5E94\u6587\u4EF6\uFF1B\u4E0D\u4F20 target \u65F6\u8DEF\u7531\u5230\u6700\u8FD1\u6D3B\u8DC3\u7684\u6587\u4EF6\u3002\u8BE6\u89C1\u8D44\u6E90 batter://guides/intro\u3002",
738
- inputSchema: { type: "object", properties: {} },
887
+ description: "\u3010\u7528\u9014\u3011\u5217\u51FA\u5B58\u6D3B\u7684\u8BBE\u8BA1\u7A3F\u63D2\u4EF6\u4F1A\u8BDD\uFF08sessionId\u3001\u6587\u4EF6\u3001\u9875\u9762\u3001\u9009\u4E2D\u6570\uFF09\u2014\u2014\u6BCF\u6B21\u8C03\u7528\u7ECF ping/pong \u5B9E\u6D4B\uFF0C\u65E0\u54CD\u5E94\u7684\u4E0D\u51FA\u73B0\u3002\u6BCF\u4E2A\u8FD0\u884C\u63D2\u4EF6\u7684\u8BBE\u8BA1\u6587\u4EF6\u5BF9\u5E94\u4E00\u4E2A\u4F1A\u8BDD\u3002\u3010\u547D\u4E2D\u4FE1\u53F7\u3011\u591A\u4E2A Pixso \u6587\u4EF6\u540C\u65F6\u5F00\u63D2\u4EF6\u65F6\uFF0C\u5148\u7528\u672C\u5DE5\u5177\u786E\u8BA4\u76EE\u6807\u6587\u4EF6\uFF1B\u53EA\u6709\u4E00\u4E2A\u4F1A\u8BDD\u65F6\u65E0\u9700\u5173\u6CE8\u672C\u5DE5\u5177\u3002\u3010\u8FD4\u56DE\u3011\u4F1A\u8BDD\u5217\u8868 JSON\u3002\u628A fileKey\uFF08\u8DE8\u91CD\u5F00\u7A33\u5B9A\uFF0C\u4F18\u5148\uFF09\u6216 sessionId \u4F5C\u4E3A\u5176\u4ED6\u5DE5\u5177\u7684\u540C\u540D\u53C2\u6570\u4F20\u5165\u5373\u8DEF\u7531\u5230\u5BF9\u5E94\u6587\u4EF6\uFF08\u8282\u70B9\u7C7B\u5DE5\u5177\u4E8C\u8005\u5FC5\u4F20\u5176\u4E00\uFF1B\u4EC5 get_selected \u4E0E\u672C\u5DE5\u5177\u53EF\u7F3A\u7701\uFF09\u3002\u8BE6\u89C1\u8D44\u6E90 batter://guides/intro\u3002",
888
+ properties: {},
739
889
  handler: async () => {
740
890
  const list = sessions.list();
741
891
  if (!list.length) {
@@ -776,16 +926,48 @@ var TOOL_REGISTRY = {
776
926
  get_screenshot: getScreenshot,
777
927
  list_sessions: listSessions
778
928
  };
929
+ function buildInputSchema(requires, properties) {
930
+ const singles = requires.filter((r) => typeof r === "string");
931
+ const xorGroups = requires.filter((r) => Array.isArray(r));
932
+ const groupSchemas = xorGroups.map((g) => ({ oneOf: g.map((p) => ({ required: [p] })) }));
933
+ return {
934
+ type: "object",
935
+ ...singles.length ? { required: singles } : {},
936
+ ...groupSchemas.length === 1 ? groupSchemas[0] : {},
937
+ ...groupSchemas.length > 1 ? { allOf: groupSchemas } : {},
938
+ properties
939
+ };
940
+ }
941
+ function validateArgs(requires, args) {
942
+ const present = (k) => args[k] !== void 0 && args[k] !== null && args[k] !== "";
943
+ for (const req of requires) {
944
+ if (typeof req === "string") {
945
+ if (!present(req)) return `\u7F3A\u5C11\u5FC5\u4F20\u53C2\u6570\uFF1A${req}`;
946
+ continue;
947
+ }
948
+ const given = req.filter(present);
949
+ if (given.length === 0) return `\u7F3A\u5C11\u53C2\u6570\uFF1A${req.join(" / ")} \u9700\u4F20\u4E00\u4E2A`;
950
+ if (given.length > 1) return `\u53C2\u6570\u4E92\u65A5\uFF1A${req.join(" / ")} \u53EA\u80FD\u4F20\u4E00\u4E2A`;
951
+ }
952
+ return null;
953
+ }
779
954
  var TOOLS = Object.fromEntries(
780
955
  Object.entries(TOOL_REGISTRY).map(([name, def]) => {
781
- const handler = def.handler ?? ((args, ctx) => run(name, args, ctx?.signal));
782
- const resolvedDef = { description: def.description, inputSchema: def.inputSchema, handler };
783
- return [name, { def: resolvedDef, tool: { name, description: def.description, inputSchema: def.inputSchema } }];
956
+ const requires = def.requires ?? [];
957
+ const inputSchema = buildInputSchema(requires, def.properties);
958
+ const base = def.handler ?? ((args, ctx) => run(name, args, ctx?.signal));
959
+ const handler = (args, ctx) => {
960
+ const err = requires.length ? validateArgs(requires, args) : null;
961
+ if (err) return Promise.resolve(errorResult(err));
962
+ return base(args, ctx);
963
+ };
964
+ const resolvedDef = { description: def.description, inputSchema, handler };
965
+ return [name, { def: resolvedDef, tool: { name, description: def.description, inputSchema } }];
784
966
  })
785
967
  );
786
968
 
787
969
  // src/agent/mcp.ts
788
- var INSTRUCTIONS = `Pixso \u8BBE\u8BA1\u7A3F\u8F6C\u4EE3\u7801\u670D\u52A1\u3002\u6309\u7528\u6237\u8F93\u5165\u8DEF\u7531\uFF1A1. \u7ED9\u51FA Pixso \u94FE\u63A5\uFF08\u4EFB\u610F\u57DF\u540D\uFF09\u2192 \u4ECE item-id \u63D0\u53D6 guid\uFF08URL \u89E3\u7801\uFF09\uFF0C\u76F4\u63A5\u8C03 get_node_dsl / get_screenshot / export_image\uFF1B2. \u8BF4"\u6211\u9009\u4E2D\u7684/\u5F53\u524D\u9009\u4E2D\u7684"\u2192 get_selected \u62FF guid\uFF1B\u5176\u4ED6\u4F9D\u8D56\u9009\u533A\u7684\u573A\u666F\u5148 get_selected \u518D\u5411\u7528\u6237\u62A5\u540D\u79F0\u786E\u8BA4\uFF1B3. "\u6837\u5F0F\u8FD8\u539F/\u8BBE\u8BA1\u8FD8\u539F/UI \u8FD8\u539F/\u8BBE\u8BA1\u8F6C\u4EE3\u7801"\u2192 get_node_dsl \u53D6\u7ED3\u6784 + get_screenshot \u53D6\u89C6\u89C9\u53C2\u7167\uFF1B4. "\u5207\u56FE/\u5BFC\u51FA\u56FE\u6807\u6216\u56FE\u7247\u7D20\u6750"\u2192 export_image\uFF08DSL \u91CC\u5DF2\u5E26 img \u7684\u8282\u70B9\u76F4\u63A5\u7528\u5176\u6587\u4EF6\u540D\uFF0C\u4E0D\u8981\u91CD\u590D\u5BFC\u51FA\uFF09\uFF1B5. \u9996\u6B21\u63A5\u89E6\u8BBE\u8BA1\u7A3F/\u53EA\u8981\u7ED3\u6784\u6982\u89C8 \u2192 get_metadata \u62FF\u5168\u6811\u9AA8\u67B6\uFF08\u65E0\u6837\u5F0F\u65E0\u8D44\u6E90\uFF0C\u4F53\u79EF\u6781\u5C0F\uFF09\u5B9A\u4F4D\u76EE\u6807\u8282\u70B9\uFF1B\u7ED3\u6784\u590D\u6742 \u2192 get_node_dsl \u7528 maxDepth \u5206\u6BB5\u4E0B\u63A2\uFF08\u622A\u65AD\u5904 more \u6807\u8BB0 + kidsSummary \u6D45\u9884\u89C8\uFF0C\u6309 id \u4E0B\u94BB\uFF09\uFF1B6. \u751F\u6210\u540E\u5BF9\u6BD4\u8FD8\u539F\u5EA6 \u2192 get_screenshot \u6821\u9A8C\u5FAA\u73AF\uFF1B7. \u591A\u4E2A Pixso \u6587\u4EF6\u540C\u65F6\u6253\u5F00 \u2192 list_sessions + target \u53C2\u6570\u8DEF\u7531\u3002\u786C\u6027\u89C4\u5219\uFF08\u8D44\u6E90\u53D6\u7528/\u7EC4\u4EF6\u6620\u5C04/\u9519\u8BEF\u6062\u590D/\u7981\u6B62\u622A\u56FE\u786C\u7F16\u7801\uFF09\u89C1\u8D44\u6E90 batter://guides/design-to-code\u2014\u2014\u5199\u4EE3\u7801\u524D\u5FC5\u8BFB\u3002`;
970
+ var INSTRUCTIONS = `Pixso \u8BBE\u8BA1\u7A3F\u8F6C\u4EE3\u7801\u670D\u52A1\u3002\u6309\u7528\u6237\u8F93\u5165\u8DEF\u7531\uFF1A1. \u7ED9\u51FA Pixso \u94FE\u63A5\uFF08\u4EFB\u610F\u57DF\u540D\uFF09\u2192 \u4ECE item-id \u63D0\u53D6 guid\uFF08URL \u89E3\u7801\uFF09\uFF0C\u76F4\u63A5\u8C03 get_node_dsl / get_screenshot / export_image\uFF1B2. \u8BF4"\u6211\u9009\u4E2D\u7684/\u5F53\u524D\u9009\u4E2D\u7684"\u2192 get_selected \u62FF guid\uFF1B\u5176\u4ED6\u4F9D\u8D56\u9009\u533A\u7684\u573A\u666F\u5148 get_selected \u518D\u5411\u7528\u6237\u62A5\u540D\u79F0\u786E\u8BA4\uFF1B3. "\u6837\u5F0F\u8FD8\u539F/\u8BBE\u8BA1\u8FD8\u539F/UI \u8FD8\u539F/\u8BBE\u8BA1\u8F6C\u4EE3\u7801"\u2192 get_node_dsl \u53D6\u7ED3\u6784 + get_screenshot \u53D6\u89C6\u89C9\u53C2\u7167\uFF1B4. "\u5207\u56FE/\u5BFC\u51FA\u56FE\u6807\u6216\u56FE\u7247\u7D20\u6750"\u2192 export_image\uFF08DSL \u91CC\u5DF2\u5E26 img \u7684\u8282\u70B9\u76F4\u63A5\u7528\u5176\u6587\u4EF6\u540D\uFF0C\u4E0D\u8981\u91CD\u590D\u5BFC\u51FA\uFF09\uFF1B5. \u9996\u6B21\u63A5\u89E6\u8BBE\u8BA1\u7A3F/\u53EA\u8981\u7ED3\u6784\u6982\u89C8 \u2192 get_metadata \u62FF\u5168\u6811\u9AA8\u67B6\uFF08\u65E0\u6837\u5F0F\u65E0\u8D44\u6E90\uFF0C\u4F53\u79EF\u6781\u5C0F\uFF09\u5B9A\u4F4D\u76EE\u6807\u8282\u70B9\uFF1B\u7ED3\u6784\u590D\u6742 \u2192 get_node_dsl \u7528 maxDepth \u5206\u6BB5\u4E0B\u63A2\uFF08\u622A\u65AD\u5904 more \u6807\u8BB0 + kidsSummary \u6D45\u9884\u89C8\uFF0C\u6309 id \u4E0B\u94BB\uFF09\uFF1B6. \u751F\u6210\u540E\u5BF9\u6BD4\u8FD8\u539F\u5EA6 \u2192 get_screenshot \u6821\u9A8C\u5FAA\u73AF\uFF1B7. \u591A\u4E2A Pixso \u6587\u4EF6\u540C\u65F6\u6253\u5F00 \u2192 list_sessions + fileKey/sessionId \u53C2\u6570\u8DEF\u7531\uFF08\u8282\u70B9\u7C7B\u5DE5\u5177\u4E8C\u8005\u5FC5\u4F20\u5176\u4E00\uFF09\u3002\u786C\u6027\u89C4\u5219\uFF08\u8D44\u6E90\u53D6\u7528/\u7EC4\u4EF6\u6620\u5C04/\u9519\u8BEF\u6062\u590D/\u7981\u6B62\u622A\u56FE\u786C\u7F16\u7801\uFF09\u89C1\u8D44\u6E90 batter://guides/design-to-code\u2014\u2014\u5199\u4EE3\u7801\u524D\u5FC5\u8BFB\u3002`;
789
971
  function createMcpServer() {
790
972
  const server = new McpServer(
791
973
  { name: HUB_NAME, version: VERSION },
@@ -841,15 +1023,28 @@ function createMcpServer() {
841
1023
  // src/agent/sessions.ts
842
1024
  var transports = /* @__PURE__ */ new Map();
843
1025
  function getMcpTransport(sessionId) {
844
- return transports.get(sessionId);
1026
+ const entry = transports.get(sessionId);
1027
+ if (entry) entry.lastSeen = Date.now();
1028
+ return entry?.transport;
845
1029
  }
846
1030
  function registerMcpTransport(sessionId, transport) {
847
- transports.set(sessionId, transport);
1031
+ transports.set(sessionId, { transport, lastSeen: Date.now() });
848
1032
  log(`MCP HTTP session initialized: ${sessionId.slice(0, 8)} (total ${transports.size})`);
849
1033
  transport.onclose = () => {
850
1034
  if (transport.sessionId) transports.delete(transport.sessionId);
851
1035
  };
852
1036
  }
1037
+ var sweeper = setInterval(() => {
1038
+ const now = Date.now();
1039
+ for (const [id, entry] of transports) {
1040
+ if (now - entry.lastSeen <= config.mcpSessionTtlMs) continue;
1041
+ transports.delete(id);
1042
+ void entry.transport.close().catch(() => {
1043
+ });
1044
+ log(`MCP HTTP session expired: ${id.slice(0, 8)} (total ${transports.size})`);
1045
+ }
1046
+ }, 6e4);
1047
+ sweeper.unref();
853
1048
 
854
1049
  // src/agent/http.ts
855
1050
  function handleMcp(req, res, url) {
@@ -905,18 +1100,18 @@ var MCP_ROUTES = [
905
1100
  // src/install/dist-resolver.ts
906
1101
  import { existsSync } from "node:fs";
907
1102
  import { readFile } from "node:fs/promises";
908
- import { join as join3 } from "node:path";
909
- import { fileURLToPath } from "node:url";
1103
+ import { join as join4 } from "node:path";
1104
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
910
1105
  function resolvePluginDir() {
911
1106
  const candidates = [
912
- fileURLToPath(new URL("./plugin/", import.meta.url)),
1107
+ fileURLToPath2(new URL("./plugin/", import.meta.url)),
913
1108
  // bundle: dist/index.js → dist/plugin/
914
- fileURLToPath(new URL("../../plugin/dist/", import.meta.url)),
1109
+ fileURLToPath2(new URL("../../plugin/dist/", import.meta.url)),
915
1110
  // bundle 开发: dist/index.js → packages/plugin/dist/
916
- fileURLToPath(new URL("../../../plugin/dist/", import.meta.url))
1111
+ fileURLToPath2(new URL("../../../plugin/dist/", import.meta.url))
917
1112
  // tsx 开发: src/install/ → packages/plugin/dist/
918
1113
  ];
919
- return candidates.find((p) => existsSync(join3(p, "manifest.json"))) ?? null;
1114
+ return candidates.find((p) => existsSync(join4(p, "manifest.json"))) ?? null;
920
1115
  }
921
1116
  var resolved = null;
922
1117
  function pluginDir() {
@@ -929,7 +1124,7 @@ function hasPluginDist() {
929
1124
  async function pluginManifest() {
930
1125
  const dir = pluginDir();
931
1126
  if (!dir) return null;
932
- const raw = JSON.parse(await readFile(join3(dir, "manifest.json"), "utf8"));
1127
+ const raw = JSON.parse(await readFile(join4(dir, "manifest.json"), "utf8"));
933
1128
  raw.networkAccess = {
934
1129
  allowedDomains: [`http://localhost:${config.httpPort}`, `http://127.0.0.1:${config.httpPort}`]
935
1130
  };
@@ -939,7 +1134,7 @@ async function readPluginFile(name) {
939
1134
  const dir = pluginDir();
940
1135
  if (!dir) return null;
941
1136
  try {
942
- return await readFile(join3(dir, name));
1137
+ return await readFile(join4(dir, name));
943
1138
  } catch {
944
1139
  return null;
945
1140
  }
@@ -1587,14 +1782,22 @@ var PLUGIN_ROUTES = [
1587
1782
 
1588
1783
  `);
1589
1784
  });
1785
+ const teardown = () => {
1786
+ clearInterval(heartbeat);
1787
+ unsubscribe();
1788
+ sessions.setConnected(session, false);
1789
+ };
1590
1790
  const heartbeat = setInterval(() => {
1791
+ if (!res.write(": ping\n\n")) {
1792
+ teardown();
1793
+ res.end();
1794
+ log(`SSE ghost reaped: session ${session.slice(0, 8)}`);
1795
+ return;
1796
+ }
1591
1797
  sessions.touchSeen(session);
1592
- res.write(": ping\n\n");
1593
1798
  }, 15e3);
1594
1799
  req.on("close", () => {
1595
- clearInterval(heartbeat);
1596
- unsubscribe();
1597
- sessions.setConnected(session, false);
1800
+ teardown();
1598
1801
  log(`SSE channel closed: session ${session.slice(0, 8)}`);
1599
1802
  });
1600
1803
  }
@@ -1624,14 +1827,9 @@ var PLUGIN_ROUTES = [
1624
1827
  ];
1625
1828
 
1626
1829
  // src/http.ts
1627
- var lastActivity = Date.now();
1628
- function getLastActivity() {
1629
- return lastActivity;
1630
- }
1631
1830
  var ROUTES = [handleInstall, handleMcp, handlePluginApi];
1632
1831
  function startHttpServer() {
1633
1832
  const server = createServer((req, res) => {
1634
- lastActivity = Date.now();
1635
1833
  res.setHeader("Access-Control-Allow-Origin", "*");
1636
1834
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
1637
1835
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id");
@@ -1682,19 +1880,11 @@ async function runHub() {
1682
1880
  log(`\u7AEF\u53E3 ${config.httpPort} \u5DF2\u6709 hub \u5728\u8FD0\u884C\uFF0C\u672C\u8FDB\u7A0B\u9000\u51FA`);
1683
1881
  process.exit(0);
1684
1882
  }
1685
- log(`hub ready (idle TTL ${config.hubIdleMs / 6e4}min)`);
1883
+ writePidFile(process.pid);
1884
+ log("hub ready\uFF08\u5E38\u9A7B\uFF0C\u4E0D\u81EA\u6BC1\uFF1B\u7248\u672C\u4E0D\u7B26\u65F6\u7531 proxy \u81EA\u52A8\u6362\u8840\uFF0C\u6216 --stop \u624B\u52A8\u505C\u6B62\uFF09");
1686
1885
  void cleanOldAssets(config.assetTtlMs);
1687
- let tick = 0;
1688
- const watchdog = setInterval(() => {
1689
- const idle = Date.now() - getLastActivity();
1690
- const anyPluginOnline = sessions.list().some((s) => s.online);
1691
- if (idle > config.hubIdleMs && !anyPluginOnline) {
1692
- log(`\u7A7A\u95F2 ${Math.round(idle / 6e4)} \u5206\u949F\u4E14\u65E0\u5728\u7EBF\u63D2\u4EF6\uFF0Chub \u81EA\u52A8\u9000\u51FA`);
1693
- process.exit(0);
1694
- }
1695
- if (++tick % 60 === 0) void cleanOldAssets(config.assetTtlMs);
1696
- }, 6e4);
1697
- watchdog.unref();
1886
+ const sweeper2 = setInterval(() => void cleanOldAssets(config.assetTtlMs), 60 * 6e4);
1887
+ sweeper2.unref();
1698
1888
  }
1699
1889
 
1700
1890
  // src/process/stdio-proxy.ts
@@ -1703,64 +1893,6 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
1703
1893
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1704
1894
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1705
1895
  import { ResultSchema } from "@modelcontextprotocol/sdk/types.js";
1706
-
1707
- // src/process/hub-process.ts
1708
- import { spawn } from "node:child_process";
1709
- import { closeSync, openSync } from "node:fs";
1710
- import { tmpdir as tmpdir2 } from "node:os";
1711
- import { join as join4 } from "node:path";
1712
- import { fileURLToPath as fileURLToPath2 } from "node:url";
1713
- async function probeHub() {
1714
- try {
1715
- const res = await fetch(`${hubBaseUrl()}/health`, { signal: AbortSignal.timeout(1500) });
1716
- if (!res.ok) return "foreign";
1717
- const body = await res.json();
1718
- if (body.ok === true && body.name === HUB_NAME) return "alive";
1719
- if (body.ok === true) return "legacy";
1720
- return "foreign";
1721
- } catch {
1722
- return "down";
1723
- }
1724
- }
1725
- var PORT_FIX_HINT = `\u7528 netstat -ano | findstr :${config.httpPort} \u67E5\u5230 PID \u540E taskkill /PID <pid> /F \u7ED3\u675F\uFF0C\u518D\u91CD\u8BD5`;
1726
- function hubStateError(state) {
1727
- if (state === "legacy") {
1728
- return new Error(
1729
- `\u7AEF\u53E3 ${config.httpPort} \u4E0A\u662F\u65E7\u7248\u672C batter-pixso-mcp \u5B9E\u4F8B\uFF08\u4E0D\u652F\u6301\u591A agent \u590D\u7528\uFF09\u3002${PORT_FIX_HINT}`
1730
- );
1731
- }
1732
- return new Error(`\u7AEF\u53E3 ${config.httpPort} \u88AB\u5176\u4ED6\u7A0B\u5E8F\u5360\u7528\u3002${PORT_FIX_HINT}\uFF0C\u6216\u4FEE\u6539 HTTP_PORT \u6362\u7AEF\u53E3`);
1733
- }
1734
- var HUB_LOG = join4(tmpdir2(), "batter-pixso-mcp-hub.log");
1735
- function spawnHub() {
1736
- const out = openSync(HUB_LOG, "a");
1737
- const entry = process.argv[1] ?? fileURLToPath2(import.meta.url);
1738
- const SKIP_FLAGS = /* @__PURE__ */ new Set(["--eval", "-e", "--print", "-p", "--watch", "--watch-preserve-output"]);
1739
- const execArgv = process.execArgv.filter(
1740
- (a, i, arr) => !SKIP_FLAGS.has(a) && !(i > 0 && SKIP_FLAGS.has(arr[i - 1]))
1741
- );
1742
- const child = spawn(process.execPath, [...execArgv, entry, "--hub"], {
1743
- detached: true,
1744
- stdio: ["ignore", out, out],
1745
- windowsHide: true,
1746
- env: { ...process.env }
1747
- });
1748
- child.unref();
1749
- closeSync(out);
1750
- log(`hub spawned (pid ${child.pid}), log: ${HUB_LOG}`);
1751
- }
1752
- async function waitForHub(timeoutMs = 15e3) {
1753
- const deadline = Date.now() + timeoutMs;
1754
- for (; ; ) {
1755
- const state = await probeHub();
1756
- if (state === "alive") return;
1757
- if (state !== "down") throw hubStateError(state);
1758
- if (Date.now() > deadline) throw new Error(`\u7B49\u5F85 hub \u5C31\u7EEA\u8D85\u65F6\uFF08${timeoutMs / 1e3}s\uFF09`);
1759
- await new Promise((r) => setTimeout(r, 200));
1760
- }
1761
- }
1762
-
1763
- // src/process/stdio-proxy.ts
1764
1896
  async function startStdioProxy() {
1765
1897
  const mcpUrl = `${hubBaseUrl()}/mcp`;
1766
1898
  let upstream = null;
@@ -1774,14 +1906,40 @@ async function startStdioProxy() {
1774
1906
  }
1775
1907
  async function ensureUpstream() {
1776
1908
  if (upstream) return upstream;
1777
- const state = await probeHub();
1778
- if (state === "down") spawnHub();
1779
- if (state === "legacy" || state === "foreign") throw hubStateError(state);
1780
- await waitForHub();
1909
+ const { state, health } = await probeHub();
1910
+ if (state === "alive" && health?.version !== VERSION) {
1911
+ log(`hub \u7248\u672C\u4E0D\u7B26\uFF08\u8FD0\u884C ${health?.version} / \u5F53\u524D ${VERSION}\uFF09\uFF0C\u81EA\u52A8\u91CD\u542F\u2026`);
1912
+ if (!killHubByPid(health.pid)) await stopHub();
1913
+ await waitForHubDown();
1914
+ spawnHub();
1915
+ } else if (state === "hung") {
1916
+ log("hub \u65E0\u54CD\u5E94\uFF08\u50F5\u5C38\u8FDB\u7A0B\uFF09\uFF0C\u5F3A\u6740\u540E\u91CD\u5EFA\u2026");
1917
+ if (!killHubByPidFile()) throw hubStateError("hung");
1918
+ await waitForHubDown(1e4);
1919
+ spawnHub();
1920
+ } else if (state === "down") {
1921
+ spawnHub();
1922
+ } else if (state === "foreign") {
1923
+ throw hubStateError(state);
1924
+ }
1925
+ await waitForHub(3e4);
1781
1926
  upstream = await connectUpstream();
1782
1927
  log(`proxy connected to ${mcpUrl}`);
1783
1928
  return upstream;
1784
1929
  }
1930
+ let rebuilding = null;
1931
+ function rebuildUpstream() {
1932
+ rebuilding ??= (async () => {
1933
+ const stale = upstream;
1934
+ upstream = null;
1935
+ if (stale) await stale.close().catch(() => {
1936
+ });
1937
+ return ensureUpstream();
1938
+ })().finally(() => {
1939
+ rebuilding = null;
1940
+ });
1941
+ return rebuilding;
1942
+ }
1785
1943
  async function callWithRecovery(fn) {
1786
1944
  const client = await ensureUpstream();
1787
1945
  try {
@@ -1793,8 +1951,7 @@ async function startStdioProxy() {
1793
1951
  );
1794
1952
  if (alive) throw err;
1795
1953
  log("hub \u4F1A\u8BDD\u5931\u8054\uFF0C\u91CD\u5EFA\u540E\u91CD\u8BD5\u2026");
1796
- upstream = null;
1797
- const rebuilt = await ensureUpstream();
1954
+ const rebuilt = await rebuildUpstream();
1798
1955
  return await fn(rebuilt);
1799
1956
  }
1800
1957
  }
@@ -1820,6 +1977,14 @@ async function main() {
1820
1977
  await runHub();
1821
1978
  return;
1822
1979
  }
1980
+ if (process.argv.includes("--stop")) {
1981
+ await stopHub();
1982
+ return;
1983
+ }
1984
+ if (process.argv.includes("--version") || process.argv.includes("-v")) {
1985
+ log(VERSION);
1986
+ return;
1987
+ }
1823
1988
  await startStdioProxy();
1824
1989
  }
1825
1990