@aiyiran/myclaw 1.1.152 → 1.1.156

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.
@@ -121,25 +121,40 @@ function appendDebugEvent(probeEvent) {
121
121
 
122
122
  /**
123
123
  * 控制日志长度,避免无限膨胀。
124
+ *
125
+ * 关键约束:作品可能在循环里疯狂打 console.log(每帧几十条),若简单地"保留
126
+ * 最近 N 条",会让 console 噪音把 page-enter / 点击 / 按键 / 报错等关键事件
127
+ * 全部挤掉。所以这里分两档:
128
+ * - 关键事件(导航 / 交互 / 各类报错)优先全量保留;
129
+ * - console 只保留最近一批,给它单独的配额。
124
130
  */
125
131
  function trimDebugEvents(events) {
126
132
  if (events.length <= MAX_EVENTS) {
127
133
  return;
128
134
  }
129
135
 
130
- const importantTypes = new Set([
136
+ const KEY_TYPES = new Set([
137
+ "probe-ready",
138
+ "page-enter",
139
+ "click",
140
+ "keydown",
131
141
  "runtime-error",
132
142
  "unhandledrejection",
133
- "resource-error"
143
+ "resource-error",
144
+ "console-truncated"
134
145
  ]);
135
146
 
136
- const importantEvents = events.filter((event) =>
137
- importantTypes.has(event.type)
138
- );
147
+ const keyEvents = events.filter((event) => KEY_TYPES.has(event.type));
148
+ const consoleEvents = events.filter((event) => !KEY_TYPES.has(event.type));
139
149
 
140
- const latestEvents = events.slice(-MAX_EVENTS);
150
+ // console 配额 = 总上限减去关键事件占用,至少留一部分给 console。
151
+ const consoleQuota = Math.max(0, MAX_EVENTS - keyEvents.length);
152
+ const keptConsole = consoleEvents.slice(-consoleQuota);
141
153
 
142
- const merged = [...importantEvents, ...latestEvents];
154
+ // 合并后按 seq 还原时间顺序,再做去重。
155
+ const merged = [...keyEvents, ...keptConsole].sort(
156
+ (a, b) => (a.seq || 0) - (b.seq || 0)
157
+ );
143
158
 
144
159
  const seen = new Set();
145
160
  const deduped = [];
@@ -317,6 +332,16 @@ function renderDebugRecordPreview(record) {
317
332
  function formatEventForStudent(event) {
318
333
  const time = event.time ? event.time.slice(11, 19) : "";
319
334
 
335
+ if (event.type === "probe-ready") {
336
+ const version = event.payload && event.payload.probeVersion;
337
+ return `${time} 调试探针启动${version ? "(" + version + ")" : ""}`;
338
+ }
339
+
340
+ if (event.type === "console-truncated") {
341
+ const limit = event.payload && event.payload.limit;
342
+ return `${time} 程序输出过多,已截断${limit ? "(上限 " + limit + " 条)" : ""}`;
343
+ }
344
+
320
345
  if (event.type === "page-enter") {
321
346
  return `${time} 进入页面:${event.pageTitle || event.pagePath || event.pageUrl}`;
322
347
  }
@@ -19,11 +19,18 @@
19
19
 
20
20
  window.__OPENCLAW_DEBUG_PROBE_INSTALLED__ = true;
21
21
 
22
- const PROBE_VERSION = "probe_003";
22
+ const PROBE_VERSION = "probe_004";
23
23
  const MAX_TEXT_LENGTH = 1000;
24
24
  const MAX_STACK_LENGTH = 3000;
25
25
  const MAX_ELEMENT_TEXT_LENGTH = 80;
26
26
 
27
+ // console 上报限流:作品可能在循环里疯狂打 log,无节制上报会刷爆父页面
28
+ // 的事件缓冲、把 page-enter / error 等关键事件挤掉。超过上限后停止上报
29
+ // console(其它类型不受影响),并补一条截断提示。
30
+ const MAX_CONSOLE_EVENTS = 100;
31
+ let _consoleEventCount = 0;
32
+ let _consoleTruncatedNotified = false;
33
+
27
34
  function now() {
28
35
  return new Date().toISOString();
29
36
  }
@@ -222,11 +229,22 @@
222
229
  original.apply(console, args);
223
230
  } catch (error) {}
224
231
 
232
+ // error 级别始终上报(数量少且关键);log/warn 受限流约束。
233
+ if (level !== "error" && _consoleEventCount >= MAX_CONSOLE_EVENTS) {
234
+ if (!_consoleTruncatedNotified) {
235
+ _consoleTruncatedNotified = true;
236
+ sendEvent("console-truncated", { limit: MAX_CONSOLE_EVENTS });
237
+ }
238
+ return;
239
+ }
240
+
225
241
  try {
226
242
  const stringArgs = args.map(function (arg) {
227
243
  return truncate(safeStringify(arg), MAX_TEXT_LENGTH);
228
244
  });
229
245
 
246
+ if (level !== "error") _consoleEventCount++;
247
+
230
248
  sendEvent("console", {
231
249
  level,
232
250
  message: truncate(stringArgs.join(" "), MAX_TEXT_LENGTH),
@@ -237,11 +255,21 @@
237
255
  });
238
256
  }
239
257
 
240
- // 去重:避免 setTimeout 拦截 和 window.error 事件 重复上报同一个错误
258
+ // 去重:避免 setTimeout 拦截 和 window.error 事件 重复上报同一个错误。
259
+ // 两条路径拿到的 message 不一致:setTimeout 拦截器拿到原始 error.message
260
+ // (如 "Cannot read..."),window error 事件则带 "Uncaught TypeError: " 前缀。
261
+ // 归一化时剥掉这个前缀,确保同一个错误只上报一次。
241
262
  const _reportedErrorKeys = new Set();
242
263
 
264
+ function normalizeErrorKey(message) {
265
+ return truncate(
266
+ String(message || "").replace(/^Uncaught\s+(\w*Error)?:?\s*/i, ""),
267
+ 200
268
+ );
269
+ }
270
+
243
271
  function reportRuntimeError(message, filename, lineno, colno, stack) {
244
- const key = truncate(message, 200);
272
+ const key = normalizeErrorKey(message);
245
273
  if (_reportedErrorKeys.has(key)) return;
246
274
  _reportedErrorKeys.add(key);
247
275
  sendEvent("runtime-error", {
@@ -361,6 +389,13 @@
361
389
  setupClickListener();
362
390
  setupKeydownListener();
363
391
 
392
+ // 启动标记:版本号 + 时间。
393
+ // - 独立页面:直接打到控制台,方便人工确认 probe 已就位、是哪个版本。
394
+ // - iframe 模式:作为本轮记录的明确起点事件,便于排查"是否新一轮"。
395
+ const startedAt = now();
396
+ _nativeLog("[PROBE] " + PROBE_VERSION + " 已启动 @ " + startedAt);
397
+ sendEvent("probe-ready", { probeVersion: PROBE_VERSION, startedAt });
398
+
364
399
  if (document.readyState === "loading") {
365
400
  document.addEventListener("DOMContentLoaded", sendPageEnter);
366
401
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiyiran/myclaw",
3
- "version": "1.1.152",
3
+ "version": "1.1.156",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -423,12 +423,31 @@ def _get_probe():
423
423
  return _probe_cache
424
424
 
425
425
 
426
+ def _find_probe_insert_pos(html):
427
+ """决定 probe 注入位置:越早越好,确保 probe 在所有作品脚本之前运行,
428
+ 从而 window error listener 能捕获 head/body 任何同步阶段的报错。
429
+ 逐级降级:<head> 开始标签后 → <html> 开始标签后 → <body> 开始标签后 → 文档开头。
430
+ 返回插入下标(在该下标处插入 tag)。"""
431
+ lower = html.lower()
432
+ for open_tag in ("<head", "<html", "<body"):
433
+ idx = lower.find(open_tag)
434
+ if idx != -1:
435
+ tag_end = html.find(">", idx)
436
+ if tag_end != -1:
437
+ return tag_end + 1
438
+ return 0 # 三个标签都没有:插到最前面
439
+
440
+
426
441
  def inject_probe_into_html(html_bytes):
427
442
  """对 HTML 字节注入/更新 probe,返回处理后的 bytes。
428
443
  - 已含当前版本 → 原样返回(②)
429
444
  - 含旧版本 → 移除旧 probe 再注入新版(③)
430
- - 不含 → 注入到 </body> 前;无 </body> 则追加末尾(③)
431
- probe 不可用、或内容非 UTF-8 文本时,原样返回。"""
445
+ - 不含 → 注入到尽可能靠前的位置(见 _find_probe_insert_pos)(③)
446
+ probe 不可用、或内容非 UTF-8 文本时,原样返回。
447
+
448
+ ⚠️ probe 必须在作品脚本之前加载,否则 init() 等同步阶段的报错会在
449
+ window error listener 安装之前发生,无法被捕获。所以注入到 <head>/<html>
450
+ 开始标签之后,而非 </body> 之前。"""
432
451
  version, src = _get_probe()
433
452
  if not version or not src:
434
453
  return html_bytes
@@ -449,17 +468,8 @@ def inject_probe_into_html(html_bytes):
449
468
  )
450
469
 
451
470
  tag = f'<script {PROBE_MARKER_ATTR}="{version}">\n{src}\n</script>'
452
- # 注入到 <body> 开头而非 </body> 前,确保 probe 在游戏脚本之前加载,
453
- # 能捕获 init() 等同步执行阶段的报错。
454
- body_open = html.lower().find("<body")
455
- if body_open != -1:
456
- body_tag_end = html.find(">", body_open)
457
- if body_tag_end != -1:
458
- html = html[:body_tag_end + 1] + "\n" + tag + html[body_tag_end + 1:]
459
- else:
460
- html = html + "\n" + tag
461
- else:
462
- html = html + "\n" + tag
471
+ pos = _find_probe_insert_pos(html)
472
+ html = html[:pos] + "\n" + tag + html[pos:]
463
473
  return html.encode("utf-8")
464
474
 
465
475