@aiyiran/myclaw 1.1.135 → 1.1.137

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.
@@ -808,8 +808,14 @@
808
808
  'background: #252536',
809
809
  ].join(';');
810
810
 
811
- // fetch → blob URL,绕过 CSP img-src 限制
811
+ // 远程环境:走 CDN(边缘缓存,速度快,不受本机出口带宽限制)
812
+ // 本地环境:走本地 API(文件尚未同步到 CDN)
813
+ // CSP img-src 限制后续在服务端统一放行,此处不做 blob 绕行
812
814
  var img = document.createElement('img');
815
+ var _imgEnv = detectEnvironment();
816
+ img.src = _imgEnv.remote
817
+ ? previewUrl + '?t=' + Date.now()
818
+ : MYCLAW_API_BASE + '/api/file?path=' + encodeURIComponent(getWorkspaceId() + '/' + asset.path) + '&t=' + Date.now();
813
819
  img.alt = asset.name || asset.path;
814
820
  img.style.cssText = [
815
821
  'max-width: 100%',
@@ -820,28 +826,13 @@
820
826
  'position: relative',
821
827
  'z-index: 1',
822
828
  ].join(';');
823
-
824
- function showImgError() {
829
+ img.onerror = function () {
825
830
  img.style.display = 'none';
826
831
  var errMsg = document.createElement('div');
827
832
  errMsg.textContent = '加载失败';
828
833
  errMsg.style.cssText = 'color:#666;font-size:13px;font-family:monospace;';
829
834
  imgArea.appendChild(errMsg);
830
- }
831
-
832
- var localImgUrl = MYCLAW_API_BASE + '/api/file?path=' + encodeURIComponent(getWorkspaceId() + '/' + asset.path) + '&t=' + Date.now();
833
- fetch(localImgUrl)
834
- .then(function (r) {
835
- if (!r.ok) throw new Error('HTTP ' + r.status);
836
- return r.blob();
837
- })
838
- .then(function (blob) {
839
- var blobUrl = URL.createObjectURL(blob);
840
- img.onload = function () { URL.revokeObjectURL(blobUrl); };
841
- img.onerror = showImgError;
842
- img.src = blobUrl;
843
- })
844
- .catch(showImgError);
835
+ };
845
836
 
846
837
  imgArea.appendChild(img);
847
838
  imgBox.appendChild(imgArea);
@@ -1179,11 +1170,17 @@
1179
1170
  var isVideo = VIDEO_EXTS.indexOf(assetExt) !== -1;
1180
1171
 
1181
1172
  if (isVideo) {
1182
- // 视频用 <video> 元素 + 本地 API,支持流式加载和拖拽进度条
1173
+ // <video> 元素支持 range 请求,流式加载,可拖拽进度条,不用 iframe
1174
+ // 远程环境:走 CDN(带宽快,不受本机出口限制)
1175
+ // 本地环境:走本地 API(文件尚未同步到 CDN)
1176
+ // CSP media-src 限制后续在服务端统一放行
1183
1177
  var video = document.createElement('video');
1184
1178
  video.controls = true;
1185
1179
  video.style.cssText = 'flex:1;width:100%;max-height:100%;background:#000;outline:none;';
1186
- video.src = MYCLAW_API_BASE + '/api/file?path=' + encodeURIComponent(getWorkspaceId() + '/' + asset.path);
1180
+ var _videoEnv = detectEnvironment();
1181
+ video.src = _videoEnv.remote
1182
+ ? previewUrl + '?t=' + Date.now()
1183
+ : MYCLAW_API_BASE + '/api/file?path=' + encodeURIComponent(getWorkspaceId() + '/' + asset.path);
1187
1184
  box.appendChild(video);
1188
1185
  overlay.appendChild(box);
1189
1186
  document.body.appendChild(overlay);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiyiran/myclaw",
3
- "version": "1.1.135",
3
+ "version": "1.1.137",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
package/patches/patch.js CHANGED
@@ -113,6 +113,19 @@ function getMyclawVersion() {
113
113
  }
114
114
  }
115
115
 
