comate-ui-inspector 0.1.5 → 0.1.6-alpha.2

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 CHANGED
@@ -3,11 +3,7 @@
3
3
  * ui-inspector HTTP bridge(零依赖)——远程开发用。
4
4
  *
5
5
  * 跑在「Comate agent / hook / MCP 所在的那台机器」(通常是远程开发机),读写和它们同一份
6
- * ~/.ui-inspector/*.json。本地浏览器扩展(transport=http)经 SSH 端口转发访问本服务:
7
- * POST /ingest {targets:[...]} → 覆盖写 pending.json,回 {ok,pending}
8
- * GET /state → 回 {ok,count,status}
9
- *
10
- * 安全:只绑 127.0.0.1,不暴露到网络;远程访问一律走 `ssh -L` 把端口转发到本地 127.0.0.1。
6
+ * ~/.ui-inspector/*.json。浏览器扩展(transport=http)直连本服务。
11
7
  */
12
8
  import {createServer} from 'node:http';
13
9
  import {readFileSync, writeFileSync, appendFileSync, mkdirSync} from 'node:fs';
@@ -19,8 +15,9 @@ const statusFile = resolve(homedir(), '.ui-inspector/status.json');
19
15
  // 录制:元信息 + append-only 事件流分离,避免多请求读改写竞态把 active 盖回 true。
20
16
  const recordingFile = resolve(homedir(), '.ui-inspector/recording.json');
21
17
  const eventsFile = resolve(homedir(), '.ui-inspector/recording-events.ndjson');
18
+ // figma「带入对话」采集的节点结构。
19
+ const figmaPendingFile = resolve(homedir(), '.ui-inspector/figma-pending.json');
22
20
  const PORT = Number(process.env.UI_INSPECTOR_BRIDGE_PORT) || 8917;
23
- // 默认只绑 127.0.0.1(走 SSH 隧道);直连模式(笔记本能直达开发机内网 IP)时用 0.0.0.0 暴露到网卡。
24
21
  const HOST = process.env.UI_INSPECTOR_BRIDGE_HOST || '127.0.0.1';
25
22
 
