batter-pixso-mcp 0.5.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.5.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
@@ -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
@@ -407,7 +430,7 @@ DSL \u662F\u5355\u884C\u7D27\u51D1 JSON\uFF1A\u77ED key\u3001\u4F4D\u7F6E\u6570\
407
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
408
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
409
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
410
- - **\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
411
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
412
435
 
413
436
  ## \u7EC4\u4EF6\u5173\u8054\uFF08comp \u5B57\u6BB5\uFF09
@@ -435,7 +458,7 @@ INSTANCE / COMPONENT / COMPONENT_SET \u8282\u70B9\u5E26 \`comp\` \u5B57\u6BB5\uF
435
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
436
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
437
460
  \u8BBE\u8BA1\u7A3F\u521A\u6539\u8FC7\u4F46\u7ED3\u679C\u5BF9\u4E0D\u4E0A\u65F6\uFF0C\u52A0 \`cache: "refresh"\` \u5F3A\u5236\u5237\u65B0\u3002
438
- - \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
439
462
  `;
440
463
  var GUIDES = [
441
464
  {
@@ -480,7 +503,7 @@ var DESIGN_TO_CODE2 = {
480
503
  `## \u6267\u884C\u6D41\u7A0B`,
481
504
  ``,
482
505
  `1. \u786E\u8BA4\u76EE\u6807\u8282\u70B9\uFF1A${targetLine}`,
483
- `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`,
484
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`,
485
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`,
486
509
  ``,
@@ -568,15 +591,131 @@ var CommandBus = class {
568
591
  };
569
592
  var commandBus = new CommandBus();
570
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
+
571
707
  // src/agent/tools/shared.ts
572
708
  var PING_TIMEOUT_MS = 800;
573
709
  async function run(type, args, signal) {
574
- const { target, ...rest } = args;
710
+ const { fileKey, sessionId, ...rest } = args;
575
711
  try {
576
- const session = await resolveAndPing(target, signal);
712
+ const session = await resolveAndPing(
713
+ { fileKey, sessionId },
714
+ signal
715
+ );
577
716
  const data = await commandBus.execute(type, rest, { sessionId: session.id, signal });
578
717
  sessions.touch(session.id);
579
- return jsonResult({ sessionId: session.id, data });
718
+ return jsonResult({ sessionId: session.id, fileKey: session.meta.fileKey, data });
580
719
  } catch (err) {
581
720
  return errorResult(err);
582
721
  }
@@ -605,29 +744,36 @@ async function pingCandidates(candidates, affinityId, signal) {
605
744
  return null;
606
745
  }
607
746
  }
608
- async function resolveAndPing(target, signal) {
609
- const s = target ? sessions.get(target) : sessions.resolve();
610
- 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;
611
751
  const candidates = s && !fileKey ? [s] : sessions.candidatesForFileKey(fileKey);
612
752
  if (candidates.length === 0) {
613
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`);
614
- throw unknownTargetError(target);
754
+ throw unknownRouteError(route);
615
755
  }
616
756
  const winner = await pingCandidates(candidates, fileKey ? sessions.affinityIdOf(fileKey) : void 0, signal);
617
757
  if (!winner) {
618
758
  for (const c of candidates) sessions.evict(c.id);
619
759
  throw new Error(
620
- `\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`
621
761
  );
622
762
  }
623
763
  if (fileKey) sessions.pinAffinity(fileKey, winner.id);
624
764
  return winner;
625
765
  }
626
- 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";
627
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";
628
- var TARGET_PROP = {
768
+ var FILE_KEY_PROP = {
629
769
  type: "string",
630
- 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"
631
777
  };
632
778
  var CACHE_PROP = {
633
779
  type: "string",
@@ -636,7 +782,8 @@ var CACHE_PROP = {
636
782
  };
637
783
  var GUID_PROP = {
638
784
  type: "string",
639
- 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'
640
787
  };
641
788
  function jsonResult(data) {
642
789
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
@@ -651,40 +798,42 @@ function errorResult(err) {
651
798
  // src/agent/tools/export_image.ts
652
799
  var exportImage = {
653
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,
654
- inputSchema: {
655
- type: "object",
656
- properties: {
657
- guid: GUID_PROP,
658
- guids: {
659
- type: "array",
660
- items: { type: "string" },
661
- 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"
662
- },
663
- format: {
664
- type: "string",
665
- enum: ["PNG", "SVG", "JPG"],
666
- 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"
667
- },
668
- scale: {
669
- type: "number",
670
- 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"
671
- },
672
- cache: CACHE_PROP,
673
- target: TARGET_PROP
674
- }
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
675
825
  }
676
826
  };
677
827
 
678
828
  // src/agent/tools/get_metadata.ts
679
829
  var getMetadata = {
680
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,
681
- inputSchema: {
682
- type: "object",
683
- properties: {
684
- guid: GUID_PROP,
685
- cache: CACHE_PROP,
686
- target: TARGET_PROP
687
- }
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
688
837
  }
689
838
  };
690
839
 
@@ -693,52 +842,50 @@ var URL_RULE = '1. \u94FE\u63A5\u8F93\u5165\uFF1A\u5FC5\u987B\u4ECE item-id \u67
693
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';
694
843
  var getNodeDsl = {
695
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,
696
- inputSchema: {
697
- type: "object",
698
- properties: {
699
- guid: GUID_PROP,
700
- maxDepth: {
701
- type: "number",
702
- 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"
703
- },
704
- cache: CACHE_PROP,
705
- target: TARGET_PROP
706
- }
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
707
855
  }
708
856
  };
709
857
 
710
858
  // src/agent/tools/get_screenshot.ts
711
859
  var getScreenshot = {
712
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,
713
- inputSchema: {
714
- type: "object",
715
- properties: {
716
- guid: GUID_PROP,
717
- format: {
718
- type: "string",
719
- enum: ["PNG", "SVG"],
720
- 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"
721
- },
722
- scale: {
723
- type: "number",
724
- description: "\u5BFC\u51FA\u500D\u7387\uFF0C\u9ED8\u8BA4 2\u3002"
725
- },
726
- cache: CACHE_PROP,
727
- target: TARGET_PROP
728
- }
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
729
876
  }
730
877
  };
731
878
 
732
879
  // src/agent/tools/get_selected.ts
733
880
  var getSelected = {
734
- 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,
735
- 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 }
736
883
  };
737
884
 
738
885
  // src/agent/tools/list_sessions.ts
739
886
  var listSessions = {
740
- 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",
741
- 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: {},
742
889
  handler: async () => {
743
890
  const list = sessions.list();
744
891
  if (!list.length) {
@@ -779,16 +926,48 @@ var TOOL_REGISTRY = {
779
926
  get_screenshot: getScreenshot,
780
927
  list_sessions: listSessions
781
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
+ }
782
954
  var TOOLS = Object.fromEntries(
783
955
  Object.entries(TOOL_REGISTRY).map(([name, def]) => {
784
- const handler = def.handler ?? ((args, ctx) => run(name, args, ctx?.signal));
785
- const resolvedDef = { description: def.description, inputSchema: def.inputSchema, handler };
786
- 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 } }];
787
966
  })
788
967
  );
789
968
 
790
969
  // src/agent/mcp.ts
791
- 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`;
792
971
  function createMcpServer() {
793
972
  const server = new McpServer(
794
973
  { name: HUB_NAME, version: VERSION },
@@ -844,15 +1023,28 @@ function createMcpServer() {
844
1023
  // src/agent/sessions.ts
845
1024
  var transports = /* @__PURE__ */ new Map();
846
1025
  function getMcpTransport(sessionId) {
847
- return transports.get(sessionId);
1026
+ const entry = transports.get(sessionId);
1027
+ if (entry) entry.lastSeen = Date.now();
1028
+ return entry?.transport;
848
1029
  }
849
1030
  function registerMcpTransport(sessionId, transport) {
850
- transports.set(sessionId, transport);
1031
+ transports.set(sessionId, { transport, lastSeen: Date.now() });
851
1032
  log(`MCP HTTP session initialized: ${sessionId.slice(0, 8)} (total ${transports.size})`);
852
1033
  transport.onclose = () => {
853
1034
  if (transport.sessionId) transports.delete(transport.sessionId);
854
1035
  };
855
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();
856
1048
 
857
1049
  // src/agent/http.ts
858
1050
  function handleMcp(req, res, url) {
@@ -908,18 +1100,18 @@ var MCP_ROUTES = [
908
1100
  // src/install/dist-resolver.ts
909
1101
  import { existsSync } from "node:fs";
910
1102
  import { readFile } from "node:fs/promises";
911
- import { join as join3 } from "node:path";
912
- import { fileURLToPath } from "node:url";
1103
+ import { join as join4 } from "node:path";
1104
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
913
1105
  function resolvePluginDir() {
914
1106
  const candidates = [
915
- fileURLToPath(new URL("./plugin/", import.meta.url)),
1107
+ fileURLToPath2(new URL("./plugin/", import.meta.url)),
916
1108
  // bundle: dist/index.js → dist/plugin/
917
- fileURLToPath(new URL("../../plugin/dist/", import.meta.url)),
1109
+ fileURLToPath2(new URL("../../plugin/dist/", import.meta.url)),
918
1110
  // bundle 开发: dist/index.js → packages/plugin/dist/
919
- fileURLToPath(new URL("../../../plugin/dist/", import.meta.url))
1111
+ fileURLToPath2(new URL("../../../plugin/dist/", import.meta.url))
920
1112
  // tsx 开发: src/install/ → packages/plugin/dist/
921
1113
  ];
922
- return candidates.find((p) => existsSync(join3(p, "manifest.json"))) ?? null;
1114
+ return candidates.find((p) => existsSync(join4(p, "manifest.json"))) ?? null;
923
1115
  }
924
1116
  var resolved = null;
925
1117
  function pluginDir() {
@@ -932,7 +1124,7 @@ function hasPluginDist() {
932
1124
  async function pluginManifest() {
933
1125
  const dir = pluginDir();
934
1126
  if (!dir) return null;
935
- const raw = JSON.parse(await readFile(join3(dir, "manifest.json"), "utf8"));
1127
+ const raw = JSON.parse(await readFile(join4(dir, "manifest.json"), "utf8"));
936
1128
  raw.networkAccess = {
937
1129
  allowedDomains: [`http://localhost:${config.httpPort}`, `http://127.0.0.1:${config.httpPort}`]
938
1130
  };
@@ -942,7 +1134,7 @@ async function readPluginFile(name) {
942
1134
  const dir = pluginDir();
943
1135
  if (!dir) return null;
944
1136
  try {
945
- return await readFile(join3(dir, name));
1137
+ return await readFile(join4(dir, name));
946
1138
  } catch {
947
1139
  return null;
948
1140
  }
@@ -1590,14 +1782,22 @@ var PLUGIN_ROUTES = [
1590
1782
 
1591
1783
  `);
1592
1784
  });
1785
+ const teardown = () => {
1786
+ clearInterval(heartbeat);
1787
+ unsubscribe();
1788
+ sessions.setConnected(session, false);
1789
+ };
1593
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
+ }
1594
1797
  sessions.touchSeen(session);
1595
- res.write(": ping\n\n");
1596
1798
  }, 15e3);
1597
1799
  req.on("close", () => {
1598
- clearInterval(heartbeat);
1599
- unsubscribe();
1600
- sessions.setConnected(session, false);
1800
+ teardown();
1601
1801
  log(`SSE channel closed: session ${session.slice(0, 8)}`);
1602
1802
  });
1603
1803
  }
@@ -1627,14 +1827,9 @@ var PLUGIN_ROUTES = [
1627
1827
  ];
1628
1828
 
1629
1829
  // src/http.ts
1630
- var lastActivity = Date.now();
1631
- function getLastActivity() {
1632
- return lastActivity;
1633
- }
1634
1830
  var ROUTES = [handleInstall, handleMcp, handlePluginApi];
1635
1831
  function startHttpServer() {
1636
1832
  const server = createServer((req, res) => {
1637
- lastActivity = Date.now();
1638
1833
  res.setHeader("Access-Control-Allow-Origin", "*");
1639
1834
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
1640
1835
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id");
@@ -1685,19 +1880,11 @@ async function runHub() {
1685
1880
  log(`\u7AEF\u53E3 ${config.httpPort} \u5DF2\u6709 hub \u5728\u8FD0\u884C\uFF0C\u672C\u8FDB\u7A0B\u9000\u51FA`);
1686
1881
  process.exit(0);
1687
1882
  }
1688
- 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");
1689
1885
  void cleanOldAssets(config.assetTtlMs);
1690
- let tick = 0;
1691
- const watchdog = setInterval(() => {
1692
- const idle = Date.now() - getLastActivity();
1693
- const anyPluginOnline = sessions.list().some((s) => s.online);
1694
- if (idle > config.hubIdleMs && !anyPluginOnline) {
1695
- log(`\u7A7A\u95F2 ${Math.round(idle / 6e4)} \u5206\u949F\u4E14\u65E0\u5728\u7EBF\u63D2\u4EF6\uFF0Chub \u81EA\u52A8\u9000\u51FA`);
1696
- process.exit(0);
1697
- }
1698
- if (++tick % 60 === 0) void cleanOldAssets(config.assetTtlMs);
1699
- }, 6e4);
1700
- watchdog.unref();
1886
+ const sweeper2 = setInterval(() => void cleanOldAssets(config.assetTtlMs), 60 * 6e4);
1887
+ sweeper2.unref();
1701
1888
  }
1702
1889
 
1703
1890
  // src/process/stdio-proxy.ts
@@ -1706,64 +1893,6 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
1706
1893
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1707
1894
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1708
1895
  import { ResultSchema } from "@modelcontextprotocol/sdk/types.js";
1709
-
1710
- // src/process/hub-process.ts
1711
- import { spawn } from "node:child_process";
1712
- import { closeSync, openSync } from "node:fs";
1713
- import { tmpdir as tmpdir2 } from "node:os";
1714
- import { join as join4 } from "node:path";
1715
- import { fileURLToPath as fileURLToPath2 } from "node:url";
1716
- async function probeHub() {
1717
- try {
1718
- const res = await fetch(`${hubBaseUrl()}/health`, { signal: AbortSignal.timeout(1500) });
1719
- if (!res.ok) return "foreign";
1720
- const body = await res.json();
1721
- if (body.ok === true && body.name === HUB_NAME) return "alive";
1722
- if (body.ok === true) return "legacy";
1723
- return "foreign";
1724
- } catch {
1725
- return "down";
1726
- }
1727
- }
1728
- var PORT_FIX_HINT = `\u7528 netstat -ano | findstr :${config.httpPort} \u67E5\u5230 PID \u540E taskkill /PID <pid> /F \u7ED3\u675F\uFF0C\u518D\u91CD\u8BD5`;
1729
- function hubStateError(state) {
1730
- if (state === "legacy") {
1731
- return new Error(
1732
- `\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}`
1733
- );
1734
- }
1735
- 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`);
1736
- }
1737
- var HUB_LOG = join4(tmpdir2(), "batter-pixso-mcp-hub.log");
1738
- function spawnHub() {
1739
- const out = openSync(HUB_LOG, "a");
1740
- const entry = process.argv[1] ?? fileURLToPath2(import.meta.url);
1741
- const SKIP_FLAGS = /* @__PURE__ */ new Set(["--eval", "-e", "--print", "-p", "--watch", "--watch-preserve-output"]);
1742
- const execArgv = process.execArgv.filter(
1743
- (a, i, arr) => !SKIP_FLAGS.has(a) && !(i > 0 && SKIP_FLAGS.has(arr[i - 1]))
1744
- );
1745
- const child = spawn(process.execPath, [...execArgv, entry, "--hub"], {
1746
- detached: true,
1747
- stdio: ["ignore", out, out],
1748
- windowsHide: true,
1749
- env: { ...process.env }
1750
- });
1751
- child.unref();
1752
- closeSync(out);
1753
- log(`hub spawned (pid ${child.pid}), log: ${HUB_LOG}`);
1754
- }
1755
- async function waitForHub(timeoutMs = 15e3) {
1756
- const deadline = Date.now() + timeoutMs;
1757
- for (; ; ) {
1758
- const state = await probeHub();
1759
- if (state === "alive") return;
1760
- if (state !== "down") throw hubStateError(state);
1761
- if (Date.now() > deadline) throw new Error(`\u7B49\u5F85 hub \u5C31\u7EEA\u8D85\u65F6\uFF08${timeoutMs / 1e3}s\uFF09`);
1762
- await new Promise((r) => setTimeout(r, 200));
1763
- }
1764
- }
1765
-
1766
- // src/process/stdio-proxy.ts
1767
1896
  async function startStdioProxy() {
1768
1897
  const mcpUrl = `${hubBaseUrl()}/mcp`;
1769
1898
  let upstream = null;
@@ -1777,14 +1906,40 @@ async function startStdioProxy() {
1777
1906
  }
1778
1907
  async function ensureUpstream() {
1779
1908
  if (upstream) return upstream;
1780
- const state = await probeHub();
1781
- if (state === "down") spawnHub();
1782
- if (state === "legacy" || state === "foreign") throw hubStateError(state);
1783
- 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);
1784
1926
  upstream = await connectUpstream();
1785
1927
  log(`proxy connected to ${mcpUrl}`);
1786
1928
  return upstream;
1787
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
+ }
1788
1943
  async function callWithRecovery(fn) {
1789
1944
  const client = await ensureUpstream();
1790
1945
  try {
@@ -1796,8 +1951,7 @@ async function startStdioProxy() {
1796
1951
  );
1797
1952
  if (alive) throw err;
1798
1953
  log("hub \u4F1A\u8BDD\u5931\u8054\uFF0C\u91CD\u5EFA\u540E\u91CD\u8BD5\u2026");
1799
- upstream = null;
1800
- const rebuilt = await ensureUpstream();
1954
+ const rebuilt = await rebuildUpstream();
1801
1955
  return await fn(rebuilt);
1802
1956
  }
1803
1957
  }
@@ -1823,6 +1977,14 @@ async function main() {
1823
1977
  await runHub();
1824
1978
  return;
1825
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
+ }
1826
1988
  await startStdioProxy();
1827
1989
  }
1828
1990