comate-ui-inspector 0.1.2 → 0.1.4-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/README.md CHANGED
@@ -42,20 +42,36 @@ ui-inspector install # 注册 native host + 个人级 hook,并打印
42
42
  ```bash
43
43
  # 远程机上(和 Comate hook/MCP 同机):装 hook/MCP 并后台起 bridge
44
44
  ui-inspector install --remote # --port 默认 8917;跳过无浏览器的 native host
45
- # 本地笔记本:把 bridge 端口 + dev server 端口一起转发到本地 127.0.0.1
46
- ui-inspector link <remote-ssh-目标> --port <dev端口>
45
+ # 本地笔记本:把远程 bridge 端口转发到本地 127.0.0.1(dev server 端口自行转发)
46
+ ui-inspector link <remote-ssh-目标> # 本地/远程 bridge 都用默认 8917
47
+ # 本地与远程 bridge 端口不同时才两个都填:
48
+ ui-inspector link <remote-ssh-目标> --bridge-port <本地> --remote-bridge-port <远程>
47
49
  ```
48
50
 
49
- `install --remote` 会打印 MCP 配置(贴进**远程**的 Comate 并 reload),并把 bridge 后台拉起(日志在 `~/.ui-inspector/bridge.log`)。`link` 本质是 `ssh -N -L <本地bridge端口>:127.0.0.1:<远程bridge端口> -L <dev端口>:127.0.0.1:<dev端口> <remote>`,保持前台、Ctrl-C 断开。远程 bridge 端口不必和本地一致:本地用 `--bridge-port`、远程用 `--remote-bridge-port`(默认两者相同);弹窗里填的是**本地**那个端口。
51
+ `install --remote` 会打印 MCP 配置(贴进**远程**的 Comate 并 reload),并把 bridge 后台拉起(日志在 `~/.ui-inspector/bridge.log`)。`link` 本质是 `ssh -N -L <本地bridge端口>:127.0.0.1:<远程bridge端口> <remote>`,保持前台、Ctrl-C 断开。端口参数只有两个:本地 `--bridge-port`、远程 `--remote-bridge-port`(默认相同,相同时只填 `--bridge-port`、省略即默认 8917);弹窗里填的是**本地**那个。dev server 端口由你的远程开发工具自行转发,`link` 不再代管。
50
52
 
51
53
  链路断了(ssh 掉线 / 远程 bridge 或 dev server 停了)时,先在本地自检:
52
54
 
53
55
  ```bash
54
- ui-inspector doctor <remote> --port <dev端口> # 定位断在哪层并打印对应恢复命令
56
+ ui-inspector doctor <remote> --dev-port <dev端口> # 定位断在哪层并打印对应恢复命令
55
57
  ```
56
58
 
57
59
  恢复顺序:先确保远程 bridge 活(`ui-inspector bridge` / `install --remote`)→ 再接本地隧道(`ui-inspector link`)→ 再看远程 dev server。
58
60
 
61
+ ### 直连模式(笔记本能直达开发机内网 IP、堡垒机禁转发时用)
62
+
63
+ 如果你的笔记本本来就能直连开发机的内网 IP(比如 `/etc/hosts` 把 `devlocal.oreateai.com` 指到 `10.25.64.64`,浏览器直接就能打开 dev 页),那连 SSH 隧道都不用——把 bridge 暴露到开发机网卡,浏览器直连即可,正好绕开「堡垒机禁止 SSH 转发」:
64
+
65
+ ```bash
66
+ # 开发机上:bridge 绑 0.0.0.0(install --remote 顺带起,或单独跑 bridge)
67
+ ui-inspector install --remote --host 0.0.0.0
68
+ # 或:ui-inspector bridge --host 0.0.0.0 --port 8917
69
+ ```
70
+
71
+ 然后扩展弹窗「传输通路」选 `远程(HTTP bridge)`,地址填**完整地址** `devlocal.oreateai.com:8917`(或 `10.25.64.64:8917`),不用 `ui-inspector link`。
72
+
73
+ > 注意:`--host 0.0.0.0` 会把 bridge 暴露在内网,谁能访问到这个端口谁就能写 pending;仅在可信内网用。dev 页若是 https、扩展连 http bridge 被浏览器拦,则退回隧道模式或给 bridge 上 https。
74
+
59
75
  本地仍需一次:加载扩展(`chrome://extensions` 选 `extension` 目录),在弹窗把传输切到 `远程(HTTP bridge)`、填本地端口、加上你的 dev 域名。bridge 只绑 127.0.0.1、不暴露到网络,访问全程走 SSH 转发,所以 https 页面连 `127.0.0.1` 不触发混合内容拦截。