26
23
  function readStore() {
@@ -45,7 +42,9 @@ function writeStore(list) {
45
42
  writeFileSync(storeFile, `${JSON.stringify(list, null, 4)}\n`, 'utf8');
46
43
  }
47
44
 
48
- // 录制累积:元信息与事件流分离,避免多请求读改写竞态。
45
+ // PLACEHOLDER_HELPERS
46
+
47
+ // 录制累积:元信息与事件流分离。
49
48
  function readMeta() {
50
49
  try {
51
50
  return JSON.parse(readFileSync(recordingFile, 'utf8'));
@@ -60,7 +59,7 @@ function writeMeta(meta) {
60
59
  function recStart(startedAt, pageUrl) {
61
60
  const cur = readMeta();
62
61
  if (cur && cur.active && cur.startedAt === startedAt) {
63
- return; // 同一会话重复 start,忽略
62
+ return;
64
63
  }
65
64
  writeMeta({active: true, startedAt, pageUrl});
66
65
  mkdirSync(dirname(eventsFile), {recursive: true});
@@ -84,6 +83,26 @@ function recStop(endedAt) {
84
83
  writeMeta(cur);
85
84
  }
86
85
 
86
+ // figma 节点采集:追加到 figma-pending.json(上限 10 个,防爆)。
87
+ function figmaCapture(node) {
88
+ if (!node) {
89
+ return;
90
+ }
91
+ let list = [];
92
+ try {
93
+ const p = JSON.parse(readFileSync(figmaPendingFile, 'utf8'));
94
+ list = Array.isArray(p) ? p : [];
95
+ } catch {
96
+ list = [];
97
+ }
98
+ list.push({node, ts: Date.now()});
99
+ if (list.length > 10) {
100
+ list = list.slice(-10);
101
+ }
102
+ mkdirSync(dirname(figmaPendingFile), {recursive: true});
103
+ writeFileSync(figmaPendingFile, `${JSON.stringify(list, null, 4)}\n`, 'utf8');
104
+ }
105
+
87
106
  function readBody(req, cb) {
88
107
  let body = '';
89
108
  req.on('data', chunk => (body += chunk));
@@ -117,18 +136,15 @@ const server = createServer((req, res) => {
117
136
  }
118
137
  const url = new URL(req.url, 'http://127.0.0.1');
119
138
  if (req.method === 'POST' && url.pathname === '/ingest') {
120
- let body = '';
121
- req.on('data', chunk => (body += chunk));
122
- req.on('end', () => {
123
- try {
124
- const {targets} = JSON.parse(body || '{}');
125
- const now = new Date().toISOString();
126
- const list = (Array.isArray(targets) ? targets : []).map(target => ({target, receivedAt: now}));
127
- writeStore(list);
128
- json(res, 200, {ok: true, pending: list.length});
129
- } catch (error) {
130
- json(res, 400, {ok: false, error: String(error)});
139
+ readBody(req, (data, err) => {
140
+ if (err) {
141
+ return json(res, 400, {ok: false, error: String(err)});
131
142
  }
143
+ const targets = Array.isArray(data.targets) ? data.targets : [];
144
+ const now = new Date().toISOString();
145
+ const list = targets.map(target => ({target, receivedAt: now}));
146
+ writeStore(list);
147
+ json(res, 200, {ok: true, pending: list.length});
132
148
  });
133
149
  return;
134
150
  }
@@ -166,6 +182,16 @@ const server = createServer((req, res) => {
166
182
  });
167
183
  return;
168
184
  }
185
+ if (req.method === 'POST' && url.pathname === '/figma/capture') {
186
+ readBody(req, (data, err) => {
187
+ if (err) {
188
+ return json(res, 400, {ok: false, error: String(err)});
189
+ }
190
+ figmaCapture(data.node);
191
+ json(res, 200, {ok: true});
192
+ });
193
+ return;
194
+ }
169
195
  json(res, 404, {ok: false, error: 'not found'});
170
196
  });
171
197
 
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
- cpSync(src, join(enhanceDstDir, id), {recursive: true});
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}) -> ${join(enhanceDstDir, id)}`);
257
+ out(`已安装增强包「${descriptor.name || id}」(${id}) -> ${dst}`);
223
258
  out('在 chrome://extensions 点「刷新」重新加载扩展后生效:弹窗出现对应「模式」tab。');
224
259
  }
225
260
 
@@ -0,0 +1,49 @@
1
+ /*
2
+ * Figma 命令执行器(MAIN world,figma.com):响应「带入对话」——把当前选中节点转成 F2C HTML 结构,
3
+ * 回传给 figma-ui 落盘待 hook 注入。页面内已解锁的 window.figma / window.F2CConverter 由
4
+ * rewrite.js + browser.global.js 提供。
5
+ */
6
+ (function () {
7
+ if (window.__oreateFigmaExec) {
8
+ return;
9
+ }
10
+ window.__oreateFigmaExec = true;
11
+
12
+ function resolveNode() {
13
+ var fig = window.figma;
14
+ if (!fig) {
15
+ throw new Error('window.figma 不可用(请确认在 figma.com 设计稿页,且扩展已解锁)');
16
+ }
17
+ var sel = fig.currentPage && fig.currentPage.selection;
18
+ if (sel && sel.length) {
19
+ return sel[0];
20
+ }
21
+ throw new Error('未选中节点:请先在画布选中一个节点');
22
+ }
23
+
24
+ window.addEventListener('message', function (ev) {
25
+ if (ev.source !== window || !ev.data || ev.data.__uiInspectorFigmaCapture !== true) {
26
+ return;
27
+ }
28
+ Promise.resolve()
29
+ .then(async function () {
30
+ if (!window.F2CConverter || typeof window.F2CConverter.convertFigmaNodeToHtml !== 'function') {
31
+ throw new Error('F2CConverter 不可用');
32
+ }
33
+ var node = resolveNode();
34
+ var html = await window.F2CConverter.convertFigmaNodeToHtml(node, {});
35
+ return {
36
+ nodeId: node.id,
37
+ name: node.name,
38
+ type: node.type,
39
+ html: typeof html === 'string' ? html : JSON.stringify(html),
40
+ };
41
+ })
42
+ .then(function (node) {
43
+ window.postMessage({__uiInspectorFigmaCaptured: {node: node}}, '*');
44
+ })
45
+ .catch(function (err) {
46
+ window.postMessage({__uiInspectorFigmaCaptured: {error: String(err)}}, '*');
47
+ });
48
+ });
49
+ })();
@@ -0,0 +1,74 @@
1
+ /*
2
+ * Figma「带入对话」浮层(ISOLATED world,figma.com):页面右下角一个按钮,点一下把当前选中节点
3
+ * 的 F2C HTML 结构采集下来,经 background 落盘 figma-pending.json;下次在 Comate 发消息时,
4
+ * hook 会把该结构注入对话,Agent 据此生成代码——用户无需手动调工具、无需填路径。
5
+ */
6
+ (function () {
7
+ if (window.__oreateFigmaUi) {
8
+ return;
9
+ }
10
+ window.__oreateFigmaUi = true;
11
+ const alive = () => !!(chrome.runtime && chrome.runtime.id);
12
+
13
+ const btn = document.createElement('button');
14
+ btn.textContent = '带入 Comate 生成代码';
15
+ btn.style.cssText =
16
+ 'position:fixed;right:16px;bottom:16px;z-index:2147483647;border:none;border-radius:999px;' +
17
+ 'padding:8px 14px;background:#0d99ff;color:#fff;font:12px/1.4 sans-serif;cursor:pointer;' +
18
+ 'box-shadow:0 2px 12px rgba(0,0,0,0.25);';
19
+
20
+ let tip = null;
21
+ function toast(text, ok) {
22
+ if (!tip) {
23
+ tip = document.createElement('div');
24
+ tip.style.cssText =
25
+ 'position:fixed;right:16px;bottom:56px;z-index:2147483647;max-width:280px;padding:6px 10px;' +
26
+ 'border-radius:8px;background:#111;color:#fff;font:12px/1.4 sans-serif;box-shadow:0 2px 12px rgba(0,0,0,0.25);';
27
+ document.documentElement.appendChild(tip);
28
+ }
29
+ tip.textContent = text;
30
+ tip.style.background = ok === false ? '#dc2626' : ok ? '#16a34a' : '#111';
31
+ tip.style.display = 'block';
32
+ clearTimeout(toast._t);
33
+ toast._t = setTimeout(() => (tip.style.display = 'none'), 3000);
34
+ }
35
+
36
+ btn.onclick = () => {
37
+ toast('采集选中节点…');
38
+ window.postMessage({__uiInspectorFigmaCapture: true}, '*');
39
+ };
40
+
41
+ window.addEventListener('message', event => {
42
+ if (event.source !== window || !event.data || !event.data.__uiInspectorFigmaCaptured) {
43
+ return;
44
+ }
45
+ const payload = event.data.__uiInspectorFigmaCaptured;
46
+ if (payload.error) {
47
+ toast('采集失败:' + payload.error, false);
48
+ return;
49
+ }
50
+ if (!alive()) {
51
+ return;
52
+ }
53
+ try {
54
+ chrome.runtime.sendMessage({type: 'figma-capture', node: payload.node}, resp => {
55
+ if (chrome.runtime.lastError || !resp || !resp.ok) {
56
+ toast('落盘失败,请重试', false);
57
+ return;
58
+ }
59
+ toast(`已带入「${payload.node.name || payload.node.nodeId}」,去 Comate 发消息即可生成代码`, true);
60
+ });
61
+ } catch {
62
+ toast('扩展上下文失效,请刷新页面', false);
63
+ }
64
+ });
65
+
66
+ function mount() {
67
+ if (document.body && !btn.isConnected) {
68
+ document.body.appendChild(btn);
69
+ }
70
+ }
71
+ mount();
72
+ document.addEventListener('DOMContentLoaded', mount);
73
+ setInterval(mount, 2000); // figma 频繁重渲染,兜底保证按钮在
74
+ })();
@@ -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,18 @@
1
+ <div class="hint">
2
+ 在
3
+ <code>www.figma.com</code>
4
+ 打开设计稿后,本模式会在页面注入 F2C 引擎(解锁 Dev Mode / 插件能力)。
5
+ </div>
6
+ <div
7
+ id="figma-status"
8
+ class="hint"
9
+ style="margin-top: 6px"
10
+ ></div>
11
+ <div
12
+ class="hint"
13
+ style="margin-top: 6px"
14
+ >
15
+ 需先在上方「设置 → 生效域名」添加
16
+ <code>https://www.figma.com</code>
17
+ 并授权,然后重载扩展。
18
+ </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": "ui",
28
+ "js": ["figma-ui.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;
@@ -83,6 +83,14 @@ async function viaHttp(target, message) {
83
83
  });
84
84
  return await r.json();
85
85
  }
86
+ if (message.type === 'figma-capture') {
87
+ const r = await fetch(`${base}/figma/capture`, {
88
+ method: 'POST',
89
+ headers: {'content-type': 'application/json'},
90
+ body: JSON.stringify({node: message.node}),
91
+ });
92
+ return await r.json();
93
+ }
86
94
  const r = await fetch(`${base}/state`);
87
95
  return await r.json();
88
96
  } catch (error) {
@@ -104,7 +112,8 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
104
112
  msg.type === 'state' ||
105
113
  msg.type === 'rec-start' ||
106
114
  msg.type === 'rec-event' ||
107
- msg.type === 'rec-stop'
115
+ msg.type === 'rec-stop' ||
116
+ msg.type === 'figma-capture'
108
117
  ) {
109
118
  dispatch(msg).then(sendResponse);
110
119
  return true; // 异步 sendResponse
@@ -137,48 +146,76 @@ async function readEnhancements() {
137
146
  async function buildScripts() {
138
147
  const plugins = await readEnhancements();
139
148
  const mainJs = [...BASE_MAIN_JS];
149
+ const extra = []; // 插件专用脚本:各自 matches/world/runAt(如 figma 的 figma.com + document_start + MAIN)
140
150
  for (const p of plugins) {
141
151
  for (const file of p.main || []) {
142
152
  mainJs.push(`enhancements/${p.id}/${file}`);
143
153
  }
154
+ for (const s of p.scripts || []) {
155
+ extra.push({
156
+ id: `enh-${p.id}-${s.id}`,
157
+ js: (s.js || []).map(f => `enhancements/${p.id}/${f}`),
158
+ matches: s.matches || [],
159
+ world: s.world || 'ISOLATED',
160
+ runAt: s.runAt || 'document_idle',
161
+ });
162
+ }
144
163
  }
145
- return [{id: 'ui-inspector-main', js: mainJs, world: 'MAIN'}, BRIDGE_SCRIPT];
164
+ const base = [
165
+ {id: 'ui-inspector-main', js: mainJs, world: 'MAIN', runAt: 'document_idle'},
166
+ {id: BRIDGE_SCRIPT.id, js: BRIDGE_SCRIPT.js, world: 'ISOLATED', runAt: 'document_idle'},
167
+ ];
168
+ return {base, extra};
146
169
  }
147
170
 
148
- async function syncScripts() {
149
- const {domains} = await getConfig();
150
- const scripts = await buildScripts();
151
- // 只注册已授予主机权限的域名;未授权的等弹窗 request 到权限后再同步。
152
- const allowed = [];
153
- for (const match of domains || []) {
171
+ // 过滤出已授予主机权限的 match(未授权的等弹窗 request 后再同步)。
172
+ async function filterGranted(matches) {
173
+ const out = [];
174
+ for (const match of matches || []) {
154
175
  try {
155
176
  if (await chrome.permissions.contains({origins: [match]})) {
156
- allowed.push(match);
177
+ out.push(match);
157
178
  }
158
179
  } catch {
159
180
  // 非法 match 模式,跳过
160
181
  }
161
182
  }
183
+ return out;
184
+ }
185
+
186
+ async function syncScripts() {
187
+ const {domains} = await getConfig();
188
+ const {base, extra} = await buildScripts();
189
+ const grantedPopup = await filterGranted(domains);
190
+
191
+ const regs = [];
192
+ // 基础脚本(选取 + 录制 main + bridge)→ 弹窗配置的生效域名
193
+ if (grantedPopup.length) {
194
+ for (const s of base) {
195
+ regs.push({id: s.id, js: s.js, matches: grantedPopup, world: s.world, runAt: s.runAt});
196
+ }
197
+ }
198
+ // 插件专用脚本 → 各自声明的 matches(按授权过滤)
199
+ for (const s of extra) {
200
+ const m = await filterGranted(s.matches);
201
+ if (m.length) {
202
+ regs.push({id: s.id, js: s.js, matches: m, world: s.world, runAt: s.runAt});
203
+ }
204
+ }
205
+
206
+ // 先清掉我们注册过的所有脚本(前缀 ui-inspector- / enh-),再按最新结果注册。
162
207
  try {
163
- const existing = await chrome.scripting.getRegisteredContentScripts({ids: scripts.map(s => s.id)});
164
- if (existing.length) {
165
- await chrome.scripting.unregisterContentScripts({ids: existing.map(s => s.id)});
208
+ const existing = await chrome.scripting.getRegisteredContentScripts();
209
+ const ours = existing.filter(s => /^(ui-inspector-|enh-)/.test(s.id)).map(s => s.id);
210
+ if (ours.length) {
211
+ await chrome.scripting.unregisterContentScripts({ids: ours});
166
212
  }
167
213
  } catch {
168
214
  // 没注册过,忽略
169
215
  }
170
- if (!allowed.length) {
171
- return;
216
+ if (regs.length) {
217
+ await chrome.scripting.registerContentScripts(regs);
172
218
  }
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
219
  }
183
220
 
184
221
  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,8 @@ 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
+ // figma「带入对话」采集的节点结构,等 hook 注入下一条消息。
26
+ const figmaPendingFile = resolve(homedir(), '.ui-inspector/figma-pending.json');
25
27
 
26
28
  function readStore() {
27
29
  try {
@@ -85,6 +87,26 @@ function recStop(endedAt) {
85
87
  writeMeta(cur); // 只有 start/stop 动 active,事件追加不碰它 → stop 后 active 不会被盖回
86
88
  }
87
89
 
90
+ // figma 节点采集:追加到 figma-pending.json(上限 10 个,防爆)。
91
+ function figmaCapture(node) {
92
+ if (!node) {
93
+ return;
94
+ }
95
+ let list = [];
96
+ try {
97
+ const p = JSON.parse(readFileSync(figmaPendingFile, 'utf8'));
98
+ list = Array.isArray(p) ? p : [];
99
+ } catch {
100
+ list = [];
101
+ }
102
+ list.push({node, ts: Date.now()});
103
+ if (list.length > 10) {
104
+ list = list.slice(-10);
105
+ }
106
+ mkdirSync(dirname(figmaPendingFile), {recursive: true});
107
+ writeFileSync(figmaPendingFile, `${JSON.stringify(list, null, 4)}\n`, 'utf8');
108
+ }
109
+
88
110
  function send(obj) {
89
111
  const body = Buffer.from(JSON.stringify(obj), 'utf8');
90
112
  const header = Buffer.alloc(4);
@@ -110,6 +132,9 @@ function handle(msg) {
110
132
  } else if (msg && msg.type === 'rec-stop') {
111
133
  recStop(msg.endedAt);
112
134
  resp = {ok: true};
135
+ } else if (msg && msg.type === 'figma-capture') {
136
+ figmaCapture(msg.node);
137
+ resp = {ok: true};
113
138
  } else {
114
139
  resp = {ok: false, error: 'unknown message'};
115
140
  }
@@ -19,6 +19,8 @@ const storeFile = resolve(homedir(), '.ui-inspector/pending.json');
19
19
  // 录制:元信息(active/startedAt/pageUrl/endedAt)+ append-only 事件流(每行一条 {kind,item})。
20
20
  const recordingFile = resolve(homedir(), '.ui-inspector/recording.json');
21
21
  const eventsFile = resolve(homedir(), '.ui-inspector/recording-events.ndjson');
22
+ // figma「带入对话」采集的节点结构(页面按钮触发落盘),注入后让 Agent 据结构生成代码。
23
+ const figmaPendingFile = resolve(homedir(), '.ui-inspector/figma-pending.json');
22
24
 
23
25
  function emit(output) {
24
26
  process.stdout.write(JSON.stringify(output));
@@ -67,8 +69,11 @@ const meta = readJson(recordingFile, null);
67
69
  const recDone = meta && meta.active === false;
68
70
  const recEvents = recDone ? readEvents() : [];
69
71
  const hasRec = recDone && recEvents.length > 0;
72
+ const figmaParsed = readJson(figmaPendingFile, []);
73
+ const figmaNodes = Array.isArray(figmaParsed) ? figmaParsed : [];
74
+ const hasFigma = figmaNodes.length > 0;
70
75
 
71
- if (!list.length && !hasRec) {
76
+ if (!list.length && !hasRec && !hasFigma) {
72
77
  emit({decision: 'allow'});
73
78
  }
74
79
 
@@ -135,6 +140,23 @@ function describeRecording(meta, evs) {
135
140
  return parts.join('\n');
136
141
  }
137
142
 
143
+ // figma「带入对话」节点:把每个节点的 F2C HTML 结构铺开,供 Agent 据此生成代码。
144
+ function describeFigma(nodes) {
145
+ const parts = [
146
+ `【ui-inspector Figma】用户在 Figma 选中并带入了 ${nodes.length} 个节点,请据下面的结构(F2C 导出的 HTML)生成对应前端代码:`,
147
+ ];
148
+ nodes.forEach((entry, i) => {
149
+ const n = (entry && entry.node) || {};
150
+ const html = String(n.html || '').slice(0, 20000);
151
+ parts.push(`—— 节点 ${i + 1}:${n.name || ''}(id ${n.nodeId || '?'},${n.type || ''})——`);
152
+ parts.push(html || '(无 HTML)');
153
+ if ((n.html || '').length > 20000) {
154
+ parts.push('…(结构过长已截断)');
155
+ }
156
+ });
157
+ return parts.join('\n');
158
+ }
159
+
138
160
  const sections = [];
139
161
  if (list.length) {
140
162
  const total = list.length;
@@ -148,11 +170,14 @@ if (list.length) {
148
170
  if (hasRec) {
149
171
  sections.push(describeRecording(meta, recEvents));
150
172
  }
173
+ if (hasFigma) {
174
+ sections.push(describeFigma(figmaNodes));
175
+ }
151
176
  sections.push(
152
177
  '请据此定位源码并按用户意见修改。页面选中态已自动取消;改动过程中可调用 MCP 工具 notify_browser 向页面推送状态(如「修改中…」「已完成」),会在浏览器弹 toast。'
153
178
  );
154
179
 
155
- // 「拿到即消费」:注入后清空 pending;录制仅在确实注入(已停止)时清空元信息与事件流,进行中的保留。
180
+ // 「拿到即消费」:注入后清空 pending / figma;录制仅在确实注入(已停止)时清空,进行中的保留。
156
181
  try {
157
182
  mkdirSync(dirname(storeFile), {recursive: true});
158
183
  writeFileSync(storeFile, '[]\n', 'utf8');
@@ -160,6 +185,9 @@ try {
160
185
  writeFileSync(recordingFile, 'null\n', 'utf8');
161
186
  writeFileSync(eventsFile, '', 'utf8');
162
187
  }
188
+ if (hasFigma) {
189
+ writeFileSync(figmaPendingFile, '[]\n', 'utf8');
190
+ }
163
191
  } catch {
164
192
  // 清空失败不影响注入
165
193
  }
package/mcp/server.mjs CHANGED
@@ -113,16 +113,16 @@ function handle(msg) {
113
113
  return;
114
114
  }
115
115
  if (msg.method === 'tools/call') {
116
- try {
117
- const result = callTool(msg.params?.name, msg.params?.arguments);
118
- send({
119
- jsonrpc: '2.0',
120
- id: msg.id,
121
- result: {content: [{type: 'text', text: JSON.stringify(result, null, 4)}]},
122
- });
123
- } catch (error) {
124
- send({jsonrpc: '2.0', id: msg.id, error: {code: -32000, message: String(error)}});
125
- }
116
+ Promise.resolve()
117
+ .then(() => callTool(msg.params?.name, msg.params?.arguments))
118
+ .then(result =>
119
+ send({
120
+ jsonrpc: '2.0',
121
+ id: msg.id,
122
+ result: {content: [{type: 'text', text: JSON.stringify(result, null, 4)}]},
123
+ })
124
+ )
125
+ .catch(error => send({jsonrpc: '2.0', id: msg.id, error: {code: -32000, message: String(error)}}));
126
126
  return;
127
127
  }
128
128
  if (msg.id !== undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comate-ui-inspector",
3
- "version": "0.1.5",
3
+ "version": "0.1.6-alpha.2",
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"