@aiyiran/myclaw 1.1.182 → 1.1.183

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.182",
3
+ "version": "1.1.183",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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>" <output-dir>
19
+ python3 scripts/extract_chat.py "<url>" <my-workspace>
20
20
 
21
21
  # 批量(逗号分隔)
22
- python3 scripts/extract_chat.py "url1,url2,url3" <output-dir>
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"]' <output-dir>
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
- /root/.openclaw/agents/{agentId}/sessions/sessions.json
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` → 复制 JSONL 到输出目录
108
- 2. **运行脚本**:`python3 scripts/extract_chat.py "<urls>" <output-dir>`
109
- 3. **渲染**:复制 HTML 模板到输出目录,用浏览器打开
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" ./output
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
- ]' ./output
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
- # 单个会话(输出到当前 workspace)
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 '[ "url1","url2","url3"]' [output-dir]
8
+ python3 extract_chat.py '["url1","url2"]' [output-dir]
9
+ python3 extract_chat.py --scan [output-dir]
21
10
 
22
- # 扫描所有 agent 的所有会话,统计表格
23
- python3 extract_chat.py --scan [--html]
11
+ 输出到 {workspace}/chat-history/ 沙盒目录(或指定的 output-dir):
12
+ - index.js:chatIndex + chatAll
13
+ - index.html:查看器
14
+ - --scan 模式额外生成 作品扫描_<时间>.html
24
15
 
25
- 输出(默认到当前 workspace /root/.openclaw/workspace-teacher):
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 = '/root/.openclaw/myclaw/server/config.json'
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'/root/.openclaw/agents/{agent_id}/sessions/sessions.json'
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
- base = '/root/.openclaw/agents'
265
- for agent_dir in sorted(glob.glob(f'{base}/*/sessions/sessions.json')):
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(f'{base}/{agent_id}/sessions', session_file)
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 = parse_jsonl_to_conversations(jsonl_path)
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> [output-dir] [--host HOST] [--html]")
517
- print(" python3 extract_chat.py 'url1,url2,url3' [output-dir] [--host HOST] [--html]")
518
- print(' python3 extract_chat.py \'["url1","url2"]\' [output-dir] [--host HOST] [--html]')
519
- print(" python3 extract_chat.py --scan [output-dir] [--host HOST] [--html]")
520
- print("Flags:")
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
- output_dir = positional[0] if positional else get_default_output_dir()
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
- output_dir = positional[1] if len(positional) > 1 else get_default_output_dir()
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())