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

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
@@ -43,6 +43,65 @@ function writeStore(list) {
43
43
  writeFileSync(storeFile, `${JSON.stringify(list, null, 4)}\n`, 'utf8');
44
44
  }
45
45
 
46
+ // 录制累积:跨刷新/跨页面逐条追加进 recording.json(缓冲在后端,不在页面内存)。
47
+ const REC_CAP = 1000;
48
+ function readRecording() {
49
+ try {
50
+ return JSON.parse(readFileSync(recordingFile, 'utf8'));
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ function writeRecording(rec) {
56
+ mkdirSync(dirname(recordingFile), {recursive: true});
57
+ writeFileSync(recordingFile, `${JSON.stringify(rec ?? null, null, 4)}\n`, 'utf8');
58
+ }
59
+ function recStart(startedAt, pageUrl) {
60
+ const cur = readRecording();
61
+ if (cur && cur.active && cur.startedAt === startedAt) {
62
+ return; // 同一会话重复 start(多标签页),忽略
63
+ }
64
+ writeRecording({active: true, startedAt, pageUrl, clicks: [], console: [], network: []});
65
+ }
66
+ function recEvent(kind, item) {
67
+ const cur = readRecording();
68
+ if (!cur || !cur.active) {
69
+ return;
70
+ }
71
+ const key = kind === 'click' ? 'clicks' : kind === 'console' ? 'console' : kind === 'network' ? 'network' : null;
72
+ if (!key) {
73
+ return;
74
+ }
75
+ if (!Array.isArray(cur[key])) {
76
+ cur[key] = [];
77
+ }
78
+ if (cur[key].length < REC_CAP) {
79
+ cur[key].push(item);
80
+ writeRecording(cur);
81
+ }
82
+ }
83
+ function recStop(endedAt) {
84
+ const cur = readRecording();
85
+ if (!cur) {
86
+ return;
87
+ }
88
+ cur.active = false;
89
+ cur.endedAt = endedAt || Date.now();
90
+ writeRecording(cur);
91
+ }
92
+
93
+ function readBody(req, cb) {
94
+ let body = '';
95
+ req.on('data', chunk => (body += chunk));
96
+ req.on('end', () => {
97
+ try {
98
+ cb(JSON.parse(body || '{}'));
99
+ } catch (error) {
100
+ cb(null, error);
101
+ }
102
+ });
103
+ }
104
+
46
105
  function cors(res) {
47
106
  res.setHeader('Access-Control-Allow-Origin', '*');
48
107
  res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
@@ -83,18 +142,33 @@ const server = createServer((req, res) => {
83
142
  json(res, 200, {ok: true, count: readStore().length, status: readStatus()});
84
143
  return;
85
144
  }
86
- if (req.method === 'POST' && url.pathname === '/recording') {
87
- let body = '';
88
- req.on('data', chunk => (body += chunk));
89
- req.on('end', () => {
90
- try {
91
- const {recording} = JSON.parse(body || '{}');
92
- mkdirSync(dirname(recordingFile), {recursive: true});
93
- writeFileSync(recordingFile, `${JSON.stringify(recording ?? null, null, 4)}\n`, 'utf8');
94
- json(res, 200, {ok: true});
95
- } catch (error) {
96
- json(res, 400, {ok: false, error: String(error)});
145
+ if (req.method === 'POST' && url.pathname === '/rec/start') {
146
+ readBody(req, (data, err) => {
147
+ if (err) {
148
+ return json(res, 400, {ok: false, error: String(err)});
149
+ }
150
+ recStart(data.startedAt, data.pageUrl);
151
+ json(res, 200, {ok: true});
152
+ });
153
+ return;
154
+ }
155
+ if (req.method === 'POST' && url.pathname === '/rec/event') {
156
+ readBody(req, (data, err) => {
157
+ if (err) {
158
+ return json(res, 400, {ok: false, error: String(err)});
159
+ }
160
+ recEvent(data.kind, data.item);
161
+ json(res, 200, {ok: true});
162
+ });
163
+ return;
164
+ }
165
+ if (req.method === 'POST' && url.pathname === '/rec/stop') {
166
+ readBody(req, (data, err) => {
167
+ if (err) {
168
+ return json(res, 400, {ok: false, error: String(err)});
97
169
  }
170
+ recStop(data.endedAt);
171
+ json(res, 200, {ok: true});
98
172
  });
99
173
  return;
100
174
  }
@@ -1,11 +1,11 @@
1
1
  /*
2
- * MAIN world 录制脚本(增强包):录制期间抓取
2
+ * MAIN world 录制脚本(增强包):录制期间抓取并**逐条上报**
3
3
  * - 点击的元素(含 Vue 组件源码链,和选择模式同源)
4
4
  * - console.log/info/warn/error
5
- * - 同域名下的 fetch / XMLHttpRequest(方法/URL/状态/耗时)
6
- * 由 bridge-content 依据 chrome.storage.recording 发来的 rec-control 起停;
7
- * 停止时把整段录制 postMessage 出去,经 bridge→background→host/bridge 落盘 recording.json。
8
- * 未开始录制时完全惰性,不打补丁、无开销。
5
+ * - 同域名下的 fetch / XMLHttpRequest(方法/URL/状态/发起与完成时间)
6
+ * 每个事件即时 postMessage 出去,经 bridge→background→host/bridge 追加进 recording.json。
7
+ * 这样页面刷新/跳转也不丢数据:缓冲在后端,不在页面内存,录制可跨页面连续进行。
8
+ * 本脚本自身无状态缓冲;未开始录制时完全惰性,不打补丁、无开销。
9
9
  */
10
10
  (function () {
11
11
  if (window.__oreateRecorder) {
@@ -13,7 +13,7 @@
13
13
  }
14
14
  window.__oreateRecorder = true;
15
15
 
16
- var rec = null; // 进行中的录制 {clicks,console,network,startedAt,pageUrl,opts}
16
+ var on = null; // 录制中:{console, network};未录制:null
17
17
  var saved = {}; // 被打补丁的原函数
18
18
 
19
19
  function cssPath(el) {
@@ -69,19 +69,25 @@
69
69
  }
70
70
  }
71
71
 
72
+ // 逐条上报:kind = click | console | network
73
+ function emit(kind, item) {
74
+ window.postMessage({__uiInspector: 'rec-event', kind: kind, item: item}, '*');
75
+ }
76
+
72
77
  function onClick(e) {
73
- if (!rec) {
78
+ if (!on) {
74
79
  return;
75
80
  }
76
81
  var el = e.target;
77
82
  if (!el || el.nodeType !== 1) {
78
83
  return;
79
84
  }
80
- rec.clicks.push({
85
+ emit('click', {
81
86
  selector: cssPath(el),
82
87
  tag: el.nodeName.toLowerCase(),
83
88
  text: (el.textContent || '').trim().slice(0, 80),
84
89
  componentChain: componentChain(el),
90
+ pageUrl: location.href,
85
91
  ts: Date.now(),
86
92
  });
87
93
  }
@@ -91,8 +97,8 @@
91
97
  saved['c_' + level] = console[level];
92
98
  console[level] = function () {
93
99
  try {
94
- if (rec) {
95
- rec.console.push({
100
+ if (on) {
101
+ emit('console', {
96
102
  level: level,
97
103
  text: Array.prototype.map.call(arguments, fmt).join(' ').slice(0, 2000),
98
104
  ts: Date.now(),
@@ -123,15 +129,15 @@
123
129
  var method = (init && init.method) || (input && input.method) || 'GET';
124
130
  var t0 = Date.now();
125
131
  var p = saved.fetch.apply(this, arguments);
126
- if (rec && sameHost(url)) {
132
+ if (on && sameHost(url)) {
127
133
  p.then(
128
134
  function (r) {
129
135
  var t1 = Date.now();
130
- rec.network.push({method: method, url: url, status: r.status, startTs: t0, endTs: t1, ms: t1 - t0});
136
+ emit('network', {method: method, url: url, status: r.status, startTs: t0, endTs: t1, ms: t1 - t0});
131
137
  },
132
138
  function () {
133
139
  var t1 = Date.now();
134
- rec.network.push({method: method, url: url, status: 0, startTs: t0, endTs: t1, ms: t1 - t0, error: true});
140
+ emit('network', {method: method, url: url, status: 0, startTs: t0, endTs: t1, ms: t1 - t0, error: true});
135
141
  }
136
142
  );
137
143
  }
@@ -149,9 +155,9 @@
149
155
  var info = xhr.__rec;
150
156
  if (info) {
151
157
  xhr.addEventListener('loadend', function () {
152
- if (rec && sameHost(info.url)) {
158
+ if (on && sameHost(info.url)) {
153
159
  var t1 = Date.now();
154
- rec.network.push({
160
+ emit('network', {
155
161
  method: info.method,
156
162
  url: info.url,
157
163
  status: xhr.status,
@@ -182,30 +188,27 @@
182
188
  }
183
189
 
184
190
  function start(opts) {
185
- if (rec) {
191
+ if (on) {
186
192
  return;
187
193
  }
188
- rec = {clicks: [], console: [], network: [], startedAt: Date.now(), pageUrl: location.href, opts: opts};
194
+ on = {console: !!opts.console, network: !!opts.network};
189
195
  document.addEventListener('click', onClick, true);
190
- if (opts.console) {
196
+ if (on.console) {
191
197
  startConsole();
192
198
  }
193
- if (opts.network) {
199
+ if (on.network) {
194
200
  startNetwork();
195
201
  }
196
202
  }
197
203
 
198
204
  function stop() {
199
- if (!rec) {
205
+ if (!on) {
200
206
  return;
201
207
  }
202
208
  document.removeEventListener('click', onClick, true);
203
209
  stopConsole();
204
210
  stopNetwork();
205
- rec.endedAt = Date.now();
206
- var payload = rec;
207
- rec = null;
208
- window.postMessage({__uiInspector: 'recording', recording: payload}, '*');
211
+ on = null;
209
212
  }
210
213
 
211
214
  window.addEventListener('message', function (ev) {
@@ -59,11 +59,27 @@ async function viaHttp(target, message) {
59
59
  });
60
60
  return await r.json();
61
61
  }
62
- if (message.type === 'ingest-recording') {
63
- const r = await fetch(`${base}/recording`, {
62
+ if (message.type === 'rec-start') {
63
+ const r = await fetch(`${base}/rec/start`, {
64
64
  method: 'POST',
65
65
  headers: {'content-type': 'application/json'},
66
- body: JSON.stringify({recording: message.recording || null}),
66
+ body: JSON.stringify({startedAt: message.startedAt, pageUrl: message.pageUrl}),
67
+ });
68
+ return await r.json();
69
+ }
70
+ if (message.type === 'rec-event') {
71
+ const r = await fetch(`${base}/rec/event`, {
72
+ method: 'POST',
73
+ headers: {'content-type': 'application/json'},
74
+ body: JSON.stringify({kind: message.kind, item: message.item}),
75
+ });
76
+ return await r.json();
77
+ }
78
+ if (message.type === 'rec-stop') {
79
+ const r = await fetch(`${base}/rec/stop`, {
80
+ method: 'POST',
81
+ headers: {'content-type': 'application/json'},
82
+ body: JSON.stringify({endedAt: message.endedAt}),
67
83
  });
68
84
  return await r.json();
69
85
  }
@@ -83,7 +99,7 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
83
99
  if (!msg || typeof msg.type !== 'string') {
84
100
  return;
85
101
  }
86
- if (msg.type === 'ingest' || msg.type === 'state' || msg.type === 'ingest-recording') {
102
+ if (msg.type === 'ingest' || msg.type === 'state' || msg.type === 'rec-start' || msg.type === 'rec-event' || msg.type === 'rec-stop') {
87
103
  dispatch(msg).then(sendResponse);
88
104
  return true; // 异步 sendResponse
89
105
  }
@@ -30,9 +30,83 @@
30
30
  }
31
31
  });
32
32
 
33
- // 录制(增强包):MAIN 的录制结果 → background 落盘 recording.json
33
+ // 录制起停:弹窗写 chrome.storage.recording,这里转成 rec-control 发给 MAIN 的 recorder。
34
+ function relayRecControl(r) {
35
+ window.postMessage(
36
+ {
37
+ __uiInspectorBridge: 'rec-control',
38
+ active: !!(r && r.active),
39
+ console: !!(r && r.console),
40
+ network: !!(r && r.network),
41
+ },
42
+ '*'
43
+ );
44
+ }
45
+
46
+ // 复用 inspector-main 的底部 pill 弹 toast(MAIN world 监听 'status')。
47
+ function toast(text, level) {
48
+ window.postMessage({__uiInspectorBridge: 'status', text: text, level: level || 'success'}, '*');
49
+ }
50
+
51
+ // 录制中页面顶部常驻提示条(ISOLATED world 建 DOM,右侧带停止按钮)。
52
+ var banner = null;
53
+ function ensureBanner() {
54
+ if (banner) {
55
+ return banner;
56
+ }
57
+ banner = document.createElement('div');
58
+ banner.style.cssText =
59
+ 'position:fixed;top:16px;left:50%;transform:translateX(-50%);z-index:2147483647;' +
60
+ 'display:none;align-items:center;gap:10px;padding:8px 10px 8px 14px;border-radius:999px;' +
61
+ 'background:#111;color:#fff;font:12px/1.4 sans-serif;box-shadow:0 2px 10px rgba(0,0,0,0.25);';
62
+ var style = document.createElement('style');
63
+ style.textContent =
64
+ '@keyframes oreateRecPulse{0%{box-shadow:0 0 0 0 rgba(255,90,90,0.7)}' +
65
+ '70%{box-shadow:0 0 0 6px rgba(255,90,90,0)}100%{box-shadow:0 0 0 0 rgba(255,90,90,0)}}';
66
+ var dot = document.createElement('span');
67
+ dot.style.cssText =
68
+ 'width:8px;height:8px;border-radius:50%;background:#ff5a5a;animation:oreateRecPulse 1.2s infinite;';
69
+ var label = document.createElement('span');
70
+ label.textContent = '正在录制…';
71
+ var stop = document.createElement('button');
72
+ stop.textContent = '停止';
73
+ stop.style.cssText =
74
+ 'border:none;border-radius:999px;padding:4px 12px;background:#ff5a5a;color:#fff;cursor:pointer;font:inherit;';
75
+ stop.onclick = stopRecording;
76
+ banner.appendChild(style);
77
+ banner.appendChild(dot);
78
+ banner.appendChild(label);
79
+ banner.appendChild(stop);
80
+ return banner;
81
+ }
82
+ function showBanner() {
83
+ ensureBanner();
84
+ if (!banner.isConnected && document.body) {
85
+ document.body.appendChild(banner);
86
+ }
87
+ banner.style.display = 'flex';
88
+ }
89
+ function hideBanner() {
90
+ if (banner) {
91
+ banner.style.display = 'none';
92
+ }
93
+ }
94
+ // 停止:置 recording.active=false(触发 onChanged → 停录 + 收起提示条),录制结果落盘后弹 toast。
95
+ function stopRecording() {
96
+ hideBanner();
97
+ if (!alive()) {
98
+ return;
99
+ }
100
+ try {
101
+ chrome.storage.local.set({recording: {active: false}}, () => void chrome.runtime.lastError);
102
+ } catch {
103
+ // 上下文失效
104
+ }
105
+ }
106
+
107
+ // MAIN 逐条上报的录制事件 → background 追加进 recording.json(跨刷新/跨页面累积)。
34
108
  window.addEventListener('message', event => {
35
- if (event.source !== window || !event.data || event.data.__uiInspector !== 'recording') {
109
+ if (event.source !== window || !event.data || event.data.__uiInspector !== 'rec-event') {
36
110
  return;
37
111
  }
38
112
  if (!alive()) {
@@ -40,7 +114,7 @@
40
114
  }
41
115
  try {
42
116
  chrome.runtime.sendMessage(
43
- {type: 'ingest-recording', recording: event.data.recording},
117
+ {type: 'rec-event', kind: event.data.kind, item: event.data.item},
44
118
  () => void chrome.runtime.lastError
45
119
  );
46
120
  } catch {
@@ -48,28 +122,56 @@
48
122
  }
49
123
  });
50
124
 
51
- // 录制起停:弹窗写 chrome.storage.recording,这里转成 rec-control 发给 MAIN 的 recorder。
52
- function relayRecControl(r) {
53
- window.postMessage(
54
- {
55
- __uiInspectorBridge: 'rec-control',
56
- active: !!(r && r.active),
57
- console: !!(r && r.console),
58
- network: !!(r && r.network),
59
- },
60
- '*'
61
- );
125
+ function recMsg(msg, cb) {
126
+ if (!alive()) {
127
+ return;
128
+ }
129
+ try {
130
+ chrome.runtime.sendMessage(msg, resp => {
131
+ void chrome.runtime.lastError;
132
+ if (cb) {
133
+ cb(resp);
134
+ }
135
+ });
136
+ } catch {
137
+ // 上下文失效
138
+ }
62
139
  }
140
+
63
141
  try {
64
142
  chrome.storage.local.get({recording: null}, o => {
65
143
  if (chrome.runtime.lastError || !o || !o.recording || !o.recording.active) {
66
144
  return;
67
145
  }
68
- relayRecControl(o.recording); // 页面在录制中途加载:补发开始
146
+ // 页面在录制中途加载:补发开始、显示提示条,但不重置后端已累积的数据(靠 startedAt 去重)。
147
+ relayRecControl(o.recording);
148
+ showBanner();
69
149
  });
70
150
  chrome.storage.onChanged.addListener((changes, area) => {
71
- if (area === 'local' && changes.recording) {
72
- relayRecControl(changes.recording.newValue);
151
+ if (area !== 'local' || !changes.recording) {
152
+ return;
153
+ }
154
+ const r = changes.recording.newValue;
155
+ const was = changes.recording.oldValue;
156
+ const nowActive = !!(r && r.active);
157
+ const wasActive = !!(was && was.active);
158
+ relayRecControl(r);
159
+ if (nowActive && !wasActive) {
160
+ // 新一轮录制:告知后端开新会话(按 startedAt 去重,多标签页不会互相清空)。
161
+ recMsg({type: 'rec-start', startedAt: (r && r.startedAt) || Date.now(), pageUrl: location.href});
162
+ showBanner();
163
+ } else if (!nowActive && wasActive) {
164
+ // 停止:收起提示条、后端收尾,成功后提示已带入会话。
165
+ hideBanner();
166
+ recMsg({type: 'rec-stop', endedAt: Date.now()}, resp => {
167
+ if (resp && resp.ok) {
168
+ toast('已更新到会话上下文中', 'success');
169
+ }
170
+ });
171
+ } else if (nowActive) {
172
+ showBanner();
173
+ } else {
174
+ hideBanner();
73
175
  }
74
176
  });
75
177
  } catch {
@@ -42,6 +42,54 @@ function writeStore(list) {
42
42
  writeFileSync(storeFile, `${JSON.stringify(list, null, 4)}\n`, 'utf8');
43
43
  }
44
44
 
45
+ // 录制累积:跨刷新/跨页面逐条追加进 recording.json(缓冲在后端,不在页面内存)。
46
+ const REC_CAP = 1000; // 每类事件上限,防爆
47
+ function readRecording() {
48
+ try {
49
+ return JSON.parse(readFileSync(recordingFile, 'utf8'));
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+ function writeRecording(rec) {
55
+ mkdirSync(dirname(recordingFile), {recursive: true});
56
+ writeFileSync(recordingFile, `${JSON.stringify(rec ?? null, null, 4)}\n`, 'utf8');
57
+ }
58
+ function recStart(startedAt, pageUrl) {
59
+ const cur = readRecording();
60
+ // 同一会话(startedAt 相同)重复 start(多标签页)直接忽略,避免互相清空。
61
+ if (cur && cur.active && cur.startedAt === startedAt) {
62
+ return;
63
+ }
64
+ writeRecording({active: true, startedAt, pageUrl, clicks: [], console: [], network: []});
65
+ }
66
+ function recEvent(kind, item) {
67
+ const cur = readRecording();
68
+ if (!cur || !cur.active) {
69
+ return; // 未在录制,丢弃
70
+ }
71
+ const key = kind === 'click' ? 'clicks' : kind === 'console' ? 'console' : kind === 'network' ? 'network' : null;
72
+ if (!key) {
73
+ return;
74
+ }
75
+ if (!Array.isArray(cur[key])) {
76
+ cur[key] = [];
77
+ }
78
+ if (cur[key].length < REC_CAP) {
79
+ cur[key].push(item);
80
+ writeRecording(cur);
81
+ }
82
+ }
83
+ function recStop(endedAt) {
84
+ const cur = readRecording();
85
+ if (!cur) {
86
+ return;
87
+ }
88
+ cur.active = false;
89
+ cur.endedAt = endedAt || Date.now();
90
+ writeRecording(cur);
91
+ }
92
+
45
93
  function send(obj) {
46
94
  const body = Buffer.from(JSON.stringify(obj), 'utf8');
47
95
  const header = Buffer.alloc(4);
@@ -58,9 +106,14 @@ function handle(msg) {
58
106
  resp = {ok: true, pending: targets.length};
59
107
  } else if (msg && msg.type === 'state') {
60
108
  resp = {ok: true, count: readStore().length, status: readStatus()};
61
- } else if (msg && msg.type === 'ingest-recording') {
62
- mkdirSync(dirname(recordingFile), {recursive: true});
63
- writeFileSync(recordingFile, `${JSON.stringify(msg.recording ?? null, null, 4)}\n`, 'utf8');
109
+ } else if (msg && msg.type === 'rec-start') {
110
+ recStart(msg.startedAt, msg.pageUrl);
111
+ resp = {ok: true};
112
+ } else if (msg && msg.type === 'rec-event') {
113
+ recEvent(msg.kind, msg.item);
114
+ resp = {ok: true};
115
+ } else if (msg && msg.type === 'rec-stop') {
116
+ recStop(msg.endedAt);
64
117
  resp = {ok: true};
65
118
  } else {
66
119
  resp = {ok: false, error: 'unknown message'};
@@ -114,6 +114,28 @@
114
114
  color: #fff;
115
115
  border-color: #1f6feb;
116
116
  }
117
+ /* 模式子 tab:与顶层「模式/设置」区分——走胶囊描边风格,选中为浅蓝底+蓝字,不填满。 */
118
+ .subtabs {
119
+ display: flex;
120
+ gap: 6px;
121
+ margin-bottom: 8px;
122
+ }
123
+ .subtab {
124
+ flex: 0 0 auto;
125
+ padding: 4px 12px;
126
+ border: 1px solid #d0d7de;
127
+ border-radius: 999px;
128
+ background: #fff;
129
+ color: #1f2328;
130
+ cursor: pointer;
131
+ font: inherit;
132
+ font-size: 12px;
133
+ }
134
+ .subtab.active {
135
+ background: #ddf4ff;
136
+ color: #0969da;
137
+ border-color: #54aeff;
138
+ }
117
139
  </style>
118
140
  </head>
119
141
  <body>
@@ -141,7 +163,7 @@
141
163
  <!-- 「模式」页:选择 Element + 各增强包模式(popup.js 读 enhancements.json 动态填充子 tab) -->
142
164
  <div id="top-mode">
143
165
  <div
144
- class="tabs"
166
+ class="subtabs"
145
167
  id="mode-tabs"
146
168
  ></div>
147
169
  <section
@@ -180,7 +180,7 @@ async function initModes() {
180
180
 
181
181
  function makeTab(key, label, active) {
182
182
  const b = document.createElement('button');
183
- b.className = active ? 'tab active' : 'tab';
183
+ b.className = active ? 'subtab active' : 'subtab';
184
184
  b.textContent = label;
185
185
  b.dataset.mode = key;
186
186
  b.onclick = () => switchMode(key);
@@ -189,7 +189,7 @@ async function initModes() {
189
189
  }
190
190
 
191
191
  function switchMode(key) {
192
- for (const b of tabsEl.querySelectorAll('.tab')) {
192
+ for (const b of tabsEl.querySelectorAll('.subtab')) {
193
193
  b.classList.toggle('active', b.dataset.mode === key);
194
194
  }
195
195
  for (const [k, el] of Object.entries(paneEls)) {
@@ -221,9 +221,12 @@ async function initModes() {
221
221
  }
222
222
  }
223
223
 
224
- // 只有「选择 Element」一个模式时,子 tab 条没有意义,隐藏它。
224
+ // 没有增强包:只有「选择 Element」一个模式,顶层「模式/设置」tab 失去意义——
225
+ // 隐藏顶层 tab 与模式页,直接展示「设置」面板内容。
225
226
  if (!plugins.length) {
226
- tabsEl.style.display = 'none';
227
+ $('top-tabs').style.display = 'none';
228
+ $('top-mode').style.display = 'none';
229
+ $('top-settings').style.display = 'block';
227
230
  }
228
231
  }
229
232
  initModes();
@@ -34,8 +34,11 @@ function readJson(file, fallback) {
34
34
  const parsed = readJson(storeFile, []);
35
35
  const list = Array.isArray(parsed) ? parsed : [];
36
36
  const recording = readJson(recordingFile, null);
37
+ // 只注入**已停止**的录制(active === false)。录制进行中(跨刷新/跨页面累积)时不消费、不清空,
38
+ // 以免中途给 Comate 发消息把整段会话吞掉。
39
+ const recDone = recording && recording.active === false;
37
40
  const hasRec =
38
- recording &&
41
+ recDone &&
39
42
  ((recording.clicks && recording.clicks.length) ||
40
43
  (recording.console && recording.console.length) ||
41
44
  (recording.network && recording.network.length));
@@ -110,11 +113,13 @@ sections.push(
110
113
  '请据此定位源码并按用户意见修改。页面选中态已自动取消;改动过程中可调用 MCP 工具 notify_browser 向页面推送状态(如「修改中…」「已完成」),会在浏览器弹 toast。'
111
114
  );
112
115
 
113
- // 「拿到即消费」:注入后清空 pending 与 recording
116
+ // 「拿到即消费」:注入后清空 pending;录制仅在确实注入(已停止)时清空,进行中的保留。
114
117
  try {
115
118
  mkdirSync(dirname(storeFile), {recursive: true});
116
119
  writeFileSync(storeFile, '[]\n', 'utf8');
117
- writeFileSync(recordingFile, 'null\n', 'utf8');
120
+ if (hasRec) {
121
+ writeFileSync(recordingFile, 'null\n', 'utf8');
122
+ }
118
123
  } catch {
119
124
  // 清空失败不影响注入
120
125
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comate-ui-inspector",
3
- "version": "0.1.5-alpha.2",
3
+ "version": "0.1.5-alpha.3",
4
4
  "description": "按住 Alt 点选页面元素,把「元素 + Vue 组件源码路径」交给 Comate 定位源码修改。浏览器扩展 + Native Messaging host + MCP + hook。",
5
5
  "type": "module",
6
6
  "license": "MIT",