batter-pixso-mcp 0.1.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 +1844 -0
- package/dist/plugin/code.js +391 -0
- package/dist/plugin/manifest.json +14 -0
- package/dist/plugin/ui.html +310 -0
- package/package.json +34 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1844 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/config.ts
|
|
13
|
+
function log(...args) {
|
|
14
|
+
console.error("[batter-pixso-mcp]", ...args);
|
|
15
|
+
}
|
|
16
|
+
var config, VERSION;
|
|
17
|
+
var init_config = __esm({
|
|
18
|
+
"src/config.ts"() {
|
|
19
|
+
"use strict";
|
|
20
|
+
config = {
|
|
21
|
+
/** 插件通信 HTTP 端口,仅监听 127.0.0.1 */
|
|
22
|
+
httpPort: Number(process.env.HTTP_PORT ?? 3210),
|
|
23
|
+
/** hub 空闲自毁阈值(无插件、无 agent 请求),默认 30 分钟 */
|
|
24
|
+
hubIdleMs: Number(process.env.HUB_IDLE_MS ?? 30 * 60 * 1e3),
|
|
25
|
+
/** 产物图片保留时长,默认 7 天 */
|
|
26
|
+
assetTtlMs: Number(process.env.ASSET_TTL_DAYS ?? 7) * 24 * 60 * 60 * 1e3
|
|
27
|
+
};
|
|
28
|
+
VERSION = "0.1.0";
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// src/assets.ts
|
|
33
|
+
import { randomBytes } from "node:crypto";
|
|
34
|
+
import { mkdir, readdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
35
|
+
import { tmpdir } from "node:os";
|
|
36
|
+
import { join } from "node:path";
|
|
37
|
+
async function saveAsset(bytes, format) {
|
|
38
|
+
await mkdir(ASSETS_DIR, { recursive: true });
|
|
39
|
+
const ext = EXT_BY_FORMAT[format.toUpperCase()] ?? ".bin";
|
|
40
|
+
const name = `${Date.now()}-${randomBytes(4).toString("hex")}${ext}`;
|
|
41
|
+
const path = join(ASSETS_DIR, name);
|
|
42
|
+
await writeFile(path, bytes);
|
|
43
|
+
log(`asset saved: ${path} (${bytes.length} bytes)`);
|
|
44
|
+
return { name, path };
|
|
45
|
+
}
|
|
46
|
+
function assetMime(name) {
|
|
47
|
+
const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
|
|
48
|
+
return MIME_BY_EXT[ext] ?? "application/octet-stream";
|
|
49
|
+
}
|
|
50
|
+
function assetPath(name) {
|
|
51
|
+
if (!/^[\w.-]+$/.test(name)) throw new Error("invalid asset name");
|
|
52
|
+
return join(ASSETS_DIR, name);
|
|
53
|
+
}
|
|
54
|
+
async function cleanOldAssets(ttlMs) {
|
|
55
|
+
let removed = 0;
|
|
56
|
+
const cutoff = Date.now() - ttlMs;
|
|
57
|
+
let names;
|
|
58
|
+
try {
|
|
59
|
+
names = await readdir(ASSETS_DIR);
|
|
60
|
+
} catch {
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
for (const name of names) {
|
|
64
|
+
try {
|
|
65
|
+
const p = join(ASSETS_DIR, name);
|
|
66
|
+
const s = await stat(p);
|
|
67
|
+
if (s.isFile() && s.mtimeMs < cutoff) {
|
|
68
|
+
await unlink(p);
|
|
69
|
+
removed++;
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (removed > 0) log(`assets cleanup: removed ${removed} expired file(s)`);
|
|
75
|
+
return removed;
|
|
76
|
+
}
|
|
77
|
+
var ASSETS_DIR, EXT_BY_FORMAT, MIME_BY_EXT;
|
|
78
|
+
var init_assets = __esm({
|
|
79
|
+
"src/assets.ts"() {
|
|
80
|
+
"use strict";
|
|
81
|
+
init_config();
|
|
82
|
+
ASSETS_DIR = join(tmpdir(), "batter-pixso-assets");
|
|
83
|
+
EXT_BY_FORMAT = {
|
|
84
|
+
PNG: ".png",
|
|
85
|
+
SVG: ".svg",
|
|
86
|
+
JPG: ".jpg",
|
|
87
|
+
JPEG: ".jpg",
|
|
88
|
+
WEBP: ".webp"
|
|
89
|
+
};
|
|
90
|
+
MIME_BY_EXT = {
|
|
91
|
+
".png": "image/png",
|
|
92
|
+
".svg": "image/svg+xml",
|
|
93
|
+
".jpg": "image/jpeg",
|
|
94
|
+
".webp": "image/webp"
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// src/core/sessions.ts
|
|
100
|
+
import { randomUUID } from "node:crypto";
|
|
101
|
+
var ONLINE_WINDOW_MS, SessionRegistry, sessions;
|
|
102
|
+
var init_sessions = __esm({
|
|
103
|
+
"src/core/sessions.ts"() {
|
|
104
|
+
"use strict";
|
|
105
|
+
init_config();
|
|
106
|
+
ONLINE_WINDOW_MS = 45e3;
|
|
107
|
+
SessionRegistry = class {
|
|
108
|
+
sessions = /* @__PURE__ */ new Map();
|
|
109
|
+
lastActiveId = null;
|
|
110
|
+
isOnline(s) {
|
|
111
|
+
return s.connected || Date.now() - s.lastSeen < ONLINE_WINDOW_MS;
|
|
112
|
+
}
|
|
113
|
+
register(meta = {}) {
|
|
114
|
+
const session = { id: randomUUID(), meta, lastSeen: Date.now(), connected: false };
|
|
115
|
+
this.sessions.set(session.id, session);
|
|
116
|
+
this.lastActiveId = session.id;
|
|
117
|
+
log(`session registered: ${session.id} (${meta.fileName ?? "unknown file"})`);
|
|
118
|
+
return this.toInfo(session);
|
|
119
|
+
}
|
|
120
|
+
touch(id) {
|
|
121
|
+
const s = this.sessions.get(id);
|
|
122
|
+
if (!s) return null;
|
|
123
|
+
s.lastSeen = Date.now();
|
|
124
|
+
this.lastActiveId = id;
|
|
125
|
+
return s;
|
|
126
|
+
}
|
|
127
|
+
setConnected(id, connected) {
|
|
128
|
+
const s = this.touch(id);
|
|
129
|
+
if (!s) return false;
|
|
130
|
+
s.connected = connected;
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
updateContext(id, meta) {
|
|
134
|
+
const s = this.touch(id);
|
|
135
|
+
if (!s) return false;
|
|
136
|
+
s.meta = { ...s.meta, ...meta };
|
|
137
|
+
log(
|
|
138
|
+
`context updated [${id.slice(0, 8)}]: ${s.meta.fileName ?? "?"} / ${s.meta.pageName ?? "?"} / ${s.meta.selection?.length ?? 0} selected`
|
|
139
|
+
);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
list() {
|
|
143
|
+
return [...this.sessions.values()].map((s) => this.toInfo(s));
|
|
144
|
+
}
|
|
145
|
+
anyOnline() {
|
|
146
|
+
return [...this.sessions.values()].some((s) => this.isOnline(s));
|
|
147
|
+
}
|
|
148
|
+
/** 路由:显式 sessionId > fileKey 匹配 > 最近活跃在线会话 */
|
|
149
|
+
resolve(target) {
|
|
150
|
+
if (target) {
|
|
151
|
+
const byId = this.sessions.get(target);
|
|
152
|
+
if (byId) return this.assertOnline(byId);
|
|
153
|
+
const byFile = [...this.sessions.values()].find((s) => s.meta.fileKey === target);
|
|
154
|
+
if (byFile) return this.assertOnline(byFile);
|
|
155
|
+
throw 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`);
|
|
156
|
+
}
|
|
157
|
+
if (this.lastActiveId) {
|
|
158
|
+
const s = this.sessions.get(this.lastActiveId);
|
|
159
|
+
if (s && this.isOnline(s)) return s;
|
|
160
|
+
}
|
|
161
|
+
const fallback = [...this.sessions.values()].find((s) => this.isOnline(s));
|
|
162
|
+
if (fallback) return fallback;
|
|
163
|
+
throw new Error("Pixso \u63D2\u4EF6\u672A\u8FDE\u63A5\uFF1A\u8BF7\u5728 Pixso \u5BA2\u6237\u7AEF\u4E2D\u8FD0\u884C Batter Pixso Bridge \u63D2\u4EF6");
|
|
164
|
+
}
|
|
165
|
+
assertOnline(s) {
|
|
166
|
+
if (!this.isOnline(s)) throw new Error(`\u76EE\u6807\u63D2\u4EF6\u5DF2\u79BB\u7EBF\uFF1A${s.meta.fileName ?? s.id}`);
|
|
167
|
+
return s;
|
|
168
|
+
}
|
|
169
|
+
toInfo(s) {
|
|
170
|
+
return {
|
|
171
|
+
sessionId: s.id,
|
|
172
|
+
fileKey: s.meta.fileKey,
|
|
173
|
+
fileName: s.meta.fileName,
|
|
174
|
+
pageName: s.meta.pageName,
|
|
175
|
+
selectionCount: s.meta.selection?.length,
|
|
176
|
+
online: this.isOnline(s),
|
|
177
|
+
lastSeen: s.lastSeen
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
sessions = new SessionRegistry();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// src/routes/http-utils.ts
|
|
186
|
+
function sendJson(res, status, body) {
|
|
187
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
188
|
+
res.end(JSON.stringify(body));
|
|
189
|
+
}
|
|
190
|
+
async function readBody(req) {
|
|
191
|
+
const chunks = [];
|
|
192
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
193
|
+
return Buffer.concat(chunks);
|
|
194
|
+
}
|
|
195
|
+
var init_http_utils = __esm({
|
|
196
|
+
"src/routes/http-utils.ts"() {
|
|
197
|
+
"use strict";
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// src/install/dist-resolver.ts
|
|
202
|
+
import { existsSync } from "node:fs";
|
|
203
|
+
import { readFile } from "node:fs/promises";
|
|
204
|
+
import { join as join2 } from "node:path";
|
|
205
|
+
import { fileURLToPath } from "node:url";
|
|
206
|
+
function resolvePluginDir() {
|
|
207
|
+
const candidates = [
|
|
208
|
+
fileURLToPath(new URL("./plugin/", import.meta.url)),
|
|
209
|
+
// bundle: dist/index.js → dist/plugin/
|
|
210
|
+
fileURLToPath(new URL("../../plugin/dist/", import.meta.url)),
|
|
211
|
+
// bundle 开发: dist/index.js → packages/plugin/dist/
|
|
212
|
+
fileURLToPath(new URL("../../../plugin/dist/", import.meta.url))
|
|
213
|
+
// tsx 开发: src/install/ → packages/plugin/dist/
|
|
214
|
+
];
|
|
215
|
+
return candidates.find((p) => existsSync(join2(p, "manifest.json"))) ?? null;
|
|
216
|
+
}
|
|
217
|
+
async function pluginManifest() {
|
|
218
|
+
if (!PLUGIN_DIR) return null;
|
|
219
|
+
const raw = JSON.parse(
|
|
220
|
+
await readFile(join2(PLUGIN_DIR, "manifest.json"), "utf8")
|
|
221
|
+
);
|
|
222
|
+
raw.networkAccess = {
|
|
223
|
+
allowedDomains: [`http://localhost:${config.httpPort}`, `http://127.0.0.1:${config.httpPort}`]
|
|
224
|
+
};
|
|
225
|
+
return Buffer.from(JSON.stringify(raw, null, 2));
|
|
226
|
+
}
|
|
227
|
+
async function readPluginFile(name) {
|
|
228
|
+
if (!PLUGIN_DIR) return null;
|
|
229
|
+
try {
|
|
230
|
+
return await readFile(join2(PLUGIN_DIR, name));
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
var PLUGIN_DIR;
|
|
236
|
+
var init_dist_resolver = __esm({
|
|
237
|
+
"src/install/dist-resolver.ts"() {
|
|
238
|
+
"use strict";
|
|
239
|
+
init_config();
|
|
240
|
+
PLUGIN_DIR = resolvePluginDir();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// src/install/templates/bookmarklet.txt
|
|
245
|
+
var bookmarklet_default;
|
|
246
|
+
var init_bookmarklet = __esm({
|
|
247
|
+
"src/install/templates/bookmarklet.txt"() {
|
|
248
|
+
bookmarklet_default = '// Batter Bridge \u4E66\u7B7E\u6CE8\u5165\u811A\u672C\n// \u5728 Pixso / MasterGo \u7F51\u9875\u7248\u8BBE\u8BA1\u6587\u4EF6\u9875\u9762\u70B9\u51FB\u540E\u6267\u884C\uFF1A\n// \u4ECE\u672C\u670D\u52A1\u62C9\u53D6 manifest + code\uFF0C\u901A\u8FC7\u5BBF\u4E3B\u6CE8\u5165 API \u8FD0\u884C\u63D2\u4EF6\u3002\n// \u670D\u52A1\u4E0D\u53EF\u8FBE\u65F6\u56DE\u9000 localStorage \u7F13\u5B58\uFF08\u4E0A\u6B21\u6210\u529F\u62C9\u53D6\u7684\u63D2\u4EF6\u5305\uFF09\u3002\n// \u6E32\u67D3\u65F6 {{ base }} \u66FF\u6362\u4E3A\u670D\u52A1\u5730\u5740\uFF08JS \u5B57\u7B26\u4E32\u8F6C\u4E49\uFF09\uFF0C\u6574\u4F53 encodeURIComponent \u540E\u4F5C\u4E3A\u4E66\u7B7E href\u3002\n(function () {\n var SERVER_BASE = "{{ base }}";\n var CACHE_KEY = "batter-bridge-cache";\n\n /** \u7528\u5BBF\u4E3B\u9875\u9762\u7684\u6CE8\u5165 API \u542F\u52A8\u63D2\u4EF6 */\n function startPlugin(codeText, manifestText) {\n try {\n var manifest = JSON.parse(manifestText);\n if (window._FicAppObj && window._FicAppObj.runTestPlugin) {\n // MasterGo \u5BBF\u4E3B\n window._FicAppObj.runTestPlugin(manifest, codeText, "", null, { startup: "DEBUG" });\n return;\n }\n if (window.runNewPlugin) {\n // Pixso \u5BBF\u4E3B\n window.runNewPlugin(codeText, null, null, { manifest: manifest, plugin: manifest });\n return;\n }\n alert("\u8BF7\u5728 Pixso / MasterGo \u8BBE\u8BA1\u6587\u4EF6\u9875\u9762\u70B9\u51FB\u6B64\u4E66\u7B7E");\n } catch (err) {\n alert("\u63D2\u4EF6\u542F\u52A8\u5931\u8D25\uFF1A" + ((err && err.message) || err));\n }\n }\n\n /** \u5C1D\u8BD5\u7528\u7F13\u5B58\u7684\u63D2\u4EF6\u5305\u542F\u52A8\uFF1B\u65E0\u7F13\u5B58\u8FD4\u56DE false */\n function startFromCache() {\n try {\n var cached = JSON.parse(localStorage.getItem(CACHE_KEY) || "null");\n if (cached && cached.manifest && cached.code) {\n startPlugin(cached.code, cached.manifest);\n return true;\n }\n } catch (err) {\n // \u7F13\u5B58\u635F\u574F\uFF1A\u89C6\u4E3A\u65E0\u7F13\u5B58\n }\n return false;\n }\n\n function saveCache(manifestText, codeText) {\n try {\n localStorage.setItem(CACHE_KEY, JSON.stringify({ manifest: manifestText, code: codeText }));\n } catch (err) {\n // \u5B58\u50A8\u8D85\u9650\u7B49\uFF1A\u7F13\u5B58\u53EA\u662F\u79BB\u7EBF\u515C\u5E95\uFF0C\u5931\u8D25\u53EF\u5FFD\u7565\n }\n }\n\n function fetchText(path) {\n return fetch(SERVER_BASE + path + "?t=" + Date.now(), { cache: "no-store" }).then(\n function (res) {\n if (!res.ok) throw new Error("HTTP " + res.status);\n return res.text();\n },\n );\n }\n\n Promise.all([fetchText("manifest.json"), fetchText("code.js")])\n .then(function (texts) {\n var manifestText = texts[0];\n var codeText = texts[1];\n saveCache(manifestText, codeText);\n startPlugin(codeText, manifestText);\n })\n .catch(function () {\n if (!startFromCache()) {\n alert("\u65E0\u6CD5\u8FDE\u63A5 batter-pixso-mcp \u670D\u52A1\uFF08" + SERVER_BASE + "\uFF09\uFF0C\u8BF7\u786E\u8BA4\u670D\u52A1\u5DF2\u542F\u52A8");\n }\n });\n})();\n';
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// src/install/templates/install-page.html
|
|
253
|
+
var install_page_default;
|
|
254
|
+
var init_install_page = __esm({
|
|
255
|
+
"src/install/templates/install-page.html"() {
|
|
256
|
+
install_page_default = `<!DOCTYPE html>
|
|
257
|
+
<html lang="zh-CN">
|
|
258
|
+
<head>
|
|
259
|
+
<meta charset="utf-8">
|
|
260
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
261
|
+
<title>Batter Pixso Bridge \xB7 \u5B89\u88C5</title>
|
|
262
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect width='24' height='24' rx='6' fill='%23575BF2'/%3E%3Cpath d='M13 4 7.5 13.5H11L10 20l6.5-9.5H12.5Z' fill='%23fff'/%3E%3C/svg%3E">
|
|
263
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
264
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
265
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
266
|
+
<style>
|
|
267
|
+
:root {
|
|
268
|
+
--ink: #1e1e1e;
|
|
269
|
+
--sub: #6b6b6b;
|
|
270
|
+
--faint: #a3a3a3;
|
|
271
|
+
--line: #e8e8ea;
|
|
272
|
+
--panel: #f7f7f8;
|
|
273
|
+
--iris: #575bf2;
|
|
274
|
+
--iris-strong: #4346e0;
|
|
275
|
+
--iris-soft: #eef0fe;
|
|
276
|
+
--ok: #14ae5c;
|
|
277
|
+
--err: #f24822;
|
|
278
|
+
--radius: 6px;
|
|
279
|
+
--mono: ui-monospace, "SF Mono", Menlo, Consolas, "Cascadia Mono", monospace;
|
|
280
|
+
}
|
|
281
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
282
|
+
html { -webkit-text-size-adjust: 100%; }
|
|
283
|
+
body {
|
|
284
|
+
background: #fff;
|
|
285
|
+
color: var(--ink);
|
|
286
|
+
font-family: Inter, -apple-system, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
|
287
|
+
font-feature-settings: "tnum" 1, "cv05" 1;
|
|
288
|
+
line-height: 1.5;
|
|
289
|
+
min-height: 100vh;
|
|
290
|
+
}
|
|
291
|
+
::selection { background: var(--iris-soft); }
|
|
292
|
+
.page {
|
|
293
|
+
max-width: 680px;
|
|
294
|
+
margin: 0 auto;
|
|
295
|
+
padding: clamp(48px, 10vh, 96px) 24px 64px;
|
|
296
|
+
animation: enter 420ms cubic-bezier(0.2, 0.6, 0.2, 1) both;
|
|
297
|
+
}
|
|
298
|
+
@keyframes enter {
|
|
299
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
300
|
+
to { opacity: 1; transform: none; }
|
|
301
|
+
}
|
|
302
|
+
a { color: var(--iris); text-decoration: none; }
|
|
303
|
+
a:focus-visible, button:focus-visible {
|
|
304
|
+
outline: 2px solid var(--iris);
|
|
305
|
+
outline-offset: 2px;
|
|
306
|
+
border-radius: 2px;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* \u2500\u2500 \u9876\u680F\uFF1A\u54C1\u724C + \u670D\u52A1\u72B6\u6001 \u2500\u2500 */
|
|
310
|
+
.top {
|
|
311
|
+
display: flex;
|
|
312
|
+
align-items: center;
|
|
313
|
+
justify-content: space-between;
|
|
314
|
+
gap: 16px;
|
|
315
|
+
margin-bottom: clamp(40px, 7vh, 72px);
|
|
316
|
+
}
|
|
317
|
+
.brand {
|
|
318
|
+
display: flex;
|
|
319
|
+
align-items: center;
|
|
320
|
+
gap: 10px;
|
|
321
|
+
font-size: 13px;
|
|
322
|
+
font-weight: 600;
|
|
323
|
+
letter-spacing: -0.01em;
|
|
324
|
+
}
|
|
325
|
+
.brand svg { display: block; }
|
|
326
|
+
.brand .ver {
|
|
327
|
+
font-family: var(--mono);
|
|
328
|
+
font-weight: 400;
|
|
329
|
+
font-size: 11px;
|
|
330
|
+
color: var(--faint);
|
|
331
|
+
margin-left: 2px;
|
|
332
|
+
}
|
|
333
|
+
.status {
|
|
334
|
+
display: inline-flex;
|
|
335
|
+
align-items: center;
|
|
336
|
+
gap: 7px;
|
|
337
|
+
padding: 5px 10px;
|
|
338
|
+
border-radius: 999px;
|
|
339
|
+
background: var(--panel);
|
|
340
|
+
border: 1px solid var(--line);
|
|
341
|
+
font-size: 11px;
|
|
342
|
+
color: var(--sub);
|
|
343
|
+
white-space: nowrap;
|
|
344
|
+
}
|
|
345
|
+
.status .dot {
|
|
346
|
+
width: 6px;
|
|
347
|
+
height: 6px;
|
|
348
|
+
border-radius: 50%;
|
|
349
|
+
background: var(--faint);
|
|
350
|
+
flex-shrink: 0;
|
|
351
|
+
animation: pulse 2.2s ease-in-out infinite;
|
|
352
|
+
}
|
|
353
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
|
|
354
|
+
.status.ok { color: #0d7a3e; }
|
|
355
|
+
.status.ok .dot { background: var(--ok); }
|
|
356
|
+
.status.err { color: var(--err); }
|
|
357
|
+
.status.err .dot { background: var(--err); animation: none; }
|
|
358
|
+
|
|
359
|
+
/* \u2500\u2500 \u4E3B\u6807\u9898\u533A \u2500\u2500 */
|
|
360
|
+
h1 {
|
|
361
|
+
font-size: clamp(28px, 5vw, 38px);
|
|
362
|
+
font-weight: 650;
|
|
363
|
+
letter-spacing: -0.025em;
|
|
364
|
+
line-height: 1.2;
|
|
365
|
+
margin-bottom: 12px;
|
|
366
|
+
}
|
|
367
|
+
.sub {
|
|
368
|
+
font-size: 14px;
|
|
369
|
+
color: var(--sub);
|
|
370
|
+
max-width: 46em;
|
|
371
|
+
line-height: 1.7;
|
|
372
|
+
margin-bottom: clamp(40px, 7vh, 64px);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/* \u2500\u2500 \u6B65\u9AA4\u5217\u8868\uFF1A\u53D1\u4E1D\u7EBF\u5206\u9694 \u2500\u2500 */
|
|
376
|
+
.steps { border-top: 1px solid var(--line); }
|
|
377
|
+
.step {
|
|
378
|
+
display: grid;
|
|
379
|
+
grid-template-columns: 44px 1fr;
|
|
380
|
+
gap: 4px 16px;
|
|
381
|
+
padding: 26px 0 28px;
|
|
382
|
+
border-bottom: 1px solid var(--line);
|
|
383
|
+
}
|
|
384
|
+
.num {
|
|
385
|
+
grid-row: 1 / span 2;
|
|
386
|
+
font-family: var(--mono);
|
|
387
|
+
font-size: 12px;
|
|
388
|
+
color: var(--faint);
|
|
389
|
+
padding-top: 3px;
|
|
390
|
+
font-variant-numeric: tabular-nums;
|
|
391
|
+
}
|
|
392
|
+
.step-head {
|
|
393
|
+
display: flex;
|
|
394
|
+
align-items: center;
|
|
395
|
+
gap: 10px;
|
|
396
|
+
flex-wrap: wrap;
|
|
397
|
+
}
|
|
398
|
+
.step-title {
|
|
399
|
+
font-size: 14px;
|
|
400
|
+
font-weight: 600;
|
|
401
|
+
letter-spacing: -0.01em;
|
|
402
|
+
}
|
|
403
|
+
.tag {
|
|
404
|
+
font-size: 11px;
|
|
405
|
+
font-weight: 500;
|
|
406
|
+
color: var(--iris);
|
|
407
|
+
background: var(--iris-soft);
|
|
408
|
+
padding: 2px 8px;
|
|
409
|
+
border-radius: 999px;
|
|
410
|
+
}
|
|
411
|
+
.step-body { grid-column: 2; }
|
|
412
|
+
.note {
|
|
413
|
+
font-size: 12.5px;
|
|
414
|
+
color: var(--sub);
|
|
415
|
+
line-height: 1.75;
|
|
416
|
+
max-width: 60em;
|
|
417
|
+
}
|
|
418
|
+
.note strong { color: var(--ink); font-weight: 600; }
|
|
419
|
+
code {
|
|
420
|
+
font-family: var(--mono);
|
|
421
|
+
background: var(--panel);
|
|
422
|
+
border: 1px solid var(--line);
|
|
423
|
+
color: var(--ink);
|
|
424
|
+
padding: 0.5px 5px;
|
|
425
|
+
border-radius: 4px;
|
|
426
|
+
font-size: 11px;
|
|
427
|
+
}
|
|
428
|
+
.actions { display: flex; align-items: center; gap: 12px; margin: 14px 0 10px; }
|
|
429
|
+
|
|
430
|
+
/* \u4E3B\u6309\u94AE\uFF1A\u53EF\u62D6\u62FD\u7684\u4E66\u7B7E */
|
|
431
|
+
.bm {
|
|
432
|
+
display: inline-flex;
|
|
433
|
+
align-items: center;
|
|
434
|
+
gap: 8px;
|
|
435
|
+
padding: 9px 18px;
|
|
436
|
+
background: var(--iris);
|
|
437
|
+
color: #fff;
|
|
438
|
+
border-radius: var(--radius);
|
|
439
|
+
font-size: 13px;
|
|
440
|
+
font-weight: 600;
|
|
441
|
+
letter-spacing: -0.01em;
|
|
442
|
+
cursor: grab;
|
|
443
|
+
box-shadow: 0 1px 2px rgba(30, 30, 30, 0.08);
|
|
444
|
+
transition: background 140ms ease, transform 140ms ease;
|
|
445
|
+
user-select: none;
|
|
446
|
+
}
|
|
447
|
+
.bm:hover { background: var(--iris-strong); }
|
|
448
|
+
.bm:active { cursor: grabbing; transform: scale(0.985); }
|
|
449
|
+
.drag-hint {
|
|
450
|
+
font-size: 12px;
|
|
451
|
+
color: var(--faint);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* \u6B21\u7EA7\u6309\u94AE */
|
|
455
|
+
.btn {
|
|
456
|
+
display: inline-flex;
|
|
457
|
+
align-items: center;
|
|
458
|
+
gap: 7px;
|
|
459
|
+
padding: 8px 14px;
|
|
460
|
+
border: 1px solid var(--line);
|
|
461
|
+
border-radius: var(--radius);
|
|
462
|
+
background: #fff;
|
|
463
|
+
color: var(--ink);
|
|
464
|
+
font-size: 13px;
|
|
465
|
+
font-weight: 500;
|
|
466
|
+
letter-spacing: -0.01em;
|
|
467
|
+
text-decoration: none;
|
|
468
|
+
transition: border-color 140ms ease, background 140ms ease;
|
|
469
|
+
}
|
|
470
|
+
.btn:hover { border-color: #d2d2d6; background: var(--panel); }
|
|
471
|
+
.btn svg { display: block; color: var(--sub); }
|
|
472
|
+
|
|
473
|
+
/* MCP \u914D\u7F6E\u4EE3\u7801\u5757 */
|
|
474
|
+
.cfg {
|
|
475
|
+
position: relative;
|
|
476
|
+
margin: 14px 0 10px;
|
|
477
|
+
}
|
|
478
|
+
pre {
|
|
479
|
+
background: var(--panel);
|
|
480
|
+
border: 1px solid var(--line);
|
|
481
|
+
border-radius: 8px;
|
|
482
|
+
padding: 14px 16px;
|
|
483
|
+
font-family: var(--mono);
|
|
484
|
+
font-size: 12px;
|
|
485
|
+
line-height: 1.7;
|
|
486
|
+
color: #3c3c3c;
|
|
487
|
+
overflow-x: auto;
|
|
488
|
+
}
|
|
489
|
+
.copy {
|
|
490
|
+
position: absolute;
|
|
491
|
+
top: 8px;
|
|
492
|
+
right: 8px;
|
|
493
|
+
padding: 4px 10px;
|
|
494
|
+
font-family: inherit;
|
|
495
|
+
font-size: 11px;
|
|
496
|
+
font-weight: 500;
|
|
497
|
+
color: var(--sub);
|
|
498
|
+
background: #fff;
|
|
499
|
+
border: 1px solid var(--line);
|
|
500
|
+
border-radius: var(--radius);
|
|
501
|
+
cursor: pointer;
|
|
502
|
+
transition: color 140ms ease, border-color 140ms ease;
|
|
503
|
+
}
|
|
504
|
+
.copy:hover { color: var(--ink); border-color: #d2d2d6; }
|
|
505
|
+
.copy.done { color: var(--ok); border-color: var(--ok); }
|
|
506
|
+
|
|
507
|
+
.footer {
|
|
508
|
+
margin-top: 40px;
|
|
509
|
+
font-size: 11px;
|
|
510
|
+
color: var(--faint);
|
|
511
|
+
font-family: var(--mono);
|
|
512
|
+
display: flex;
|
|
513
|
+
gap: 8px;
|
|
514
|
+
flex-wrap: wrap;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
@media (max-width: 560px) {
|
|
518
|
+
.step { grid-template-columns: 32px 1fr; gap: 4px 10px; }
|
|
519
|
+
.top { align-items: flex-start; flex-direction: column; gap: 12px; }
|
|
520
|
+
}
|
|
521
|
+
@media (prefers-reduced-motion: reduce) {
|
|
522
|
+
.page { animation: none; }
|
|
523
|
+
.status .dot { animation: none; }
|
|
524
|
+
.bm, .btn, .copy { transition: none; }
|
|
525
|
+
}
|
|
526
|
+
</style>
|
|
527
|
+
</head>
|
|
528
|
+
<body>
|
|
529
|
+
<div class="page">
|
|
530
|
+
<header class="top">
|
|
531
|
+
<div class="brand">
|
|
532
|
+
<svg width="22" height="22" viewBox="0 0 24 24" aria-hidden="true"><rect width="24" height="24" rx="6" fill="#575BF2"/><path d="M13 4 7.5 13.5H11L10 20l6.5-9.5H12.5Z" fill="#fff"/></svg>
|
|
533
|
+
Batter Pixso Bridge
|
|
534
|
+
<span class="ver">v{{ version }}</span>
|
|
535
|
+
</div>
|
|
536
|
+
<div class="status" id="status">
|
|
537
|
+
<span class="dot"></span><span id="status-text">\u6B63\u5728\u8FDE\u63A5\u670D\u52A1\u2026</span>
|
|
538
|
+
</div>
|
|
539
|
+
</header>
|
|
540
|
+
|
|
541
|
+
<h1>\u628A\u8BBE\u8BA1\u7A3F\u4EA4\u7ED9 AI</h1>
|
|
542
|
+
<p class="sub">\u4E00\u6B21\u5B89\u88C5\uFF0C\u8BA9 Claude\u3001Cursor \u7B49 AI \u7F16\u7A0B\u52A9\u624B\u76F4\u63A5\u8BFB\u53D6 Pixso / MasterGo \u753B\u5E03\u7684\u7ED3\u6784\u3001\u6837\u5F0F\u4E0E\u622A\u56FE\u3002\u6309\u4E0B\u9762\u56DB\u6B65\u5B8C\u6210\u8BBE\u7F6E\u3002</p>
|
|
543
|
+
|
|
544
|
+
<div class="steps">
|
|
545
|
+
<div class="step">
|
|
546
|
+
<span class="num">01</span>
|
|
547
|
+
<div class="step-head">
|
|
548
|
+
<span class="step-title">\u6DFB\u52A0\u4E66\u7B7E</span>
|
|
549
|
+
<span class="tag">\u7F51\u9875\u7248 \xB7 \u63A8\u8350</span>
|
|
550
|
+
</div>
|
|
551
|
+
<div class="step-body">
|
|
552
|
+
<div class="actions">
|
|
553
|
+
<a class="bm" href="{{ href }}">Batter Bridge</a>
|
|
554
|
+
<span class="drag-hint">\u2190 \u628A\u8FD9\u4E2A\u6309\u94AE\u62D6\u5230\u6D4F\u89C8\u5668\u4E66\u7B7E\u680F</span>
|
|
555
|
+
</div>
|
|
556
|
+
<p class="note">
|
|
557
|
+
<strong>\u53EA\u9700\u62D6\u62FD\u4E00\u6B21\u3002</strong>\u4EE5\u540E\u6253\u5F00 Pixso / MasterGo \u8BBE\u8BA1\u6587\u4EF6\u65F6\uFF0C\u70B9\u4E00\u4E0B\u4E66\u7B7E\u680F\u91CC\u7684\u300CBatter Bridge\u300D\uFF0C\u63D2\u4EF6\u5C31\u4F1A\u6CE8\u5165\u5F53\u524D\u9875\u9762\u3002
|
|
558
|
+
\u4E66\u7B7E\u6307\u5411\u672C\u673A\u670D\u52A1 <code>{{ base }}</code>\uFF0C\u670D\u52A1\u91CD\u542F\u540E\u4F9D\u7136\u6709\u6548\u3002\u770B\u4E0D\u5230\u4E66\u7B7E\u680F\uFF1F\u6309 <code>Ctrl + Shift + B</code> \u6253\u5F00\u3002
|
|
559
|
+
</p>
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
562
|
+
|
|
563
|
+
<div class="step">
|
|
564
|
+
<span class="num">02</span>
|
|
565
|
+
<div class="step-head">
|
|
566
|
+
<span class="step-title">\u5B89\u88C5\u5230 Pixso \u5BA2\u6237\u7AEF</span>
|
|
567
|
+
</div>
|
|
568
|
+
<div class="step-body">
|
|
569
|
+
<div class="actions">
|
|
570
|
+
<a class="btn" href="/plugin.zip" download="batter-pixso-bridge.zip">
|
|
571
|
+
<svg width="12" height="12" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12"/><path d="m7 11 5 5 5-5"/><path d="M4 21h16"/></svg>
|
|
572
|
+
\u4E0B\u8F7D\u63D2\u4EF6\u5305\uFF08.zip\uFF09
|
|
573
|
+
</a>
|
|
574
|
+
</div>
|
|
575
|
+
<p class="note">\u89E3\u538B\u540E\uFF0C\u5728\u5BA2\u6237\u7AEF\u7684\u8BBE\u8BA1\u6587\u4EF6\u4E2D\uFF1A\u53F3\u952E \u2192 \u63D2\u4EF6 \u2192 \u5F00\u53D1\u63D2\u4EF6 \u2192 \u5BFC\u5165 <code>manifest.json</code>\u3002</p>
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
|
|
579
|
+
<div class="step">
|
|
580
|
+
<span class="num">03</span>
|
|
581
|
+
<div class="step-head">
|
|
582
|
+
<span class="step-title">\u914D\u7F6E AI \u5BA2\u6237\u7AEF</span>
|
|
583
|
+
</div>
|
|
584
|
+
<div class="step-body">
|
|
585
|
+
<div class="cfg">
|
|
586
|
+
<pre id="cfg">{
|
|
587
|
+
"mcpServers": {
|
|
588
|
+
"batter-pixso": {
|
|
589
|
+
"command": "npx",
|
|
590
|
+
"args": ["-y", "batter-pixso-mcp"],
|
|
591
|
+
"env": { "HTTP_PORT": "{{ httpPort }}" }
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}</pre>
|
|
595
|
+
<button class="copy" id="copy" type="button">\u590D\u5236</button>
|
|
596
|
+
</div>
|
|
597
|
+
<p class="note">\u7C98\u8D34\u5230 Cursor / Claude Code \u7684 MCP \u914D\u7F6E\u6587\u4EF6\u3002\u7CFB\u7EDF Node \u4F4E\u4E8E 18 \u65F6\u6539\u7528 volta \u5305\u88F9\u542F\u52A8\uFF0C\u8BE6\u89C1 README\u3002</p>
|
|
598
|
+
</div>
|
|
599
|
+
</div>
|
|
600
|
+
|
|
601
|
+
<div class="step">
|
|
602
|
+
<span class="num">04</span>
|
|
603
|
+
<div class="step-head">
|
|
604
|
+
<span class="step-title">\u5F00\u59CB\u4F7F\u7528</span>
|
|
605
|
+
</div>
|
|
606
|
+
<div class="step-body">
|
|
607
|
+
<p class="note" style="margin-top:10px">
|
|
608
|
+
\u63D2\u4EF6\u8FD0\u884C\u540E\uFF0C\u5728\u753B\u5E03\u4E2D\u9009\u4E2D\u56FE\u5C42\uFF0C\u5BF9 AI \u8BF4\u300C\u5B9E\u73B0\u8FD9\u4E2A\u8BBE\u8BA1\u300D\u3002
|
|
609
|
+
AI \u4F1A\u81EA\u52A8\u8C03\u7528 <code>get_node_dsl</code>\u3001<code>get_screenshot</code> \u7B49\u5DE5\u5177\u8BFB\u53D6\u8BBE\u8BA1\u5E76\u751F\u6210\u4EE3\u7801\u3002
|
|
610
|
+
</p>
|
|
611
|
+
</div>
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
|
|
615
|
+
<div class="footer">
|
|
616
|
+
<span>batter-pixso-mcp</span>
|
|
617
|
+
<span>\xB7</span>
|
|
618
|
+
<span>v{{ version }}</span>
|
|
619
|
+
<span>\xB7</span>
|
|
620
|
+
<span>{{ base }}</span>
|
|
621
|
+
</div>
|
|
622
|
+
</div>
|
|
623
|
+
<script>
|
|
624
|
+
fetch("/health").then(function (r) { return r.json(); }).then(function (h) {
|
|
625
|
+
var el = document.getElementById("status");
|
|
626
|
+
el.className = "status ok";
|
|
627
|
+
document.getElementById("status-text").textContent =
|
|
628
|
+
"\u670D\u52A1\u8FD0\u884C\u4E2D \xB7 \u63D2\u4EF6 " + h.pluginsOnline + " \u5728\u7EBF";
|
|
629
|
+
}).catch(function () {
|
|
630
|
+
var el = document.getElementById("status");
|
|
631
|
+
el.className = "status err";
|
|
632
|
+
document.getElementById("status-text").textContent = "\u670D\u52A1\u672A\u8FDE\u63A5";
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
var copyBtn = document.getElementById("copy");
|
|
636
|
+
copyBtn.addEventListener("click", function () {
|
|
637
|
+
var text = document.getElementById("cfg").textContent;
|
|
638
|
+
function done() {
|
|
639
|
+
copyBtn.textContent = "\u5DF2\u590D\u5236";
|
|
640
|
+
copyBtn.className = "copy done";
|
|
641
|
+
setTimeout(function () {
|
|
642
|
+
copyBtn.textContent = "\u590D\u5236";
|
|
643
|
+
copyBtn.className = "copy";
|
|
644
|
+
}, 1600);
|
|
645
|
+
}
|
|
646
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
647
|
+
navigator.clipboard.writeText(text).then(done, done);
|
|
648
|
+
} else {
|
|
649
|
+
var ta = document.createElement("textarea");
|
|
650
|
+
ta.value = text;
|
|
651
|
+
document.body.appendChild(ta);
|
|
652
|
+
ta.select();
|
|
653
|
+
try { document.execCommand("copy"); } catch (e) {}
|
|
654
|
+
document.body.removeChild(ta);
|
|
655
|
+
done();
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
</script>
|
|
659
|
+
</body>
|
|
660
|
+
</html>
|
|
661
|
+
`;
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
// src/install/template.ts
|
|
666
|
+
function escapeHtml(s) {
|
|
667
|
+
return s.replace(/[&<>"']/g, (c) => HTML_ESCAPES[c]);
|
|
668
|
+
}
|
|
669
|
+
function escapeJsString(s) {
|
|
670
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r/g, "\\r").replace(/\n/g, "\\n");
|
|
671
|
+
}
|
|
672
|
+
function renderTemplate(tpl, vars, escape = escapeHtml) {
|
|
673
|
+
const lookup = (key) => {
|
|
674
|
+
const v = vars[key];
|
|
675
|
+
if (v === void 0) throw new Error(`\u6A21\u677F\u53D8\u91CF\u7F3A\u5931\uFF1A{{${key}}}`);
|
|
676
|
+
return String(v);
|
|
677
|
+
};
|
|
678
|
+
return tpl.replace(/\{\{\{\s*(\w+)\s*\}\}\}/g, (_, key) => lookup(key)).replace(/\{\{\s*(\w+)\s*\}\}/g, (_, key) => escape(lookup(key)));
|
|
679
|
+
}
|
|
680
|
+
function loadTemplate(name) {
|
|
681
|
+
return TEMPLATES[name];
|
|
682
|
+
}
|
|
683
|
+
var HTML_ESCAPES, TEMPLATES;
|
|
684
|
+
var init_template = __esm({
|
|
685
|
+
"src/install/template.ts"() {
|
|
686
|
+
"use strict";
|
|
687
|
+
init_bookmarklet();
|
|
688
|
+
init_install_page();
|
|
689
|
+
HTML_ESCAPES = {
|
|
690
|
+
"&": "&",
|
|
691
|
+
"<": "<",
|
|
692
|
+
">": ">",
|
|
693
|
+
'"': """,
|
|
694
|
+
"'": "'"
|
|
695
|
+
};
|
|
696
|
+
TEMPLATES = {
|
|
697
|
+
"bookmarklet.txt": bookmarklet_default,
|
|
698
|
+
"install-page.html": install_page_default
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// src/install/page.ts
|
|
704
|
+
function bookmarkletHref(base) {
|
|
705
|
+
const source = renderTemplate(loadTemplate("bookmarklet.txt"), { base }, escapeJsString);
|
|
706
|
+
return `javascript:${encodeURIComponent(source)}`;
|
|
707
|
+
}
|
|
708
|
+
function renderInstallPage(base, httpPort) {
|
|
709
|
+
return renderTemplate(loadTemplate("install-page.html"), {
|
|
710
|
+
version: VERSION,
|
|
711
|
+
href: bookmarkletHref(base),
|
|
712
|
+
base,
|
|
713
|
+
httpPort
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
var init_page = __esm({
|
|
717
|
+
"src/install/page.ts"() {
|
|
718
|
+
"use strict";
|
|
719
|
+
init_config();
|
|
720
|
+
init_template();
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// src/install/zip.ts
|
|
725
|
+
import { Buffer as Buffer2 } from "node:buffer";
|
|
726
|
+
function crc32(buf) {
|
|
727
|
+
let crc = 4294967295;
|
|
728
|
+
for (const byte of buf) crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
|
|
729
|
+
return (crc ^ 4294967295) >>> 0;
|
|
730
|
+
}
|
|
731
|
+
function buildZip(entries) {
|
|
732
|
+
const chunks = [];
|
|
733
|
+
const central = [];
|
|
734
|
+
let offset = 0;
|
|
735
|
+
for (const { name, data } of entries) {
|
|
736
|
+
const nameBuf = Buffer2.from(name, "utf8");
|
|
737
|
+
const crc = crc32(data);
|
|
738
|
+
const local = Buffer2.alloc(30);
|
|
739
|
+
local.writeUInt32LE(67324752, 0);
|
|
740
|
+
local.writeUInt16LE(20, 4);
|
|
741
|
+
local.writeUInt16LE(2048, 6);
|
|
742
|
+
local.writeUInt16LE(0, 8);
|
|
743
|
+
local.writeUInt16LE(0, 10);
|
|
744
|
+
local.writeUInt16LE(33, 12);
|
|
745
|
+
local.writeUInt32LE(crc, 14);
|
|
746
|
+
local.writeUInt32LE(data.length, 18);
|
|
747
|
+
local.writeUInt32LE(data.length, 22);
|
|
748
|
+
local.writeUInt16LE(nameBuf.length, 26);
|
|
749
|
+
local.writeUInt16LE(0, 28);
|
|
750
|
+
chunks.push(local, nameBuf, data);
|
|
751
|
+
const cen = Buffer2.alloc(46);
|
|
752
|
+
cen.writeUInt32LE(33639248, 0);
|
|
753
|
+
cen.writeUInt16LE(20, 4);
|
|
754
|
+
cen.writeUInt16LE(20, 6);
|
|
755
|
+
cen.writeUInt16LE(2048, 8);
|
|
756
|
+
cen.writeUInt16LE(0, 10);
|
|
757
|
+
cen.writeUInt16LE(0, 12);
|
|
758
|
+
cen.writeUInt16LE(33, 14);
|
|
759
|
+
cen.writeUInt32LE(crc, 16);
|
|
760
|
+
cen.writeUInt32LE(data.length, 20);
|
|
761
|
+
cen.writeUInt32LE(data.length, 24);
|
|
762
|
+
cen.writeUInt16LE(nameBuf.length, 28);
|
|
763
|
+
cen.writeUInt16LE(0, 30);
|
|
764
|
+
cen.writeUInt16LE(0, 32);
|
|
765
|
+
cen.writeUInt16LE(0, 34);
|
|
766
|
+
cen.writeUInt16LE(0, 36);
|
|
767
|
+
cen.writeUInt32LE(0, 38);
|
|
768
|
+
cen.writeUInt32LE(offset, 42);
|
|
769
|
+
central.push(Buffer2.concat([cen, nameBuf]));
|
|
770
|
+
offset += 30 + nameBuf.length + data.length;
|
|
771
|
+
}
|
|
772
|
+
const centralBuf = Buffer2.concat(central);
|
|
773
|
+
const eocd = Buffer2.alloc(22);
|
|
774
|
+
eocd.writeUInt32LE(101010256, 0);
|
|
775
|
+
eocd.writeUInt16LE(entries.length, 8);
|
|
776
|
+
eocd.writeUInt16LE(entries.length, 10);
|
|
777
|
+
eocd.writeUInt32LE(centralBuf.length, 12);
|
|
778
|
+
eocd.writeUInt32LE(offset, 16);
|
|
779
|
+
return Buffer2.concat([...chunks, centralBuf, eocd]);
|
|
780
|
+
}
|
|
781
|
+
var CRC_TABLE;
|
|
782
|
+
var init_zip = __esm({
|
|
783
|
+
"src/install/zip.ts"() {
|
|
784
|
+
"use strict";
|
|
785
|
+
CRC_TABLE = (() => {
|
|
786
|
+
const table = new Uint32Array(256);
|
|
787
|
+
for (let n = 0; n < 256; n++) {
|
|
788
|
+
let c = n;
|
|
789
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
790
|
+
table[n] = c >>> 0;
|
|
791
|
+
}
|
|
792
|
+
return table;
|
|
793
|
+
})();
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
// src/routes/install.ts
|
|
798
|
+
async function handleInstall(req, res, url) {
|
|
799
|
+
if (req.method !== "GET") return false;
|
|
800
|
+
const path = url.pathname;
|
|
801
|
+
if (path === "/") {
|
|
802
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
803
|
+
res.end(renderInstallPage(`http://127.0.0.1:${config.httpPort}/`, config.httpPort));
|
|
804
|
+
return true;
|
|
805
|
+
}
|
|
806
|
+
if (path === "/manifest.json") {
|
|
807
|
+
const buf = await pluginManifest().catch(() => null);
|
|
808
|
+
if (!buf) {
|
|
809
|
+
sendJson(res, 404, { error: NO_DIST });
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
|
|
813
|
+
res.end(buf);
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
816
|
+
if (path === "/code.js" || path === "/ui.html") {
|
|
817
|
+
const buf = await readPluginFile(path.slice(1));
|
|
818
|
+
if (!buf) {
|
|
819
|
+
sendJson(res, 404, { error: PLUGIN_DIR ? "not found" : NO_DIST });
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
822
|
+
res.writeHead(200, {
|
|
823
|
+
"Content-Type": path.endsWith(".js") ? "text/javascript; charset=utf-8" : "text/html; charset=utf-8",
|
|
824
|
+
"Cache-Control": "no-store"
|
|
825
|
+
});
|
|
826
|
+
res.end(buf);
|
|
827
|
+
return true;
|
|
828
|
+
}
|
|
829
|
+
if (path === "/plugin.zip") {
|
|
830
|
+
if (!PLUGIN_DIR) {
|
|
831
|
+
sendJson(res, 404, { error: NO_DIST });
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
try {
|
|
835
|
+
const [manifest, code, ui] = await Promise.all([
|
|
836
|
+
pluginManifest(),
|
|
837
|
+
readPluginFile("code.js"),
|
|
838
|
+
readPluginFile("ui.html")
|
|
839
|
+
]);
|
|
840
|
+
const zip = buildZip([
|
|
841
|
+
{ name: "manifest.json", data: manifest },
|
|
842
|
+
{ name: "code.js", data: code },
|
|
843
|
+
{ name: "ui.html", data: ui }
|
|
844
|
+
]);
|
|
845
|
+
res.writeHead(200, {
|
|
846
|
+
"Content-Type": "application/zip",
|
|
847
|
+
"Content-Disposition": 'attachment; filename="batter-pixso-bridge.zip"'
|
|
848
|
+
});
|
|
849
|
+
res.end(zip);
|
|
850
|
+
} catch {
|
|
851
|
+
sendJson(res, 500, { error: "zip build failed" });
|
|
852
|
+
}
|
|
853
|
+
return true;
|
|
854
|
+
}
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
var NO_DIST;
|
|
858
|
+
var init_install = __esm({
|
|
859
|
+
"src/routes/install.ts"() {
|
|
860
|
+
"use strict";
|
|
861
|
+
init_config();
|
|
862
|
+
init_dist_resolver();
|
|
863
|
+
init_page();
|
|
864
|
+
init_zip();
|
|
865
|
+
init_http_utils();
|
|
866
|
+
NO_DIST = "plugin dist not found\uFF08\u8FD0\u884C npm run build \u751F\u6210\uFF09";
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
// src/guides.ts
|
|
871
|
+
var INTRO, DESIGN_TO_CODE, GUIDES;
|
|
872
|
+
var init_guides = __esm({
|
|
873
|
+
"src/guides.ts"() {
|
|
874
|
+
"use strict";
|
|
875
|
+
INTRO = `# batter-pixso-mcp \u4F7F\u7528\u6307\u5357
|
|
876
|
+
|
|
877
|
+
\u672C\u670D\u52A1\u628A Pixso \u8BBE\u8BA1\u7A3F\u80FD\u529B\u4EE5 MCP \u5DE5\u5177\u66B4\u9732\u7ED9 AI\u3002\u6570\u636E\u94FE\u8DEF\uFF1A
|
|
878
|
+
|
|
879
|
+
\`\`\`
|
|
880
|
+
AI \u5BA2\u6237\u7AEF \u2500\u2500MCP(stdio/HTTP)\u2500\u2500\u25B6 batter-pixso-mcp \u2500\u2500HTTP \u957F\u8F6E\u8BE2\u2500\u2500\u25B6 Pixso \u63D2\u4EF6(\u8BBE\u8BA1\u6587\u4EF6\u5185)
|
|
881
|
+
\`\`\`
|
|
882
|
+
|
|
883
|
+
## \u524D\u63D0
|
|
884
|
+
|
|
885
|
+
1. batter-pixso-mcp \u670D\u52A1\u5DF2\u542F\u52A8\uFF08agent \u7684 MCP \u914D\u7F6E\u4F1A\u81EA\u52A8\u62C9\u8D77\uFF09\u3002
|
|
886
|
+
2. Pixso \u5BA2\u6237\u7AEF\u4E2D\u5DF2\u5BFC\u5165\u5E76\u8FD0\u884C **Batter Pixso Bridge** \u63D2\u4EF6\uFF0C\u63D2\u4EF6 UI \u663E\u793A\u300C\u5DF2\u8FDE\u63A5\u672C\u5730\u670D\u52A1\u300D\u3002
|
|
887
|
+
3. \u76EE\u6807\u8BBE\u8BA1\u6587\u4EF6\u5904\u4E8E\u6253\u5F00\u72B6\u6001\u3002
|
|
888
|
+
|
|
889
|
+
## \u5DE5\u5177\u603B\u89C8
|
|
890
|
+
|
|
891
|
+
| \u5DE5\u5177 | \u7528\u9014 |
|
|
892
|
+
| --- | --- |
|
|
893
|
+
| \`get_selected\` | \u8BFB\u753B\u5E03\u5F53\u524D\u9009\u4E2D\u56FE\u5C42\uFF08guid\u3001\u540D\u79F0\u3001\u7C7B\u578B\u3001\u5C3A\u5BF8\uFF09 |
|
|
894
|
+
| \`get_node_dsl\` | \u751F\u6210\u8282\u70B9\u7684\u5F00\u53D1\u5411\u7CBE\u7B80 DSL\uFF08\u7ED3\u6784 + \u6837\u5F0F + \u8D44\u6E90 URL\uFF09\uFF0C\u8BBE\u8BA1\u8F6C\u4EE3\u7801\u7684\u6838\u5FC3\u5165\u53E3 |
|
|
895
|
+
| \`get_screenshot\` | \u5BFC\u51FA\u8282\u70B9\u622A\u56FE\uFF0C\u8FD4\u56DE\u56FE\u7247 URL\uFF0C\u7528\u4E8E\u89C6\u89C9\u53C2\u7167/\u6821\u9A8C |
|
|
896
|
+
| \`list_sessions\` | \u5217\u51FA\u5168\u90E8\u63D2\u4EF6\u4F1A\u8BDD\uFF08\u591A\u6587\u4EF6\u573A\u666F\u8DEF\u7531\u7528\uFF09 |
|
|
897
|
+
|
|
898
|
+
## \u94FE\u63A5\u5373\u8F93\u5165\uFF08\u9AD8\u9891\u573A\u666F\uFF09
|
|
899
|
+
|
|
900
|
+
\u7528\u6237\u63D0\u4F9B Pixso \u94FE\u63A5\u65F6\uFF08\u5B98\u65B9 \`pixso.cn\` \u6216\u79C1\u6709\u5316\u90E8\u7F72\u5982 \`pixso.ky-tech.com.cn\`\uFF0C\u4EFB\u610F\u57DF\u540D\uFF09\uFF0C
|
|
901
|
+
\u94FE\u63A5\u672C\u8EAB\u5C31\u662F\u4EFB\u52A1\u8F93\u5165\uFF1A\u63D0\u53D6 \`item-id\` \u67E5\u8BE2\u53C2\u6570 \u2192 URL \u89E3\u7801 \u2192 \u5F97\u5230\u8282\u70B9 guid\uFF0C
|
|
902
|
+
\u76F4\u63A5\u8C03 \`get_node_dsl(guid)\` / \`get_screenshot(guid)\`\uFF0C\u65E0\u9700\u5148\u95EE\u7528\u6237\u9009\u4E2D\u4EC0\u4E48\u3002
|
|
903
|
+
|
|
904
|
+
## \u591A\u6587\u4EF6\u573A\u666F
|
|
905
|
+
|
|
906
|
+
\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
|
|
907
|
+
\u5148\u7528 \`list_sessions\` \u786E\u8BA4\u76EE\u6807\u6587\u4EF6\uFF0C\u518D\u628A\u8FD4\u56DE\u7684 \`sessionId\` \u6216 \`fileKey\`
|
|
908
|
+
\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
|
|
909
|
+
|
|
910
|
+
\u8BE6\u7EC6\u5DE5\u4F5C\u6D41\u89C1\u8D44\u6E90 \`batter://guides/design-to-code\`\u3002
|
|
911
|
+
`;
|
|
912
|
+
DESIGN_TO_CODE = `# \u8BBE\u8BA1\u8F6C\u4EE3\u7801\u5DE5\u4F5C\u6D41
|
|
913
|
+
|
|
914
|
+
## \u63A8\u8350\u6B65\u9AA4
|
|
915
|
+
|
|
916
|
+
1. **\u786E\u8BA4\u76EE\u6807\u8282\u70B9**
|
|
917
|
+
- \u7528\u6237\u8BF4"\u8FD9\u4E2A/\u9009\u4E2D\u7684"\uFF1A\u8C03 \`get_selected\` \u62FF guid\u3002
|
|
918
|
+
- \u7528\u6237\u7ED9\u4E86 Pixso \u94FE\u63A5\uFF08\u4EFB\u610F\u57DF\u540D\u90FD\u6709\u6548\u2014\u2014\u5B98\u65B9 \`pixso.cn\` \u6216\u79C1\u6709\u5316\u90E8\u7F72\u5982
|
|
919
|
+
\`https://pixso.ky-tech.com.cn/app/design/<\u6587\u4EF6>?item-id=1%3A2\`\uFF0C\u4E5F\u53EF\u80FD\u662F \`/app/editor/\` \u8DEF\u5F84\uFF09\uFF1A
|
|
920
|
+
\u4ECE \`item-id\` \u67E5\u8BE2\u53C2\u6570\u63D0\u53D6 guid \u5E76 URL \u89E3\u7801\uFF08\`1%3A2\` \u2192 \`"1:2"\`\uFF09\uFF0C\u4F5C\u4E3A \`guid\` \u4F20\u5165\u3002
|
|
921
|
+
\u4E25\u7981\u76F4\u63A5\u4F20\u5B8C\u6574 URL\uFF1B\u4E0D\u8981\u56E0\u57DF\u540D\u4E0D\u8BA4\u8BC6\u800C\u5FFD\u7565\u94FE\u63A5\u3002
|
|
922
|
+
2. **\u53D6\u7ED3\u6784**\uFF1A\`get_node_dsl(guid)\` \u62FF\u7CBE\u7B80 DSL\u3002
|
|
923
|
+
3. **\u53D6\u89C6\u89C9\u53C2\u7167**\uFF1A\`get_screenshot(guid)\` \u62FF\u622A\u56FE URL\uFF0C\u4E0E DSL \u5BF9\u7167\u7406\u89E3\u89C6\u89C9\u5C42\u7EA7\u3002
|
|
924
|
+
4. **\u751F\u6210\u4EE3\u7801**\uFF1A\u6309\u4E0B\u6587 DSL \u8BED\u4E49\u6620\u5C04\u5230\u76EE\u6807\u6846\u67B6\u3002
|
|
925
|
+
5. **\u6821\u9A8C**\uFF1A\u751F\u6210\u540E\u5982\u6709\u504F\u5DEE\uFF0C\u518D\u53D6\u4E00\u6B21\u622A\u56FE\u5BF9\u6BD4\u3002
|
|
926
|
+
|
|
927
|
+
## DSL \u5B57\u6BB5\u8BED\u4E49
|
|
928
|
+
|
|
929
|
+
\`\`\`jsonc
|
|
930
|
+
{
|
|
931
|
+
"id": "297:133032", // \u8282\u70B9 guid
|
|
932
|
+
"name": "\u65B0\u5BA2", // \u56FE\u5C42\u540D
|
|
933
|
+
"type": "FRAME", // Pixso \u8282\u70B9\u7C7B\u578B\uFF1BICON/IMAGE \u4E3A\u584C\u7F29\u53F6\u5B50
|
|
934
|
+
"rect": { "x": 0, "y": 0, "width": 375, "height": 200 }, // \u7EDD\u5BF9\u5B9A\u4F4D
|
|
935
|
+
"layout": { // auto-layout \u2192 flex \u76F4\u8BD1\uFF08\u4EC5\u5BB9\u5668\u6709\uFF09
|
|
936
|
+
"direction": "row", // row | column
|
|
937
|
+
"gap": 12,
|
|
938
|
+
"padding": [16, 16, 16, 16], // \u4E0A\u53F3\u4E0B\u5DE6
|
|
939
|
+
"align": { "primary": "flex-start", "counter": "center" },
|
|
940
|
+
"sizing": { "horizontal": "hug", "vertical": "fixed" } // hug|fixed|fill
|
|
941
|
+
},
|
|
942
|
+
"text": { // \u4EC5\u6587\u672C\u8282\u70B9
|
|
943
|
+
"content": "...", "fontSize": 14, "fontFamily": "PingFang SC",
|
|
944
|
+
"fontWeight": "Medium", "lineHeight": "22px", "align": "left",
|
|
945
|
+
"decoration": "underline", "letterSpacing": "0.5px", "link": "https://...", // \u6309\u9700
|
|
946
|
+
"segments": [ // \u4EC5\u6DF7\u6392\u6587\u672C\uFF1Abase \u4E3A\u6700\u957F\u6BB5\u6837\u5F0F\uFF0C\u6BCF\u6BB5\u53EA\u5E26\u5DEE\u5F02\u5B57\u6BB5
|
|
947
|
+
{ "start": 0, "end": 4, "text": "\u7406\u8D54\u4E2D\u5FC3", "fontWeight": "Bold", "color": "#f5222d" }
|
|
948
|
+
]
|
|
949
|
+
},
|
|
950
|
+
"fills": ["#1a73e8", "linear-gradient(90deg, #fff 0%, #000 100%)"], // CSS \u989C\u8272
|
|
951
|
+
"stroke": "#e5e5e5",
|
|
952
|
+
"radius": 8, // \u6216 [\u4E0A\u5DE6, \u4E0A\u53F3, \u4E0B\u53F3, \u4E0B\u5DE6]
|
|
953
|
+
"shadow": "0px 2px 8px rgba(0,0,0,0.1), inset 0px 1px 0px #fff", // CSS box-shadow\uFF0C\u591A\u9634\u5F71\u9017\u53F7\u62FC\u63A5
|
|
954
|
+
"blur": "4px", // \u56FE\u5C42\u6A21\u7CCA \u2192 CSS filter: blur()
|
|
955
|
+
"backdropBlur": "8px", // \u80CC\u666F\u6A21\u7CCA \u2192 CSS backdrop-filter
|
|
956
|
+
"constraints": { "h": "right", "v": "bottom" }, // \u7EDD\u5BF9\u5B9A\u4F4D\u5BB9\u5668\u5185\u7684\u7EA6\u675F\uFF08h: left|center|right|stretch|scale\uFF09\uFF0C\u9ED8\u8BA4 left/top \u4E0D\u8F93\u51FA
|
|
957
|
+
"opacity": 0.9,
|
|
958
|
+
"image": "http://127.0.0.1:3210/assets/xxx.svg", // \u53F6\u5B50\u8D44\u6E90 URL\uFF08\u89C1\u4E0B\uFF09
|
|
959
|
+
"children": [] // \u9012\u5F52\u7ED3\u6784
|
|
960
|
+
}
|
|
961
|
+
\`\`\`
|
|
962
|
+
|
|
963
|
+
## \u53F6\u5B50\u8282\u70B9\u4E0E\u56FE\u7247\u8D44\u6E90
|
|
964
|
+
|
|
965
|
+
- \`type: "ICON"\`\uFF1A\u7EAF\u77E2\u91CF\u5C0F\u8282\u70B9\uFF08icon\uFF09\uFF0C\`image\` \u4E3A **SVG** URL\u3002
|
|
966
|
+
- \`type: "IMAGE"\`\uFF1A\u56FE\u7247\u586B\u5145\u8282\u70B9\uFF0C\`image\` \u4E3A **PNG** URL\u3002
|
|
967
|
+
- URL \u7531\u672C\u5730\u670D\u52A1\u6258\u7BA1\uFF08immutable \u7F13\u5B58\uFF0C\u670D\u52A1\u505C\u6B62\u5373\u5931\u6548\uFF09\uFF0C**\u4E25\u7981\u76F4\u63A5\u5199\u8FDB\u4EA4\u4ED8\u4EE3\u7801**
|
|
968
|
+
\uFF08\`<img src>\`\u3001CSS \`background-image\` \u7B49\uFF09\u3002\u5FC5\u987B\u5148\u7528
|
|
969
|
+
\`curl -L -o <\u672C\u5730\u8DEF\u5F84> <URL>\`\uFF08Windows \u4E5F\u53EF\u7528 \`Invoke-WebRequest\`\uFF09\u4E0B\u8F7D\u5230\u9879\u76EE\u8D44\u6E90\u76EE\u5F55\uFF0C
|
|
970
|
+
\u518D\u4EE5\u672C\u5730\u76F8\u5BF9\u8DEF\u5F84\u5F15\u7528\u3002\u76F4\u63A5\u5F15\u7528 URL \u4EC5\u9650\u4E34\u65F6\u9884\u89C8\u573A\u666F\u3002
|
|
971
|
+
- \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
|
|
972
|
+
|
|
973
|
+
## \u6CE8\u610F
|
|
974
|
+
|
|
975
|
+
- 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
|
|
976
|
+
- \u591A\u6587\u4EF6\u573A\u666F\u6240\u6709\u5DE5\u5177\u90FD\u652F\u6301 \`target\` \u53C2\u6570\uFF08sessionId/fileKey\uFF09\uFF0C\u89C1 \`batter://guides/intro\`\u3002
|
|
977
|
+
`;
|
|
978
|
+
GUIDES = [
|
|
979
|
+
{
|
|
980
|
+
name: "intro",
|
|
981
|
+
uri: "batter://guides/intro",
|
|
982
|
+
description: "batter-pixso-mcp \u6982\u89C8\uFF1A\u67B6\u6784\u524D\u63D0\u3001\u5DE5\u5177\u6E05\u5355\u3001\u591A\u6587\u4EF6\u4F1A\u8BDD\u8DEF\u7531\u3002\u4F7F\u7528\u524D\u5FC5\u8BFB\u3002",
|
|
983
|
+
text: INTRO
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
name: "design-to-code",
|
|
987
|
+
uri: "batter://guides/design-to-code",
|
|
988
|
+
description: "\u8BBE\u8BA1\u8F6C\u4EE3\u7801\u5DE5\u4F5C\u6D41\uFF1A\u9009\u533A\u786E\u8BA4\u3001URL \u63D0\u53D6 guid\u3001DSL \u5B57\u6BB5\u8BED\u4E49\u3001\u56FE\u7247\u8D44\u6E90\u5904\u7406\u3001\u89C6\u89C9\u6821\u9A8C\u3002",
|
|
989
|
+
text: DESIGN_TO_CODE
|
|
990
|
+
}
|
|
991
|
+
];
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
// src/prompts.ts
|
|
996
|
+
var DESIGN_TO_CODE2, PROMPTS;
|
|
997
|
+
var init_prompts = __esm({
|
|
998
|
+
"src/prompts.ts"() {
|
|
999
|
+
"use strict";
|
|
1000
|
+
DESIGN_TO_CODE2 = {
|
|
1001
|
+
name: "design-to-code",
|
|
1002
|
+
description: "\u4ECE Pixso \u94FE\u63A5\u6216\u5F53\u524D\u9009\u533A\u751F\u6210\u524D\u7AEF\u4EE3\u7801\uFF1A\u53D6 DSL + \u622A\u56FE \u2192 \u751F\u6210 \u2192 \u622A\u56FE\u6821\u9A8C\u3002",
|
|
1003
|
+
arguments: [
|
|
1004
|
+
{
|
|
1005
|
+
name: "url",
|
|
1006
|
+
description: "Pixso \u94FE\u63A5\uFF08\u542B item-id \u67E5\u8BE2\u53C2\u6570\uFF0C\u4EFB\u610F\u57DF\u540D\uFF1A\u5B98\u65B9 pixso.cn \u6216\u79C1\u6709\u5316\u90E8\u7F72\uFF09\u3002\u7559\u7A7A\u5219\u4F7F\u7528\u753B\u5E03\u5F53\u524D\u9009\u4E2D\u56FE\u5C42\u3002",
|
|
1007
|
+
required: false
|
|
1008
|
+
}
|
|
1009
|
+
],
|
|
1010
|
+
render: (args) => {
|
|
1011
|
+
const targetLine = args.url?.trim() ? `\u76EE\u6807\u8282\u70B9\u6765\u81EA\u94FE\u63A5\uFF1A${args.url.trim()}\u3002\u5148\u4ECE\u67E5\u8BE2\u53C2\u6570 item-id \u63D0\u53D6\u8282\u70B9 guid \u5E76 URL \u89E3\u7801\uFF081%3A2 \u2192 "1:2"\uFF09\uFF0C\u4E25\u7981\u628A\u6574\u4E2A URL \u5F53 guid \u4F20\uFF0C\u4E5F\u4E0D\u8981\u56E0\u57DF\u540D\u4E0D\u8BA4\u8BC6\u800C\u5FFD\u7565\u94FE\u63A5\u3002` : `\u76EE\u6807\u8282\u70B9\u4E3A\u5F53\u524D\u9009\u533A\uFF1A\u8C03 get_selected \u62FF\u5230\u9009\u4E2D\u56FE\u5C42\u7684 guid\u3002`;
|
|
1012
|
+
return [
|
|
1013
|
+
{
|
|
1014
|
+
role: "user",
|
|
1015
|
+
content: {
|
|
1016
|
+
type: "text",
|
|
1017
|
+
text: [
|
|
1018
|
+
`\u628A Pixso \u8BBE\u8BA1\u7A3F\u8FD8\u539F\u4E3A\u524D\u7AEF\u4EE3\u7801\uFF0C\u4E25\u683C\u6309\u4EE5\u4E0B\u6D41\u7A0B\uFF1A`,
|
|
1019
|
+
``,
|
|
1020
|
+
`1. ${targetLine}`,
|
|
1021
|
+
`2. \u8C03 get_node_dsl(guid) \u53D6\u7ED3\u6784\u4E0E\u6837\u5F0F DSL\uFF1B\u8C03 get_screenshot(guid) \u53D6\u89C6\u89C9\u53C2\u7167\u622A\u56FE\u3002`,
|
|
1022
|
+
`3. \u52A8\u624B\u5199\u4EE3\u7801\u524D\uFF0C\u5148\u8BFB\u8D44\u6E90 batter://guides/design-to-code \u4E86\u89E3 DSL \u5B57\u6BB5\u8BED\u4E49\u3001\u56FE\u7247\u8D44\u6E90\uFF08SVG/PNG URL\uFF09\u5904\u7406\u4E0E\u591A\u6587\u4EF6\u8DEF\u7531\uFF08target \u53C2\u6570\uFF09\uFF1B\u603B\u89C8\u89C1 batter://guides/intro\u3002`,
|
|
1023
|
+
`4. \u56FE\u7247\u8D44\u6E90\u5904\u7406\uFF08\u5FC5\u987B\u9075\u5B88\uFF09\uFF1ADSL \u4E2D icon/\u56FE\u7247\u8282\u70B9\u7684 http://127.0.0.1 \u8D44\u6E90 URL \u7531\u672C\u5730\u670D\u52A1\u6258\u7BA1\uFF0C\u4E25\u7981\u76F4\u63A5\u5199\u8FDB\u4EA4\u4ED8\u4EE3\u7801\uFF08<img src>\u3001background-image \u7B49\uFF09\u2014\u2014\u670D\u52A1\u4E00\u505C\u94FE\u63A5\u5373\u5931\u6548\u3002\u5FC5\u987B\u5148\u7528 curl -L -o <\u672C\u5730\u8DEF\u5F84> <URL>\uFF08Windows \u4E5F\u53EF\u7528 Invoke-WebRequest\uFF09\u4E0B\u8F7D\u5230\u9879\u76EE\u8D44\u6E90\u76EE\u5F55\uFF0C\u518D\u4EE5\u672C\u5730\u76F8\u5BF9\u8DEF\u5F84\u5F15\u7528\u3002`,
|
|
1024
|
+
`5. \u751F\u6210\u4EE3\u7801\uFF1Aauto-layout \u76F4\u8BD1 flex\uFF0C\u989C\u8272/\u5706\u89D2/\u9634\u5F71\u7528 DSL \u7684 CSS \u503C\uFF0C\u56FE\u7247\u4E00\u5F8B\u7528\u7B2C 4 \u6B65\u4E0B\u8F7D\u540E\u7684\u672C\u5730\u8DEF\u5F84\u3002`,
|
|
1025
|
+
`6. \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\u3002`
|
|
1026
|
+
].join("\n")
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
];
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
PROMPTS = [DESIGN_TO_CODE2];
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
// src/tools/builtin.ts
|
|
1037
|
+
var BUILTIN_TOOLS;
|
|
1038
|
+
var init_builtin = __esm({
|
|
1039
|
+
"src/tools/builtin.ts"() {
|
|
1040
|
+
"use strict";
|
|
1041
|
+
init_sessions();
|
|
1042
|
+
BUILTIN_TOOLS = [
|
|
1043
|
+
{
|
|
1044
|
+
name: "list_sessions",
|
|
1045
|
+
description: "\u5217\u51FA\u5F53\u524D\u8FDE\u63A5\u5230\u672C\u670D\u52A1\u7684\u6240\u6709 Pixso \u63D2\u4EF6\u4F1A\u8BDD\uFF08sessionId\u3001\u6587\u4EF6\u3001\u9875\u9762\u3001\u5728\u7EBF\u72B6\u6001\uFF09\u3002\u6BCF\u4E2A\u8FD0\u884C\u63D2\u4EF6\u7684\u8BBE\u8BA1\u6587\u4EF6\u5BF9\u5E94\u4E00\u4E2A\u4F1A\u8BDD\u3002\u591A\u4E2A\u6587\u4EF6\u540C\u65F6\u6253\u5F00\u65F6\uFF1A\u5148\u7528\u672C\u5DE5\u5177\u786E\u8BA4\u76EE\u6807\u6587\u4EF6\uFF0C\u518D\u628A\u8FD4\u56DE\u7684 sessionId \u6216 fileKey \u4F5C\u4E3A get_selected / get_node_dsl / get_screenshot \u7684 target \u53C2\u6570\u4F20\u5165\uFF1B\u53EA\u6709\u4E00\u4E2A\u4F1A\u8BDD\u65F6\u65E0\u9700\u5173\u6CE8\u672C\u5DE5\u5177\u3002\u591A\u6587\u4EF6\u8DEF\u7531\u8BE6\u89C1\u8D44\u6E90 batter://guides/intro\u3002",
|
|
1046
|
+
inputSchema: { type: "object", properties: {} },
|
|
1047
|
+
handler: async () => {
|
|
1048
|
+
const list = sessions.list();
|
|
1049
|
+
const text = list.length ? JSON.stringify(list, null, 2) : "\u5F53\u524D\u6CA1\u6709\u63D2\u4EF6\u8FDE\u63A5\uFF1A\u8BF7\u5728 Pixso \u5BA2\u6237\u7AEF\u4E2D\u8FD0\u884C Batter Pixso Bridge \u63D2\u4EF6\u3002";
|
|
1050
|
+
return { content: [{ type: "text", text }] };
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
];
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
// src/core/commands.ts
|
|
1058
|
+
var CommandBus, commandBus;
|
|
1059
|
+
var init_commands = __esm({
|
|
1060
|
+
"src/core/commands.ts"() {
|
|
1061
|
+
"use strict";
|
|
1062
|
+
init_config();
|
|
1063
|
+
init_sessions();
|
|
1064
|
+
CommandBus = class {
|
|
1065
|
+
queues = /* @__PURE__ */ new Map();
|
|
1066
|
+
pollWaiters = /* @__PURE__ */ new Map();
|
|
1067
|
+
sseListeners = /* @__PURE__ */ new Map();
|
|
1068
|
+
pendingById = /* @__PURE__ */ new Map();
|
|
1069
|
+
seq = 0;
|
|
1070
|
+
/** AI 侧:执行命令 */
|
|
1071
|
+
execute(type, args, opts = {}) {
|
|
1072
|
+
const session = sessions.resolve(opts.target);
|
|
1073
|
+
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
1074
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
1075
|
+
const command = { id: `cmd-${++this.seq}`, type, args };
|
|
1076
|
+
const entry = {
|
|
1077
|
+
command,
|
|
1078
|
+
sessionId: session.id,
|
|
1079
|
+
resolve,
|
|
1080
|
+
reject,
|
|
1081
|
+
dispatched: false,
|
|
1082
|
+
timer: setTimeout(() => {
|
|
1083
|
+
this.drop(entry);
|
|
1084
|
+
reject(new Error(`\u547D\u4EE4 ${command.id} \u8D85\u65F6\uFF1A\u63D2\u4EF6 ${timeoutMs / 1e3}s \u5185\u672A\u8FD4\u56DE\u7ED3\u679C`));
|
|
1085
|
+
}, timeoutMs)
|
|
1086
|
+
};
|
|
1087
|
+
opts.signal?.addEventListener("abort", () => {
|
|
1088
|
+
if (!this.pendingById.has(command.id)) return;
|
|
1089
|
+
this.drop(entry);
|
|
1090
|
+
reject(new Error(`\u547D\u4EE4 ${command.id} \u5DF2\u88AB\u8C03\u7528\u65B9\u53D6\u6D88`));
|
|
1091
|
+
});
|
|
1092
|
+
this.pendingById.set(command.id, entry);
|
|
1093
|
+
this.queueOf(session.id).push(entry);
|
|
1094
|
+
this.dispatch(session.id);
|
|
1095
|
+
return promise;
|
|
1096
|
+
}
|
|
1097
|
+
/** 插件侧(长轮询兜底通道):取命令,超时返回 null */
|
|
1098
|
+
async take(sessionId, timeoutMs = 25e3) {
|
|
1099
|
+
const entry = this.nextUndispatched(sessionId);
|
|
1100
|
+
if (entry) {
|
|
1101
|
+
entry.dispatched = true;
|
|
1102
|
+
log(`dispatch command ${entry.command.id}: ${entry.command.type} -> session ${sessionId.slice(0, 8)} (poll)`);
|
|
1103
|
+
return entry.command;
|
|
1104
|
+
}
|
|
1105
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
1106
|
+
const waiters = this.waitersOf(sessionId);
|
|
1107
|
+
waiters.push(resolve);
|
|
1108
|
+
setTimeout(() => {
|
|
1109
|
+
const i = waiters.indexOf(resolve);
|
|
1110
|
+
if (i >= 0) {
|
|
1111
|
+
waiters.splice(i, 1);
|
|
1112
|
+
resolve(null);
|
|
1113
|
+
}
|
|
1114
|
+
}, timeoutMs);
|
|
1115
|
+
return promise;
|
|
1116
|
+
}
|
|
1117
|
+
/** 插件侧(SSE 主通道):订阅即时推送,返回退订函数 */
|
|
1118
|
+
subscribe(sessionId, listener) {
|
|
1119
|
+
let subs = this.sseListeners.get(sessionId);
|
|
1120
|
+
if (!subs) {
|
|
1121
|
+
subs = /* @__PURE__ */ new Set();
|
|
1122
|
+
this.sseListeners.set(sessionId, subs);
|
|
1123
|
+
}
|
|
1124
|
+
subs.add(listener);
|
|
1125
|
+
this.dispatch(sessionId);
|
|
1126
|
+
return () => {
|
|
1127
|
+
subs.delete(listener);
|
|
1128
|
+
if (subs.size === 0) this.sseListeners.delete(sessionId);
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
/** 插件侧:回传结果 */
|
|
1132
|
+
complete(id, ok, resultOrError) {
|
|
1133
|
+
const entry = this.pendingById.get(id);
|
|
1134
|
+
if (!entry) return false;
|
|
1135
|
+
this.drop(entry);
|
|
1136
|
+
if (ok) {
|
|
1137
|
+
entry.resolve(resultOrError);
|
|
1138
|
+
} else {
|
|
1139
|
+
entry.reject(resultOrError instanceof Error ? resultOrError : new Error(String(resultOrError)));
|
|
1140
|
+
}
|
|
1141
|
+
return true;
|
|
1142
|
+
}
|
|
1143
|
+
dispatch(sessionId) {
|
|
1144
|
+
const entry = this.nextUndispatched(sessionId);
|
|
1145
|
+
if (!entry) return;
|
|
1146
|
+
const subs = this.sseListeners.get(sessionId);
|
|
1147
|
+
if (subs && subs.size > 0) {
|
|
1148
|
+
entry.dispatched = true;
|
|
1149
|
+
log(`dispatch command ${entry.command.id}: ${entry.command.type} -> session ${sessionId.slice(0, 8)} (sse)`);
|
|
1150
|
+
for (const fn of subs) fn(entry.command);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
const waiter = this.waitersOf(sessionId).shift();
|
|
1154
|
+
if (!waiter) return;
|
|
1155
|
+
entry.dispatched = true;
|
|
1156
|
+
log(`dispatch command ${entry.command.id}: ${entry.command.type} -> session ${sessionId.slice(0, 8)} (poll)`);
|
|
1157
|
+
waiter(entry.command);
|
|
1158
|
+
}
|
|
1159
|
+
nextUndispatched(sessionId) {
|
|
1160
|
+
return this.queueOf(sessionId).find((e) => !e.dispatched);
|
|
1161
|
+
}
|
|
1162
|
+
queueOf(sessionId) {
|
|
1163
|
+
let q = this.queues.get(sessionId);
|
|
1164
|
+
if (!q) {
|
|
1165
|
+
q = [];
|
|
1166
|
+
this.queues.set(sessionId, q);
|
|
1167
|
+
}
|
|
1168
|
+
return q;
|
|
1169
|
+
}
|
|
1170
|
+
waitersOf(sessionId) {
|
|
1171
|
+
let w = this.pollWaiters.get(sessionId);
|
|
1172
|
+
if (!w) {
|
|
1173
|
+
w = [];
|
|
1174
|
+
this.pollWaiters.set(sessionId, w);
|
|
1175
|
+
}
|
|
1176
|
+
return w;
|
|
1177
|
+
}
|
|
1178
|
+
/** 从队列与 pending 表移除(完成/超时/取消共用) */
|
|
1179
|
+
drop(entry) {
|
|
1180
|
+
clearTimeout(entry.timer);
|
|
1181
|
+
this.pendingById.delete(entry.command.id);
|
|
1182
|
+
const q = this.queues.get(entry.sessionId);
|
|
1183
|
+
if (q) this.queues.set(entry.sessionId, q.filter((e) => e !== entry));
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
commandBus = new CommandBus();
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
// src/tools/plugin.ts
|
|
1191
|
+
function jsonResult(data) {
|
|
1192
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1193
|
+
}
|
|
1194
|
+
function errorResult(err) {
|
|
1195
|
+
return {
|
|
1196
|
+
content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
|
|
1197
|
+
isError: true
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
async function run(type, args, signal) {
|
|
1201
|
+
const { target, ...rest } = args;
|
|
1202
|
+
try {
|
|
1203
|
+
return jsonResult(
|
|
1204
|
+
await commandBus.execute(type, rest, { target, signal })
|
|
1205
|
+
);
|
|
1206
|
+
} catch (err) {
|
|
1207
|
+
return errorResult(err);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
var URL_RULE, TARGET_PROP, PLUGIN_TOOLS;
|
|
1211
|
+
var init_plugin = __esm({
|
|
1212
|
+
"src/tools/plugin.ts"() {
|
|
1213
|
+
"use strict";
|
|
1214
|
+
init_commands();
|
|
1215
|
+
URL_RULE = '\u3010\u94FE\u63A5\u5904\u7406\u89C4\u5219\uFF0C\u5FC5\u987B\u9075\u5B88\u3011\u7528\u6237\u63D0\u4F9B\u7684 Pixso \u94FE\u63A5\u53EF\u80FD\u662F\u4EFB\u610F\u57DF\u540D\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\u770B\u5230\u8FD9\u7C7B\u94FE\u63A5\u65F6\uFF1A\u5148\u4ECE\u67E5\u8BE2\u53C2\u6570 item-id \u63D0\u53D6\u8282\u70B9 guid \u5E76 URL \u89E3\u7801\uFF081%3A2 \u2192 "1:2"\uFF09\uFF0C\u628A\u89E3\u7801\u540E\u7684\u503C\u4F5C\u4E3A guid \u53C2\u6570\u4F20\u5165\uFF1B\u4E25\u7981\u628A\u6574\u4E2A URL \u5F53 guid \u4F20\uFF0C\u4E5F\u4E0D\u8981\u56E0\u4E3A\u57DF\u540D\u4E0D\u8BA4\u8BC6\u5C31\u5FFD\u7565\u94FE\u63A5\u3002';
|
|
1216
|
+
TARGET_PROP = {
|
|
1217
|
+
type: "string",
|
|
1218
|
+
description: "\u76EE\u6807\u63D2\u4EF6\u4F1A\u8BDD\uFF08sessionId \u6216 fileKey\uFF09\u3002\u4EC5\u5F53\u591A\u4E2A Pixso \u6587\u4EF6\u540C\u65F6\u8FD0\u884C\u63D2\u4EF6\u65F6\u9700\u8981\u6307\u5B9A\uFF1B\u7F3A\u7701\u8DEF\u7531\u5230\u6700\u8FD1\u6D3B\u8DC3\u7684\u6587\u4EF6\u3002\u53EF\u7528 list_sessions \u67E5\u770B\u5F53\u524D\u4F1A\u8BDD\u3002"
|
|
1219
|
+
};
|
|
1220
|
+
PLUGIN_TOOLS = [
|
|
1221
|
+
{
|
|
1222
|
+
name: "get_selected",
|
|
1223
|
+
description: '\u83B7\u53D6 Pixso \u753B\u5E03\u5F53\u524D\u9009\u4E2D\u7684\u56FE\u5C42\u5217\u8868\uFF08guid\u3001\u540D\u79F0\u3001\u7C7B\u578B\u3001\u5C3A\u5BF8\uFF09\uFF0C\u5B9E\u65F6\u8BFB\u53D6\u3002\u5F53\u7528\u6237\u8BF4"\u8FD9\u4E2A/\u5F53\u524D\u9009\u4E2D\u7684"\u65F6\uFF0C\u5148\u8C03\u7528\u672C\u5DE5\u5177\u786E\u8BA4\u9009\u4E2D\u5BF9\u8C61\uFF0C\u518D\u628A\u8FD4\u56DE\u7684 guid \u4F20\u7ED9 get_node_dsl \u6216 get_screenshot\u3002\u4E0D\u8981\u7528\u5B83\u6765\u9A8C\u8BC1\u591A\u6587\u4EF6\u8DEF\u7531\u2014\u2014\u591A\u6587\u4EF6\u573A\u666F\u7528 list_sessions\u3002\u6CE8\u610F\uFF1A\u5982\u679C\u7528\u6237\u5DF2\u7ECF\u63D0\u4F9B\u4E86 Pixso \u94FE\u63A5\uFF08\u4EFB\u610F\u57DF\u540D\uFF0C\u542B\u79C1\u6709\u5316\u90E8\u7F72\uFF09\uFF0C\u4E0D\u8981\u8C03\u7528\u672C\u5DE5\u5177\u2014\u2014\u76F4\u63A5\u4ECE\u94FE\u63A5\u7684 item-id \u63D0\u53D6 guid \u4F20\u7ED9 get_node_dsl / get_screenshot \u5373\u53EF\u3002',
|
|
1224
|
+
inputSchema: { type: "object", properties: { target: TARGET_PROP } },
|
|
1225
|
+
handler: (args, ctx) => run("get_selected", args, ctx?.signal)
|
|
1226
|
+
},
|
|
1227
|
+
{
|
|
1228
|
+
name: "get_node_dsl",
|
|
1229
|
+
description: "\u751F\u6210\u8282\u70B9\u7684\u5F00\u53D1\u5411\u7CBE\u7B80 DSL\uFF0C\u662F\u8BBE\u8BA1\u8F6C\u4EE3\u7801\u7684\u6838\u5FC3\u5165\u53E3\uFF0C\u4F18\u5148\u7528\u672C\u5DE5\u5177\u7406\u89E3\u8BBE\u8BA1\u7ED3\u6784\u3002\u7528\u6237\u8981\u6C42\u8FD8\u539F/\u5B9E\u73B0\u67D0\u4E2A\u8BBE\u8BA1\u7A3F\u8282\u70B9\u65F6\u9ED8\u8BA4\u4ECE\u672C\u5DE5\u5177\u5F00\u59CB\u3002\u8FD4\u56DE\u5C42\u7EA7\u5316 JSON\uFF1Arect\uFF08\u7EDD\u5BF9\u5B9A\u4F4D\uFF09\u3001layout\uFF08auto-layout\u2192flex \u76F4\u8BD1\uFF1Adirection/gap/padding/align/sizing\uFF09\u3001 text\uFF08\u5B57\u53F7/\u5B57\u65CF/\u5B57\u91CD/\u884C\u9AD8/\u989C\u8272\uFF1B\u6DF7\u6392\u6587\u672C\u5E26 segments \u5206\u6BB5\u6837\u5F0F\uFF09\u3001fills/stroke/radius/shadow/blur\uFF08CSS \u5316\uFF09\u3001 constraints\uFF08\u7EDD\u5BF9\u5B9A\u4F4D\u5BB9\u5668\u5185\u5B50\u8282\u70B9\u7EA6\u675F\u610F\u56FE\uFF09\u3001children \u9012\u5F52\u3002\u53F6\u5B50\u584C\u7F29\u89C4\u5219\uFF1Aicon/\u7EAF\u77E2\u91CF\u5C0F\u8282\u70B9\uFF08\u226464px \u65E0\u6587\u672C\uFF09\u5BFC\u51FA\u4E3A SVG URL\u3001\u56FE\u7247\u586B\u5145\u8282\u70B9\u5BFC\u51FA\u4E3A PNG URL\u3001\u8BBE\u8BA1\u5E08\u6807\u8BB0\u5BFC\u51FA\u7684\u8282\u70B9\u6309\u5176\u8BBE\u7F6E\u5BFC\u51FA\uFF1B\u8FD9\u4E9B URL \u7531\u672C\u5730\u670D\u52A1\u6258\u7BA1\uFF08immutable \u7F13\u5B58\uFF09\uFF0C\u4E25\u7981\u76F4\u63A5\u5199\u8FDB\u4EA4\u4ED8\u4EE3\u7801\u2014\u2014\u5FC5\u987B\u5148\u7528 curl \u7B49\u4E0B\u8F7D\u5230\u9879\u76EE\u8D44\u6E90\u76EE\u5F55\uFF0C\u518D\u4EE5\u672C\u5730\u76F8\u5BF9\u8DEF\u5F84\u5F15\u7528\u3002DSL \u662F\u6709\u635F\u7CBE\u7B80\uFF08\u7701\u7565\u8499\u7248\u3001\u5E03\u5C14\u8FD0\u7B97\u8DEF\u5F84\u7B49\uFF09\uFF0C\u89C6\u89C9\u590D\u6742\u8282\u70B9\u914D\u5408 get_screenshot \u5BF9\u7167\u3002\u672A\u63D0\u4F9B guid \u65F6\u4F7F\u7528\u5F53\u524D\u9009\u4E2D\u56FE\u5C42\u3002" + URL_RULE + "\u5B57\u6BB5\u8BED\u4E49\u8BE6\u89C1\u8D44\u6E90 batter://guides/design-to-code\u3002",
|
|
1230
|
+
inputSchema: {
|
|
1231
|
+
type: "object",
|
|
1232
|
+
properties: {
|
|
1233
|
+
guid: {
|
|
1234
|
+
type: "string",
|
|
1235
|
+
description: '\u8282\u70B9 GUID\uFF0C\u5982 "297:133032"\u3002\u7F3A\u7701\u4F7F\u7528\u5F53\u524D\u9009\u4E2D\u56FE\u5C42\u3002\u7528\u6237\u63D0\u4F9B Pixso \u94FE\u63A5\u65F6\uFF1A\u4ECE item-id \u67E5\u8BE2\u53C2\u6570\u63D0\u53D6\u5E76 URL \u89E3\u7801\uFF081%3A2 \u2192 "1:2"\uFF09\uFF0C\u4E0D\u8981\u4F20\u5B8C\u6574 URL\u3002'
|
|
1236
|
+
},
|
|
1237
|
+
target: TARGET_PROP
|
|
1238
|
+
}
|
|
1239
|
+
},
|
|
1240
|
+
handler: (args, ctx) => run("get_node_dsl", args, ctx?.signal)
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
name: "get_screenshot",
|
|
1244
|
+
description: '\u5BFC\u51FA\u5355\u4E2A\u8282\u70B9\u7684\u622A\u56FE\uFF0C\u8FD4\u56DE\u53EF\u8BBF\u95EE\u7684\u56FE\u7247 URL\uFF0C\u7528\u4E8E\u83B7\u53D6\u8BBE\u8BA1\u7684\u89C6\u89C9\u53C2\u7167\u3002\u6BCF\u6B21\u53EA\u5904\u7406\u4E00\u4E2A\u8282\u70B9\uFF1B\u591A\u8282\u70B9\u573A\u666F\u9010\u4E2A\u8C03\u7528\uFF0C\u4E0D\u8981\u6307\u671B\u6279\u91CF\u3002\u7528\u6237\u8BF4"\u770B\u770B\u8FD9\u4E2A\u8BBE\u8BA1\u957F\u4EC0\u4E48\u6837""\u7ED9\u6211\u622A\u56FE"\u65F6\u7528\u672C\u5DE5\u5177\u3002\u8BBE\u8BA1\u8FD8\u539F\u7C7B\u4EFB\u52A1\u5EFA\u8BAE\u4E0E get_node_dsl \u914D\u5408\uFF1ADSL \u5B9A\u7ED3\u6784\uFF0C\u622A\u56FE\u6821\u9A8C\u89C6\u89C9\u3002\u751F\u6210\u4EE3\u7801\u540E\u53EF\u518D\u6B21\u8C03\u7528\u672C\u5DE5\u5177\uFF0C\u4E0E\u6E32\u67D3\u7ED3\u679C\u5BF9\u6BD4\u68C0\u67E5\u8FD8\u539F\u5EA6\u3002\u672A\u63D0\u4F9B guid \u65F6\u4F7F\u7528\u5F53\u524D\u9009\u4E2D\u56FE\u5C42\u3002' + URL_RULE,
|
|
1245
|
+
inputSchema: {
|
|
1246
|
+
type: "object",
|
|
1247
|
+
properties: {
|
|
1248
|
+
guid: {
|
|
1249
|
+
type: "string",
|
|
1250
|
+
description: '\u8282\u70B9 GUID\uFF0C\u5982 "297:133032"\u3002\u7F3A\u7701\u4F7F\u7528\u5F53\u524D\u9009\u4E2D\u56FE\u5C42\u3002\u7528\u6237\u63D0\u4F9B Pixso \u94FE\u63A5\u65F6\uFF1A\u4ECE item-id \u67E5\u8BE2\u53C2\u6570\u63D0\u53D6\u5E76 URL \u89E3\u7801\uFF081%3A2 \u2192 "1:2"\uFF09\uFF0C\u4E0D\u8981\u4F20\u5B8C\u6574 URL\u3002'
|
|
1251
|
+
},
|
|
1252
|
+
format: {
|
|
1253
|
+
type: "string",
|
|
1254
|
+
enum: ["PNG", "SVG", "JPG", "WEBP"],
|
|
1255
|
+
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"
|
|
1256
|
+
},
|
|
1257
|
+
scale: {
|
|
1258
|
+
type: "number",
|
|
1259
|
+
description: "\u5BFC\u51FA\u500D\u7387\uFF0C\u9ED8\u8BA4 2\u3002"
|
|
1260
|
+
},
|
|
1261
|
+
target: TARGET_PROP
|
|
1262
|
+
}
|
|
1263
|
+
},
|
|
1264
|
+
handler: (args, ctx) => run("get_screenshot", args, ctx?.signal)
|
|
1265
|
+
}
|
|
1266
|
+
];
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
// src/tools/registry.ts
|
|
1271
|
+
function resolveTools() {
|
|
1272
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
1273
|
+
const resolved = [];
|
|
1274
|
+
for (const def of TOOL_REGISTRY) {
|
|
1275
|
+
if (seenNames.has(def.name)) continue;
|
|
1276
|
+
seenNames.add(def.name);
|
|
1277
|
+
resolved.push({
|
|
1278
|
+
def,
|
|
1279
|
+
tool: {
|
|
1280
|
+
name: def.name,
|
|
1281
|
+
description: def.description,
|
|
1282
|
+
inputSchema: def.inputSchema
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
return resolved;
|
|
1287
|
+
}
|
|
1288
|
+
var TOOL_REGISTRY;
|
|
1289
|
+
var init_registry = __esm({
|
|
1290
|
+
"src/tools/registry.ts"() {
|
|
1291
|
+
"use strict";
|
|
1292
|
+
init_builtin();
|
|
1293
|
+
init_plugin();
|
|
1294
|
+
TOOL_REGISTRY = [
|
|
1295
|
+
...PLUGIN_TOOLS,
|
|
1296
|
+
...BUILTIN_TOOLS
|
|
1297
|
+
];
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
// src/mcp-local.ts
|
|
1302
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1303
|
+
import {
|
|
1304
|
+
CallToolRequestSchema,
|
|
1305
|
+
GetPromptRequestSchema,
|
|
1306
|
+
ListPromptsRequestSchema,
|
|
1307
|
+
ListResourceTemplatesRequestSchema,
|
|
1308
|
+
ListToolsRequestSchema
|
|
1309
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
1310
|
+
function createMcpServer() {
|
|
1311
|
+
const server = new McpServer(
|
|
1312
|
+
{ name: "batter-pixso-mcp", version: VERSION },
|
|
1313
|
+
{ capabilities: { tools: {}, resources: {}, prompts: {} } }
|
|
1314
|
+
);
|
|
1315
|
+
for (const guide of GUIDES) {
|
|
1316
|
+
server.registerResource(
|
|
1317
|
+
guide.name,
|
|
1318
|
+
guide.uri,
|
|
1319
|
+
{
|
|
1320
|
+
description: guide.description,
|
|
1321
|
+
mimeType: "text/markdown",
|
|
1322
|
+
// 纯 agent 消费的工作流文档,高优先级提示读取
|
|
1323
|
+
annotations: { audience: ["assistant"], priority: 0.9 }
|
|
1324
|
+
},
|
|
1325
|
+
async (uri) => ({
|
|
1326
|
+
contents: [{ uri: uri.href, mimeType: "text/markdown", text: guide.text }]
|
|
1327
|
+
})
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
server.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
|
|
1331
|
+
resourceTemplates: []
|
|
1332
|
+
}));
|
|
1333
|
+
const tools = resolveTools();
|
|
1334
|
+
const byName = {};
|
|
1335
|
+
for (const t of tools) {
|
|
1336
|
+
byName[t.def.name] = t;
|
|
1337
|
+
}
|
|
1338
|
+
server.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1339
|
+
tools: tools.map((t) => t.tool)
|
|
1340
|
+
}));
|
|
1341
|
+
server.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
1342
|
+
const { name, arguments: args = {} } = request.params;
|
|
1343
|
+
const entry = byName[name];
|
|
1344
|
+
if (!entry) {
|
|
1345
|
+
throw new Error(`unknown tool: ${name}`);
|
|
1346
|
+
}
|
|
1347
|
+
let finalArgs = args;
|
|
1348
|
+
if (entry.def.beforeCall) {
|
|
1349
|
+
finalArgs = entry.def.beforeCall(finalArgs);
|
|
1350
|
+
}
|
|
1351
|
+
let result = await entry.def.handler(finalArgs, { signal: extra.signal });
|
|
1352
|
+
if (entry.def.afterCall) {
|
|
1353
|
+
result = entry.def.afterCall(result);
|
|
1354
|
+
}
|
|
1355
|
+
return result;
|
|
1356
|
+
});
|
|
1357
|
+
server.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
1358
|
+
prompts: PROMPTS.map((p) => ({
|
|
1359
|
+
name: p.name,
|
|
1360
|
+
description: p.description,
|
|
1361
|
+
arguments: p.arguments
|
|
1362
|
+
}))
|
|
1363
|
+
}));
|
|
1364
|
+
server.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
1365
|
+
const entry = PROMPTS.find((p) => p.name === request.params.name);
|
|
1366
|
+
if (!entry) {
|
|
1367
|
+
throw new Error(`unknown prompt: ${request.params.name}`);
|
|
1368
|
+
}
|
|
1369
|
+
const args = request.params.arguments ?? {};
|
|
1370
|
+
return { description: entry.description, messages: entry.render(args) };
|
|
1371
|
+
});
|
|
1372
|
+
return server;
|
|
1373
|
+
}
|
|
1374
|
+
var init_mcp_local = __esm({
|
|
1375
|
+
"src/mcp-local.ts"() {
|
|
1376
|
+
"use strict";
|
|
1377
|
+
init_config();
|
|
1378
|
+
init_guides();
|
|
1379
|
+
init_prompts();
|
|
1380
|
+
init_registry();
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
// src/routes/mcp-http.ts
|
|
1385
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1386
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1387
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
1388
|
+
async function handleMcp(req, res, url) {
|
|
1389
|
+
if (url.pathname !== "/mcp") return false;
|
|
1390
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
1391
|
+
if (req.method === "POST") {
|
|
1392
|
+
const raw = await readBody(req);
|
|
1393
|
+
let body;
|
|
1394
|
+
try {
|
|
1395
|
+
body = raw.length ? JSON.parse(raw.toString("utf8")) : void 0;
|
|
1396
|
+
} catch {
|
|
1397
|
+
sendJson(res, 400, { jsonrpc: "2.0", error: { code: -32700, message: "Parse error" }, id: null });
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1400
|
+
let transport2 = sessionId ? mcpTransports.get(sessionId) : void 0;
|
|
1401
|
+
if (!transport2) {
|
|
1402
|
+
if (!sessionId && isInitializeRequest(body)) {
|
|
1403
|
+
const t = new StreamableHTTPServerTransport({
|
|
1404
|
+
sessionIdGenerator: () => randomUUID2(),
|
|
1405
|
+
onsessioninitialized: (id) => {
|
|
1406
|
+
mcpTransports.set(id, t);
|
|
1407
|
+
log(`MCP HTTP session initialized: ${id.slice(0, 8)} (total ${mcpTransports.size})`);
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
t.onclose = () => {
|
|
1411
|
+
if (t.sessionId) mcpTransports.delete(t.sessionId);
|
|
1412
|
+
};
|
|
1413
|
+
await createMcpServer().connect(t);
|
|
1414
|
+
transport2 = t;
|
|
1415
|
+
} else {
|
|
1416
|
+
sendJson(res, 400, {
|
|
1417
|
+
jsonrpc: "2.0",
|
|
1418
|
+
error: { code: -32e3, message: "Bad Request: missing or unknown MCP session" },
|
|
1419
|
+
id: null
|
|
1420
|
+
});
|
|
1421
|
+
return true;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
await transport2.handleRequest(req, res, body);
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|
|
1427
|
+
const transport = sessionId ? mcpTransports.get(sessionId) : void 0;
|
|
1428
|
+
if (!transport) {
|
|
1429
|
+
sendJson(res, 400, { error: "missing or unknown MCP session" });
|
|
1430
|
+
return true;
|
|
1431
|
+
}
|
|
1432
|
+
await transport.handleRequest(req, res);
|
|
1433
|
+
return true;
|
|
1434
|
+
}
|
|
1435
|
+
var mcpTransports;
|
|
1436
|
+
var init_mcp_http = __esm({
|
|
1437
|
+
"src/routes/mcp-http.ts"() {
|
|
1438
|
+
"use strict";
|
|
1439
|
+
init_config();
|
|
1440
|
+
init_mcp_local();
|
|
1441
|
+
init_http_utils();
|
|
1442
|
+
mcpTransports = /* @__PURE__ */ new Map();
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
// src/routes/plugin-api.ts
|
|
1447
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
1448
|
+
async function handlePluginApi(req, res, url) {
|
|
1449
|
+
const path = url.pathname;
|
|
1450
|
+
const session = url.searchParams.get("session");
|
|
1451
|
+
if (req.method === "POST" && path === "/sessions/register") {
|
|
1452
|
+
try {
|
|
1453
|
+
const buf = await readBody(req);
|
|
1454
|
+
const meta = buf.length ? JSON.parse(buf.toString("utf8")) : {};
|
|
1455
|
+
sendJson(res, 201, sessions.register(meta));
|
|
1456
|
+
} catch {
|
|
1457
|
+
sendJson(res, 400, { error: "invalid json" });
|
|
1458
|
+
}
|
|
1459
|
+
return true;
|
|
1460
|
+
}
|
|
1461
|
+
if (req.method === "POST" && path === "/context") {
|
|
1462
|
+
if (!session) {
|
|
1463
|
+
sendJson(res, 400, { error: "missing session" });
|
|
1464
|
+
return true;
|
|
1465
|
+
}
|
|
1466
|
+
try {
|
|
1467
|
+
const buf = await readBody(req);
|
|
1468
|
+
const meta = buf.length ? JSON.parse(buf.toString("utf8")) : {};
|
|
1469
|
+
if (!sessions.updateContext(session, meta)) {
|
|
1470
|
+
sendJson(res, 404, { error: "unknown_session" });
|
|
1471
|
+
return true;
|
|
1472
|
+
}
|
|
1473
|
+
sendJson(res, 200, { ok: true });
|
|
1474
|
+
} catch {
|
|
1475
|
+
sendJson(res, 400, { error: "invalid json" });
|
|
1476
|
+
}
|
|
1477
|
+
return true;
|
|
1478
|
+
}
|
|
1479
|
+
if (req.method === "GET" && path === "/commands/stream") {
|
|
1480
|
+
if (!session || !sessions.setConnected(session, true)) {
|
|
1481
|
+
sendJson(res, 404, { error: "unknown_session" });
|
|
1482
|
+
return true;
|
|
1483
|
+
}
|
|
1484
|
+
res.writeHead(200, {
|
|
1485
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
1486
|
+
"Cache-Control": "no-store",
|
|
1487
|
+
Connection: "keep-alive"
|
|
1488
|
+
});
|
|
1489
|
+
res.write(": connected\n\n");
|
|
1490
|
+
const unsubscribe = commandBus.subscribe(session, (command) => {
|
|
1491
|
+
res.write(`data: ${JSON.stringify({ command })}
|
|
1492
|
+
|
|
1493
|
+
`);
|
|
1494
|
+
});
|
|
1495
|
+
const heartbeat = setInterval(() => {
|
|
1496
|
+
sessions.touch(session);
|
|
1497
|
+
res.write(": ping\n\n");
|
|
1498
|
+
}, 15e3);
|
|
1499
|
+
req.on("close", () => {
|
|
1500
|
+
clearInterval(heartbeat);
|
|
1501
|
+
unsubscribe();
|
|
1502
|
+
sessions.setConnected(session, false);
|
|
1503
|
+
log(`SSE channel closed: session ${session.slice(0, 8)}`);
|
|
1504
|
+
});
|
|
1505
|
+
return true;
|
|
1506
|
+
}
|
|
1507
|
+
if (req.method === "GET" && path === "/commands/next") {
|
|
1508
|
+
if (!session) {
|
|
1509
|
+
sendJson(res, 400, { error: "missing session" });
|
|
1510
|
+
return true;
|
|
1511
|
+
}
|
|
1512
|
+
if (!sessions.touch(session)) {
|
|
1513
|
+
sendJson(res, 404, { error: "unknown_session" });
|
|
1514
|
+
return true;
|
|
1515
|
+
}
|
|
1516
|
+
try {
|
|
1517
|
+
const command = await commandBus.take(session);
|
|
1518
|
+
if (command === null) {
|
|
1519
|
+
res.writeHead(204);
|
|
1520
|
+
res.end();
|
|
1521
|
+
return true;
|
|
1522
|
+
}
|
|
1523
|
+
sendJson(res, 200, { command });
|
|
1524
|
+
} catch {
|
|
1525
|
+
sendJson(res, 500, { error: "internal error" });
|
|
1526
|
+
}
|
|
1527
|
+
return true;
|
|
1528
|
+
}
|
|
1529
|
+
const resultMatch = path.match(/^\/commands\/([\w-]+)\/result$/);
|
|
1530
|
+
if (req.method === "POST" && resultMatch) {
|
|
1531
|
+
try {
|
|
1532
|
+
const buf = await readBody(req);
|
|
1533
|
+
const payload = JSON.parse(buf.toString("utf8"));
|
|
1534
|
+
const found = commandBus.complete(resultMatch[1], payload.ok, payload.ok ? payload.result : payload.error);
|
|
1535
|
+
sendJson(res, found ? 200 : 404, found ? { ok: true } : { error: "unknown command" });
|
|
1536
|
+
} catch {
|
|
1537
|
+
sendJson(res, 400, { error: "invalid json" });
|
|
1538
|
+
}
|
|
1539
|
+
return true;
|
|
1540
|
+
}
|
|
1541
|
+
if (req.method === "POST" && path === "/assets") {
|
|
1542
|
+
try {
|
|
1543
|
+
const buf = await readBody(req);
|
|
1544
|
+
const saved = await saveAsset(buf, url.searchParams.get("format") ?? "PNG");
|
|
1545
|
+
sendJson(res, 201, {
|
|
1546
|
+
name: saved.name,
|
|
1547
|
+
url: `http://127.0.0.1:${config.httpPort}/assets/${saved.name}`
|
|
1548
|
+
});
|
|
1549
|
+
} catch {
|
|
1550
|
+
sendJson(res, 500, { error: "asset save failed" });
|
|
1551
|
+
}
|
|
1552
|
+
return true;
|
|
1553
|
+
}
|
|
1554
|
+
const assetMatch = path.match(/^\/assets\/([\w.-]+)$/);
|
|
1555
|
+
if (req.method === "GET" && assetMatch) {
|
|
1556
|
+
try {
|
|
1557
|
+
const buf = await readFile2(assetPath(assetMatch[1]));
|
|
1558
|
+
res.writeHead(200, {
|
|
1559
|
+
"Content-Type": assetMime(assetMatch[1]),
|
|
1560
|
+
"Cache-Control": "public, max-age=31536000, immutable"
|
|
1561
|
+
});
|
|
1562
|
+
res.end(buf);
|
|
1563
|
+
} catch {
|
|
1564
|
+
sendJson(res, 404, { error: "not found" });
|
|
1565
|
+
}
|
|
1566
|
+
return true;
|
|
1567
|
+
}
|
|
1568
|
+
return false;
|
|
1569
|
+
}
|
|
1570
|
+
var init_plugin_api = __esm({
|
|
1571
|
+
"src/routes/plugin-api.ts"() {
|
|
1572
|
+
"use strict";
|
|
1573
|
+
init_assets();
|
|
1574
|
+
init_commands();
|
|
1575
|
+
init_sessions();
|
|
1576
|
+
init_config();
|
|
1577
|
+
init_http_utils();
|
|
1578
|
+
}
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
// src/server.ts
|
|
1582
|
+
import { createServer } from "node:http";
|
|
1583
|
+
function getLastActivity() {
|
|
1584
|
+
return lastActivity;
|
|
1585
|
+
}
|
|
1586
|
+
function startHttpServer() {
|
|
1587
|
+
const server = createServer((req, res) => {
|
|
1588
|
+
lastActivity = Date.now();
|
|
1589
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1590
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
1591
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id");
|
|
1592
|
+
res.setHeader("Access-Control-Expose-Headers", "mcp-session-id");
|
|
1593
|
+
if (req.method === "OPTIONS") {
|
|
1594
|
+
res.writeHead(204);
|
|
1595
|
+
res.end();
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
1599
|
+
if (req.method === "GET" && url.pathname === "/health") {
|
|
1600
|
+
const all = sessions.list();
|
|
1601
|
+
sendJson(res, 200, {
|
|
1602
|
+
ok: true,
|
|
1603
|
+
name: "batter-pixso-mcp",
|
|
1604
|
+
pid: process.pid,
|
|
1605
|
+
version: VERSION,
|
|
1606
|
+
sessions: all.length,
|
|
1607
|
+
pluginsOnline: all.filter((s) => s.online).length
|
|
1608
|
+
});
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
(async () => {
|
|
1612
|
+
for (const handler of ROUTES) {
|
|
1613
|
+
if (await handler(req, res, url)) return;
|
|
1614
|
+
}
|
|
1615
|
+
sendJson(res, 404, { error: "not found" });
|
|
1616
|
+
})().catch((err) => {
|
|
1617
|
+
log("route error:", err);
|
|
1618
|
+
if (!res.headersSent) sendJson(res, 500, { error: "internal error" });
|
|
1619
|
+
});
|
|
1620
|
+
});
|
|
1621
|
+
return new Promise((resolve, reject) => {
|
|
1622
|
+
server.once("error", (err) => {
|
|
1623
|
+
if (err.code === "EADDRINUSE") {
|
|
1624
|
+
resolve(null);
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
reject(err);
|
|
1628
|
+
});
|
|
1629
|
+
server.listen(config.httpPort, "127.0.0.1", () => {
|
|
1630
|
+
log(`plugin HTTP + MCP server on http://127.0.0.1:${config.httpPort} (MCP endpoint: /mcp, pid ${process.pid})`);
|
|
1631
|
+
resolve(server);
|
|
1632
|
+
});
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
var lastActivity, ROUTES;
|
|
1636
|
+
var init_server = __esm({
|
|
1637
|
+
"src/server.ts"() {
|
|
1638
|
+
"use strict";
|
|
1639
|
+
init_config();
|
|
1640
|
+
init_sessions();
|
|
1641
|
+
init_http_utils();
|
|
1642
|
+
init_install();
|
|
1643
|
+
init_mcp_http();
|
|
1644
|
+
init_plugin_api();
|
|
1645
|
+
lastActivity = Date.now();
|
|
1646
|
+
ROUTES = [handleInstall, handleMcp, handlePluginApi];
|
|
1647
|
+
}
|
|
1648
|
+
});
|
|
1649
|
+
|
|
1650
|
+
// src/lifecycle/hub-mode.ts
|
|
1651
|
+
async function runHub() {
|
|
1652
|
+
const server = await startHttpServer();
|
|
1653
|
+
if (!server) {
|
|
1654
|
+
log(`\u7AEF\u53E3 ${config.httpPort} \u5DF2\u6709 hub \u5728\u8FD0\u884C\uFF0C\u672C\u8FDB\u7A0B\u9000\u51FA`);
|
|
1655
|
+
process.exit(0);
|
|
1656
|
+
}
|
|
1657
|
+
log(`hub ready (idle TTL ${config.hubIdleMs / 6e4}min)`);
|
|
1658
|
+
const watchdog = setInterval(() => {
|
|
1659
|
+
const idle = Date.now() - getLastActivity();
|
|
1660
|
+
if (idle > config.hubIdleMs) {
|
|
1661
|
+
log(`\u7A7A\u95F2 ${Math.round(idle / 6e4)} \u5206\u949F\uFF0Chub \u81EA\u52A8\u9000\u51FA`);
|
|
1662
|
+
process.exit(0);
|
|
1663
|
+
}
|
|
1664
|
+
void cleanOldAssets(config.assetTtlMs);
|
|
1665
|
+
}, 6e4);
|
|
1666
|
+
watchdog.unref();
|
|
1667
|
+
}
|
|
1668
|
+
var init_hub_mode = __esm({
|
|
1669
|
+
"src/lifecycle/hub-mode.ts"() {
|
|
1670
|
+
"use strict";
|
|
1671
|
+
init_assets();
|
|
1672
|
+
init_config();
|
|
1673
|
+
init_server();
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
// src/lifecycle/hub-process.ts
|
|
1678
|
+
import { spawn } from "node:child_process";
|
|
1679
|
+
import { openSync } from "node:fs";
|
|
1680
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
1681
|
+
import { join as join3 } from "node:path";
|
|
1682
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1683
|
+
function hubBaseUrl() {
|
|
1684
|
+
return `http://127.0.0.1:${config.httpPort}`;
|
|
1685
|
+
}
|
|
1686
|
+
async function probeHub() {
|
|
1687
|
+
try {
|
|
1688
|
+
const res = await fetch(`${hubBaseUrl()}/health`, { signal: AbortSignal.timeout(1500) });
|
|
1689
|
+
if (!res.ok) return "foreign";
|
|
1690
|
+
const body = await res.json();
|
|
1691
|
+
if (body.ok === true && body.name === "batter-pixso-mcp") return "alive";
|
|
1692
|
+
if (body.ok === true) return "legacy";
|
|
1693
|
+
return "foreign";
|
|
1694
|
+
} catch {
|
|
1695
|
+
return "down";
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
function hubStateError(state) {
|
|
1699
|
+
if (state === "legacy") {
|
|
1700
|
+
return new Error(
|
|
1701
|
+
`\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}`
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
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`);
|
|
1705
|
+
}
|
|
1706
|
+
function spawnHub() {
|
|
1707
|
+
const out = openSync(HUB_LOG, "a");
|
|
1708
|
+
const entry = process.argv[1] ?? fileURLToPath2(import.meta.url);
|
|
1709
|
+
const SKIP_FLAGS = /* @__PURE__ */ new Set(["--eval", "-e", "--print", "-p", "--watch", "--watch-preserve-output"]);
|
|
1710
|
+
const execArgv = process.execArgv.filter(
|
|
1711
|
+
(a, i, arr) => !SKIP_FLAGS.has(a) && !(i > 0 && SKIP_FLAGS.has(arr[i - 1]))
|
|
1712
|
+
);
|
|
1713
|
+
const child = spawn(process.execPath, [...execArgv, entry, "--hub"], {
|
|
1714
|
+
detached: true,
|
|
1715
|
+
stdio: ["ignore", out, out],
|
|
1716
|
+
windowsHide: true,
|
|
1717
|
+
env: { ...process.env }
|
|
1718
|
+
});
|
|
1719
|
+
child.unref();
|
|
1720
|
+
log(`hub spawned (pid ${child.pid}), log: ${HUB_LOG}`);
|
|
1721
|
+
}
|
|
1722
|
+
async function waitForHub(timeoutMs = 15e3) {
|
|
1723
|
+
const deadline = Date.now() + timeoutMs;
|
|
1724
|
+
for (; ; ) {
|
|
1725
|
+
const state = await probeHub();
|
|
1726
|
+
if (state === "alive") return;
|
|
1727
|
+
if (state !== "down") throw hubStateError(state);
|
|
1728
|
+
if (Date.now() > deadline) throw new Error(`\u7B49\u5F85 hub \u5C31\u7EEA\u8D85\u65F6\uFF08${timeoutMs / 1e3}s\uFF09`);
|
|
1729
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
var PORT_FIX_HINT, HUB_LOG;
|
|
1733
|
+
var init_hub_process = __esm({
|
|
1734
|
+
"src/lifecycle/hub-process.ts"() {
|
|
1735
|
+
"use strict";
|
|
1736
|
+
init_config();
|
|
1737
|
+
PORT_FIX_HINT = `\u7528 netstat -ano | findstr :${config.httpPort} \u67E5\u5230 PID \u540E taskkill /PID <pid> /F \u7ED3\u675F\uFF0C\u518D\u91CD\u8BD5`;
|
|
1738
|
+
HUB_LOG = join3(tmpdir2(), "batter-pixso-mcp-hub.log");
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1741
|
+
|
|
1742
|
+
// src/stdio-proxy.ts
|
|
1743
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
1744
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
1745
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1746
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1747
|
+
import { ResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
1748
|
+
async function startStdioProxy() {
|
|
1749
|
+
const mcpUrl = `${hubBaseUrl()}/mcp`;
|
|
1750
|
+
let upstream = null;
|
|
1751
|
+
async function connectUpstream() {
|
|
1752
|
+
const client = new Client(
|
|
1753
|
+
{ name: "batter-pixso-mcp-proxy", version: VERSION },
|
|
1754
|
+
{ capabilities: {} }
|
|
1755
|
+
);
|
|
1756
|
+
await client.connect(new StreamableHTTPClientTransport(new URL(mcpUrl)));
|
|
1757
|
+
return client;
|
|
1758
|
+
}
|
|
1759
|
+
async function ensureUpstream() {
|
|
1760
|
+
if (upstream) {
|
|
1761
|
+
try {
|
|
1762
|
+
await upstream.ping();
|
|
1763
|
+
return upstream;
|
|
1764
|
+
} catch {
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
const state = await probeHub();
|
|
1768
|
+
if (state === "down") spawnHub();
|
|
1769
|
+
if (state === "legacy" || state === "foreign") throw hubStateError(state);
|
|
1770
|
+
await waitForHub();
|
|
1771
|
+
upstream = await connectUpstream();
|
|
1772
|
+
log(`proxy connected to ${mcpUrl}`);
|
|
1773
|
+
return upstream;
|
|
1774
|
+
}
|
|
1775
|
+
async function callWithRecovery(fn) {
|
|
1776
|
+
const client = await ensureUpstream();
|
|
1777
|
+
try {
|
|
1778
|
+
return await fn(client);
|
|
1779
|
+
} catch (err) {
|
|
1780
|
+
if (await probeHub() !== "down") throw err;
|
|
1781
|
+
log("hub \u5931\u8054\uFF0C\u91CD\u5EFA\u540E\u91CD\u8BD5\u2026");
|
|
1782
|
+
upstream = null;
|
|
1783
|
+
const rebuilt = await ensureUpstream();
|
|
1784
|
+
return await fn(rebuilt);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
const upstreamClient = await ensureUpstream();
|
|
1788
|
+
const server = new Server(
|
|
1789
|
+
{ name: "batter-pixso-mcp", version: VERSION },
|
|
1790
|
+
{
|
|
1791
|
+
capabilities: upstreamClient.getServerCapabilities() ?? {}
|
|
1792
|
+
}
|
|
1793
|
+
);
|
|
1794
|
+
server.fallbackRequestHandler = (req, extra) => callWithRecovery((c) => c.request(req, ResultSchema, { signal: extra.signal }));
|
|
1795
|
+
server.onclose = () => {
|
|
1796
|
+
log("agent disconnected, proxy exiting");
|
|
1797
|
+
process.exit(0);
|
|
1798
|
+
};
|
|
1799
|
+
await server.connect(new StdioServerTransport());
|
|
1800
|
+
log("proxy ready on stdio");
|
|
1801
|
+
}
|
|
1802
|
+
var init_stdio_proxy = __esm({
|
|
1803
|
+
"src/stdio-proxy.ts"() {
|
|
1804
|
+
"use strict";
|
|
1805
|
+
init_config();
|
|
1806
|
+
init_hub_process();
|
|
1807
|
+
}
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
// src/main.ts
|
|
1811
|
+
var main_exports = {};
|
|
1812
|
+
__export(main_exports, {
|
|
1813
|
+
main: () => main
|
|
1814
|
+
});
|
|
1815
|
+
async function main() {
|
|
1816
|
+
if (process.argv.includes("--hub")) {
|
|
1817
|
+
await runHub();
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
await startStdioProxy();
|
|
1821
|
+
}
|
|
1822
|
+
var init_main = __esm({
|
|
1823
|
+
"src/main.ts"() {
|
|
1824
|
+
"use strict";
|
|
1825
|
+
init_hub_mode();
|
|
1826
|
+
init_stdio_proxy();
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
|
|
1830
|
+
// src/index.ts
|
|
1831
|
+
var major = Number(process.versions.node.split(".")[0]);
|
|
1832
|
+
if (major < 18) {
|
|
1833
|
+
console.error(
|
|
1834
|
+
`[batter-pixso-mcp] \u9700\u8981 Node \u2265 18\uFF08\u5F53\u524D ${process.version}\uFF09\u3002
|
|
1835
|
+
\u7CFB\u7EDF Node \u8FC7\u65E7\u65F6\u53EF\u7528 Volta \u6307\u5B9A\u8FD0\u884C\u65F6\uFF1A
|
|
1836
|
+
MCP \u914D\u7F6E: { "command": "volta", "args": ["run", "--node", "22", "npx", "-y", "batter-pixso-mcp"] }`
|
|
1837
|
+
);
|
|
1838
|
+
process.exit(1);
|
|
1839
|
+
}
|
|
1840
|
+
var { main: main2 } = await Promise.resolve().then(() => (init_main(), main_exports));
|
|
1841
|
+
main2().catch((err) => {
|
|
1842
|
+
console.error("[batter-pixso-mcp] fatal:", err);
|
|
1843
|
+
process.exit(1);
|
|
1844
|
+
});
|