@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
|
@@ -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>
|
|
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 [
|
|
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
|
|
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://
|
|
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} {'作品链接':<
|
|
247
|
-
print(f"{'-'*3} {'-'*16} {'-'*28} {'-'*6} {'-'*20} {'-'*20} {'-'*
|
|
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:<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
384
|
-
output_dir =
|
|
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
|
-
|
|
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
|
|