@aiyiran/myclaw 1.1.182 → 1.1.184
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
|
@@ -16,15 +16,18 @@ description: Extract and render chat history from OpenClaw session URLs. Use whe
|
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
# 单个
|
|
19
|
-
python3 scripts/extract_chat.py "<url>" <
|
|
19
|
+
python3 scripts/extract_chat.py "<url>" <my-workspace>
|
|
20
20
|
|
|
21
21
|
# 批量(逗号分隔)
|
|
22
|
-
python3 scripts/extract_chat.py "url1,url2,url3" <
|
|
22
|
+
python3 scripts/extract_chat.py "url1,url2,url3" <my-workspace>
|
|
23
23
|
|
|
24
24
|
# 批量(JSON 数组)
|
|
25
|
-
python3 scripts/extract_chat.py '["url1","url2","url3"]' <
|
|
25
|
+
python3 scripts/extract_chat.py '["url1","url2","url3"]' <my-workspace>
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
`<my-workspace>` 是**执行命令的 agent 自己的 workspace 路径**(不是被分析的对话所在的 workspace)。
|
|
29
|
+
输出固定到 `<my-workspace>/chat-history/` 子目录。
|
|
30
|
+
|
|
28
31
|
## 输出文件
|
|
29
32
|
|
|
30
33
|
**只产出两个文件**,不再为每个会话单独落 js(所有会话的完整数据都汇入 `index.js`):
|
|
@@ -74,7 +77,7 @@ URL 示例:`https://claw1.kekouen.cn/chat?session=agent%3Ac108-v1-1811%3Amain`
|
|
|
74
77
|
### Step 2: 定位 JSONL 文件
|
|
75
78
|
|
|
76
79
|
```
|
|
77
|
-
|
|
80
|
+
~/.openclaw/agents/{agentId}/sessions/sessions.json
|
|
78
81
|
```
|
|
79
82
|
|
|
80
83
|
用 session key 在 `sessions.json` 中查找 `sessionFile` 路径。
|
|
@@ -104,22 +107,18 @@ URL 示例:`https://claw1.kekouen.cn/chat?session=agent%3Ac108-v1-1811%3Amain`
|
|
|
104
107
|
|
|
105
108
|
如果不使用脚本(如当前 agent 直接操作):
|
|
106
109
|
|
|
107
|
-
1. **收集**:解析 URL → 查 `sessions.json` →
|
|
108
|
-
2. **运行脚本**:`python3 scripts/extract_chat.py "<urls>" <
|
|
109
|
-
3.
|
|
110
|
+
1. **收集**:解析 URL → 查 `sessions.json` → 确认 JSONL 存在
|
|
111
|
+
2. **运行脚本**:`python3 scripts/extract_chat.py "<urls>" <my-workspace>`
|
|
112
|
+
3. **渲染**:用浏览器打开 `<my-workspace>/chat-history/index.html`
|
|
110
113
|
|
|
111
114
|
## 示例
|
|
112
115
|
|
|
113
116
|
### 单个会话
|
|
114
117
|
|
|
115
118
|
```bash
|
|
116
|
-
python3 scripts/extract_chat.py "https://claw1.kekouen.cn/chat?session=agent%3Ac108-v1-1811%3Amain"
|
|
119
|
+
python3 scripts/extract_chat.py "https://claw1.kekouen.cn/chat?session=agent%3Ac108-v1-1811%3Amain" /root/.openclaw/workspace-teacher
|
|
117
120
|
```
|
|
118
121
|
|
|
119
|
-
输出:
|
|
120
|
-
- `index.js`(含 1 个会话)
|
|
121
|
-
- `index.html`
|
|
122
|
-
|
|
123
122
|
### 批量处理
|
|
124
123
|
|
|
125
124
|
```bash
|
|
@@ -128,9 +127,7 @@ python3 scripts/extract_chat.py '[
|
|
|
128
127
|
"https://claw6.kekouen.cn/chat?session=agent%3Ausa%3Amain",
|
|
129
128
|
"https://claw6.kekouen.cn/chat?session=agent%3Ac109-v3-1813%3A...",
|
|
130
129
|
"https://claw6.kekouen.cn/chat?session=agent%3Ac108-v1-1810%3A..."
|
|
131
|
-
]'
|
|
130
|
+
]' /root/.openclaw/workspace-teacher
|
|
132
131
|
```
|
|
133
132
|
|
|
134
|
-
|
|
135
|
-
- `index.js`(含 4 个会话,全部数据在 `chatAll` 里)
|
|
136
|
-
- `index.html`
|
|
133
|
+
输出在 `<workspace>/chat-history/` 下:`index.js` + `index.html`。
|
|
@@ -1,47 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
3
|
Extract chat history from OpenClaw session URL(s) and generate JS data file(s).
|
|
4
|
-
"""
|
|
5
|
-
import os as _os
|
|
6
|
-
def _get_script_dir():
|
|
7
|
-
try:
|
|
8
|
-
return os.path.dirname(os.path.abspath(__file__))
|
|
9
|
-
except NameError:
|
|
10
|
-
return os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
11
|
-
|
|
12
4
|
|
|
13
|
-
"""
|
|
14
5
|
Usage:
|
|
15
|
-
|
|
16
|
-
python3 extract_chat.py <session-url-or-key>
|
|
17
|
-
|
|
18
|
-
# 多个会话(逗号分隔 / JSON 数组)
|
|
6
|
+
python3 extract_chat.py <session-url-or-key> [output-dir]
|
|
19
7
|
python3 extract_chat.py "url1,url2,url3" [output-dir]
|
|
20
|
-
python3 extract_chat.py '[
|
|
8
|
+
python3 extract_chat.py '["url1","url2"]' [output-dir]
|
|
9
|
+
python3 extract_chat.py --scan [output-dir]
|
|
21
10
|
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
输出到 {workspace}/chat-history/ 沙盒目录(或指定的 output-dir):
|
|
12
|
+
- index.js:chatIndex + chatAll
|
|
13
|
+
- index.html:查看器
|
|
14
|
+
- --scan 模式额外生成 作品扫描_<时间>.html
|
|
24
15
|
|
|
25
|
-
|
|
26
|
-
- index.js:唯一数据产物,含 chatIndex(会话元信息列表,含 work_url 字段)
|
|
27
|
-
+ chatAll(每个会话的完整对话,按 session_id 索引)
|
|
28
|
-
- index.html:查看器,复制自 assets/chat-history-template.html,只加载 index.js
|
|
29
|
-
- 不再为每个会话单独落 js 文件
|
|
30
|
-
- --scan 模式额外生成 作品扫描_<时间>.html(含可点击链接)
|
|
31
|
-
|
|
32
|
-
Host 检测:
|
|
33
|
-
- 自动从 /root/.openclaw/myclaw/server/config.json 读取
|
|
34
|
-
- 可用 --host 覆盖
|
|
35
|
-
|
|
36
|
-
work_url 字段:
|
|
37
|
-
- 默认为空字符串
|
|
38
|
-
- AI 可后续编辑 index.js 中对应会话的 work_url 填入学生作品链接
|
|
16
|
+
Host 自动从 {openclaw_root}/myclaw/server/config.json 读取,可用 --host 覆盖。
|
|
39
17
|
"""
|
|
40
18
|
|
|
41
19
|
import json
|
|
42
20
|
import sys
|
|
43
21
|
import os
|
|
44
|
-
import re
|
|
45
22
|
import glob
|
|
46
23
|
from urllib.parse import urlparse, parse_qs, quote
|
|
47
24
|
from datetime import datetime, timezone, timedelta
|
|
@@ -49,10 +26,20 @@ import time
|
|
|
49
26
|
|
|
50
27
|
tz_beijing = timezone(timedelta(hours=8))
|
|
51
28
|
|
|
29
|
+
REPORT_SUBDIR = 'chat-history'
|
|
30
|
+
_base = os.environ.get('OPENCLAW_BASE', os.path.expanduser('~/.openclaw'))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_script_dir():
|
|
34
|
+
try:
|
|
35
|
+
return os.path.dirname(os.path.abspath(__file__))
|
|
36
|
+
except NameError:
|
|
37
|
+
return os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
38
|
+
|
|
52
39
|
|
|
53
40
|
def detect_chat_host():
|
|
54
41
|
"""Auto-detect chat host from config.json."""
|
|
55
|
-
config_path = '/
|
|
42
|
+
config_path = f'{_base}/myclaw/server/config.json'
|
|
56
43
|
try:
|
|
57
44
|
with open(config_path) as f:
|
|
58
45
|
cfg = json.load(f)
|
|
@@ -62,17 +49,6 @@ def detect_chat_host():
|
|
|
62
49
|
return 'claw5.kekouen.cn'
|
|
63
50
|
|
|
64
51
|
|
|
65
|
-
def get_default_output_dir():
|
|
66
|
-
"""Return the current workspace directory."""
|
|
67
|
-
# OPENCLAW_WORKSPACE is set to the workspace path
|
|
68
|
-
ws = os.environ.get('OPENCLAW_WORKSPACE', '')
|
|
69
|
-
if ws and os.path.isdir(ws):
|
|
70
|
-
return ws
|
|
71
|
-
# Fallback: derive from cwd (e.g. /root/.openclaw/workspace-teacher)
|
|
72
|
-
cwd = os.getcwd()
|
|
73
|
-
if '/workspace-' in cwd:
|
|
74
|
-
return cwd
|
|
75
|
-
return cwd
|
|
76
52
|
|
|
77
53
|
|
|
78
54
|
def parse_time(ts_str):
|
|
@@ -119,7 +95,7 @@ def find_session_file(session_key):
|
|
|
119
95
|
if len(parts) < 2:
|
|
120
96
|
return None
|
|
121
97
|
agent_id = parts[1]
|
|
122
|
-
sessions_json_path = f'/
|
|
98
|
+
sessions_json_path = f'{_base}/agents/{agent_id}/sessions/sessions.json'
|
|
123
99
|
|
|
124
100
|
if not os.path.exists(sessions_json_path):
|
|
125
101
|
return None
|
|
@@ -130,9 +106,7 @@ def find_session_file(session_key):
|
|
|
130
106
|
if session_key in sessions:
|
|
131
107
|
session_file = sessions[session_key].get('sessionFile')
|
|
132
108
|
if session_file and not os.path.isabs(session_file):
|
|
133
|
-
session_file = os.path.join(
|
|
134
|
-
f'/root/.openclaw/agents/{agent_id}/sessions', session_file
|
|
135
|
-
)
|
|
109
|
+
session_file = os.path.join(f'{_base}/agents/{agent_id}/sessions', session_file)
|
|
136
110
|
return session_file
|
|
137
111
|
return None
|
|
138
112
|
|
|
@@ -185,13 +159,13 @@ def count_conversations(jsonl_path):
|
|
|
185
159
|
ai_messages.append(messages[j])
|
|
186
160
|
j += 1
|
|
187
161
|
ai_text = '\n\n'.join([m['text'] for m in ai_messages if m['has_text']])
|
|
188
|
-
user_time = msg.get('timestamp', '')
|
|
189
|
-
ai_time = ai_messages[0].get('timestamp', '') if ai_messages else ''
|
|
162
|
+
user_time = parse_time(msg.get('timestamp', '')) or ''
|
|
163
|
+
ai_time = parse_time(ai_messages[0].get('timestamp', '')) if ai_messages else ''
|
|
190
164
|
conversations.append({
|
|
191
165
|
'user': user_text,
|
|
192
166
|
'ai': ai_text,
|
|
193
|
-
'user_time': user_time,
|
|
194
|
-
'ai_time': ai_time,
|
|
167
|
+
'user_time': user_time or '',
|
|
168
|
+
'ai_time': ai_time or '',
|
|
195
169
|
})
|
|
196
170
|
i += 1
|
|
197
171
|
else:
|
|
@@ -200,30 +174,6 @@ def count_conversations(jsonl_path):
|
|
|
200
174
|
return conversations, first_timestamp, last_timestamp
|
|
201
175
|
|
|
202
176
|
|
|
203
|
-
def parse_jsonl_to_conversations(jsonl_path):
|
|
204
|
-
"""Full parse returning conversation pairs with timestamps."""
|
|
205
|
-
conversations, first_ts, last_ts = count_conversations(jsonl_path)
|
|
206
|
-
return conversations, first_ts, last_ts
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def sanitize_filename(name):
|
|
210
|
-
safe = re.sub(r'[^\w\u4e00-\u9fff\u3400-\u4dbf-]', '-', name)
|
|
211
|
-
safe = re.sub(r'-+', '-', safe).strip('-')
|
|
212
|
-
return safe or 'session'
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def build_filename(session_key):
|
|
216
|
-
parts = session_key.split(':')
|
|
217
|
-
agent_id = parts[1] if len(parts) > 1 else 'unknown'
|
|
218
|
-
session_name = parts[2] if len(parts) > 2 else 'main'
|
|
219
|
-
# Use last part (UUID or 'main') as unique suffix to avoid collisions
|
|
220
|
-
unique_id = parts[-1] if len(parts) > 3 else session_name
|
|
221
|
-
agent_safe = sanitize_filename(agent_id)
|
|
222
|
-
name_safe = sanitize_filename(session_name)
|
|
223
|
-
unique_safe = sanitize_filename(unique_id)
|
|
224
|
-
if unique_safe == name_safe:
|
|
225
|
-
return f"{agent_safe}_{name_safe}.js"
|
|
226
|
-
return f"{agent_safe}_{name_safe}_{unique_safe}.js"
|
|
227
177
|
|
|
228
178
|
|
|
229
179
|
def build_chat_data(conversations, session_key, first_ts, last_ts):
|
|
@@ -256,18 +206,15 @@ def session_key_to_url(session_key, host=None):
|
|
|
256
206
|
|
|
257
207
|
|
|
258
208
|
def scan_all_agents():
|
|
259
|
-
"""
|
|
260
|
-
Scan /root/.openclaw/agents/*/sessions/sessions.json
|
|
261
|
-
Return list of (session_key, agent_id, session_name, jsonl_path) for all sessions.
|
|
262
|
-
"""
|
|
209
|
+
"""Scan {_base}/agents/*/sessions/sessions.json."""
|
|
263
210
|
results = []
|
|
264
|
-
|
|
265
|
-
for agent_dir in sorted(glob.glob(f'{
|
|
211
|
+
agents_dir = f'{_base}/agents'
|
|
212
|
+
for agent_dir in sorted(glob.glob(f'{agents_dir}/*/sessions/sessions.json')):
|
|
266
213
|
agent_id = agent_dir.split('/agents/')[1].split('/sessions/')[0]
|
|
267
214
|
try:
|
|
268
215
|
with open(agent_dir) as f:
|
|
269
216
|
sessions = json.load(f)
|
|
270
|
-
except:
|
|
217
|
+
except Exception:
|
|
271
218
|
continue
|
|
272
219
|
for session_key, meta in sessions.items():
|
|
273
220
|
if not isinstance(meta, dict):
|
|
@@ -276,7 +223,7 @@ def scan_all_agents():
|
|
|
276
223
|
if not session_file:
|
|
277
224
|
continue
|
|
278
225
|
if not os.path.isabs(session_file):
|
|
279
|
-
session_file = os.path.join(
|
|
226
|
+
session_file = os.path.join(agents_dir, agent_id, 'sessions', session_file)
|
|
280
227
|
if not os.path.exists(session_file):
|
|
281
228
|
continue
|
|
282
229
|
parts = session_key.split(':')
|
|
@@ -286,12 +233,9 @@ def scan_all_agents():
|
|
|
286
233
|
|
|
287
234
|
|
|
288
235
|
def print_summary_table(entries, host=None):
|
|
236
|
+
"""Print the summary table to stdout."""
|
|
289
237
|
if host is None:
|
|
290
238
|
host = detect_chat_host()
|
|
291
|
-
"""
|
|
292
|
-
Print the summary table.
|
|
293
|
-
entries: list of dicts with keys: index, session_name, agent_id, total_pairs, work_url, session_key, first_time, last_time
|
|
294
|
-
"""
|
|
295
239
|
print(f"\n{'#':>3} {'workspace':<16} {'会话名':<28} {'对话数':>6} {'起始时间':<20} {'最后更新':<20} {'作品链接':<14} {'对话链接'}")
|
|
296
240
|
print(f"{'-'*3} {'-'*16} {'-'*28} {'-'*6} {'-'*20} {'-'*20} {'-'*14} {'-'*50}")
|
|
297
241
|
for e in entries:
|
|
@@ -390,7 +334,7 @@ def process_one(url_or_key, output_dir, index):
|
|
|
390
334
|
print(f"[{index}] ERROR: JSONL not found: {jsonl_path}")
|
|
391
335
|
return None
|
|
392
336
|
|
|
393
|
-
conversations, first_ts, last_ts =
|
|
337
|
+
conversations, first_ts, last_ts = count_conversations(jsonl_path)
|
|
394
338
|
print(f"[{index}] {len(conversations)} pairs")
|
|
395
339
|
|
|
396
340
|
data = build_chat_data(conversations, session_key, first_ts, last_ts)
|
|
@@ -513,25 +457,34 @@ def main():
|
|
|
513
457
|
|
|
514
458
|
if not positional and not scan_mode:
|
|
515
459
|
print("Usage:")
|
|
516
|
-
print(" python3 extract_chat.py <url-or-key>
|
|
517
|
-
print(" python3 extract_chat.py 'url1,url2,url3'
|
|
518
|
-
print(' python3 extract_chat.py \'["url1","url2"]\'
|
|
519
|
-
print(" python3 extract_chat.py --scan
|
|
520
|
-
print("
|
|
460
|
+
print(" python3 extract_chat.py <url-or-key> <workspace> [--host HOST]")
|
|
461
|
+
print(" python3 extract_chat.py 'url1,url2,url3' <workspace> [--host HOST]")
|
|
462
|
+
print(' python3 extract_chat.py \'["url1","url2"]\' <workspace> [--host HOST]')
|
|
463
|
+
print(" python3 extract_chat.py --scan <workspace> [--host HOST]")
|
|
464
|
+
print("\n <workspace> 执行命令的 agent 自己的 workspace 路径")
|
|
465
|
+
print(" 输出固定到: <workspace>/%s/" % REPORT_SUBDIR)
|
|
466
|
+
print("\nFlags:")
|
|
521
467
|
print(" --host HOST Override chat host (default: auto-detect from config.json)")
|
|
522
|
-
print(" --html Also generate summary.html with clickable links")
|
|
523
468
|
print(" --scan Scan all agents")
|
|
524
469
|
sys.exit(1)
|
|
525
470
|
|
|
526
471
|
# --scan mode
|
|
527
472
|
if scan_mode:
|
|
528
|
-
|
|
473
|
+
if not positional:
|
|
474
|
+
print("ERROR: --scan requires <workspace> argument")
|
|
475
|
+
sys.exit(1)
|
|
476
|
+
workspace = positional[0]
|
|
477
|
+
output_dir = os.path.join(workspace, REPORT_SUBDIR)
|
|
529
478
|
run_scan(output_dir, host=host)
|
|
530
479
|
return
|
|
531
480
|
|
|
532
|
-
# Normal mode
|
|
481
|
+
# Normal mode: first arg = urls, second arg = workspace
|
|
482
|
+
if len(positional) < 2:
|
|
483
|
+
print("ERROR: missing <workspace> argument")
|
|
484
|
+
sys.exit(1)
|
|
533
485
|
arg1 = positional[0]
|
|
534
|
-
|
|
486
|
+
workspace = positional[1]
|
|
487
|
+
output_dir = os.path.join(workspace, REPORT_SUBDIR)
|
|
535
488
|
items = parse_input(arg1)
|
|
536
489
|
print("Input: %d session(s)" % len(items))
|
|
537
490
|
print("Host: %s" % detect_chat_host())
|