@aiyiran/myclaw 1.1.135 → 1.1.136

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiyiran/myclaw",
3
- "version": "1.1.135",
3
+ "version": "1.1.136",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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