comate-ui-inspector 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,57 @@
1
- # Temporary Holding Version
1
+ # ui-inspector
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 在浏览器里按住 **Alt 点元素多选**,把「选中元素 + Vue 组件源码路径」交给 Comate,然后在对话里写修改意见让 AI 改代码。不改任何构建代码、日常不用手动起服务。
4
+
5
+ ## 目录
6
+
7
+ - `extension/`:Chrome/Edge MV3 扩展(`manifest.json` + MAIN/ISOLATED 内容脚本 + background SW)。
8
+ - `extension/native-host/host.mjs`:Native Messaging host(浏览器按需拉起,零依赖)。
9
+ - `mcp/server.mjs`:MCP stdio server,`clear_pending_edits` = 取消选中。
10
+ - `hooks/inject-ui-inspector.mjs`:Comate `UserPromptSubmit` hook,提交时自动注入全部选中元素。
11
+ - `cli.mjs`:命令入口,提供 `install` / `uninstall` / `status`(发布为 npm 包后暴露为 `ui-inspector` 命令)。
12
+
13
+ 数据落在 `~/.ui-inspector/pending.json`(用户主目录、一台机器一份)。hook 注册为**个人级**(`~/.comate/hooks.json`,对所有项目生效),所以**整机只装一次、所有仓库通用**,命令在哪个目录跑都行。
14
+
15
+ ## 安装(macOS / Linux,整机一次)
16
+
17
+ ```bash
18
+ npm i -g comate-ui-inspector # 公网 npm;或本地:npm i -g <本包目录>
19
+ ui-inspector install # 注册 native host + 个人级 hook,并打印 MCP 配置与扩展目录
20
+ ```
21
+
22
+ `install` 打印的两样东西要手动接上:
23
+
24
+ 1. 把打印的 **MCP 配置**贴进 Comate 的 MCP 设置,**reload Comate**。
25
+ 2. `chrome://extensions` → 开发者模式 →「加载已解压的扩展程序」→ 选打印出来的 **扩展目录**(只需一次)。
26
+
27
+ ## 卸载 / 状态
28
+
29
+ ```bash
30
+ ui-inspector status # 查看 host 清单 / 个人 hook / 待处理数 / 扩展目录
31
+ ui-inspector uninstall # 移除 native host 清单 + 个人级 hook(加 --purge 连 ~/.ui-inspector 一起删)
32
+ npm un -g comate-ui-inspector # 再删除命令本身
33
+ ```
34
+
35
+ > 卸载后仍需手动:在 Comate MCP 配置里删掉 ui-inspector、在 `chrome://extensions` 里移除扩展(这两处 CLI 碰不到)。
36
+
37
+ ## 发布(维护者)
38
+
39
+ ```bash
40
+ cd tools/ui-inspector
41
+ npm publish # 发到公网 npm(package.json 的 publishConfig 已指向 registry.npmjs.org;需 npm 账号)
42
+ ```
43
+
44
+ ## 用法
45
+
46
+ - 按住 **Alt**,点页面元素(可多选,绿框;再点取消)。
47
+ - 回 Comate **直接写修改意见发送**,选中元素会自动附带。
48
+ - AI 改完调用 `clear_pending_edits`,页面高亮消失。
49
+
50
+ ## 说明 / 限制
51
+
52
+ - 扩展默认只在 `local.oreateai.com` / `localhost` / `127.0.0.1` 生效;别的开发域名改 `extension/manifest.json` 的 `content_scripts[].matches`。
53
+ - 只有 **dev 构建** 拿得到组件源码路径(Vue dev 的 `__file`);生产构建组件链为空,退回选择器/文本定位。
54
+ - **Windows 暂不支持**(native host 需 `.bat` 包装器)。
55
+ - 扩展 ID 用 `manifest.json` 的公钥 `key` 钉死,所有人加载后一致;私钥未使用、未入库。
56
+ - 多仓库共用一份落盘(`~/.ui-inspector/`),同一时刻只保留最近一次选中;跨仓库并行编辑时以最后一次选择为准。
57
+ - 个人级 hook 会在**所有项目**的每次提交时触发;无选中时它直接返回 `allow`(空操作、不注入),对其它项目无副作用。
package/cli.mjs ADDED
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * ui-inspector CLI。发布到 npm 后全局安装即可获得 `ui-inspector` 命令:
4
+ * npm i -g comate-ui-inspector
5
+ * ui-inspector install # 注册 native host + 写个人级 hook + 打印 MCP 配置与扩展目录
6
+ * ui-inspector uninstall # 移除 native host 清单 + 个人级 hook(--purge 连 ~/.ui-inspector 一起删)
7
+ * ui-inspector status # 查看安装状态
8
+ *
9
+ * 全部按「整机一份、个人级」接入,不绑定任何仓库。仅 macOS / Linux。
10
+ */
11
+ import {chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync} from 'node:fs';
12
+ import {dirname, join, resolve} from 'node:path';
13
+ import {fileURLToPath} from 'node:url';
14
+ import {homedir, platform} from 'node:os';
15
+
16
+ const HOST_NAME = 'com.oreate.ui_inspector';
17
+ const EXT_ID = 'akpiagdjfeeaioifjepbmonpallececo';
18
+
19
+ const pkgRoot = dirname(fileURLToPath(import.meta.url));
20
+ const hostPath = resolve(pkgRoot, 'extension/native-host/host.mjs');
21
+ const serverPath = resolve(pkgRoot, 'mcp/server.mjs');
22
+ const hookPath = resolve(pkgRoot, 'hooks/inject-ui-inspector.mjs');
23
+ const extensionDir = resolve(pkgRoot, 'extension');
24
+ const home = homedir();
25
+ const hooksFile = resolve(home, '.comate/hooks.json');
26
+ const pendingDir = resolve(home, '.ui-inspector');
27
+
28
+ function nmDirs() {
29
+ const rels =
30
+ platform() === 'darwin'
31
+ ? [
32
+ 'Library/Application Support/Google/Chrome/NativeMessagingHosts',
33
+ 'Library/Application Support/Google/Chrome Canary/NativeMessagingHosts',
34
+ 'Library/Application Support/Microsoft Edge/NativeMessagingHosts',
35
+ 'Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts',
36
+ ]
37
+ : ['.config/google-chrome/NativeMessagingHosts', '.config/microsoft-edge/NativeMessagingHosts', '.config/chromium/NativeMessagingHosts'];
38
+ return rels.map(rel => join(home, rel));
39
+ }
40
+
41
+ function readHooks() {
42
+ try {
43
+ return JSON.parse(readFileSync(hooksFile, 'utf8'));
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ function out(text) {
50
+ process.stdout.write(`${text}\n`);
51
+ }
52
+ function install() {
53
+ chmodSync(hostPath, 0o755);
54
+ const manifest = {
55
+ name: HOST_NAME,
56
+ description: 'ui-inspector native messaging host',
57
+ path: hostPath,
58
+ type: 'stdio',
59
+ allowed_origins: [`chrome-extension://${EXT_ID}/`],
60
+ };
61
+ for (const dir of nmDirs()) {
62
+ try {
63
+ mkdirSync(dir, {recursive: true});
64
+ writeFileSync(join(dir, `${HOST_NAME}.json`), `${JSON.stringify(manifest, null, 2)}\n`);
65
+ out(`host manifest -> ${join(dir, `${HOST_NAME}.json`)}`);
66
+ } catch (error) {
67
+ out(`skip ${dir}: ${String(error)}`);
68
+ }
69
+ }
70
+
71
+ const hookCommand = `node "${hookPath}"`;
72
+ const config = readHooks() || {hooks: {}};
73
+ config.hooks = config.hooks || {};
74
+ const list = Array.isArray(config.hooks.UserPromptSubmit) ? config.hooks.UserPromptSubmit : [];
75
+ if (!JSON.stringify(list).includes('inject-ui-inspector')) {
76
+ list.push({hooks: [{type: 'command', command: hookCommand, timeout: 10}]});
77
+ config.hooks.UserPromptSubmit = list;
78
+ mkdirSync(dirname(hooksFile), {recursive: true});
79
+ writeFileSync(hooksFile, `${JSON.stringify(config, null, 4)}\n`);
80
+ out(`hook installed -> ${hooksFile}`);
81
+ } else {
82
+ out('hook already present, skip');
83
+ }
84
+
85
+ out('\n== 把下面加进 Comate 的 MCP 配置(stdio),然后 reload Comate ==');
86
+ out(JSON.stringify({mcpServers: {'ui-inspector': {command: 'node', args: [serverPath]}}}, null, 4));
87
+ out('\n== 加载浏览器扩展(chrome://extensions → 开发者模式 → 加载已解压 → 选下面这个目录) ==');
88
+ out(extensionDir);
89
+ out('\n完成后:按住 Alt 点元素多选 → 回 Comate 写修改意见。');
90
+ }
91
+
92
+ function uninstall({purge}) {
93
+ for (const dir of nmDirs()) {
94
+ const file = join(dir, `${HOST_NAME}.json`);
95
+ if (existsSync(file)) {
96
+ rmSync(file);
97
+ out(`removed host manifest -> ${file}`);
98
+ }
99
+ }
100
+ const config = readHooks();
101
+ if (config?.hooks?.UserPromptSubmit) {
102
+ const before = config.hooks.UserPromptSubmit.length;
103
+ config.hooks.UserPromptSubmit = config.hooks.UserPromptSubmit.filter(
104
+ entry => !JSON.stringify(entry).includes('inject-ui-inspector')
105
+ );
106
+ if (config.hooks.UserPromptSubmit.length !== before) {
107
+ writeFileSync(hooksFile, `${JSON.stringify(config, null, 4)}\n`);
108
+ out(`removed hook <- ${hooksFile}`);
109
+ }
110
+ }
111
+ if (purge && existsSync(pendingDir)) {
112
+ rmSync(pendingDir, {recursive: true, force: true});
113
+ out(`purged ${pendingDir}`);
114
+ }
115
+ out('\n已移除本地注册。还需手动:① 在 Comate MCP 配置里删掉 ui-inspector;② chrome://extensions 里移除扩展。');
116
+ }
117
+
118
+ function status() {
119
+ out('ui-inspector 状态:');
120
+ for (const dir of nmDirs()) {
121
+ const file = join(dir, `${HOST_NAME}.json`);
122
+ out(` host 清单 ${existsSync(file) ? '✓' : '✗'} ${file}`);
123
+ }
124
+ const hooked = JSON.stringify(readHooks()?.hooks?.UserPromptSubmit || []).includes('inject-ui-inspector');
125
+ out(` 个人 hook ${hooked ? '✓ 已注册' : '✗ 未注册'}(${hooksFile})`);
126
+ const pending = resolve(pendingDir, 'pending.json');
127
+ let count = 0;
128
+ try {
129
+ count = JSON.parse(readFileSync(pending, 'utf8')).length;
130
+ } catch {
131
+ count = 0;
132
+ }
133
+ out(` 当前待处理选中:${count} 条(${pending})`);
134
+ out(` 扩展目录:${extensionDir}`);
135
+ }
136
+
137
+ function help() {
138
+ out('用法:ui-inspector <install|uninstall|status>');
139
+ out(' install 注册 native host + 个人级 hook,并打印 MCP 配置与扩展目录');
140
+ out(' uninstall 移除上述注册(加 --purge 连 ~/.ui-inspector 数据一起删)');
141
+ out(' status 查看安装状态');
142
+ }
143
+
144
+ const cmd = process.argv[2] || 'help';
145
+ if (cmd === 'install') {
146
+ install();
147
+ } else if (cmd === 'uninstall') {
148
+ uninstall({purge: process.argv.includes('--purge')});
149
+ } else if (cmd === 'status') {
150
+ status();
151
+ } else {
152
+ help();
153
+ }
154
+
@@ -0,0 +1,36 @@
1
+ /*
2
+ * ui-inspector 扩展的 background service worker。
3
+ *
4
+ * 改用 Native Messaging:不再 fetch 本地 HTTP(免去混合内容问题与手动起 server),
5
+ * 由 chrome.runtime.sendNativeMessage 触发浏览器**按需拉起**本地 node host(host.mjs),
6
+ * host 读写 .ui-inspector/pending.json 后退出。每次请求一问一答。
7
+ */
8
+ const HOST = 'com.oreate.ui_inspector';
9
+
10
+ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
11
+ if (!msg || typeof msg.type !== 'string') {
12
+ return;
13
+ }
14
+
15
+ if (msg.type === 'ingest') {
16
+ chrome.runtime.sendNativeMessage(HOST, {type: 'ingest', targets: msg.targets || []}, resp => {
17
+ if (chrome.runtime.lastError) {
18
+ sendResponse({ok: false, error: chrome.runtime.lastError.message});
19
+ return;
20
+ }
21
+ sendResponse(resp);
22
+ });
23
+ return true; // 异步 sendResponse
24
+ }
25
+
26
+ if (msg.type === 'state') {
27
+ chrome.runtime.sendNativeMessage(HOST, {type: 'state'}, resp => {
28
+ if (chrome.runtime.lastError) {
29
+ sendResponse({ok: false, error: chrome.runtime.lastError.message});
30
+ return;
31
+ }
32
+ sendResponse(resp); // {ok, count}
33
+ });
34
+ return true;
35
+ }
36
+ });
@@ -0,0 +1,29 @@
1
+ /*
2
+ * ISOLATED world 内容脚本:MAIN world 与 background 之间的中继。
3
+ *
4
+ * - 收到 MAIN 发来的 selection(window.postMessage)→ 转给 background 写入本地桥;
5
+ * - 每 1.5s 问 background 要一次 /state 的 count;count 为 0(AI 调 MCP 清空后)
6
+ * → 回发 cleared 给 MAIN,让它撤掉页面高亮。
7
+ */
8
+ (function () {
9
+ window.addEventListener('message', event => {
10
+ if (event.source !== window) {
11
+ return;
12
+ }
13
+ const data = event.data;
14
+ if (data && data.__uiInspector === 'selection') {
15
+ chrome.runtime.sendMessage({type: 'ingest', targets: data.targets || []});
16
+ }
17
+ });
18
+
19
+ setInterval(() => {
20
+ chrome.runtime.sendMessage({type: 'state'}, resp => {
21
+ if (chrome.runtime.lastError) {
22
+ return;
23
+ }
24
+ if (resp && resp.ok && resp.count === 0) {
25
+ window.postMessage({__uiInspectorBridge: 'cleared'}, '*');
26
+ }
27
+ });
28
+ }, 1500);
29
+ })();
@@ -0,0 +1,223 @@
1
+ /*
2
+ * MAIN world 内容脚本:和原 Vite 客户端同款逻辑,但不 fetch——
3
+ * 选中集合通过 window.postMessage 交给 ISOLATED 的 bridge-content.js。
4
+ * 读 el.__vueParentComponent / type.__file 必须在 MAIN world(页面 JS 上下文)才拿得到。
5
+ */
6
+ (function () {
7
+ if (window.__oreateUiInspector) {
8
+ return;
9
+ }
10
+ window.__oreateUiInspector = true;
11
+
12
+ var active = false;
13
+ var current = null;
14
+ var selected = []; // 每项 {el, info, box}
15
+
16
+ var overlay = document.createElement('div');
17
+ overlay.style.cssText =
18
+ 'position:fixed;z-index:2147483646;pointer-events:none;border:2px solid #2f6bff;' +
19
+ 'background:rgba(47,107,255,0.12);border-radius:4px;display:none;transition:all 0.05s;';
20
+ document.body.appendChild(overlay);
21
+
22
+ var hint = document.createElement('div');
23
+ hint.textContent = '按住 Alt 点元素可多选';
24
+ hint.style.cssText =
25
+ 'position:fixed;left:50%;bottom:16px;transform:translateX(-50%);z-index:2147483647;' +
26
+ 'padding:6px 12px;border-radius:999px;background:#111;color:#fff;font:12px/1.4 sans-serif;' +
27
+ 'opacity:0;transition:opacity 0.15s;pointer-events:none;';
28
+ document.body.appendChild(hint);
29
+
30
+ function showHint(text) {
31
+ hint.textContent = text;
32
+ hint.style.opacity = '1';
33
+ window.clearTimeout(showHint._t);
34
+ showHint._t = window.setTimeout(function () {
35
+ hint.style.opacity = '0';
36
+ }, 1800);
37
+ }
38
+ function cssPath(el) {
39
+ var parts = [];
40
+ var node = el;
41
+ while (node && node.nodeType === 1 && parts.length < 6) {
42
+ var seg = node.nodeName.toLowerCase();
43
+ if (node.id) {
44
+ seg += '#' + node.id;
45
+ parts.unshift(seg);
46
+ break;
47
+ }
48
+ var cls = (node.getAttribute('class') || '').trim().split(/\s+/).filter(Boolean).slice(0, 2);
49
+ if (cls.length) {
50
+ seg += '.' + cls.join('.');
51
+ }
52
+ parts.unshift(seg);
53
+ node = node.parentElement;
54
+ }
55
+ return parts.join(' > ');
56
+ }
57
+
58
+ function componentChain(el) {
59
+ var chain = [];
60
+ var instance = el.__vueParentComponent;
61
+ while (instance && chain.length < 6) {
62
+ var type = instance.type || {};
63
+ var file = type.__file || null;
64
+ var name = type.__name || type.name || null;
65
+ if (file || name) {
66
+ chain.push({name: name, file: file});
67
+ }
68
+ instance = instance.parent;
69
+ }
70
+ return chain;
71
+ }
72
+
73
+ function collect(el) {
74
+ var rect = el.getBoundingClientRect();
75
+ return {
76
+ tag: el.nodeName.toLowerCase(),
77
+ id: el.id || null,
78
+ classList: Array.prototype.slice.call(el.classList),
79
+ selector: cssPath(el),
80
+ text: (el.textContent || '').trim().slice(0, 200),
81
+ rect: {x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height)},
82
+ componentChain: componentChain(el),
83
+ pageUrl: location.href,
84
+ };
85
+ }
86
+ function moveOverlay(el) {
87
+ var rect = el.getBoundingClientRect();
88
+ overlay.style.display = 'block';
89
+ overlay.style.left = rect.left + 'px';
90
+ overlay.style.top = rect.top + 'px';
91
+ overlay.style.width = rect.width + 'px';
92
+ overlay.style.height = rect.height + 'px';
93
+ }
94
+
95
+ function makeSelBox() {
96
+ var box = document.createElement('div');
97
+ box.style.cssText =
98
+ 'position:fixed;z-index:2147483645;pointer-events:none;border:2px solid #16a34a;' +
99
+ 'background:rgba(22,163,74,0.10);border-radius:4px;';
100
+ document.body.appendChild(box);
101
+ return box;
102
+ }
103
+
104
+ function placeBox(box, el) {
105
+ var rect = el.getBoundingClientRect();
106
+ box.style.left = rect.left + 'px';
107
+ box.style.top = rect.top + 'px';
108
+ box.style.width = rect.width + 'px';
109
+ box.style.height = rect.height + 'px';
110
+ }
111
+
112
+ function repositionBoxes() {
113
+ for (var i = 0; i < selected.length; i++) {
114
+ placeBox(selected[i].box, selected[i].el);
115
+ }
116
+ }
117
+
118
+ function clearSelection() {
119
+ for (var i = 0; i < selected.length; i++) {
120
+ selected[i].box.remove();
121
+ }
122
+ selected = [];
123
+ }
124
+
125
+ function indexOfEl(el) {
126
+ for (var i = 0; i < selected.length; i++) {
127
+ if (selected[i].el === el) {
128
+ return i;
129
+ }
130
+ }
131
+ return -1;
132
+ }
133
+
134
+ function postSelection() {
135
+ window.postMessage(
136
+ {
137
+ __uiInspector: 'selection',
138
+ targets: selected.map(function (s) {
139
+ return s.info;
140
+ }),
141
+ },
142
+ '*'
143
+ );
144
+ }
145
+
146
+ function toggleSelect(el) {
147
+ var idx = indexOfEl(el);
148
+ if (idx >= 0) {
149
+ selected[idx].box.remove();
150
+ selected.splice(idx, 1);
151
+ showHint('已取消选中(当前 ' + selected.length + ' 个)');
152
+ } else {
153
+ var box = makeSelBox();
154
+ placeBox(box, el);
155
+ selected.push({el: el, info: collect(el), box: box});
156
+ showHint('已选中 ' + selected.length + ' 个,可继续选,或回对话写意见');
157
+ }
158
+ postSelection();
159
+ }
160
+ window.addEventListener(
161
+ 'keydown',
162
+ function (e) {
163
+ if (e.key === 'Alt' && !active) {
164
+ active = true;
165
+ showHint('选择模式:点击元素可多选,再点取消;松开 Alt 退出');
166
+ }
167
+ },
168
+ true
169
+ );
170
+ window.addEventListener(
171
+ 'keyup',
172
+ function (e) {
173
+ if (e.key === 'Alt') {
174
+ active = false;
175
+ overlay.style.display = 'none';
176
+ }
177
+ },
178
+ true
179
+ );
180
+ window.addEventListener(
181
+ 'mousemove',
182
+ function (e) {
183
+ if (!active) {
184
+ return;
185
+ }
186
+ var el = document.elementFromPoint(e.clientX, e.clientY);
187
+ if (el && el !== current && el !== overlay && el !== hint) {
188
+ current = el;
189
+ moveOverlay(el);
190
+ }
191
+ },
192
+ true
193
+ );
194
+ window.addEventListener(
195
+ 'click',
196
+ function (e) {
197
+ if (!active || !e.altKey) {
198
+ return;
199
+ }
200
+ e.preventDefault();
201
+ e.stopPropagation();
202
+ var el = document.elementFromPoint(e.clientX, e.clientY);
203
+ if (el && el !== overlay && el !== hint) {
204
+ toggleSelect(el);
205
+ }
206
+ },
207
+ true
208
+ );
209
+
210
+ window.addEventListener('scroll', repositionBoxes, true);
211
+ window.addEventListener('resize', repositionBoxes, true);
212
+
213
+ // bridge-content 轮询到服务端 pending 已清空时回发 cleared,撤掉全部高亮
214
+ window.addEventListener('message', function (event) {
215
+ if (event.source !== window) {
216
+ return;
217
+ }
218
+ if (event.data && event.data.__uiInspectorBridge === 'cleared' && selected.length) {
219
+ clearSelection();
220
+ showHint('已按 AI 指令取消全部选中');
221
+ }
222
+ });
223
+ })();
@@ -0,0 +1,24 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "ui-inspector bridge",
4
+ "version": "0.0.1",
5
+ "description": "按住 Alt 点元素多选,经 Native Messaging 交给本地 host,供 Comate 定位源码修改。仅开发用。",
6
+ "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArHbA4zi22qf48xgfUtkMM6vXEtWiTtmhlppNAMlfG/uFjujAK9EEGssy6674woq5W9ugl+FlVE4rf477m9drGnGjnvaaFc9U8xCVqUoe/+vTmRyDWAtSPyu2aulAXd9P78WCia5ROSpecian7b0pG/iPyqvgZsyJfYk1qC3VyEBOnOqVeWawawKE23OaxUZJaci4wB10xNrWBHBIAztL8xmRDSkkByImwNshtlzbo2pE17LHZGr35FMbQXdRBKWxsdKlT1t0j7ezK73bCFjsYLWoAXVSpK+eAjltGTqKtOTvNxV94TXwr/j0v7ikgz80XD4ffzXV0qDbQ8HCxRSZMwIDAQAB",
7
+ "permissions": ["nativeMessaging"],
8
+ "background": {
9
+ "service_worker": "background.js"
10
+ },
11
+ "content_scripts": [
12
+ {
13
+ "matches": ["https://local.oreateai.com/*", "http://localhost/*", "http://127.0.0.1/*"],
14
+ "js": ["inspector-main.js"],
15
+ "world": "MAIN",
16
+ "run_at": "document_idle"
17
+ },
18
+ {
19
+ "matches": ["https://local.oreateai.com/*", "http://localhost/*", "http://127.0.0.1/*"],
20
+ "js": ["bridge-content.js"],
21
+ "run_at": "document_idle"
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * ui-inspector Native Messaging host(零依赖)。
4
+ *
5
+ * 由浏览器扩展经 chrome.runtime.sendNativeMessage 按需拉起:读一条 stdin 消息、处理、回一条、退出。
6
+ * Native Messaging 帧格式:4 字节 LE 长度前缀 + UTF-8 的 JSON。
7
+ *
8
+ * 消息:
9
+ * {type:'ingest', targets:[...]} → replace 写入 .ui-inspector/pending.json,回 {ok,pending}
10
+ * {type:'state'} → 回 {ok,count}
11
+ *
12
+ * 数据文件与 MCP server / hook 共用:统一在用户主目录 ~/.ui-inspector/pending.json(与仓库位置无关)。
13
+ */
14
+ import {readFileSync, writeFileSync, mkdirSync} from 'node:fs';
15
+ import {dirname, resolve} from 'node:path';
16
+ import {homedir} from 'node:os';
17
+
18
+ // 共享落盘:一台机器一份,放用户主目录,不绑定任何仓库——一份代码可服务多个项目。
19
+ const storeFile = resolve(homedir(), '.ui-inspector/pending.json');
20
+
21
+ function readStore() {
22
+ try {
23
+ const parsed = JSON.parse(readFileSync(storeFile, 'utf8'));
24
+ return Array.isArray(parsed) ? parsed : [];
25
+ } catch {
26
+ return [];
27
+ }
28
+ }
29
+
30
+ function writeStore(list) {
31
+ mkdirSync(dirname(storeFile), {recursive: true});
32
+ writeFileSync(storeFile, `${JSON.stringify(list, null, 4)}\n`, 'utf8');
33
+ }
34
+
35
+ function send(obj) {
36
+ const body = Buffer.from(JSON.stringify(obj), 'utf8');
37
+ const header = Buffer.alloc(4);
38
+ header.writeUInt32LE(body.length, 0);
39
+ process.stdout.write(Buffer.concat([header, body]));
40
+ }
41
+
42
+ function handle(msg) {
43
+ let resp;
44
+ if (msg && msg.type === 'ingest') {
45
+ const targets = Array.isArray(msg.targets) ? msg.targets : [];
46
+ const now = new Date().toISOString();
47
+ writeStore(targets.map(target => ({target, receivedAt: now})));
48
+ resp = {ok: true, pending: targets.length};
49
+ } else if (msg && msg.type === 'state') {
50
+ resp = {ok: true, count: readStore().length};
51
+ } else {
52
+ resp = {ok: false, error: 'unknown message'};
53
+ }
54
+ send(resp);
55
+ process.exit(0);
56
+ }
57
+
58
+ let buf = Buffer.alloc(0);
59
+ process.stdin.on('data', chunk => {
60
+ buf = Buffer.concat([buf, chunk]);
61
+ if (buf.length < 4) {
62
+ return;
63
+ }
64
+ const len = buf.readUInt32LE(0);
65
+ if (buf.length >= 4 + len) {
66
+ handle(JSON.parse(buf.slice(4, 4 + len).toString('utf8')));
67
+ }
68
+ });
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Comate UserPromptSubmit hook:把 ui-inspector 在页面上选中的元素,自动注入到本轮对话。
4
+ *
5
+ * 流程:浏览器里按住 Alt 点元素(可多选,持久高亮)→ 落盘 .ui-inspector/pending.json;
6
+ * 回到 Comate 写修改意见提交 → 本 hook 读取**全部**选中元素,作为 additionalContext 注入给 Agent。
7
+ *
8
+ * 注意:这里**不清空** pending —— 选中态要一直保持到 AI 改完、主动调 MCP clear_pending_edits
9
+ * 才取消(页面轮询到 count=0 后撤掉高亮)。否则第一轮提交就会把多选清掉、页面提前取消高亮。
10
+ *
11
+ * 入参由 stdin 传入(UserPromptSubmit 的 JSON),这里用不到;输出 JSON 到 stdout。
12
+ */
13
+ import {readFileSync} from 'node:fs';
14
+ import {resolve} from 'node:path';
15
+ import {homedir} from 'node:os';
16
+
17
+ // 共享落盘:读用户主目录下的 pending(与 host / mcp 同一路径),一份代码可服务多个仓库。
18
+ const storeFile = resolve(homedir(), '.ui-inspector/pending.json');
19
+
20
+ function emit(output) {
21
+ process.stdout.write(JSON.stringify(output));
22
+ process.exit(0);
23
+ }
24
+
25
+ let list = [];
26
+ try {
27
+ const parsed = JSON.parse(readFileSync(storeFile, 'utf8'));
28
+ list = Array.isArray(parsed) ? parsed : [];
29
+ } catch {
30
+ emit({decision: 'allow'});
31
+ }
32
+
33
+ if (!list.length) {
34
+ emit({decision: 'allow'});
35
+ }
36
+
37
+ function describe(target, index, total) {
38
+ const files = (target.componentChain || []).map(item => item && item.file).filter(Boolean);
39
+ const head = total > 1 ? `【元素 ${index + 1}/${total}】` : '';
40
+ return [
41
+ head + (files.length ? `组件源码(由近及远): ${files.join(' <- ')}` : '组件源码: 未捕获(不在 Vue 组件树内)'),
42
+ ` CSS 选择器: ${target.selector || '(无)'}`,
43
+ ` 标签/文本: <${target.tag || '?'}> ${JSON.stringify((target.text || '').slice(0, 120))}`,
44
+ target.pageUrl ? ` 页面: ${target.pageUrl}` : '',
45
+ ]
46
+ .filter(Boolean)
47
+ .join('\n');
48
+ }
49
+
50
+ const total = list.length;
51
+ const body = list.map((entry, index) => describe(entry?.target ?? {}, index, total)).join('\n');
52
+ const additionalContext = [
53
+ `【ui-inspector】用户在页面上选中了 ${total} 个元素,接下来这条消息是针对它们的修改意见:`,
54
+ body,
55
+ '请据此定位源码并按用户意见修改;改完调用 MCP 工具 clear_pending_edits 取消页面选中。',
56
+ ].join('\n');
57
+
58
+ emit({decision: 'allow', additionalContext});
package/mcp/server.mjs ADDED
@@ -0,0 +1,120 @@
1
+ /*
2
+ * ui-inspector 的 MCP server(零依赖,stdio 传输)。
3
+ *
4
+ * Comate 作为 MCP client 启动本进程,读取 ui-inspector 桥落盘的待处理修改意见。
5
+ * 数据文件:~/.ui-inspector/pending.json(由扩展的 Native Messaging host(native-host/host.mjs)写入)。
6
+ *
7
+ * MCP stdio 传输是「一行一条 JSON-RPC 2.0 消息」,这里手写最小实现:
8
+ * initialize / notifications/initialized / tools/list / tools/call。
9
+ *
10
+ * 启动:node tools/ui-inspector/mcp/server.mjs
11
+ */
12
+ import {readFileSync, writeFileSync, mkdirSync} from 'node:fs';
13
+ import {dirname, resolve} from 'node:path';
14
+ import {homedir} from 'node:os';
15
+ import {createInterface} from 'node:readline';
16
+
17
+ const storeFile = resolve(homedir(), '.ui-inspector/pending.json');
18
+
19
+ function readStore() {
20
+ try {
21
+ const parsed = JSON.parse(readFileSync(storeFile, 'utf8'));
22
+ return Array.isArray(parsed) ? parsed : [];
23
+ } catch {
24
+ return [];
25
+ }
26
+ }
27
+
28
+ function clearStore() {
29
+ const count = readStore().length;
30
+ mkdirSync(dirname(storeFile), {recursive: true});
31
+ writeFileSync(storeFile, '[]\n', 'utf8');
32
+ return count;
33
+ }
34
+
35
+ const TOOLS = [
36
+ {
37
+ name: 'list_pending_edits',
38
+ description: '列出 ui-inspector 收到的全部待处理修改意见(含选中元素信息与 Vue 组件源码文件链)。',
39
+ inputSchema: {type: 'object', properties: {}},
40
+ },
41
+ {
42
+ name: 'get_latest_edit',
43
+ description: '取最新一条待处理修改意见。',
44
+ inputSchema: {type: 'object', properties: {}},
45
+ },
46
+ {
47
+ name: 'clear_pending_edits',
48
+ description: '处理完后清空待处理列表,返回被清空的条数。',
49
+ inputSchema: {type: 'object', properties: {}},
50
+ },
51
+ ];
52
+
53
+ function callTool(name) {
54
+ if (name === 'list_pending_edits') {
55
+ return readStore();
56
+ }
57
+ if (name === 'get_latest_edit') {
58
+ const list = readStore();
59
+ return list.length ? list[list.length - 1] : null;
60
+ }
61
+ if (name === 'clear_pending_edits') {
62
+ return {cleared: clearStore()};
63
+ }
64
+ throw new Error('unknown tool: ' + name);
65
+ }
66
+
67
+ function send(message) {
68
+ process.stdout.write(JSON.stringify(message) + '\n');
69
+ }
70
+
71
+ function handle(msg) {
72
+ if (msg.method === 'initialize') {
73
+ send({
74
+ jsonrpc: '2.0',
75
+ id: msg.id,
76
+ result: {
77
+ protocolVersion: msg.params?.protocolVersion ?? '2024-11-05',
78
+ capabilities: {tools: {}},
79
+ serverInfo: {name: 'oreate-ui-inspector', version: '0.0.0'},
80
+ },
81
+ });
82
+ return;
83
+ }
84
+ if (msg.method === 'notifications/initialized') {
85
+ return;
86
+ }
87
+ if (msg.method === 'tools/list') {
88
+ send({jsonrpc: '2.0', id: msg.id, result: {tools: TOOLS}});
89
+ return;
90
+ }
91
+ if (msg.method === 'tools/call') {
92
+ try {
93
+ const result = callTool(msg.params?.name);
94
+ send({
95
+ jsonrpc: '2.0',
96
+ id: msg.id,
97
+ result: {content: [{type: 'text', text: JSON.stringify(result, null, 4)}]},
98
+ });
99
+ } catch (error) {
100
+ send({jsonrpc: '2.0', id: msg.id, error: {code: -32000, message: String(error)}});
101
+ }
102
+ return;
103
+ }
104
+ if (msg.id !== undefined) {
105
+ send({jsonrpc: '2.0', id: msg.id, error: {code: -32601, message: 'method not found: ' + msg.method}});
106
+ }
107
+ }
108
+
109
+ const rl = createInterface({input: process.stdin});
110
+ rl.on('line', line => {
111
+ const text = line.trim();
112
+ if (!text) {
113
+ return;
114
+ }
115
+ try {
116
+ handle(JSON.parse(text));
117
+ } catch {
118
+ // 坏帧直接丢弃,不回错(没有可用的 id 关联)
119
+ }
120
+ });
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
- "name": "comate-ui-inspector",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "comate-ui-inspector",
3
+ "version": "0.1.0",
4
+ "description": "按住 Alt 点选页面元素,把「元素 + Vue 组件源码路径」交给 Comate 定位源码修改。浏览器扩展 + Native Messaging host + MCP + hook。",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "ui-inspector": "./cli.mjs"
9
+ },
10
+ "files": [
11
+ "cli.mjs",
12
+ "extension",
13
+ "mcp",
14
+ "hooks",
15
+ "README.md"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "os": [
21
+ "darwin",
22
+ "linux"
23
+ ],
24
+ "keywords": [
25
+ "comate",
26
+ "vue",
27
+ "inspector",
28
+ "native-messaging",
29
+ "mcp",
30
+ "devtools"
31
+ ],
32
+ "publishConfig": {
33
+ "registry": "https://registry.npmjs.org",
34
+ "access": "public"
35
+ }
36
+ }