116
+ /**
117
+ * 获取已安装的 OpenClaw 版本号
118
+ * uiDir 形如 <pkg>/dist/control-ui,包根目录为 uiDir/../..
119
+ */
120
+ function getOpenclawVersion(uiDir) {
121
+ try {
122
+ const pkgRoot = path.resolve(uiDir, '..', '..');
123
+ return require(path.join(pkgRoot, 'package.json')).version;
124
+ } catch {
125
+ return 'unknown';
126
+ }
127
+ }
128
+
116
129
  /**
117
130
  * 执行注入
118
131
  */
@@ -250,6 +263,13 @@ function patch() {
250
263
  let patched = false;
251
264
  let foundTarget = false;
252
265
 
266
+ // img-src / media-src 放宽仅针对特定 OpenClaw 版本(2026.6.9)。
267
+ // 该版本 CSP 中 img-src/media-src 为 'self' data: blob:,会拦截 CDN 图片/视频;
268
+ // 限定版本可避免误改其它版本(CSP 结构可能不同,盲目放宽有风险)。
269
+ const ocVersion = getOpenclawVersion(uiDir);
270
+ const needsMediaCspFix = ocVersion === '2026.6.9';
271
+ console.log('[myclaw-patch] → OpenClaw 版本: ' + ocVersion + (needsMediaCspFix ? ' (将额外放宽 img-src/media-src)' : ''));
272
+
253
273
  for (const f of distFiles) {
254
274
  // 兼容多版本:gateway-cli-*.js(旧)、server-*.js(中)、control-ui-*.js(新)
255
275
  const isTarget = (f.startsWith('gateway-cli-') || f.startsWith('server-') || f.startsWith('control-ui-')) && f.endsWith('.js');
@@ -264,7 +284,9 @@ function patch() {
264
284
  const needsFrameSrc = !content.includes('"frame-src *');
265
285
  const needsConnectSrc = !content.includes("connect-src *");
266
286
  const needsFrameAncestors = !content.includes("frame-ancestors *");
267
- const needsAnyCspPatch = needsFrameSrc || needsConnectSrc || needsFrameAncestors;
287
+ const needsImgSrc = needsMediaCspFix && !content.includes('"img-src *');
288
+ const needsMediaSrc = needsMediaCspFix && !content.includes('"media-src *');
289
+ const needsAnyCspPatch = needsFrameSrc || needsConnectSrc || needsFrameAncestors || needsImgSrc || needsMediaSrc;
268
290
 
269
291
  if (needsMicrophonePatch || needsAnyCspPatch) {
270
292
  // 备份
@@ -306,6 +328,16 @@ function patch() {
306
328
  content = content.replace(/\"frame-ancestors[^"]*\"/g, '"frame-ancestors *"');
307
329
  cspPatches.push('frame-ancestors *');
308
330
  }
331
+ // img-src → 允许所有(仅 2026.6.9,放行 CDN 图片)
332
+ if (needsImgSrc) {
333
+ content = content.replace(/\"img-src[^"]*\"/g, '"img-src *"');
334
+ cspPatches.push('img-src *');
335
+ }
336
+ // media-src → 允许所有(仅 2026.6.9,放行 CDN 视频)
337
+ if (needsMediaSrc) {
338
+ content = content.replace(/\"media-src[^"]*\"/g, '"media-src *"');
339
+ cspPatches.push('media-src *');
340
+ }
309
341
  if (cspPatches.length > 0) {
310
342
  console.log('[myclaw-patch] ✅ 已修复 CSP (' + cspPatches.join(', ') + '): ' + f);
311
343
  }
@@ -3,20 +3,25 @@
3
3
  Extract chat history from OpenClaw session URL(s) and generate JS data file(s).
4
4
 
5
5
  Usage:
6
- # 单个会话
7
- python3 extract_chat.py <session-url-or-key> [output-dir]
6
+ # 单个会话(输出到当前 workspace)
7
+ python3 extract_chat.py <session-url-or-key>
8
8
 
9
9
  # 多个会话(逗号分隔 / JSON 数组)
10
10
  python3 extract_chat.py "url1,url2,url3" [output-dir]
11
- python3 extract_chat.py '["url1","url2","url3"]' [output-dir]
11
+ python3 extract_chat.py '[ "url1","url2","url3"]' [output-dir]
12
12
 
13
13
  # 扫描所有 agent 的所有会话,统计表格
14
- python3 extract_chat.py --scan [output-dir]
14
+ python3 extract_chat.py --scan [--html]
15
15
 
16
- 输出:
16
+ 输出(默认到当前 workspace /root/.openclaw/workspace-teacher):
17
17
  - 每个会话生成独立的 <agentId>_<sessionName>_<date>_<time>.js
18
18
  - 生成 index.js,包含所有会话的元信息列表(含 work_url 字段)
19
19
  - 生成 chat_history.js(向后兼容,指向第一个会话)
20
+ - --html 时额外生成 summary.html(含可点击链接)
21
+
22
+ Host 检测:
23
+ - 自动从 /root/.openclaw/myclaw/server/config.json 读取
24
+ - 可用 --host 覆盖
20
25
 
21
26
  work_url 字段:
22
27
  - 默认为空字符串
@@ -34,6 +39,31 @@ from datetime import datetime, timezone, timedelta
34
39
  tz_beijing = timezone(timedelta(hours=8))
35
40
 
36
41
 
42
+ def detect_chat_host():
43
+ """Auto-detect chat host from config.json."""
44
+ config_path = '/root/.openclaw/myclaw/server/config.json'
45
+ try:
46
+ with open(config_path) as f:
47
+ cfg = json.load(f)
48
+ claw = cfg.get('claw', 'claw5')
49
+ return f"{claw}.kekouen.cn"
50
+ except Exception:
51
+ return 'claw5.kekouen.cn'
52
+
53
+
54
+ def get_default_output_dir():
55
+ """Return the current workspace directory."""
56
+ # OPENCLAW_WORKSPACE is set to the workspace path
57
+ ws = os.environ.get('OPENCLAW_WORKSPACE', '')
58
+ if ws and os.path.isdir(ws):
59
+ return ws
60
+ # Fallback: derive from cwd (e.g. /root/.openclaw/workspace-teacher)
61
+ cwd = os.getcwd()
62
+ if '/workspace-' in cwd:
63
+ return cwd
64
+ return cwd
65
+
66
+
37
67
  def parse_time(ts_str):
38
68
  try:
39
69
  ts_str = ts_str.replace('Z', '')
@@ -202,10 +232,12 @@ const chatData = {json.dumps(output_data, ensure_ascii=False, indent=2)};
202
232
  return len(conversations), session_name
203
233
 
204
234
 
205
- def session_key_to_url(session_key):
206
- """Build the claw6 chat URL from a session key."""
235
+ def session_key_to_url(session_key, host=None):
236
+ """Build the chat URL from a session key. Host auto-detected from config.json."""
237
+ if host is None:
238
+ host = detect_chat_host()
207
239
  encoded = quote(session_key, safe='')
208
- return f"https://claw6.kekouen.cn/chat?session={encoded}"
240
+ return f"https://{host}/chat?session={encoded}&token=aiyiran"
209
241
 
210
242
 
211
243
  def scan_all_agents():
@@ -238,21 +270,141 @@ def scan_all_agents():
238
270
  return results
239
271
 
240
272
 
241
- def print_summary_table(entries):
273
+ def print_summary_table(entries, host=None):
274
+ if host is None:
275
+ host = detect_chat_host()
242
276
  """
243
277
  Print the summary table.
244
278
  entries: list of dicts with keys: index, session_name, agent_id, total_pairs, work_url, session_key, first_time, last_time
245
279
  """
246
- print(f"\n{'#':>3} {'workspace':<16} {'会话名':<28} {'对话数':>6} {'起始时间':<20} {'最后更新':<20} {'作品链接':<12} {'聊天记录URL'}")
247
- print(f"{'-'*3} {'-'*16} {'-'*28} {'-'*6} {'-'*20} {'-'*20} {'-'*12} {'-'*50}")
280
+ print(f"\n{'#':>3} {'workspace':<16} {'会话名':<28} {'对话数':>6} {'起始时间':<20} {'最后更新':<20} {'作品链接':<14} {'对话链接'}")
281
+ print(f"{'-'*3} {'-'*16} {'-'*28} {'-'*6} {'-'*20} {'-'*20} {'-'*14} {'-'*50}")
248
282
  for e in entries:
249
283
  work = e.get('work_url') or '(待填)'
250
- url = session_key_to_url(e['session_key'])
284
+ url = session_key_to_url(e['session_key'], host)
251
285
  name = (e['session_name'] or '')[:26]
252
286
  agent = (e.get('agent_id') or '')[:16]
253
287
  first_t = (e.get('first_time') or '')[:19]
254
288
  last_t = (e.get('last_time') or '')[:19]
255
- print(f"{e['index']:>3} {agent:<16} {name:<28} {e['total_pairs']:>6} {first_t:<20} {last_t:<20} {work:<12} {url}")
289
+ print(f"{e['index']:>3} {agent:<16} {name:<28} {e['total_pairs']:>6} {first_t:<20} {last_t:<20} {work:<14} {url}")
290
+
291
+
292
+ def generate_html_table(entries, host, output_path):
293
+ """Generate an HTML file with a clickable table of all sessions."""
294
+ total_pairs = sum(e['total_pairs'] for e in entries)
295
+ rows_html = []
296
+ for e in entries:
297
+ work_url = e.get('work_url', '')
298
+ if work_url:
299
+ work_cell = f'<a href="{work_url}" target="_blank" class="link">🔗 查看作品</a>'
300
+ else:
301
+ work_cell = '<span class="pending">(待填)</span>'
302
+
303
+ chat_url = session_key_to_url(e['session_key'], host)
304
+ name = e.get('session_name', '') or ''
305
+ agent = e.get('agent_id', '') or ''
306
+ chat_cell = f'<a href="{chat_url}" target="_blank" class="link">💬 打开对话</a>'
307
+
308
+ rows_html.append(f"""
309
+ <tr>
310
+ <td class="num">{e['index']}</td>
311
+ <td>{agent}</td>
312
+ <td>{name}</td>
313
+ <td class="num">{e['total_pairs']}</td>
314
+ <td>{e.get('first_time', '')}</td>
315
+ <td>{e.get('last_time', '')}</td>
316
+ <td>{work_cell}</td>
317
+ <td>{chat_cell}</td>
318
+ </tr>""")
319
+
320
+ rows = '\n'.join(rows_html)
321
+ html = f'''<!DOCTYPE html>
322
+ <html lang="zh-CN">
323
+ <head>
324
+ <meta charset="UTF-8">
325
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
326
+ <title>作品扫描 - {len(entries)} sessions</title>
327
+ <style>
328
+ * {{ box-sizing: border-box; }}
329
+ body {{
330
+ font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
331
+ background: linear-gradient(135deg, #f5f7fa 0%, #e9ecf3 100%);
332
+ margin: 0; padding: 24px; min-height: 100vh; color: #2d3748;
333
+ }}
334
+ .container {{ max-width: 1200px; margin: 0 auto; }}
335
+ .header {{
336
+ background: linear-gradient(135deg, #667eea, #764ba2);
337
+ color: white; padding: 30px; border-radius: 16px; margin-bottom: 24px;
338
+ box-shadow: 0 10px 30px rgba(102,126,234,0.3);
339
+ }}
340
+ .header h1 {{ margin: 0 0 8px 0; font-size: 1.8em; }}
341
+ .header p {{ margin: 0; opacity: 0.9; }}
342
+ .summary-bar {{
343
+ display: flex; gap: 16px; margin-top: 16px; flex-wrap: wrap;
344
+ }}
345
+ .summary-bar .stat {{
346
+ background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 8px;
347
+ backdrop-filter: blur(10px);
348
+ }}
349
+ .stat strong {{ font-size: 1.3em; margin-right: 6px; }}
350
+ table {{
351
+ width: 100%; background: white; border-radius: 12px; overflow: hidden;
352
+ border-collapse: collapse; box-shadow: 0 4px 20px rgba(0,0,0,0.08);
353
+ }}
354
+ th {{
355
+ background: #f7fafc; padding: 14px 12px; text-align: left;
356
+ font-weight: 600; color: #4a5568; border-bottom: 2px solid #e2e8f0;
357
+ font-size: 0.9em;
358
+ }}
359
+ td {{ padding: 12px; border-bottom: 1px solid #edf2f7; font-size: 0.92em; }}
360
+ tr:hover td {{ background: #f7fafc; }}
361
+ .num {{ text-align: right; font-variant-numeric: tabular-nums; }}
362
+ .link {{
363
+ color: #667eea; text-decoration: none; font-weight: 500;
364
+ padding: 4px 10px; border-radius: 6px; background: #edf2f7;
365
+ transition: all 0.2s; display: inline-block;
366
+ }}
367
+ .link:hover {{ background: #667eea; color: white; transform: translateY(-1px); }}
368
+ .pending {{ color: #a0aec0; font-style: italic; }}
369
+ .footer {{
370
+ text-align: center; margin-top: 24px; color: #718096; font-size: 0.85em;
371
+ }}
372
+ </style>
373
+ </head>
374
+ <body>
375
+ <div class="container">
376
+ <div class="header">
377
+ <h1>📋 作品扫描</h1>
378
+ <p>Generated by chat-history-extractor</p>
379
+ <div class="summary-bar">
380
+ <div class="stat"><strong>{len(entries)}</strong> sessions</div>
381
+ <div class="stat"><strong>{total_pairs}</strong> conversation pairs</div>
382
+ <div class="stat"><strong>host:</strong> {host}</div>
383
+ </div>
384
+ </div>
385
+ <table>
386
+ <thead>
387
+ <tr>
388
+ <th class="num">#</th>
389
+ <th>workspace</th>
390
+ <th>会话名</th>
391
+ <th class="num">对话数</th>
392
+ <th>起始时间</th>
393
+ <th>最后更新</th>
394
+ <th>作品链接</th>
395
+ <th>对话链接</th>
396
+ </tr>
397
+ </thead>
398
+ <tbody>{rows}
399
+ </tbody>
400
+ </table>
401
+ <p class="footer">提示:点击「作品链接」和「对话链接」在新页面打开。后续可在 JS 文件中编辑 work_url 字段补上作品链接。</p>
402
+ </div>
403
+ </body>
404
+ </html>
405
+ '''
406
+ with open(output_path, 'w', encoding='utf-8') as f:
407
+ f.write(html)
256
408
 
257
409
 
258
410
  def process_one(url_or_key, output_dir, index):
@@ -316,8 +468,10 @@ const chatIndex = {json.dumps(sessions, ensure_ascii=False, indent=2)};
316
468
  print(f"\nIndex: {index_path} ({len(sessions)} sessions)")
317
469
 
318
470
 
319
- def run_scan(output_dir=None):
320
- """Scan all agents, count conversations, show table. Optionally generate JS files."""
471
+ def run_scan(output_dir=None, host=None, html_output=False):
472
+ if host is None:
473
+ host = detect_chat_host()
474
+ """Scan all agents, count conversations, show table. Optionally generate JS files + HTML."""
321
475
  all_sessions = scan_all_agents()
322
476
  print(f"Found {len(all_sessions)} session(s) across all agents.\n")
323
477
 
@@ -340,7 +494,13 @@ def run_scan(output_dir=None):
340
494
  except Exception as e:
341
495
  print(f" [SKIP] {session_key}: {e}")
342
496
 
343
- print_summary_table(entries)
497
+ # Sort by first_time descending (newest first)
498
+ entries.sort(key=lambda e: (e.get('first_time') or '') or '', reverse=True)
499
+ # Re-assign index after sorting
500
+ for i, e in enumerate(entries, 1):
501
+ e['index'] = i
502
+
503
+ print_summary_table(entries, host)
344
504
 
345
505
  # Generate JS files if output_dir specified
346
506
  if output_dir:
@@ -363,32 +523,71 @@ def run_scan(output_dir=None):
363
523
  import shutil
364
524
  shutil.copy2(first_js, compat_js)
365
525
 
526
+ # Generate HTML table with clickable links
527
+ if html_output:
528
+ target_dir = output_dir if output_dir else os.getcwd()
529
+ os.makedirs(target_dir, exist_ok=True)
530
+ now_str = datetime.now().strftime('%Y%m%d_%H%M%S')
531
+ html_filename = f"作品扫描_{now_str}.html"
532
+ html_path = os.path.join(target_dir, html_filename)
533
+ generate_html_table(entries, host, html_path)
534
+ print(f"HTML: {html_path}")
535
+
366
536
  total_pairs = sum(e['total_pairs'] for e in entries)
367
537
  print(f"\n合计:{len(entries)} 个会话,{total_pairs} 对对话。")
368
538
  return entries
369
539
 
370
540
 
371
541
  def main():
372
- if len(sys.argv) < 2:
542
+ # Parse flags: --host HOST, --html, --scan
543
+ # Position: positional args (no --) come first, flags can be interleaved
544
+ args = sys.argv[1:]
545
+
546
+ host = detect_chat_host()
547
+ html_output = False
548
+ scan_mode = False
549
+ positional = []
550
+
551
+ i = 0
552
+ while i < len(args):
553
+ a = args[i]
554
+ if a == '--host' and i + 1 < len(args):
555
+ host = args[i + 1]
556
+ i += 2
557
+ elif a == '--html':
558
+ html_output = True
559
+ i += 1
560
+ elif a == '--scan':
561
+ scan_mode = True
562
+ i += 1
563
+ else:
564
+ positional.append(a)
565
+ i += 1
566
+
567
+ if not positional and not scan_mode:
373
568
  print("Usage:")
374
- print(" python3 extract_chat.py <url-or-key> [output-dir]")
375
- print(" python3 extract_chat.py 'url1,url2,url3' [output-dir]")
376
- print(' python3 extract_chat.py \'["url1","url2"]\' [output-dir]')
377
- print(" python3 extract_chat.py --scan [output-dir]")
569
+ print(" python3 extract_chat.py <url-or-key> [output-dir] [--host HOST] [--html]")
570
+ print(" python3 extract_chat.py 'url1,url2,url3' [output-dir] [--host HOST] [--html]")
571
+ print(' python3 extract_chat.py \'["url1","url2"]\' [output-dir] [--host HOST] [--html]')
572
+ print(" python3 extract_chat.py --scan [output-dir] [--host HOST] [--html]")
573
+ print("Flags:")
574
+ print(" --host HOST Override chat host (default: auto-detect from config.json)")
575
+ print(" --html Also generate summary.html with clickable links")
576
+ print(" --scan Scan all agents")
378
577
  sys.exit(1)
379
578
 
380
- arg1 = sys.argv[1]
381
-
382
579
  # --scan mode
383
- if arg1 == '--scan':
384
- output_dir = sys.argv[2] if len(sys.argv) > 2 else None
385
- run_scan(output_dir)
580
+ if scan_mode:
581
+ output_dir = positional[0] if positional else get_default_output_dir()
582
+ run_scan(output_dir, host=host, html_output=html_output)
386
583
  return
387
584
 
388
585
  # Normal mode
389
- output_dir = sys.argv[2] if len(sys.argv) > 2 else os.getcwd()
586
+ arg1 = positional[0]
587
+ output_dir = positional[1] if len(positional) > 1 else get_default_output_dir()
390
588
  items = parse_input(arg1)
391
589
  print(f"Input: {len(items)} session(s)")
590
+ print(f"Host: {detect_chat_host()}")
392
591
 
393
592
  os.makedirs(output_dir, exist_ok=True)
394
593
 
@@ -413,7 +612,14 @@ def main():
413
612
  print(f"Compat: chat_history.js -> {results[0]['js_file']}")
414
613
 
415
614
  # Summary table
416
- print_summary_table(results)
615
+ print_summary_table(results, host)
616
+
617
+ # HTML output
618
+ if html_output:
619
+ html_path = os.path.join(output_dir, 'summary.html')
620
+ generate_html_table(results, host, html_path)
621
+ print(f"HTML: {html_path}")
622
+
417
623
  total_pairs = sum(r['total_pairs'] for r in results)
418
624
  print(f"\n合计:{len(results)} 个会话,{total_pairs} 对对话。")
419
625