60
76
 
61
77
  ## 卸载 / 状态
package/bridge/server.mjs CHANGED
@@ -17,6 +17,8 @@ import {homedir} from 'node:os';
17
17
  const storeFile = resolve(homedir(), '.ui-inspector/pending.json');
18
18
  const statusFile = resolve(homedir(), '.ui-inspector/status.json');
19
19
  const PORT = Number(process.env.UI_INSPECTOR_BRIDGE_PORT) || 8917;
20
+ // 默认只绑 127.0.0.1(走 SSH 隧道);直连模式(笔记本能直达开发机内网 IP)时用 0.0.0.0 暴露到网卡。
21
+ const HOST = process.env.UI_INSPECTOR_BRIDGE_HOST || '127.0.0.1';
20
22
 
21
23
  function readStore() {
22
24
  try {
@@ -83,6 +85,6 @@ const server = createServer((req, res) => {
83
85
  json(res, 404, {ok: false, error: 'not found'});
84
86
  });
85
87
 
86
- server.listen(PORT, '127.0.0.1', () => {
87
- process.stdout.write(`ui-inspector bridge listening on http://127.0.0.1:${PORT}\n`);
88
+ server.listen(PORT, HOST, () => {
89
+ process.stdout.write(`ui-inspector bridge listening on http://${HOST}:${PORT}\n`);
88
90
  });
package/cli.mjs CHANGED
@@ -4,8 +4,8 @@
4
4
  * npm i -g comate-ui-inspector
5
5
  * ui-inspector install # 注册 native host + 写个人级 hook + 打印 MCP 配置与扩展目录
6
6
  * ui-inspector install --remote # 远程开发机:装 hook/MCP 并后台起 bridge(跳过无浏览器的 native host)
7
- * ui-inspector link <remote> --port <devport> # 本地:SSH 把 bridge 端口 + dev 端口转发到本地 127.0.0.1
8
- * ui-inspector doctor <remote> --port <devport> # 本地:自检远程链路断在哪层并打印恢复命令
7
+ * ui-inspector link <remote> --bridge-port <本地> [--remote-bridge-port <远程>] # 本地:SSH 把 bridge 端口转发到本地 127.0.0.1
8
+ * ui-inspector doctor <remote> --bridge-port <本地> [--dev-port <dev端口>] # 本地:自检远程链路断在哪层并打印恢复命令
9
9
  * ui-inspector uninstall # 移除 native host 清单 + 个人级 hook(--purge 连 ~/.ui-inspector 一起删)
10
10
  * ui-inspector status # 查看安装状态
11
11
  * ui-inspector bridge # 启动 HTTP bridge(远程开发用,--port 默认 8917)
@@ -61,7 +61,10 @@ function out(text) {
61
61
  process.stdout.write(`${text}\n`);
62
62
  }
63
63
  function installHookAndMcp() {
64
- const hookCommand = `node "${hookPath}"`;
64
+ // 用绝对路径的 node(当前运行 install 的这个),而不是裸 `node`:
65
+ // Comate 拉起 hook/MCP 时 PATH 极简(不含 nvm/fnm 目录),裸 `node` 会「未找到命令」。
66
+ const nodeBin = process.execPath;
67
+ const hookCommand = `"${nodeBin}" "${hookPath}"`;
65
68
  const config = readHooks() || {hooks: {}};
66
69
  config.hooks = config.hooks || {};
67
70
  const list = Array.isArray(config.hooks.UserPromptSubmit) ? config.hooks.UserPromptSubmit : [];
@@ -75,7 +78,7 @@ function installHookAndMcp() {
75
78
  out('hook already present, skip');
76
79
  }
77
80
  out('\n== 把下面加进 Comate 的 MCP 配置(stdio),然后 reload Comate ==');
78
- out(JSON.stringify({mcpServers: {'ui-inspector': {command: 'node', args: [serverPath]}}}, null, 4));
81
+ out(JSON.stringify({mcpServers: {'ui-inspector': {command: nodeBin, args: [serverPath]}}}, null, 4));
79
82
  }
80
83
 
81
84
  function installNativeHost() {
@@ -104,26 +107,34 @@ function installNativeHost() {
104
107
  }
105
108
 
106
109
  // 后台拉起 bridge 并与本进程脱钩,install --remote 用:装完 hook/MCP 顺手把 bridge 跑起来。
107
- function startBridgeDetached(port) {
110
+ function startBridgeDetached(port, host) {
108
111
  mkdirSync(pendingDir, {recursive: true});
109
112
  const logFile = resolve(pendingDir, 'bridge.log');
110
113
  const fd = openSync(logFile, 'a');
114
+ const env = {...process.env, UI_INSPECTOR_BRIDGE_PORT: String(port)};
115
+ if (host) {
116
+ env.UI_INSPECTOR_BRIDGE_HOST = String(host);
117
+ }
111
118
  const child = spawn(process.execPath, [resolve(pkgRoot, 'bridge/server.mjs')], {
112
119
  detached: true,
113
120
  stdio: ['ignore', fd, fd],
114
- env: {...process.env, UI_INSPECTOR_BRIDGE_PORT: String(port)},
121
+ env,
115
122
  });
116
123
  child.unref();
117
- out(`bridge 已后台启动(pid ${child.pid},端口 ${port}),日志:${logFile}`);
124
+ out(`bridge 已后台启动(pid ${child.pid},监听 ${host || '127.0.0.1'}:${port}),日志:${logFile}`);
118
125
  out(` 停止:kill ${child.pid}`);
119
126
  }
120
127
 
121
- function install({remote, port}) {
128
+ function install({remote, port, host}) {
122
129
  installHookAndMcp();
123
130
  if (remote) {
124
131
  out('\n== 远程开发机:跳过 native host/扩展(这台没浏览器),后台启动 bridge ==');
125
- startBridgeDetached(port);
126
- out(`\n本地笔记本上执行:ui-inspector link <这台机器的 ssh 目标> --port <dev端口>`);
132
+ startBridgeDetached(port, host);
133
+ if (host && host !== '127.0.0.1') {
134
+ out(`\n直连模式:浏览器可直接访问本机 ${host}:${port},弹窗里填该地址,无需 ui-inspector link。`);
135
+ } else {
136
+ out(`\n本地笔记本上执行:ui-inspector link <这台机器的 ssh 目标>`);
137
+ }
127
138
  return;
128
139
  }
129
140
  installNativeHost();
@@ -133,39 +144,41 @@ function install({remote, port}) {
133
144
  out('完成后:按住 Alt 点元素多选 → 回 Comate 写修改意见。');
134
145
  }
135
146
 
136
- // 本地笔记本:把远程 bridge 端口 + dev server 端口都转发到本地 127.0.0.1。
137
- // 远程 bridge 监听端口可与本地不同:-L <本地bridge端口>:127.0.0.1:<远程bridge端口>。
138
- function link({remote, devPort, bridgePort, remoteBridgePort}) {
147
+ // 本地笔记本:把远程 bridge 端口转发到本地 127.0.0.1。端口参数只有两个:
148
+ // --bridge-port <本地>(默认 8917)、--remote-bridge-port <远程>(默认与本地相同)。
149
+ // 本地=远程时只填 --bridge-port;不同才两个都填。dev server 端口自行转发。
150
+ function link({remote, bridgePort, remoteBridgePort}) {
139
151
  if (!remote) {
140
- out(
141
- '用法:ui-inspector link <remote-ssh-目标> --port <dev端口> [--bridge-port 8917] [--remote-bridge-port <远程端口>]'
142
- );
143
- out('例: ui-inspector link dev-box --port 5173');
152
+ out('用法:ui-inspector link <remote-ssh-目标> [--bridge-port <本地端口>] [--remote-bridge-port <远程端口>]');
153
+ out('例: ui-inspector link dev-box # 本地/远程都用默认 8917');
154
+ out(' ui-inspector link dev-box --bridge-port 9000 # 本地=远程=9000');
155
+ out(' ui-inspector link dev-box --bridge-port 9000 --remote-bridge-port 8917 # 本地 9000 → 远程 8917');
144
156
  return;
145
157
  }
146
158
  const rbp = remoteBridgePort || bridgePort;
147
- const args = ['-N', '-L', `${bridgePort}:127.0.0.1:${rbp}`];
148
- if (devPort) {
149
- args.push('-L', `${devPort}:127.0.0.1:${devPort}`);
150
- }
151
- args.push(remote);
159
+ const args = ['-N', '-L', `${bridgePort}:127.0.0.1:${rbp}`, remote];
152
160
  out(`转发中(Ctrl-C 断开):ssh ${args.join(' ')}`);
153
161
  out(
154
- ` bridge → 本地 127.0.0.1:${bridgePort} ⇢ 远程 127.0.0.1:${rbp}${devPort ? `;dev → 127.0.0.1:${devPort}` : ''}`
155
- );
156
- out(
157
- ` 提示:弹窗里的端口填本地的 ${bridgePort};远程 bridge 需在 ${rbp} 上监听(ui-inspector bridge --port ${rbp})。`
162
+ ` bridge → 本地 127.0.0.1:${bridgePort} ⇢ 远程 127.0.0.1:${rbp}(弹窗里填本地 ${bridgePort};远程 bridge 需在 ${rbp} 监听)`
158
163
  );
164
+ out(` 注:dev server 端口请自行转发(远程开发工具通常已转发,或另跑 ssh -L <dev>:127.0.0.1:<dev> ${remote})`);
159
165
  const child = spawn('ssh', args, {stdio: 'inherit'});
160
166
  child.on('exit', code => process.exit(code ?? 0));
161
167
  }
162
168
 
163
169
  function bridge() {
164
- const i = process.argv.indexOf('--port');
165
- const port = i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : '8917';
170
+ const port = getOpt('--port', '8917');
171
+ const host = getOpt('--host', null);
166
172
  process.env.UI_INSPECTOR_BRIDGE_PORT = String(port);
167
- out(`启动 HTTP bridge(远程开发传输):127.0.0.1:${port}`);
168
- out('本地浏览器经 SSH 端口转发访问:ssh -L ' + port + ':127.0.0.1:' + port + ' <remote>');
173
+ if (host) {
174
+ process.env.UI_INSPECTOR_BRIDGE_HOST = String(host);
175
+ }
176
+ out(`启动 HTTP bridge:监听 ${host || '127.0.0.1'}:${port}`);
177
+ if (host && host !== '127.0.0.1') {
178
+ out('直连模式:浏览器可直接访问本机该地址(弹窗里填完整地址),无需 SSH 隧道。');
179
+ } else {
180
+ out('隧道模式:本地经 SSH 转发访问,ssh -L ' + port + ':127.0.0.1:' + port + ' <remote>');
181
+ }
169
182
  import(resolve(pkgRoot, 'bridge/server.mjs'));
170
183
  }
171
184
 
@@ -226,22 +239,22 @@ function getOpt(name, def) {
226
239
  function parseLink() {
227
240
  const rest = process.argv.slice(3);
228
241
  let remote;
229
- let devPort;
230
242
  let bridgePort = '8917';
231
243
  let remoteBridgePort;
244
+ let devPort;
232
245
  for (let i = 0; i < rest.length; i++) {
233
246
  const a = rest[i];
234
- if (a === '--port') {
235
- devPort = rest[++i];
236
- } else if (a === '--bridge-port') {
247
+ if (a === '--bridge-port') {
237
248
  bridgePort = rest[++i];
238
249
  } else if (a === '--remote-bridge-port') {
239
250
  remoteBridgePort = rest[++i];
251
+ } else if (a === '--dev-port') {
252
+ devPort = rest[++i]; // 仅 doctor 用;放这里一并消费,避免其值被当成 remote
240
253
  } else if (!a.startsWith('-')) {
241
254
  remote = a;
242
255
  }
243
256
  }
244
- return {remote, devPort, bridgePort, remoteBridgePort};
257
+ return {remote, bridgePort, remoteBridgePort, devPort};
245
258
  }
246
259
 
247
260
  // 探测本地某端口的 http 可达性(2s 超时)。用 node:http 而非 fetch,兼容 node 18+。
@@ -262,9 +275,12 @@ function probe(port, path) {
262
275
 
263
276
  // 一键自检:定位远程开发链路(本地隧道 → 远程 bridge → 远程 dev server)断在哪层,并打印恢复命令。
264
277
  async function doctor() {
265
- const {remote, devPort, bridgePort} = parseLink();
278
+ const {remote, bridgePort, remoteBridgePort, devPort} = parseLink();
266
279
  const rm = remote || '<remote>';
267
- const dp = devPort || '<dev端口>';
280
+ const linkCmd =
281
+ `ui-inspector link ${rm}` +
282
+ (bridgePort !== '8917' ? ` --bridge-port ${bridgePort}` : '') +
283
+ (remoteBridgePort && remoteBridgePort !== bridgePort ? ` --remote-bridge-port ${remoteBridgePort}` : '');
268
284
  out('ui-inspector doctor(远程开发链路自检):');
269
285
 
270
286
  const b = await probe(bridgePort, '/state');
@@ -272,7 +288,7 @@ async function doctor() {
272
288
  out(` [1] bridge ✓ 127.0.0.1:${bridgePort} 可达——本地→隧道→远程 bridge 全通`);
273
289
  } else if (b.state === 'refused') {
274
290
  out(` [1] bridge ✗ 本地 127.0.0.1:${bridgePort} 无人监听 → SSH 隧道断了(或没起)`);
275
- out(` 恢复:ui-inspector link ${rm} --port ${dp}`);
291
+ out(` 恢复:${linkCmd}`);
276
292
  } else {
277
293
  out(` [1] bridge ✗ 隧道在、但远程 bridge 无响应(${b.state})`);
278
294
  out(` 恢复:登远程机跑 ui-inspector bridge(或 install --remote),看 ~/.ui-inspector/bridge.log`);
@@ -281,35 +297,33 @@ async function doctor() {
281
297
  if (devPort) {
282
298
  const d = await probe(devPort, '/');
283
299
  if (d.state === 'refused') {
284
- out(` [2] dev ✗ 本地 127.0.0.1:${devPort} 不可达 → 隧道没转发该端口,或远程 dev server 没起`);
285
- out(` 恢复:① 远程起 dev server;② 本地 ui-inspector link ${rm} --port ${devPort}`);
300
+ out(` [2] dev ✗ 本地 127.0.0.1:${devPort} 不可达 → 该端口没转发,或远程 dev server 没起`);
286
301
  } else if (d.state === 'timeout') {
287
302
  out(` [2] dev ✗ 127.0.0.1:${devPort} 超时 → 远程 dev server 可能挂了`);
288
- out(` 恢复:远程重启 dev server`);
289
303
  } else {
290
304
  out(` [2] dev ✓ 127.0.0.1:${devPort} 可达(HTTP ${d.code})`);
291
305
  }
292
306
  } else {
293
- out(' [2] dev - 未传 --port,跳过 dev server 检查');
307
+ out(' [2] dev - 未传 --dev-port,跳过 dev server 检查');
294
308
  }
295
309
 
296
- out('\n恢复顺序:先确保远程 bridge 活 → 再接本地隧道(link)→ 再看远程 dev server。');
310
+ out('\n恢复顺序:先确保远程 bridge 活 → 再接本地隧道(link)→ 再看远程 dev server(自行转发)。');
297
311
  }
298
312
 
299
313
  function help() {
300
314
  out('用法:ui-inspector <install|uninstall|status|bridge|link|doctor>');
301
315
  out(' install 注册 native host + 个人级 hook,并打印 MCP 配置与扩展目录');
302
- out(' install --remote 远程开发机:装 hook/MCP 并后台起 bridge(跳过 native host,--port 默认 8917)');
303
- out(' link <remote> 本地:SSH 把 bridge 端口 + dev 端口转发到本地(--port <dev端口> [--bridge-port 8917])');
304
- out(' doctor <remote> 本地:自检远程开发链路断在哪层并打印恢复命令(--port <dev端口> [--bridge-port 8917])');
316
+ out(' install --remote 远程开发机:装 hook/MCP 并后台起 bridge(--port 默认 8917,--host 0.0.0.0 可直连免隧道)');
317
+ out(' link <remote> 本地:SSH 把 bridge 端口转发到本地(--bridge-port <本地> [--remote-bridge-port <远程>])');
318
+ out(' doctor <remote> 本地:自检远程链路断在哪层并打印恢复命令(--bridge-port <本地> [--dev-port <dev端口>])');
305
319
  out(' uninstall 移除上述注册(加 --purge 连 ~/.ui-inspector 数据一起删)');
306
320
  out(' status 查看安装状态');
307
- out(' bridge 启动 HTTP bridge(远程开发用,--port 默认 8917)');
321
+ out(' bridge 启动 HTTP bridge(--port 默认 8917,--host 默认 127.0.0.1;直连填 0.0.0.0)');
308
322
  }
309
323
 
310
324
  const cmd = process.argv[2] || 'help';
311
325
  if (cmd === 'install') {
312
- install({remote: process.argv.includes('--remote'), port: getOpt('--port', '8917')});
326
+ install({remote: process.argv.includes('--remote'), port: getOpt('--port', '8917'), host: getOpt('--host', null)});
313
327
  } else if (cmd === 'link') {
314
328
  link(parseLink());
315
329
  } else if (cmd === 'doctor') {
@@ -4,24 +4,37 @@
4
4
  * 两件事:
5
5
  * 1) 传输适配:把内容脚本的 ingest/state 转发给本地后端。
6
6
  * - native(默认,本地开发):chrome.runtime.sendNativeMessage 按需拉起 host.mjs。
7
- * - http(远程开发):fetch http://127.0.0.1:<bridgePort>(/ingest、/state),远程机上跑 bridge/server.mjs。
7
+ * - http(远程开发):fetch bridge 的 /ingest、/state。bridgeTarget 为端口(→127.0.0.1:端口)或完整地址。
8
8
  * 2) 动态注册内容脚本:目标域名不再硬编码在 manifest,改由弹窗(popup)写入 storage,
9
9
  * 这里按配置用 chrome.scripting 注册(MAIN + ISOLATED 两份)。
10
10
  */
11
11
  const HOST = 'com.oreate.ui_inspector';
12
12
 
13
13
  // localhost / 127.0.0.1 是通用开发地址,作默认种子;项目专属域名(如 local.oreateai.com)在弹窗里加。
14
- // 远程模式下扩展永远连本地 SSH 转发出来的 127.0.0.1,host 固定,只需配端口。
14
+ // bridgeTarget:隧道模式填端口(→ 127.0.0.1:端口);直连模式填 host:port 或完整 URL。
15
15
  const DEFAULTS = {
16
16
  domains: ['http://localhost/*', 'http://127.0.0.1/*'],
17
17
  transport: 'native',
18
- bridgePort: '8917',
18
+ bridgeTarget: '8917',
19
19
  };
20
20
 
21
21
  function getConfig() {
22
22
  return chrome.storage.local.get(DEFAULTS);
23
23
  }
24
24
 
25
+ // 把弹窗里的 bridgeTarget 归一成请求 base:
26
+ // 纯数字 → http://127.0.0.1:<端口>;带 http(s):// → 原样;其余 → 补 http://
27
+ function bridgeBase(target) {
28
+ const t = String(target || '').trim();
29
+ if (/^\d+$/.test(t)) {
30
+ return `http://127.0.0.1:${t}`;
31
+ }
32
+ if (/^https?:\/\//i.test(t)) {
33
+ return t.replace(/\/+$/, '');
34
+ }
35
+ return `http://${t}`.replace(/\/+$/, '');
36
+ }
37
+
25
38
  // ---------- 传输 ----------
26
39
  function viaNative(message) {
27
40
  return new Promise(resolve => {
@@ -35,8 +48,8 @@ function viaNative(message) {
35
48
  });
36
49
  }
37
50
 
38
- async function viaHttp(port, message) {
39
- const base = `http://127.0.0.1:${port}`;
51
+ async function viaHttp(target, message) {
52
+ const base = bridgeBase(target);
40
53
  try {
41
54
  if (message.type === 'ingest') {
42
55
  const r = await fetch(`${base}/ingest`, {
@@ -55,7 +68,7 @@ async function viaHttp(port, message) {
55
68
 
56
69
  async function dispatch(message) {
57
70
  const cfg = await getConfig();
58
- return cfg.transport === 'http' ? viaHttp(cfg.bridgePort, message) : viaNative(message);
71
+ return cfg.transport === 'http' ? viaHttp(cfg.bridgeTarget, message) : viaNative(message);
59
72
  }
60
73
 
61
74
  chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
@@ -126,9 +126,30 @@
126
126
  box.style.height = rect.height + 'px';
127
127
  }
128
128
 
129
+ // 元素还在页面上渲染吗:被移出 DOM 或 display:none 时 getClientRects() 为空。
130
+ function isRendered(el) {
131
+ return el && el.isConnected && el.getClientRects().length > 0;
132
+ }
133
+
134
+ // 每帧跟随元素位置;同时剔除已消失(被删除/隐藏)的选中项,连同高亮框一起移除。
129
135
  function repositionBoxes() {
130
- for (var i = 0; i < selected.length; i++) {
131
- placeBox(selected[i].box, selected[i].el);
136
+ var removed = 0;
137
+ for (var i = selected.length - 1; i >= 0; i--) {
138
+ if (isRendered(selected[i].el)) {
139
+ placeBox(selected[i].box, selected[i].el);
140
+ } else {
141
+ selected[i].box.remove();
142
+ selected.splice(i, 1);
143
+ removed++;
144
+ }
145
+ }
146
+ if (removed) {
147
+ postSelection();
148
+ showHint(
149
+ selected.length
150
+ ? '有选中元素已消失,已移除(当前 ' + selected.length + ' 个)'
151
+ : '选中元素已消失,已全部移除'
152
+ );
132
153
  }
133
154
  }
134
155
 
@@ -233,6 +254,14 @@
233
254
  window.addEventListener('scroll', repositionBoxes, true);
234
255
  window.addEventListener('resize', repositionBoxes, true);
235
256
 
257
+ // 每帧跟随并清理:元素被删/隐藏后,repositionBoxes 会移除它的高亮框(scroll/resize 不一定触发)。
258
+ (function raf() {
259
+ if (selected.length) {
260
+ repositionBoxes();
261
+ }
262
+ window.requestAnimationFrame(raf);
263
+ })();
264
+
236
265
  // bridge-content 轮询回发的消息:cleared(撤高亮)/ status(toast 提示)
237
266
  window.addEventListener('message', function (event) {
238
267
  if (event.source !== window || !event.data) {
@@ -106,7 +106,7 @@
106
106
  <input
107
107
  id="domain-input"
108
108
  type="text"
109
- placeholder="如 https://local.oreateai.com"
109
+ placeholder="如 https://example.com"
110
110
  />
111
111
  <button
112
112
  id="add-domain"
@@ -142,21 +142,15 @@
142
142
  class="row"
143
143
  style="margin-top: 6px"
144
144
  >
145
- <span style="color: #656d76">127.0.0.1 :</span>
146
145
  <input
147
- id="bridge-port"
146
+ id="bridge-target"
148
147
  type="text"
149
- inputmode="numeric"
150
- placeholder="8917"
151
- style="max-width: 90px"
148
+ placeholder="8917 或 devlocal.oreateai.com:8917"
152
149
  />
153
150
  </div>
154
151
  <div class="hint">
155
- 只填本地端口(host 固定 127.0.0.1)。远程开发:远程跑
156
- <code>ui-inspector bridge</code>
157
- ,
158
- <code>ui-inspector link</code>
159
- 把端口转发到本地。
152
+ 隧道模式填端口(如 <code>8917</code>,默认连本地 127.0.0.1);直连模式填完整地址(如
153
+ <code>devlocal.oreateai.com:8917</code>)。
160
154
  </div>
161
155
  </div>
162
156
  </section>
@@ -2,7 +2,7 @@
2
2
  const DEFAULTS = {
3
3
  domains: ['http://localhost/*', 'http://127.0.0.1/*'],
4
4
  transport: 'native',
5
- bridgePort: '8917',
5
+ bridgeTarget: '8917',
6
6
  };
7
7
  const PROTECTED = new Set(DEFAULTS.domains); // 默认项不可删、不回收权限
8
8
 
@@ -106,23 +106,22 @@ function toggleBridge() {
106
106
  }
107
107
 
108
108
  async function renderTransport() {
109
- const {transport, bridgePort} = await getConfig();
109
+ const {transport, bridgeTarget} = await getConfig();
110
110
  for (const r of document.querySelectorAll('input[name="transport"]')) {
111
111
  r.checked = r.value === transport;
112
112
  }
113
- $('bridge-port').value = bridgePort;
113
+ $('bridge-target').value = bridgeTarget;
114
114
  toggleBridge();
115
115
  }
116
116
 
117
117
  async function saveTransport() {
118
118
  const transport = document.querySelector('input[name="transport"]:checked')?.value || 'native';
119
- const raw = $('bridge-port').value.trim();
120
- if (transport === 'http' && !/^\d+$/.test(raw)) {
121
- msg('端口要是数字', false);
119
+ const bridgeTarget = $('bridge-target').value.trim() || DEFAULTS.bridgeTarget;
120
+ if (transport === 'http' && !bridgeTarget) {
121
+ msg('请填 bridge 端口或地址', false);
122
122
  return;
123
123
  }
124
- const bridgePort = raw || DEFAULTS.bridgePort;
125
- await chrome.storage.local.set({transport, bridgePort});
124
+ await chrome.storage.local.set({transport, bridgeTarget});
126
125
  msg('已保存传输设置');
127
126
  }
128
127
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comate-ui-inspector",
3
- "version": "0.1.2",
3
+ "version": "0.1.4-alpha.1",
4
4
  "description": "按住 Alt 点选页面元素,把「元素 + Vue 组件源码路径」交给 Comate 定位源码修改。浏览器扩展 + Native Messaging host + MCP + hook。",
5
5
  "type": "module",
6
6
  "license": "MIT",