comate-ui-inspector 0.1.5 → 0.1.6-alpha.1
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/bridge/server.mjs +75 -1
- package/cli.mjs +38 -3
- package/enhancements/figma/figma-bridge.js +46 -0
- package/enhancements/figma/figma-exec.js +83 -0
- package/enhancements/figma/figma.js +1 -0
- package/enhancements/figma/pane.html +9 -0
- package/enhancements/figma/pane.js +17 -0
- package/enhancements/figma/plugin.json +43 -0
- package/enhancements/figma/rewrite.js +2 -0
- package/extension/background.js +65 -23
- package/extension/native-host/host.mjs +67 -1
- package/mcp/server.mjs +99 -10
- package/package.json +6 -2
package/bridge/server.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 安全:只绑 127.0.0.1,不暴露到网络;远程访问一律走 `ssh -L` 把端口转发到本地 127.0.0.1。
|
|
11
11
|
*/
|
|
12
12
|
import {createServer} from 'node:http';
|
|
13
|
-
import {readFileSync, writeFileSync, appendFileSync, mkdirSync} from 'node:fs';
|
|
13
|
+
import {readFileSync, writeFileSync, appendFileSync, mkdirSync, readdirSync} from 'node:fs';
|
|
14
14
|
import {dirname, resolve} from 'node:path';
|
|
15
15
|
import {homedir} from 'node:os';
|
|
16
16
|
|
|
@@ -19,6 +19,8 @@ const statusFile = resolve(homedir(), '.ui-inspector/status.json');
|
|
|
19
19
|
// 录制:元信息 + append-only 事件流分离,避免多请求读改写竞态把 active 盖回 true。
|
|
20
20
|
const recordingFile = resolve(homedir(), '.ui-inspector/recording.json');
|
|
21
21
|
const eventsFile = resolve(homedir(), '.ui-inspector/recording-events.ndjson');
|
|
22
|
+
// Agent→浏览器命令队列:每条命令一个文件 commands/<id>.json。
|
|
23
|
+
const cmdDir = resolve(homedir(), '.ui-inspector/commands');
|
|
22
24
|
const PORT = Number(process.env.UI_INSPECTOR_BRIDGE_PORT) || 8917;
|
|
23
25
|
// 默认只绑 127.0.0.1(走 SSH 隧道);直连模式(笔记本能直达开发机内网 IP)时用 0.0.0.0 暴露到网卡。
|
|
24
26
|
const HOST = process.env.UI_INSPECTOR_BRIDGE_HOST || '127.0.0.1';
|
|
@@ -84,6 +86,64 @@ function recStop(endedAt) {
|
|
|
84
86
|
writeMeta(cur);
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
// 命令队列:浏览器轮询取 pending(标记 taken),写回结果。
|
|
90
|
+
function cmdId(id) {
|
|
91
|
+
return String(id || '').replace(/[^\w.-]/g, '');
|
|
92
|
+
}
|
|
93
|
+
function cmdPoll() {
|
|
94
|
+
const out = [];
|
|
95
|
+
let files = [];
|
|
96
|
+
try {
|
|
97
|
+
files = readdirSync(cmdDir);
|
|
98
|
+
} catch {
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
for (const f of files) {
|
|
102
|
+
if (!f.endsWith('.json')) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const fp = resolve(cmdDir, f);
|
|
106
|
+
let c;
|
|
107
|
+
try {
|
|
108
|
+
c = JSON.parse(readFileSync(fp, 'utf8'));
|
|
109
|
+
} catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (c && c.status === 'pending') {
|
|
113
|
+
c.status = 'taken';
|
|
114
|
+
try {
|
|
115
|
+
writeFileSync(fp, JSON.stringify(c), 'utf8');
|
|
116
|
+
} catch {
|
|
117
|
+
// 忽略
|
|
118
|
+
}
|
|
119
|
+
out.push({id: c.id, type: c.type, params: c.params || {}});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
function cmdResult(id, result, error) {
|
|
125
|
+
const fp = resolve(cmdDir, `${cmdId(id)}.json`);
|
|
126
|
+
let c;
|
|
127
|
+
try {
|
|
128
|
+
c = JSON.parse(readFileSync(fp, 'utf8'));
|
|
129
|
+
} catch {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
c.status = error ? 'error' : 'done';
|
|
133
|
+
if (error) {
|
|
134
|
+
c.error = String(error);
|
|
135
|
+
} else {
|
|
136
|
+
c.result = result;
|
|
137
|
+
}
|
|
138
|
+
c.doneAt = Date.now();
|
|
139
|
+
try {
|
|
140
|
+
mkdirSync(cmdDir, {recursive: true});
|
|
141
|
+
writeFileSync(fp, JSON.stringify(c), 'utf8');
|
|
142
|
+
} catch {
|
|
143
|
+
// 忽略
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
87
147
|
function readBody(req, cb) {
|
|
88
148
|
let body = '';
|
|
89
149
|
req.on('data', chunk => (body += chunk));
|
|
@@ -166,6 +226,20 @@ const server = createServer((req, res) => {
|
|
|
166
226
|
});
|
|
167
227
|
return;
|
|
168
228
|
}
|
|
229
|
+
if (req.method === 'GET' && url.pathname === '/cmd/poll') {
|
|
230
|
+
json(res, 200, {ok: true, commands: cmdPoll()});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (req.method === 'POST' && url.pathname === '/cmd/result') {
|
|
234
|
+
readBody(req, (data, err) => {
|
|
235
|
+
if (err) {
|
|
236
|
+
return json(res, 400, {ok: false, error: String(err)});
|
|
237
|
+
}
|
|
238
|
+
cmdResult(data.id, data.result, data.error);
|
|
239
|
+
json(res, 200, {ok: true});
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
169
243
|
json(res, 404, {ok: false, error: 'not found'});
|
|
170
244
|
});
|
|
171
245
|
|
package/cli.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
openSync,
|
|
21
21
|
readdirSync,
|
|
22
22
|
readFileSync,
|
|
23
|
+
statSync,
|
|
23
24
|
writeFileSync,
|
|
24
25
|
rmSync,
|
|
25
26
|
} from 'node:fs';
|
|
@@ -196,7 +197,7 @@ function availableEnhancements() {
|
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
|
|
199
|
-
function addEnhancement(name) {
|
|
200
|
+
async function addEnhancement(name) {
|
|
200
201
|
if (!name) {
|
|
201
202
|
out(`用法:ui-inspector add <名>。可用:${availableEnhancements().join(', ') || '(无)'}`);
|
|
202
203
|
return;
|
|
@@ -215,11 +216,45 @@ function addEnhancement(name) {
|
|
|
215
216
|
return;
|
|
216
217
|
}
|
|
217
218
|
const id = descriptor.id || name;
|
|
218
|
-
|
|
219
|
+
const dst = join(enhanceDstDir, id);
|
|
220
|
+
cpSync(src, dst, {recursive: true});
|
|
221
|
+
// 按需下载大文件(如 figma 引擎):主包不内置以保持精简,首次 add 时拉取。
|
|
222
|
+
// 本地源目录已带该文件(开发/仓库场景)则跳过,离线可用。
|
|
223
|
+
for (const d of descriptor.download || []) {
|
|
224
|
+
const target = join(dst, d.file);
|
|
225
|
+
let haveLocal = false;
|
|
226
|
+
try {
|
|
227
|
+
haveLocal = existsSync(target) && statSync(target).size >= (d.minBytes || 1);
|
|
228
|
+
} catch {
|
|
229
|
+
haveLocal = false;
|
|
230
|
+
}
|
|
231
|
+
if (haveLocal) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
out(`按需下载 ${d.file} …(${d.url})`);
|
|
235
|
+
try {
|
|
236
|
+
const resp = await fetch(d.url);
|
|
237
|
+
if (!resp.ok) {
|
|
238
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
239
|
+
}
|
|
240
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
241
|
+
if (d.minBytes && buf.length < d.minBytes) {
|
|
242
|
+
throw new Error(`文件过小(${buf.length}B),疑似下载失败`);
|
|
243
|
+
}
|
|
244
|
+
mkdirSync(dirname(target), {recursive: true});
|
|
245
|
+
writeFileSync(target, buf);
|
|
246
|
+
out(` 已写入 ${d.file}(${buf.length}B)`);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
rmSync(dst, {recursive: true, force: true});
|
|
249
|
+
out(`下载 ${d.file} 失败:${String(error)}`);
|
|
250
|
+
out(`增强包「${id}」安装已回滚。请检查网络后重试,或手动把该文件放到 ${target}。`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
219
254
|
const list = readRegistry().filter(p => p.id !== id);
|
|
220
255
|
list.push(descriptor);
|
|
221
256
|
writeRegistry(list);
|
|
222
|
-
out(`已安装增强包「${descriptor.name || id}」(${id}) -> ${
|
|
257
|
+
out(`已安装增强包「${descriptor.name || id}」(${id}) -> ${dst}`);
|
|
223
258
|
out('在 chrome://extensions 点「刷新」重新加载扩展后生效:弹窗出现对应「模式」tab。');
|
|
224
259
|
}
|
|
225
260
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Figma 命令中继(ISOLATED world,figma.com):轮询 background 要 Agent 下发的命令,转给 MAIN 的
|
|
3
|
+
* figma-exec 执行;把执行结果回传 background(写回命令队列,供 MCP server 取回)。
|
|
4
|
+
* 只在 figma.com 运行,故命令只在真正能执行的页面被取走。
|
|
5
|
+
*/
|
|
6
|
+
(function () {
|
|
7
|
+
const alive = () => !!(chrome.runtime && chrome.runtime.id);
|
|
8
|
+
|
|
9
|
+
// MAIN 执行完的结果 → background → 命令队列
|
|
10
|
+
window.addEventListener('message', event => {
|
|
11
|
+
if (event.source !== window || !event.data || !event.data.__uiInspectorCmdResult) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (!alive()) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const r = event.data.__uiInspectorCmdResult;
|
|
18
|
+
try {
|
|
19
|
+
chrome.runtime.sendMessage(
|
|
20
|
+
{type: 'cmd-result', id: r.id, result: r.result, error: r.error},
|
|
21
|
+
() => void chrome.runtime.lastError
|
|
22
|
+
);
|
|
23
|
+
} catch {
|
|
24
|
+
// 上下文失效
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const timer = setInterval(() => {
|
|
29
|
+
if (!alive()) {
|
|
30
|
+
clearInterval(timer);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
chrome.runtime.sendMessage({type: 'cmd-poll'}, resp => {
|
|
35
|
+
if (chrome.runtime.lastError || !resp || !resp.ok || !Array.isArray(resp.commands)) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
for (const cmd of resp.commands) {
|
|
39
|
+
window.postMessage({__uiInspectorCmd: cmd}, '*');
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
clearInterval(timer);
|
|
44
|
+
}
|
|
45
|
+
}, 1200);
|
|
46
|
+
})();
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Figma 命令执行器(MAIN world,figma.com):收 figma-bridge 转来的命令,调用页面内已解锁的
|
|
3
|
+
* window.figma / window.F2CConverter 完成导出,再把结果 postMessage 回 figma-bridge。
|
|
4
|
+
* 注意:window.figma / F2CConverter 的具体形态由 rewrite.js + browser.global.js 解锁后提供,
|
|
5
|
+
* 这里按官方扩展观察到的 API 名调用(convertFigmaNodeToHtml / exportAsync),真机可能需微调。
|
|
6
|
+
*/
|
|
7
|
+
(function () {
|
|
8
|
+
if (window.__oreateFigmaExec) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
window.__oreateFigmaExec = true;
|
|
12
|
+
|
|
13
|
+
function reply(id, result, error) {
|
|
14
|
+
window.postMessage({__uiInspectorCmdResult: {id: id, result: result, error: error ? String(error) : undefined}}, '*');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveNode(params) {
|
|
18
|
+
var fig = window.figma;
|
|
19
|
+
if (!fig) {
|
|
20
|
+
throw new Error('window.figma 不可用(请确认已在 figma.com 设计稿页,且扩展已解锁)');
|
|
21
|
+
}
|
|
22
|
+
if (params && params.nodeId && typeof fig.getNodeById === 'function') {
|
|
23
|
+
var n = fig.getNodeById(params.nodeId);
|
|
24
|
+
if (n) {
|
|
25
|
+
return n;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
var sel = fig.currentPage && fig.currentPage.selection;
|
|
29
|
+
if (sel && sel.length) {
|
|
30
|
+
return sel[0];
|
|
31
|
+
}
|
|
32
|
+
throw new Error('未找到目标节点:传入 nodeId 或先在画布选中一个节点');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toBase64(bytes) {
|
|
36
|
+
var bin = '';
|
|
37
|
+
var arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
38
|
+
for (var i = 0; i < arr.length; i++) {
|
|
39
|
+
bin += String.fromCharCode(arr[i]);
|
|
40
|
+
}
|
|
41
|
+
return btoa(bin);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function exec(cmd) {
|
|
45
|
+
var type = cmd.type;
|
|
46
|
+
var params = cmd.params || {};
|
|
47
|
+
if (type === 'figma_export_node_html') {
|
|
48
|
+
if (!window.F2CConverter || typeof window.F2CConverter.convertFigmaNodeToHtml !== 'function') {
|
|
49
|
+
throw new Error('window.F2CConverter.convertFigmaNodeToHtml 不可用');
|
|
50
|
+
}
|
|
51
|
+
var node = resolveNode(params);
|
|
52
|
+
var html = await window.F2CConverter.convertFigmaNodeToHtml(node, params.options || {});
|
|
53
|
+
return {html: typeof html === 'string' ? html : JSON.stringify(html)};
|
|
54
|
+
}
|
|
55
|
+
if (type === 'figma_export_node_image') {
|
|
56
|
+
var n = resolveNode(params);
|
|
57
|
+
if (!n || typeof n.exportAsync !== 'function') {
|
|
58
|
+
throw new Error('该节点不支持 exportAsync 导出');
|
|
59
|
+
}
|
|
60
|
+
var format = (params.format || 'PNG').toUpperCase();
|
|
61
|
+
var bytes = await n.exportAsync({format: format});
|
|
62
|
+
return {base64: toBase64(bytes), format: format};
|
|
63
|
+
}
|
|
64
|
+
throw new Error('未知命令类型:' + type);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
window.addEventListener('message', function (ev) {
|
|
68
|
+
if (ev.source !== window || !ev.data || !ev.data.__uiInspectorCmd) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
var cmd = ev.data.__uiInspectorCmd;
|
|
72
|
+
Promise.resolve()
|
|
73
|
+
.then(function () {
|
|
74
|
+
return exec(cmd);
|
|
75
|
+
})
|
|
76
|
+
.then(function (result) {
|
|
77
|
+
reply(cmd.id, result, null);
|
|
78
|
+
})
|
|
79
|
+
.catch(function (err) {
|
|
80
|
+
reply(cmd.id, null, err);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var figma=(function(){function e(e){return e==null||typeof e==`function`?{main:e}:e}let t=[{markers:[`.appModel.isReadOnly`],replacements:[{pattern:`.appModel.isReadOnly`,replacer:`.appModel.__isReadOnly__`}]},{markers:[`const __html__ = (() => {`],replacements:[{pattern:/([A-Za-z_$][A-Za-z0-9_$]*)=\(0,([A-Za-z_$][A-Za-z0-9_$]*\.[A-Za-z_$][A-Za-z0-9_$]*)\)\(\)\|\|([A-Za-z_$][A-Za-z0-9_$]*);if\(!\1\)/,replacer:`$1=(0,$2)()||$3;if(false)`},{pattern:/if\(\(0,([A-Za-z_$][A-Za-z0-9_$]*\.[A-Za-z_$][A-Za-z0-9_$]*)\)\(\)\)return;([A-Za-z_$][A-Za-z0-9_$]*)&&/,replacer:`if((0,$1)())return;true&&`}]},{markers:[`dispnf.fyufotjpo;00`,`np{.fyufotjpo;00`],replacements:[{pattern:/dispnf\.fyufotjpo;00|np{\.fyufotjpo;00/g,replacer:`FIGMA_PLEASE_STOP`}]},{markers:[`{type:"global",closePluginFunc:`],replacements:[{pattern:/{type:"global",closePluginFunc:[A-Za-z_$][A-Za-z0-9_$]*}/,replacer:`{type:"global",closePluginFunc:()=>{}}`}]},{markers:[`ext_init_wdf`],replacements:[{pattern:`ext_init_wdf`,replacer:`__ext_init_wdf__`}]},{markers:[`let{canRunExtensions:`],replacements:[{pattern:/let\{canRunExtensions:([A-Za-z_$][A-Za-z0-9_$]*),canAccessFullDevMode:([A-Za-z_$][A-Za-z0-9_$]*)\}=([A-Za-z_$][A-Za-z0-9_$]*).openFile;/,replacer:`let{canRunExtensions:$1,canAccessFullDevMode:$2}=$3.openFile;$1=true;`}]}];var n=`[tempad-dev]`,r=e=>{if(!e.length)return[n];let[t,...r]=e;return typeof t==`string`?t.startsWith(n)?e:[`${n} ${t}`,...r]:[n,...e]};let i={log:(...e)=>{console.log(...r(e))},warn:(...e)=>{console.warn(...r(e))},error:(...e)=>{console.error(...r(e))},debug:(...e)=>{}};function a(e,t){return(t.markers||[]).every(t=>e.includes(t))}function o(e,t){let n=e,r=[],o=[];for(let[e,s]of t.entries()){if(!a(n,s))continue;r.push(e);let t=!1;for(let{pattern:e,replacer:r}of s.replacements){let a=n;n=typeof e==`string`?n.replaceAll(e,r):n.replace(e,r),n===a?i.warn(`Replacement had no effect: ${e} -> ${r}`):(t=!0,i.log(`Applied replacement: ${e} -> ${r}`))}t&&o.push(e)}return{content:n,changed:n!==e,matchedGroups:r,rewrittenGroups:o}}var s=e(()=>{async function e(){let e=document.currentScript,n=e?.src;if(!e||!n)return;let r=Object.getOwnPropertyDescriptor(Document.prototype,`currentScript`);function a(t){let n=document.createElement(`script`);n.src=t,n.defer=!0,e.replaceWith(n)}try{let{content:r,changed:a}=o(await(await fetch(n)).text(),t);a&&i.log(`Rewrote script: ${n}`);let s=r.replaceAll(`delete window.figma`,`window.figma = undefined`);Object.defineProperty(document,`currentScript`,{configurable:!0,get(){return e}}),Function(s)()}catch(e){i.error(e),a(`${n}?fallback`)}finally{r?Object.defineProperty(document,`currentScript`,r):delete document.currentScript}}e()}),c={debug:(...e)=>(console.debug,[...e],void 0),log:(...e)=>(console.log,[...e],void 0),warn:(...e)=>(console.warn,[...e],void 0),error:(...e)=>(console.error,[...e],void 0)};return(()=>{let e;try{e=s.main(),e instanceof Promise&&(e=e.catch(e=>{throw c.error(`The unlisted script "figma" crashed on startup!`,e),e}))}catch(e){throw c.error(`The unlisted script "figma" crashed on startup!`,e),e}return e})()})();figma;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<div class="hint">在 <code>www.figma.com</code> 打开设计稿后,本模式会在页面注入 F2C 引擎(解锁 Dev Mode / 插件能力)。</div>
|
|
2
|
+
<div
|
|
3
|
+
id="figma-status"
|
|
4
|
+
class="hint"
|
|
5
|
+
style="margin-top: 6px"
|
|
6
|
+
></div>
|
|
7
|
+
<div class="hint" style="margin-top: 6px">
|
|
8
|
+
需先在上方「设置 → 生效域名」添加 <code>https://www.figma.com</code> 并授权,然后重载扩展。
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Figma 增强包弹窗面板:只做状态提示。真正的注入在 figma.com 页面内由 rewrite.js + browser.global.js
|
|
3
|
+
* (MAIN world,document_start)完成;导出能力(节点→HTML/图片)由后续的命令通道 + MCP 工具提供。
|
|
4
|
+
*/
|
|
5
|
+
(function () {
|
|
6
|
+
const el = document.getElementById('figma-status');
|
|
7
|
+
if (!el) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
chrome.permissions.contains({origins: ['https://www.figma.com/*']}, granted => {
|
|
11
|
+
if (chrome.runtime.lastError) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
el.textContent = granted ? '✓ 已授权 figma.com,打开设计稿即生效' : '✗ 尚未授权 figma.com';
|
|
15
|
+
el.style.color = granted ? '#1a7f37' : '#cf222e';
|
|
16
|
+
});
|
|
17
|
+
})();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "figma",
|
|
3
|
+
"name": "Figma 转代码",
|
|
4
|
+
"scripts": [
|
|
5
|
+
{
|
|
6
|
+
"id": "rewrite",
|
|
7
|
+
"js": ["rewrite.js"],
|
|
8
|
+
"matches": ["https://www.figma.com/*"],
|
|
9
|
+
"world": "MAIN",
|
|
10
|
+
"runAt": "document_start"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"id": "converter",
|
|
14
|
+
"js": ["browser.global.js"],
|
|
15
|
+
"matches": ["https://www.figma.com/*"],
|
|
16
|
+
"world": "MAIN",
|
|
17
|
+
"runAt": "document_start"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "exec",
|
|
21
|
+
"js": ["figma-exec.js"],
|
|
22
|
+
"matches": ["https://www.figma.com/*"],
|
|
23
|
+
"world": "MAIN",
|
|
24
|
+
"runAt": "document_idle"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "bridge",
|
|
28
|
+
"js": ["figma-bridge.js"],
|
|
29
|
+
"matches": ["https://www.figma.com/*"],
|
|
30
|
+
"world": "ISOLATED",
|
|
31
|
+
"runAt": "document_idle"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"pane": "pane.html",
|
|
35
|
+
"paneJs": "pane.js",
|
|
36
|
+
"download": [
|
|
37
|
+
{
|
|
38
|
+
"file": "browser.global.js",
|
|
39
|
+
"url": "https://cdn.jsdelivr.net/npm/comate-ui-inspector-figma@latest/browser.global.js",
|
|
40
|
+
"minBytes": 100000
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var rewrite=function(){"use strict";function j(r){return r}const b={matches:["https://www.figma.com/*"],runAt:"document_start",world:"MAIN",main(){const r="https://ecomfe.github.io/tempad-dev/figma.json",v=[{markers:[".appModel.isReadOnly"],replacements:[{pattern:".appModel.isReadOnly",replacer:".appModel.__isReadOnly__"}]},{markers:['{type:"global",closePluginFunc:'],replacements:[{pattern:/{type:"global",closePluginFunc:[A-Za-z_$][A-Za-z0-9_$]*}/,replacer:'{type:"global",closePluginFunc:()=>{}}'}]},{markers:["let{canRunExtensions:"],replacements:[{pattern:/let\{canRunExtensions:([A-Za-z_$][A-Za-z0-9_$]*),canAccessFullDevMode:([A-Za-z_$][A-Za-z0-9_$]*)\}=([A-Za-z_$][A-Za-z0-9_$]*).openFile;/,replacer:"let{canRunExtensions:$1,canAccessFullDevMode:$2}=$3.openFile;$1=true;"}]}],L=[{id:2,priority:1,action:{type:"redirect",redirect:{extensionPath:"/figma.js"}},condition:{regexFilter:"/webpack-artifacts/assets/(?:figma_app[^.]+|[0-9a-zA-Z-]+)\\.min\\.js(?:\\.br)?$",resourceTypes:["script"]}}];function U(e,t){return(t.markers||[]).every(c=>e.includes(c))}function F(e,t){let n=e;const c=[],o=[];for(const[l,i]of t.entries()){if(!U(n,i))continue;c.push(l);let E=!1;for(const{pattern:f,replacer:A}of i.replacements){const S=n;typeof f=="string"?n=n.replaceAll(f,A):n=n.replace(f,A),n!==S&&(E=!0)}E&&o.push(l)}return{content:n,changed:n!==e,matchedGroups:c,rewrittenGroups:o}}function d(e){var t;try{const n=e.find(c=>c.id===2);return(t=n==null?void 0:n.condition)!=null&&t.regexFilter?new RegExp(n.condition.regexFilter,"i"):null}catch{return null}}async function k(e){try{const t=await fetch(e,{credentials:"omit",cache:"no-cache"});return t.ok?d(await t.json()):null}catch{return null}}let g;Reflect.defineProperty(window,"figma",{set(e){e!==void 0&&(g=e)},get(){return g}});let u=d(L);k(r).then(e=>{e&&(u=e)});const{appendChild:h,insertBefore:m}=Element.prototype,y=new WeakSet,s=new Map;function z(e){return!!u&&u.test(e)}function w(e){return e instanceof HTMLScriptElement&&typeof e.src=="string"&&e.src.length>0}function R(e,t){const n=s.get(e);if(!(!n||n.url!==t)&&(n.ref-=1,n.ref<=0)){try{URL.revokeObjectURL(n.url)}catch{}s.delete(e)}}async function M(e){const t=s.get(e);if(t)return t.ref+=1,{url:t.url,release:()=>R(e,t.url)};const c=await(await fetch(e,{credentials:"include",cache:"force-cache"})).text(),{content:o}=F(c,v),l=new Blob([o],{type:"application/javascript; charset=utf-8"}),i=URL.createObjectURL(l);return s.set(e,{url:i,ref:1}),{url:i,release:()=>R(e,i)}}function p(e,t,n){n?m.call(e,t,n):h.call(e,t)}async function _(e,t,n){if(y.has(t)){p(e,t,n);return}if(y.add(t),!z(t.src)){p(e,t,n);return}try{const{url:c,release:o}=await M(t.src);t.removeAttribute("integrity"),t.addEventListener("load",o,{once:!0}),t.addEventListener("error",o,{once:!0}),t.src=c}catch{}p(e,t,n)}const Z=function(e){return w(e)?(_(this,e,null),e):(h.call(this,e),e)},I=function(e,t){return w(e)?(_(this,e,t),e):(m.call(this,e,t),e)};Element.prototype.appendChild=Z,Element.prototype.insertBefore=I}};function P(){}function a(r,...$){}const x={debug:(...r)=>a(console.debug,...r),log:(...r)=>a(console.log,...r),warn:(...r)=>a(console.warn,...r),error:(...r)=>a(console.error,...r)};return(async()=>{try{return await b.main()}catch(r){throw x.error('The content script "rewrite" crashed on startup!',r),r}})()}();
|
|
2
|
+
rewrite;
|
package/extension/background.js
CHANGED
|
@@ -83,6 +83,18 @@ async function viaHttp(target, message) {
|
|
|
83
83
|
});
|
|
84
84
|
return await r.json();
|
|
85
85
|
}
|
|
86
|
+
if (message.type === 'cmd-poll') {
|
|
87
|
+
const r = await fetch(`${base}/cmd/poll`);
|
|
88
|
+
return await r.json();
|
|
89
|
+
}
|
|
90
|
+
if (message.type === 'cmd-result') {
|
|
91
|
+
const r = await fetch(`${base}/cmd/result`, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: {'content-type': 'application/json'},
|
|
94
|
+
body: JSON.stringify({id: message.id, result: message.result, error: message.error}),
|
|
95
|
+
});
|
|
96
|
+
return await r.json();
|
|
97
|
+
}
|
|
86
98
|
const r = await fetch(`${base}/state`);
|
|
87
99
|
return await r.json();
|
|
88
100
|
} catch (error) {
|
|
@@ -104,7 +116,9 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
|
|
104
116
|
msg.type === 'state' ||
|
|
105
117
|
msg.type === 'rec-start' ||
|
|
106
118
|
msg.type === 'rec-event' ||
|
|
107
|
-
msg.type === 'rec-stop'
|
|
119
|
+
msg.type === 'rec-stop' ||
|
|
120
|
+
msg.type === 'cmd-poll' ||
|
|
121
|
+
msg.type === 'cmd-result'
|
|
108
122
|
) {
|
|
109
123
|
dispatch(msg).then(sendResponse);
|
|
110
124
|
return true; // 异步 sendResponse
|
|
@@ -137,48 +151,76 @@ async function readEnhancements() {
|
|
|
137
151
|
async function buildScripts() {
|
|
138
152
|
const plugins = await readEnhancements();
|
|
139
153
|
const mainJs = [...BASE_MAIN_JS];
|
|
154
|
+
const extra = []; // 插件专用脚本:各自 matches/world/runAt(如 figma 的 figma.com + document_start + MAIN)
|
|
140
155
|
for (const p of plugins) {
|
|
141
156
|
for (const file of p.main || []) {
|
|
142
157
|
mainJs.push(`enhancements/${p.id}/${file}`);
|
|
143
158
|
}
|
|
159
|
+
for (const s of p.scripts || []) {
|
|
160
|
+
extra.push({
|
|
161
|
+
id: `enh-${p.id}-${s.id}`,
|
|
162
|
+
js: (s.js || []).map(f => `enhancements/${p.id}/${f}`),
|
|
163
|
+
matches: s.matches || [],
|
|
164
|
+
world: s.world || 'ISOLATED',
|
|
165
|
+
runAt: s.runAt || 'document_idle',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
144
168
|
}
|
|
145
|
-
|
|
169
|
+
const base = [
|
|
170
|
+
{id: 'ui-inspector-main', js: mainJs, world: 'MAIN', runAt: 'document_idle'},
|
|
171
|
+
{id: BRIDGE_SCRIPT.id, js: BRIDGE_SCRIPT.js, world: 'ISOLATED', runAt: 'document_idle'},
|
|
172
|
+
];
|
|
173
|
+
return {base, extra};
|
|
146
174
|
}
|
|
147
175
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
const allowed = [];
|
|
153
|
-
for (const match of domains || []) {
|
|
176
|
+
// 过滤出已授予主机权限的 match(未授权的等弹窗 request 后再同步)。
|
|
177
|
+
async function filterGranted(matches) {
|
|
178
|
+
const out = [];
|
|
179
|
+
for (const match of matches || []) {
|
|
154
180
|
try {
|
|
155
181
|
if (await chrome.permissions.contains({origins: [match]})) {
|
|
156
|
-
|
|
182
|
+
out.push(match);
|
|
157
183
|
}
|
|
158
184
|
} catch {
|
|
159
185
|
// 非法 match 模式,跳过
|
|
160
186
|
}
|
|
161
187
|
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function syncScripts() {
|
|
192
|
+
const {domains} = await getConfig();
|
|
193
|
+
const {base, extra} = await buildScripts();
|
|
194
|
+
const grantedPopup = await filterGranted(domains);
|
|
195
|
+
|
|
196
|
+
const regs = [];
|
|
197
|
+
// 基础脚本(选取 + 录制 main + bridge)→ 弹窗配置的生效域名
|
|
198
|
+
if (grantedPopup.length) {
|
|
199
|
+
for (const s of base) {
|
|
200
|
+
regs.push({id: s.id, js: s.js, matches: grantedPopup, world: s.world, runAt: s.runAt});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// 插件专用脚本 → 各自声明的 matches(按授权过滤)
|
|
204
|
+
for (const s of extra) {
|
|
205
|
+
const m = await filterGranted(s.matches);
|
|
206
|
+
if (m.length) {
|
|
207
|
+
regs.push({id: s.id, js: s.js, matches: m, world: s.world, runAt: s.runAt});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 先清掉我们注册过的所有脚本(前缀 ui-inspector- / enh-),再按最新结果注册。
|
|
162
212
|
try {
|
|
163
|
-
const existing = await chrome.scripting.getRegisteredContentScripts(
|
|
164
|
-
|
|
165
|
-
|
|
213
|
+
const existing = await chrome.scripting.getRegisteredContentScripts();
|
|
214
|
+
const ours = existing.filter(s => /^(ui-inspector-|enh-)/.test(s.id)).map(s => s.id);
|
|
215
|
+
if (ours.length) {
|
|
216
|
+
await chrome.scripting.unregisterContentScripts({ids: ours});
|
|
166
217
|
}
|
|
167
218
|
} catch {
|
|
168
219
|
// 没注册过,忽略
|
|
169
220
|
}
|
|
170
|
-
if (
|
|
171
|
-
|
|
221
|
+
if (regs.length) {
|
|
222
|
+
await chrome.scripting.registerContentScripts(regs);
|
|
172
223
|
}
|
|
173
|
-
await chrome.scripting.registerContentScripts(
|
|
174
|
-
scripts.map(s => ({
|
|
175
|
-
id: s.id,
|
|
176
|
-
js: s.js,
|
|
177
|
-
matches: allowed,
|
|
178
|
-
world: s.world,
|
|
179
|
-
runAt: 'document_idle',
|
|
180
|
-
}))
|
|
181
|
-
);
|
|
182
224
|
}
|
|
183
225
|
|
|
184
226
|
async function ensureDefaults() {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* 数据文件与 MCP server / hook 共用:统一在用户主目录 ~/.ui-inspector/pending.json(与仓库位置无关)。
|
|
13
13
|
*/
|
|
14
|
-
import {readFileSync, writeFileSync, appendFileSync, mkdirSync} from 'node:fs';
|
|
14
|
+
import {readFileSync, writeFileSync, appendFileSync, mkdirSync, readdirSync} from 'node:fs';
|
|
15
15
|
import {dirname, resolve} from 'node:path';
|
|
16
16
|
import {homedir} from 'node:os';
|
|
17
17
|
|
|
@@ -22,6 +22,9 @@ const statusFile = resolve(homedir(), '.ui-inspector/status.json');
|
|
|
22
22
|
// 分开是为了避免「每条事件读改写整份 JSON」的多进程竞态把 active 盖回 true——事件只追加,不碰 active。
|
|
23
23
|
const recordingFile = resolve(homedir(), '.ui-inspector/recording.json');
|
|
24
24
|
const eventsFile = resolve(homedir(), '.ui-inspector/recording-events.ndjson');
|
|
25
|
+
// Agent→浏览器命令队列:每条命令一个文件 commands/<id>.json(避免单文件读改写竞态)。
|
|
26
|
+
// MCP server 写入 pending;浏览器轮询取走执行;host 写回 done/error 结果。
|
|
27
|
+
const cmdDir = resolve(homedir(), '.ui-inspector/commands');
|
|
25
28
|
|
|
26
29
|
function readStore() {
|
|
27
30
|
try {
|
|
@@ -85,6 +88,64 @@ function recStop(endedAt) {
|
|
|
85
88
|
writeMeta(cur); // 只有 start/stop 动 active,事件追加不碰它 → stop 后 active 不会被盖回
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
// 命令队列:浏览器轮询取 pending(取走即标记 taken,避免重复执行);host 写回结果。
|
|
92
|
+
function cmdId(id) {
|
|
93
|
+
return String(id || '').replace(/[^\w.-]/g, ''); // 防路径穿越
|
|
94
|
+
}
|
|
95
|
+
function cmdPoll() {
|
|
96
|
+
const out = [];
|
|
97
|
+
let files = [];
|
|
98
|
+
try {
|
|
99
|
+
files = readdirSync(cmdDir);
|
|
100
|
+
} catch {
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
for (const f of files) {
|
|
104
|
+
if (!f.endsWith('.json')) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const fp = resolve(cmdDir, f);
|
|
108
|
+
let c;
|
|
109
|
+
try {
|
|
110
|
+
c = JSON.parse(readFileSync(fp, 'utf8'));
|
|
111
|
+
} catch {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (c && c.status === 'pending') {
|
|
115
|
+
c.status = 'taken';
|
|
116
|
+
try {
|
|
117
|
+
writeFileSync(fp, JSON.stringify(c), 'utf8');
|
|
118
|
+
} catch {
|
|
119
|
+
// 忽略
|
|
120
|
+
}
|
|
121
|
+
out.push({id: c.id, type: c.type, params: c.params || {}});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
function cmdResult(id, result, error) {
|
|
127
|
+
const fp = resolve(cmdDir, `${cmdId(id)}.json`);
|
|
128
|
+
let c;
|
|
129
|
+
try {
|
|
130
|
+
c = JSON.parse(readFileSync(fp, 'utf8'));
|
|
131
|
+
} catch {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
c.status = error ? 'error' : 'done';
|
|
135
|
+
if (error) {
|
|
136
|
+
c.error = String(error);
|
|
137
|
+
} else {
|
|
138
|
+
c.result = result;
|
|
139
|
+
}
|
|
140
|
+
c.doneAt = Date.now();
|
|
141
|
+
try {
|
|
142
|
+
mkdirSync(cmdDir, {recursive: true});
|
|
143
|
+
writeFileSync(fp, JSON.stringify(c), 'utf8');
|
|
144
|
+
} catch {
|
|
145
|
+
// 忽略
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
88
149
|
function send(obj) {
|
|
89
150
|
const body = Buffer.from(JSON.stringify(obj), 'utf8');
|
|
90
151
|
const header = Buffer.alloc(4);
|
|
@@ -110,6 +171,11 @@ function handle(msg) {
|
|
|
110
171
|
} else if (msg && msg.type === 'rec-stop') {
|
|
111
172
|
recStop(msg.endedAt);
|
|
112
173
|
resp = {ok: true};
|
|
174
|
+
} else if (msg && msg.type === 'cmd-poll') {
|
|
175
|
+
resp = {ok: true, commands: cmdPoll()};
|
|
176
|
+
} else if (msg && msg.type === 'cmd-result') {
|
|
177
|
+
cmdResult(msg.id, msg.result, msg.error);
|
|
178
|
+
resp = {ok: true};
|
|
113
179
|
} else {
|
|
114
180
|
resp = {ok: false, error: 'unknown message'};
|
|
115
181
|
}
|
package/mcp/server.mjs
CHANGED
|
@@ -16,6 +16,8 @@ import {createInterface} from 'node:readline';
|
|
|
16
16
|
|
|
17
17
|
const storeFile = resolve(homedir(), '.ui-inspector/pending.json');
|
|
18
18
|
const statusFile = resolve(homedir(), '.ui-inspector/status.json');
|
|
19
|
+
// Agent→浏览器命令队列(每条一个文件)。MCP 写 pending,浏览器 figma-bridge 取走执行并写回结果。
|
|
20
|
+
const cmdDir = resolve(homedir(), '.ui-inspector/commands');
|
|
19
21
|
|
|
20
22
|
function readStore() {
|
|
21
23
|
try {
|
|
@@ -41,6 +43,41 @@ function writeStatus(message, level) {
|
|
|
41
43
|
return status;
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
// 下发一条命令到队列,返回 id。
|
|
47
|
+
function enqueueCmd(type, params) {
|
|
48
|
+
const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
49
|
+
mkdirSync(cmdDir, {recursive: true});
|
|
50
|
+
writeFileSync(resolve(cmdDir, `${id}.json`), JSON.stringify({id, type, params: params || {}, status: 'pending', ts: Date.now()}), 'utf8');
|
|
51
|
+
return id;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 轮询命令结果,直到 done/error 或超时。浏览器需在 figma.com 页打开才会有执行者。
|
|
55
|
+
function waitCmd(id, timeoutMs = 30000) {
|
|
56
|
+
const fp = resolve(cmdDir, `${id}.json`);
|
|
57
|
+
const deadline = Date.now() + timeoutMs;
|
|
58
|
+
return new Promise((res, rej) => {
|
|
59
|
+
const tick = () => {
|
|
60
|
+
let c = null;
|
|
61
|
+
try {
|
|
62
|
+
c = JSON.parse(readFileSync(fp, 'utf8'));
|
|
63
|
+
} catch {
|
|
64
|
+
c = null;
|
|
65
|
+
}
|
|
66
|
+
if (c && c.status === 'done') {
|
|
67
|
+
return res(c.result);
|
|
68
|
+
}
|
|
69
|
+
if (c && c.status === 'error') {
|
|
70
|
+
return rej(new Error(c.error || 'command failed'));
|
|
71
|
+
}
|
|
72
|
+
if (Date.now() > deadline) {
|
|
73
|
+
return rej(new Error('命令超时(确认已在 figma.com 设计稿页打开,且扩展已授权 figma.com 并重载)'));
|
|
74
|
+
}
|
|
75
|
+
setTimeout(tick, 400);
|
|
76
|
+
};
|
|
77
|
+
tick();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
44
81
|
const TOOLS = [
|
|
45
82
|
{
|
|
46
83
|
name: 'list_pending_edits',
|
|
@@ -69,6 +106,32 @@ const TOOLS = [
|
|
|
69
106
|
required: ['message'],
|
|
70
107
|
},
|
|
71
108
|
},
|
|
109
|
+
{
|
|
110
|
+
name: 'figma_export_node_html',
|
|
111
|
+
description:
|
|
112
|
+
'把 figma.com 当前设计稿里的某个节点导出为 HTML(F2C),写入本地文件。需浏览器已在 figma.com 设计稿页、扩展安装 figma 增强包并授权 figma.com。',
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
nodeId: {type: 'string', description: '目标节点 id;省略则用画布当前选中节点'},
|
|
117
|
+
outputPath: {type: 'string', description: '导出 HTML 写入的本地绝对路径'},
|
|
118
|
+
},
|
|
119
|
+
required: ['outputPath'],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'figma_export_node_image',
|
|
124
|
+
description: '把 figma.com 当前设计稿里的某个节点导出为图片(PNG/JPG/SVG),写入本地文件。前置条件同上。',
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: {
|
|
128
|
+
nodeId: {type: 'string', description: '目标节点 id;省略则用画布当前选中节点'},
|
|
129
|
+
outputPath: {type: 'string', description: '导出图片写入的本地绝对路径'},
|
|
130
|
+
format: {type: 'string', enum: ['PNG', 'JPG', 'SVG'], description: '图片格式,默认 PNG'},
|
|
131
|
+
},
|
|
132
|
+
required: ['outputPath'],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
72
135
|
];
|
|
73
136
|
|
|
74
137
|
function callTool(name, args) {
|
|
@@ -85,6 +148,32 @@ function callTool(name, args) {
|
|
|
85
148
|
if (name === 'notify_browser') {
|
|
86
149
|
return writeStatus(args?.message, args?.level);
|
|
87
150
|
}
|
|
151
|
+
if (name === 'figma_export_node_html') {
|
|
152
|
+
const outputPath = args?.outputPath;
|
|
153
|
+
if (!outputPath) {
|
|
154
|
+
throw new Error('outputPath 必填');
|
|
155
|
+
}
|
|
156
|
+
const id = enqueueCmd('figma_export_node_html', {nodeId: args?.nodeId});
|
|
157
|
+
return waitCmd(id).then(result => {
|
|
158
|
+
const html = (result && result.html) || '';
|
|
159
|
+
mkdirSync(dirname(outputPath), {recursive: true});
|
|
160
|
+
writeFileSync(outputPath, html, 'utf8');
|
|
161
|
+
return {ok: true, path: outputPath, bytes: Buffer.byteLength(html)};
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (name === 'figma_export_node_image') {
|
|
165
|
+
const outputPath = args?.outputPath;
|
|
166
|
+
if (!outputPath) {
|
|
167
|
+
throw new Error('outputPath 必填');
|
|
168
|
+
}
|
|
169
|
+
const id = enqueueCmd('figma_export_node_image', {nodeId: args?.nodeId, format: args?.format});
|
|
170
|
+
return waitCmd(id).then(result => {
|
|
171
|
+
const base64 = (result && result.base64) || '';
|
|
172
|
+
mkdirSync(dirname(outputPath), {recursive: true});
|
|
173
|
+
writeFileSync(outputPath, Buffer.from(base64, 'base64'));
|
|
174
|
+
return {ok: true, path: outputPath, format: (result && result.format) || 'PNG'};
|
|
175
|
+
});
|
|
176
|
+
}
|
|
88
177
|
throw new Error('unknown tool: ' + name);
|
|
89
178
|
}
|
|
90
179
|
|
|
@@ -113,16 +202,16 @@ function handle(msg) {
|
|
|
113
202
|
return;
|
|
114
203
|
}
|
|
115
204
|
if (msg.method === 'tools/call') {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
205
|
+
Promise.resolve()
|
|
206
|
+
.then(() => callTool(msg.params?.name, msg.params?.arguments))
|
|
207
|
+
.then(result =>
|
|
208
|
+
send({
|
|
209
|
+
jsonrpc: '2.0',
|
|
210
|
+
id: msg.id,
|
|
211
|
+
result: {content: [{type: 'text', text: JSON.stringify(result, null, 4)}]},
|
|
212
|
+
})
|
|
213
|
+
)
|
|
214
|
+
.catch(error => send({jsonrpc: '2.0', id: msg.id, error: {code: -32000, message: String(error)}}));
|
|
126
215
|
return;
|
|
127
216
|
}
|
|
128
217
|
if (msg.id !== undefined) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "comate-ui-inspector",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6-alpha.1",
|
|
4
4
|
"description": "按住 Alt 点选页面元素,把「元素 + Vue 组件源码路径」交给 Comate 定位源码修改。浏览器扩展 + Native Messaging host + MCP + hook。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,7 +14,11 @@
|
|
|
14
14
|
"mcp",
|
|
15
15
|
"bridge",
|
|
16
16
|
"hooks",
|
|
17
|
-
"README.md"
|
|
17
|
+
"README.md",
|
|
18
|
+
"!extension/enhancements",
|
|
19
|
+
"!extension/enhancements.json",
|
|
20
|
+
"!extension/native-host/host-wrapper.sh",
|
|
21
|
+
"!enhancements/figma/browser.global.js"
|
|
18
22
|
],
|
|
19
23
|
"engines": {
|
|
20
24
|
"node": ">=18"
|