comate-ui-inspector 0.1.6-alpha.1 → 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,14 +3,10 @@
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
- import {readFileSync, writeFileSync, appendFileSync, mkdirSync, readdirSync} from 'node:fs';
9
+ import {readFileSync, writeFileSync, appendFileSync, mkdirSync} from 'node:fs';
14
10
  import {dirname, resolve} from 'node:path';
15
11
  import {homedir} from 'node:os';
16
12
 
@@ -19,10 +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');
22
- // Agent→浏览器命令队列:每条命令一个文件 commands/<id>.json。
23
- const cmdDir = resolve(homedir(), '.ui-inspector/commands');
18
+ // figma「带入对话」采集的节点结构。
19
+ const figmaPendingFile = resolve(homedir(), '.ui-inspector/figma-pending.json');
24
20
  const PORT = Number(process.env.UI_INSPECTOR_BRIDGE_PORT) || 8917;
25
- // 默认只绑 127.0.0.1(走 SSH 隧道);直连模式(笔记本能直达开发机内网 IP)时用 0.0.0.0 暴露到网卡。
26
21
  const HOST = process.env.UI_INSPECTOR_BRIDGE_HOST || '127.0.0.1';
27
22
 
28
23
  function readStore() {
@@ -47,7 +42,9 @@ function writeStore(list) {
47
42
  writeFileSync(storeFile, `${JSON.stringify(list, null, 4)}\n`, 'utf8');
48
43
  }
49
44
 
50
- // 录制累积:元信息与事件流分离,避免多请求读改写竞态。
45
+ // PLACEHOLDER_HELPERS
46
+
47
+ // 录制累积:元信息与事件流分离。
51
48
  function readMeta() {
52
49
  try {
53
50
  return JSON.parse(readFileSync(recordingFile, 'utf8'));
@@ -62,7 +59,7 @@ function writeMeta(meta) {
62
59
  function recStart(startedAt, pageUrl) {
63
60
  const cur = readMeta();
64
61
  if (cur && cur.active && cur.startedAt === startedAt) {
65
- return; // 同一会话重复 start,忽略
62
+ return;
66
63
  }
67
64
  writeMeta({active: true, startedAt, pageUrl});
68
65
  mkdirSync(dirname(eventsFile), {recursive: true});
@@ -86,62 +83,24 @@ function recStop(endedAt) {
86
83
  writeMeta(cur);
87
84
  }
88
85
 
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 {
86
+ // figma 节点采集:追加到 figma-pending.json(上限 10 个,防爆)。
87
+ function figmaCapture(node) {
88
+ if (!node) {
130
89
  return;
131
90
  }
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();
91
+ let list = [];
139
92
  try {
140
- mkdirSync(cmdDir, {recursive: true});
141
- writeFileSync(fp, JSON.stringify(c), 'utf8');
93
+ const p = JSON.parse(readFileSync(figmaPendingFile, 'utf8'));
94
+ list = Array.isArray(p) ? p : [];
142
95
  } catch {
143
- // 忽略
96
+ list = [];
97
+ }
98
+ list.push({node, ts: Date.now()});
99
+ if (list.length > 10) {
100
+ list = list.slice(-10);
144
101
  }
102
+ mkdirSync(dirname(figmaPendingFile), {recursive: true});
103
+ writeFileSync(figmaPendingFile, `${JSON.stringify(list, null, 4)}\n`, 'utf8');
145
104
  }
146
105
 
147
106
  function readBody(req, cb) {
@@ -177,18 +136,15 @@ const server = createServer((req, res) => {
177
136
  }
178
137
  const url = new URL(req.url, 'http://127.0.0.1');
179
138
  if (req.method === 'POST' && url.pathname === '/ingest') {
180
- let body = '';
181
- req.on('data', chunk => (body += chunk));
182
- req.on('end', () => {
183
- try {
184
- const {targets} = JSON.parse(body || '{}');
185
- const now = new Date().toISOString();
186
- const list = (Array.isArray(targets) ? targets : []).map(target => ({target, receivedAt: now}));
187
- writeStore(list);
188
- json(res, 200, {ok: true, pending: list.length});
189
- } catch (error) {
190
- 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)});
191
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});
192
148
  });
193
149
  return;
194
150
  }
@@ -226,16 +182,12 @@ const server = createServer((req, res) => {
226
182
  });
227
183
  return;
228
184
  }
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') {
185
+ if (req.method === 'POST' && url.pathname === '/figma/capture') {
234
186
  readBody(req, (data, err) => {
235
187
  if (err) {
236
188
  return json(res, 400, {ok: false, error: String(err)});
237
189
  }
238
- cmdResult(data.id, data.result, data.error);
190
+ figmaCapture(data.node);
239
191
  json(res, 200, {ok: true});
240
192
  });
241
193
  return;
@@ -1,8 +1,7 @@
1
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),真机可能需微调。
2
+ * Figma 命令执行器(MAIN world,figma.com):响应「带入对话」——把当前选中节点转成 F2C HTML 结构,
3
+ * 回传给 figma-ui 落盘待 hook 注入。页面内已解锁的 window.figma / window.F2CConverter 由
4
+ * rewrite.js + browser.global.js 提供。
6
5
  */
7
6
  (function () {
8
7
  if (window.__oreateFigmaExec) {
@@ -10,74 +9,41 @@
10
9
  }
11
10
  window.__oreateFigmaExec = true;
12
11
 
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) {
12
+ function resolveNode() {
18
13
  var fig = window.figma;
19
14
  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
- }
15
+ throw new Error('window.figma 不可用(请确认在 figma.com 设计稿页,且扩展已解锁)');
27
16
  }
28
17
  var sel = fig.currentPage && fig.currentPage.selection;
29
18
  if (sel && sel.length) {
30
19
  return sel[0];
31
20
  }
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);
21
+ throw new Error('未选中节点:请先在画布选中一个节点');
65
22
  }
66
23
 
67
24
  window.addEventListener('message', function (ev) {
68
- if (ev.source !== window || !ev.data || !ev.data.__uiInspectorCmd) {
25
+ if (ev.source !== window || !ev.data || ev.data.__uiInspectorFigmaCapture !== true) {
69
26
  return;
70
27
  }
71
- var cmd = ev.data.__uiInspectorCmd;
72
28
  Promise.resolve()
73
- .then(function () {
74
- return exec(cmd);
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
+ };
75
41
  })
76
- .then(function (result) {
77
- reply(cmd.id, result, null);
42
+ .then(function (node) {
43
+ window.postMessage({__uiInspectorFigmaCaptured: {node: node}}, '*');
78
44
  })
79
45
  .catch(function (err) {
80
- reply(cmd.id, null, err);
46
+ window.postMessage({__uiInspectorFigmaCaptured: {error: String(err)}}, '*');
81
47
  });
82
48
  });
83
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
+ })();
@@ -1,9 +1,18 @@
1
- <div class="hint">在 <code>www.figma.com</code> 打开设计稿后,本模式会在页面注入 F2C 引擎(解锁 Dev Mode / 插件能力)。</div>
1
+ <div class="hint">
2
+ 在
3
+ <code>www.figma.com</code>
4
+ 打开设计稿后,本模式会在页面注入 F2C 引擎(解锁 Dev Mode / 插件能力)。
5
+ </div>
2
6
  <div
3
7
  id="figma-status"
4
8
  class="hint"
5
9
  style="margin-top: 6px"
6
10
  ></div>
7
- <div class="hint" style="margin-top: 6px">
8
- 需先在上方「设置 → 生效域名」添加 <code>https://www.figma.com</code> 并授权,然后重载扩展。
11
+ <div
12
+ class="hint"
13
+ style="margin-top: 6px"
14
+ >
15
+ 需先在上方「设置 → 生效域名」添加
16
+ <code>https://www.figma.com</code>
17
+ 并授权,然后重载扩展。
9
18
  </div>
@@ -24,8 +24,8 @@
24
24
  "runAt": "document_idle"
25
25
  },
26
26
  {
27
- "id": "bridge",
28
- "js": ["figma-bridge.js"],
27
+ "id": "ui",
28
+ "js": ["figma-ui.js"],
29
29
  "matches": ["https://www.figma.com/*"],
30
30
  "world": "ISOLATED",
31
31
  "runAt": "document_idle"
@@ -83,15 +83,11 @@ 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`, {
86
+ if (message.type === 'figma-capture') {
87
+ const r = await fetch(`${base}/figma/capture`, {
92
88
  method: 'POST',
93
89
  headers: {'content-type': 'application/json'},
94
- body: JSON.stringify({id: message.id, result: message.result, error: message.error}),
90
+ body: JSON.stringify({node: message.node}),
95
91
  });
96
92
  return await r.json();
97
93
  }
@@ -117,8 +113,7 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
117
113
  msg.type === 'rec-start' ||
118
114
  msg.type === 'rec-event' ||
119
115
  msg.type === 'rec-stop' ||
120
- msg.type === 'cmd-poll' ||
121
- msg.type === 'cmd-result'
116
+ msg.type === 'figma-capture'
122
117
  ) {
123
118
  dispatch(msg).then(sendResponse);
124
119
  return true; // 异步 sendResponse
@@ -22,9 +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
- // Agent→浏览器命令队列:每条命令一个文件 commands/<id>.json(避免单文件读改写竞态)。
26
- // MCP server 写入 pending;浏览器轮询取走执行;host 写回 done/error 结果。
27
- const cmdDir = resolve(homedir(), '.ui-inspector/commands');
25
+ // figma「带入对话」采集的节点结构,等 hook 注入下一条消息。
26
+ const figmaPendingFile = resolve(homedir(), '.ui-inspector/figma-pending.json');
28
27
 
29
28
  function readStore() {
30
29
  try {
@@ -88,62 +87,24 @@ function recStop(endedAt) {
88
87
  writeMeta(cur); // 只有 start/stop 动 active,事件追加不碰它 → stop 后 active 不会被盖回
89
88
  }
90
89
 
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 {
90
+ // figma 节点采集:追加到 figma-pending.json(上限 10 个,防爆)。
91
+ function figmaCapture(node) {
92
+ if (!node) {
132
93
  return;
133
94
  }
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();
95
+ let list = [];
141
96
  try {
142
- mkdirSync(cmdDir, {recursive: true});
143
- writeFileSync(fp, JSON.stringify(c), 'utf8');
97
+ const p = JSON.parse(readFileSync(figmaPendingFile, 'utf8'));
98
+ list = Array.isArray(p) ? p : [];
144
99
  } catch {
145
- // 忽略
100
+ list = [];
101
+ }
102
+ list.push({node, ts: Date.now()});
103
+ if (list.length > 10) {
104
+ list = list.slice(-10);
146
105
  }
106
+ mkdirSync(dirname(figmaPendingFile), {recursive: true});
107
+ writeFileSync(figmaPendingFile, `${JSON.stringify(list, null, 4)}\n`, 'utf8');
147
108
  }
148
109
 
149
110
  function send(obj) {
@@ -171,10 +132,8 @@ function handle(msg) {
171
132
  } else if (msg && msg.type === 'rec-stop') {
172
133
  recStop(msg.endedAt);
173
134
  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);
135
+ } else if (msg && msg.type === 'figma-capture') {
136
+ figmaCapture(msg.node);
178
137
  resp = {ok: true};
179
138
  } else {
180
139
  resp = {ok: false, error: 'unknown message'};
@@ -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
@@ -16,8 +16,6 @@ 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');
21
19
 
22
20
  function readStore() {
23
21
  try {
@@ -43,41 +41,6 @@ function writeStatus(message, level) {
43
41
  return status;
44
42
  }
45
43
 
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
-
81
44
  const TOOLS = [
82
45
  {
83
46
  name: 'list_pending_edits',
@@ -106,32 +69,6 @@ const TOOLS = [
106
69
  required: ['message'],
107
70
  },
108
71
  },
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
- },
135
72
  ];
136
73
 
137
74
  function callTool(name, args) {
@@ -148,32 +85,6 @@ function callTool(name, args) {
148
85
  if (name === 'notify_browser') {
149
86
  return writeStatus(args?.message, args?.level);
150
87
  }
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
- }
177
88
  throw new Error('unknown tool: ' + name);
178
89
  }
179
90
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comate-ui-inspector",
3
- "version": "0.1.6-alpha.1",
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",
@@ -1,46 +0,0 @@
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
- })();