@aiyiran/myclaw 1.1.130 → 1.1.132

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.
@@ -376,22 +376,7 @@
376
376
  }
377
377
 
378
378
  function autoPreloadImages(data) {
379
- if (!data || !data.assets) return;
380
- data.assets.forEach(function (asset) {
381
- if (!isImageAsset(asset)) return;
382
- var url = buildPreviewUrl(data, asset.path);
383
- if (!url || _preloadCache[url]) return;
384
- var pre = new Image();
385
- pre.onload = function () {
386
- console.log('[preload] ✓ ' + asset.path);
387
- };
388
- pre.onerror = function () {
389
- console.warn('[preload] ✗ ' + asset.path);
390
- delete _preloadCache[url]; // 失败的下次可以重试
391
- };
392
- pre.src = url;
393
- _preloadCache[url] = pre; // 占位,防止并发重复触发
394
- });
379
+ // 图片通过 fetch→blob 方式加载,不做 CDN 预加载(CSP 会拦截)
395
380
  }
396
381
 
397
382
  function fetchArtifacts(contentEl) {
@@ -823,9 +808,8 @@
823
808
  'background: #252536',
824
809
  ].join(';');
825
810
 
826
- // img 直接渲染,浏览器自然从上往下加载
811
+ // fetch → blob URL,绕过 CSP img-src 限制
827
812
  var img = document.createElement('img');
828
- img.src = previewUrl;
829
813
  img.alt = asset.name || asset.path;
830
814
  img.style.cssText = [
831
815
  'max-width: 100%',
@@ -836,13 +820,28 @@
836
820
  'position: relative',
837
821
  'z-index: 1',
838
822
  ].join(';');
839
- img.onerror = function () {
823
+
824
+ function showImgError() {
840
825
  img.style.display = 'none';
841
826
  var errMsg = document.createElement('div');
842
827
  errMsg.textContent = '加载失败';
843
828
  errMsg.style.cssText = 'color:#666;font-size:13px;font-family:monospace;';
844
829
  imgArea.appendChild(errMsg);
845
- };
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);
846
845
 
847
846
  imgArea.appendChild(img);
848
847
  imgBox.appendChild(imgArea);
@@ -1175,22 +1174,32 @@
1175
1174
  'background: #fff',
1176
1175
  ].join(';');
1177
1176
 
1178
- // 1. 视频和 HTML 走 CDN
1179
- // 2. 其余类型(文本、音频等)走本地 API (避免乱码或 CDN 延迟)
1180
1177
  var VIDEO_EXTS = ['mp4', 'webm', 'mov', 'avi', 'mkv'];
1181
1178
  var isHtml = assetExt === 'html' || assetExt === 'htm';
1182
1179
  var isVideo = VIDEO_EXTS.indexOf(assetExt) !== -1;
1183
1180
 
1184
- if (isHtml || isVideo) {
1185
- iframe.src = previewUrl + '?t=' + Date.now();
1181
+ if (isVideo) {
1182
+ // 视频用 <video> 元素 + 本地 API,支持流式加载和拖拽进度条
1183
+ var video = document.createElement('video');
1184
+ video.controls = true;
1185
+ 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);
1187
+ box.appendChild(video);
1188
+ overlay.appendChild(box);
1189
+ document.body.appendChild(overlay);
1190
+ video.focus();
1186
1191
  } else {
1187
- iframe.src = MYCLAW_API_BASE + '/api/file?path=' + encodeURIComponent(getWorkspaceId() + '/' + asset.path) + '&t=' + Date.now();
1192
+ // HTML CDN;其余类型走本地 API
1193
+ if (isHtml) {
1194
+ iframe.src = previewUrl + '?t=' + Date.now();
1195
+ } else {
1196
+ iframe.src = MYCLAW_API_BASE + '/api/file?path=' + encodeURIComponent(getWorkspaceId() + '/' + asset.path) + '&t=' + Date.now();
1197
+ }
1198
+ box.appendChild(iframe);
1199
+ overlay.appendChild(box);
1200
+ document.body.appendChild(overlay);
1201
+ iframe.focus();
1188
1202
  }
1189
-
1190
- box.appendChild(iframe);
1191
- overlay.appendChild(box);
1192
- document.body.appendChild(overlay);
1193
- iframe.focus();
1194
1203
  return;
1195
1204
  }
1196
1205
 
@@ -1597,7 +1606,11 @@
1597
1606
  });
1598
1607
  });
1599
1608
  statsTimerObserver.observe(document.body, { childList: true });
1609
+ var mTitleVersion = document.createElement('span');
1610
+ mTitleVersion.textContent = 'v1.0';
1611
+ mTitleVersion.style.cssText = 'font-size:10px;color:#555;background:#2a2a3e;border:1px solid #3a3a5a;border-radius:3px;padding:1px 5px;';
1600
1612
  mTitle.appendChild(mTitleText);
1613
+ mTitle.appendChild(mTitleVersion);
1601
1614
  mTitle.appendChild(mTitleTime);
1602
1615
  var mClose = document.createElement('span');
1603
1616
  mClose.textContent = '✕';
package/index.js CHANGED
@@ -108,12 +108,14 @@ function runInstall() {
108
108
  // ============================================================================
109
109
 
110
110
  const OPENCLAW_VERSION = '2026.5.7';
111
+ const OPENCLAW_VERSION_BETA = '2026.6.9';
111
112
 
112
- function runReinstall() {
113
+ function runReinstall(version, isBeta) {
114
+ const targetVersion = version || OPENCLAW_VERSION;
113
115
  const bar = '────────────────────────────────────────';
114
116
  console.log('');
115
117
  console.log(bar);
116
- console.log(' 🔄 OpenClaw 重装工具 (锁定版本: ' + colors.green + OPENCLAW_VERSION + colors.nc + ')');
118
+ console.log(' 🔄 OpenClaw 重装工具 (锁定版本: ' + colors.green + targetVersion + colors.nc + ')');
117
119
  console.log(bar);
118
120
  console.log('');
119
121
 
@@ -157,9 +159,9 @@ function runReinstall() {
157
159
  console.log('');
158
160
 
159
161
  // 5. 重新安装特定版本
160
- console.log('[5/8] 📦 安装 openclaw@' + OPENCLAW_VERSION + '...');
162
+ console.log('[5/8] 📦 安装 openclaw@' + targetVersion + '...');
161
163
  try {
162
- execSync('npm install -g openclaw@' + OPENCLAW_VERSION, {
164
+ execSync('npm install -g openclaw@' + targetVersion, {
163
165
  stdio: 'inherit',
164
166
  env: { ...process.env, npm_config_progress: 'true' }
165
167
  });
@@ -200,6 +202,19 @@ function runReinstall() {
200
202
  }
201
203
  console.log('');
202
204
 
205
+ // 9. (--beta 专属) 注入 MiniMax 6.X 兼容配置
206
+ if (isBeta) {
207
+ console.log('[9/9] 🔧 inject-minimax --beta — 修复 6.X 兼容配置...');
208
+ try {
209
+ const minimax = require('./injects/inject-minimax');
210
+ minimax.run(['--beta']);
211
+ console.log(' ' + colors.green + '✓ MiniMax 6.X 配置已修复' + colors.nc);
212
+ } catch (err) {
213
+ console.log(' ' + colors.yellow + '⚠ inject-minimax 失败: ' + err.message + colors.nc);
214
+ }
215
+ console.log('');
216
+ }
217
+
203
218
  console.log(bar);
204
219
  console.log(colors.green + ' ✅ 重装完成!' + colors.nc);
205
220
  console.log(bar);
@@ -2520,7 +2535,8 @@ function showHelp() {
2520
2535
  console.log('命令:');
2521
2536
  console.log(' start 智能启动(图标 & 命令行通用入口)');
2522
2537
  console.log(' install,i 安装 OpenClaw 服务');
2523
- console.log(' longxia 重装 OpenClaw (清理缓存 + 锁定版本)');
2538
+ console.log(' longxia 重装 OpenClaw (锁定 5.X 稳定版 ' + OPENCLAW_VERSION + ')');
2539
+ console.log(' longxia --beta 重装 OpenClaw (锁定 6.X 测试版 ' + OPENCLAW_VERSION_BETA + ')');
2524
2540
  console.log(' uninstall 卸载 MyClaw (恢复 npm 官方源)');
2525
2541
  console.log(' status 获取控制台网址');
2526
2542
  console.log(' update 自动升级 MyClaw 到最新版本');
@@ -2596,7 +2612,8 @@ if (!command) {
2596
2612
  } else if (command === 'install' || command === 'i') {
2597
2613
  runInstall();
2598
2614
  } else if (command === 'longxia') {
2599
- runReinstall();
2615
+ const isBeta = args.includes('--beta');
2616
+ runReinstall(isBeta ? OPENCLAW_VERSION_BETA : undefined, isBeta);
2600
2617
  } else if (command === 'status') {
2601
2618
  runStatus();
2602
2619
  } else if (command === 'new') {
@@ -9,6 +9,7 @@
9
9
  * myclaw inject-minimax # 仅追加 minimax provider,不改默认,副作用最小
10
10
  * myclaw inject-minimax --default # 追加 + 设为默认 + 全部对话迁移到 MiniMax-M3
11
11
  * myclaw inject-minimax --only # 追加 + 设为默认 + 全部对话迁移 + 清掉所有其他 provider
12
+ * myclaw inject-minimax --beta # --only 的全部行为 + 去掉 authHeader (适配 openclaw 6.X)
12
13
  * myclaw inject-minimax --key sk-xxx # 使用指定 API Key
13
14
  */
14
15
 
@@ -27,6 +28,7 @@ function run(cliArgs) {
27
28
  let apiKey = null;
28
29
  let setDefault = false;
29
30
  let onlyMode = false;
31
+ let betaMode = false;
30
32
 
31
33
  for (let i = 0; i < cliArgs.length; i++) {
32
34
  if (cliArgs[i] === '--key' && cliArgs[i + 1]) {
@@ -37,6 +39,10 @@ function run(cliArgs) {
37
39
  } else if (cliArgs[i] === '--only') {
38
40
  onlyMode = true;
39
41
  setDefault = true; // --only 隐含 --default
42
+ } else if (cliArgs[i] === '--beta') {
43
+ betaMode = true;
44
+ onlyMode = true; // --beta 隐含 --only
45
+ setDefault = true;
40
46
  }
41
47
  }
42
48
 
@@ -55,7 +61,7 @@ function run(cliArgs) {
55
61
  }
56
62
 
57
63
  console.log('📍 找到配置: ' + configPath);
58
- console.log('📌 模式: ' + (onlyMode ? '--only (独占)' : setDefault ? '--default (设默认)' : '追加'));
64
+ console.log('📌 模式: ' + (betaMode ? '--beta (独占+6.X兼容)' : onlyMode ? '--only (独占)' : setDefault ? '--default (设默认)' : '追加'));
59
65
 
60
66
  // ── Step 1:--only 清掉所有其他 provider ──
61
67
  if (onlyMode) {
@@ -100,10 +106,9 @@ function run(cliArgs) {
100
106
  if (!config.models.mode) config.models.mode = "merge";
101
107
  if (!config.models.providers) config.models.providers = {};
102
108
 
103
- config.models.providers.minimax = {
109
+ const minimaxProvider = {
104
110
  baseUrl: "https://api.minimaxi.com/anthropic",
105
111
  api: "anthropic-messages",
106
- authHeader: true,
107
112
  models: [
108
113
  {
109
114
  id: "MiniMax-M3",
@@ -125,6 +130,8 @@ function run(cliArgs) {
125
130
  }
126
131
  ]
127
132
  };
133
+ if (!betaMode) minimaxProvider.authHeader = true;
134
+ config.models.providers.minimax = minimaxProvider;
128
135
 
129
136
  if (!config.agents) config.agents = {};
130
137
  if (!config.agents.defaults) config.agents.defaults = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiyiran/myclaw",
3
- "version": "1.1.130",
3
+ "version": "1.1.132",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -10,6 +10,7 @@ description: Extract and render chat history from OpenClaw session URLs. Use whe
10
10
  - **单个 URL/key**:处理一个会话
11
11
  - **逗号分隔**:`url1,url2,url3` 批量处理
12
12
  - **JSON 数组**:`["url1","url2","url3"]` 批量处理
13
+ - **`--scan` 模式**:自动扫描所有 agent 的所有会话,统计表格
13
14
 
14
15
  ## 快速使用(推荐)
15
16
 
@@ -6,42 +6,53 @@ Usage:
6
6
  # 单个会话
7
7
  python3 extract_chat.py <session-url-or-key> [output-dir]
8
8
 
9
- # 多个会话(逗号分隔)
9
+ # 多个会话(逗号分隔 / JSON 数组)
10
10
  python3 extract_chat.py "url1,url2,url3" [output-dir]
11
-
12
- # 多个会话(JSON 数组)
13
11
  python3 extract_chat.py '["url1","url2","url3"]' [output-dir]
14
12
 
13
+ # 扫描所有 agent 的所有会话,统计表格
14
+ python3 extract_chat.py --scan [output-dir]
15
+
15
16
  输出:
16
- - 每个会话生成独立的 <index>-<session-name>.js
17
- - 生成 index.js,包含所有会话的元信息列表
17
+ - 每个会话生成独立的 <agentId>_<sessionName>_<date>_<time>.js
18
+ - 生成 index.js,包含所有会话的元信息列表(含 work_url 字段)
18
19
  - 生成 chat_history.js(向后兼容,指向第一个会话)
20
+
21
+ work_url 字段:
22
+ - 默认为空字符串
23
+ - AI 可后续手动编辑 JS 文件填入学生作品链接
19
24
  """
20
25
 
21
26
  import json
22
27
  import sys
23
28
  import os
24
29
  import re
25
- from urllib.parse import urlparse, parse_qs
30
+ import glob
31
+ from urllib.parse import urlparse, parse_qs, quote
26
32
  from datetime import datetime, timezone, timedelta
27
33
 
28
34
  tz_beijing = timezone(timedelta(hours=8))
29
35
 
30
36
 
31
37
  def parse_time(ts_str):
32
- """Parse ISO timestamp string to Beijing time string."""
33
38
  try:
34
39
  ts_str = ts_str.replace('Z', '')
35
40
  dt = datetime.fromisoformat(ts_str)
36
- dt_utc = dt.replace(tzinfo=timezone.utc)
37
- dt_beijing = dt_utc.astimezone(tz_beijing)
38
- return dt_beijing.strftime('%Y-%m-%d %H:%M:%S')
41
+ return dt.replace(tzinfo=timezone.utc).astimezone(tz_beijing).strftime('%Y-%m-%d %H:%M:%S')
39
42
  except:
40
43
  return None
41
44
 
42
45
 
46
+ def parse_time_for_filename(ts_str):
47
+ try:
48
+ ts_str = ts_str.replace('Z', '')
49
+ dt = datetime.fromisoformat(ts_str)
50
+ return dt.replace(tzinfo=timezone.utc).astimezone(tz_beijing).strftime('%Y%m%d_%H%M')
51
+ except:
52
+ return 'unknown'
53
+
54
+
43
55
  def extract_session_key(url_or_key):
44
- """Extract session key from URL or return as-is if already a key."""
45
56
  if 'session=' in url_or_key:
46
57
  parsed = urlparse(url_or_key)
47
58
  params = parse_qs(parsed.query)
@@ -51,28 +62,18 @@ def extract_session_key(url_or_key):
51
62
 
52
63
 
53
64
  def parse_input(input_str):
54
- """
55
- Parse input into a list of session URLs/keys.
56
- Supports: comma-separated, JSON array, or single value.
57
- """
58
65
  input_str = input_str.strip()
59
-
60
- # Try JSON array
61
66
  if input_str.startswith('['):
62
67
  try:
63
68
  arr = json.loads(input_str)
64
69
  return [item.strip() for item in arr if isinstance(item, str) and item.strip()]
65
70
  except json.JSONDecodeError:
66
71
  pass
67
-
68
- # Comma-separated (but not commas inside URL-encoded params like %3A)
69
- # URLs use %3A for colon, so raw commas are safe as separators
70
72
  parts = [p.strip() for p in input_str.split(',') if p.strip()]
71
73
  return parts if len(parts) > 1 else [input_str]
72
74
 
73
75
 
74
76
  def find_session_file(session_key):
75
- """Find the JSONL file path for a given session key."""
76
77
  parts = session_key.split(':')
77
78
  if len(parts) < 2:
78
79
  return None
@@ -95,9 +96,12 @@ def find_session_file(session_key):
95
96
  return None
96
97
 
97
98
 
98
- def parse_jsonl_to_conversations(jsonl_path):
99
- """Parse JSONL file and build conversation pairs."""
99
+ def count_conversations(jsonl_path):
100
+ """Parse JSONL and return (conversation_count, first_ts, last_ts)."""
100
101
  messages = []
102
+ first_timestamp = None
103
+ last_timestamp = None
104
+
101
105
  with open(jsonl_path, 'r', encoding='utf-8') as f:
102
106
  for line in f:
103
107
  line = line.strip()
@@ -109,134 +113,197 @@ def parse_jsonl_to_conversations(jsonl_path):
109
113
  content = obj['message'].get('content', [])
110
114
  timestamp = obj.get('timestamp', '')
111
115
 
116
+ if timestamp:
117
+ if first_timestamp is None:
118
+ first_timestamp = timestamp
119
+ last_timestamp = timestamp
120
+
112
121
  if isinstance(content, list):
113
- text = ''.join([
114
- c.get('text', '') for c in content if c.get('type') == 'text'
115
- ])
122
+ text = ''.join([c.get('text', '') for c in content if c.get('type') == 'text'])
116
123
  else:
117
124
  text = str(content)
118
125
 
119
126
  messages.append({
120
127
  'role': role,
121
- 'timestamp': timestamp,
122
128
  'text': text.strip(),
123
129
  'has_text': bool(text.strip())
124
130
  })
125
131
 
126
- # Build conversation pairs
132
+ # Count conversation pairs (user messages with at least one AI reply)
127
133
  conversations = []
128
134
  i = 0
129
135
  while i < len(messages):
130
136
  msg = messages[i]
131
137
  if msg['role'] == 'user':
132
138
  user_text = msg['text']
133
- user_time = msg['timestamp']
134
-
135
139
  ai_messages = []
136
140
  j = i + 1
137
141
  while j < len(messages) and messages[j]['role'] != 'user':
138
142
  if messages[j]['role'] == 'assistant':
139
143
  ai_messages.append(messages[j])
140
144
  j += 1
141
-
142
145
  ai_text = '\n\n'.join([m['text'] for m in ai_messages if m['has_text']])
143
- ai_time = ai_messages[-1]['timestamp'] if ai_messages else ''
144
-
145
146
  conversations.append({
146
147
  'user': user_text,
147
- 'user_time': parse_time(user_time) if user_time else '',
148
148
  'ai': ai_text,
149
- 'ai_time': parse_time(ai_time) if ai_time else ''
150
149
  })
151
150
  i += 1
152
151
  else:
153
152
  i += 1
154
153
 
155
- return conversations
154
+ return conversations, first_timestamp, last_timestamp
155
+
156
+
157
+ def parse_jsonl_to_conversations(jsonl_path):
158
+ """Full parse returning conversation pairs with timestamps."""
159
+ conversations, first_ts, last_ts = count_conversations(jsonl_path)
160
+ return conversations, first_ts, last_ts
156
161
 
157
162
 
158
163
  def sanitize_filename(name):
159
- """Make a safe filename from session name."""
160
- # Keep Chinese chars, alphanumerics, replace others with -
161
164
  safe = re.sub(r'[^\w\u4e00-\u9fff\u3400-\u4dbf-]', '-', name)
162
165
  safe = re.sub(r'-+', '-', safe).strip('-')
163
166
  return safe or 'session'
164
167
 
165
168
 
166
- def generate_js(conversations, session_key, output_path):
167
- """Generate JS file from conversations."""
168
- session_name = session_key.split(':')[-1] if ':' in session_key else session_key
169
+ def build_filename(session_key, first_timestamp):
170
+ parts = session_key.split(':')
171
+ agent_id = parts[1] if len(parts) > 1 else 'unknown'
172
+ session_name = parts[2] if len(parts) > 2 else 'main'
173
+ agent_safe = sanitize_filename(agent_id)
174
+ name_safe = sanitize_filename(session_name)
175
+ time_str = parse_time_for_filename(first_timestamp) if first_timestamp else 'unknown'
176
+ return f"{agent_safe}_{name_safe}_{time_str}.js"
177
+
178
+
179
+ def generate_js(conversations, session_key, first_ts, last_ts, output_path):
180
+ parts = session_key.split(':')
181
+ workspace_name = parts[1] if len(parts) > 1 else 'unknown'
182
+ session_name = parts[2] if len(parts) > 2 else 'main'
169
183
 
170
184
  output_data = {
171
- 'session': session_name,
185
+ 'workspace_name': workspace_name,
186
+ 'session_name': session_name,
172
187
  'session_id': session_key,
188
+ 'first_time': parse_time(first_ts) if first_ts else '',
189
+ 'last_time': parse_time(last_ts) if last_ts else '',
173
190
  'total_pairs': len(conversations),
174
- 'initiator': 'session initiator',
191
+ 'work_url': '',
175
192
  'conversations': conversations
176
193
  }
177
-
178
194
  js_content = f'''// Chat History - {session_name}
179
195
  // Session Key: {session_key}
180
196
  // Generated by chat-history-extractor skill
181
197
 
182
198
  const chatData = {json.dumps(output_data, ensure_ascii=False, indent=2)};
183
199
  '''
184
-
185
200
  with open(output_path, 'w', encoding='utf-8') as f:
186
201
  f.write(js_content)
187
-
188
202
  return len(conversations), session_name
189
203
 
190
204
 
191
- def process_one(url_or_key, output_dir, index):
205
+ def session_key_to_url(session_key):
206
+ """Build the claw6 chat URL from a session key."""
207
+ encoded = quote(session_key, safe='')
208
+ return f"https://claw6.kekouen.cn/chat?session={encoded}"
209
+
210
+
211
+ def scan_all_agents():
192
212
  """
193
- Process a single session URL/key.
194
- Returns dict with results or None on failure.
213
+ Scan /root/.openclaw/agents/*/sessions/sessions.json
214
+ Return list of (session_key, agent_id, session_name, jsonl_path) for all sessions.
195
215
  """
216
+ results = []
217
+ base = '/root/.openclaw/agents'
218
+ for agent_dir in sorted(glob.glob(f'{base}/*/sessions/sessions.json')):
219
+ agent_id = agent_dir.split('/agents/')[1].split('/sessions/')[0]
220
+ try:
221
+ with open(agent_dir) as f:
222
+ sessions = json.load(f)
223
+ except:
224
+ continue
225
+ for session_key, meta in sessions.items():
226
+ if not isinstance(meta, dict):
227
+ continue
228
+ session_file = meta.get('sessionFile')
229
+ if not session_file:
230
+ continue
231
+ if not os.path.isabs(session_file):
232
+ session_file = os.path.join(f'{base}/{agent_id}/sessions', session_file)
233
+ if not os.path.exists(session_file):
234
+ continue
235
+ parts = session_key.split(':')
236
+ session_name = parts[2] if len(parts) > 2 else 'main'
237
+ results.append((session_key, agent_id, session_name, session_file))
238
+ return results
239
+
240
+
241
+ def print_summary_table(entries):
242
+ """
243
+ Print the summary table.
244
+ entries: list of dicts with keys: index, session_name, agent_id, total_pairs, work_url, session_key, first_time, last_time
245
+ """
246
+ print(f"\n{'#':>3} {'workspace':<16} {'会话名':<28} {'对话数':>6} {'起始时间':<20} {'最后更新':<20} {'作品链接':<12} {'聊天记录URL'}")
247
+ print(f"{'-'*3} {'-'*16} {'-'*28} {'-'*6} {'-'*20} {'-'*20} {'-'*12} {'-'*50}")
248
+ for e in entries:
249
+ work = e.get('work_url') or '(待填)'
250
+ url = session_key_to_url(e['session_key'])
251
+ name = (e['session_name'] or '')[:26]
252
+ agent = (e.get('agent_id') or '')[:16]
253
+ first_t = (e.get('first_time') or '')[:19]
254
+ 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}")
256
+
257
+
258
+ def process_one(url_or_key, output_dir, index):
196
259
  session_key = extract_session_key(url_or_key)
197
- session_short = sanitize_filename(session_key.split(':')[-1] if ':' in session_key else session_key)
260
+ parts = session_key.split(':')
261
+ agent_id = parts[1] if len(parts) > 1 else 'unknown'
262
+ session_name = parts[2] if len(parts) > 2 else 'main'
263
+
198
264
  print(f"\n[{index}] Session key: {session_key}")
199
265
 
200
266
  jsonl_path = find_session_file(session_key)
201
267
  if not jsonl_path:
202
- print(f"[{index}] ERROR: Could not find session file for {session_key}")
268
+ print(f"[{index}] ERROR: Could not find session file")
203
269
  return None
204
-
205
270
  if not os.path.exists(jsonl_path):
206
- print(f"[{index}] ERROR: JSONL file not found: {jsonl_path}")
271
+ print(f"[{index}] ERROR: JSONL not found: {jsonl_path}")
207
272
  return None
208
273
 
209
- print(f"[{index}] JSONL: {jsonl_path}")
210
- conversations = parse_jsonl_to_conversations(jsonl_path)
211
- print(f"[{index}] Found {len(conversations)} conversation pairs")
274
+ conversations, first_ts, last_ts = parse_jsonl_to_conversations(jsonl_path)
275
+ print(f"[{index}] {len(conversations)} pairs")
212
276
 
213
- # Generate per-session JS with index prefix
214
- js_filename = f"{index:02d}-{session_short}.js"
277
+ js_filename = build_filename(session_key, first_ts)
215
278
  js_path = os.path.join(output_dir, js_filename)
216
- count, session_name = generate_js(conversations, session_key, js_path)
217
- print(f"[{index}] Generated: {js_filename} ({count} pairs)")
279
+ count, _ = generate_js(conversations, session_key, first_ts, last_ts, js_path)
280
+ print(f"[{index}] Generated: {js_filename}")
218
281
 
219
282
  return {
220
283
  'index': index,
221
284
  'session_key': session_key,
222
285
  'session_name': session_name,
286
+ 'agent_id': agent_id,
223
287
  'js_file': js_filename,
224
288
  'total_pairs': count,
225
- 'jsonl_path': jsonl_path,
289
+ 'first_time': parse_time(first_ts) if first_ts else '',
290
+ 'last_time': parse_time(last_ts) if last_ts else '',
291
+ 'work_url': '',
226
292
  }
227
293
 
228
294
 
229
295
  def generate_index_js(results, output_dir):
230
- """Generate an index.js listing all sessions."""
231
- sessions = []
232
- for r in results:
233
- sessions.append({
234
- 'index': r['index'],
235
- 'session': r['session_name'],
236
- 'session_id': r['session_key'],
237
- 'js_file': r['js_file'],
238
- 'total_pairs': r['total_pairs'],
239
- })
296
+ sessions = [{
297
+ 'index': r['index'],
298
+ 'workspace_name': r['agent_id'],
299
+ 'session_name': r['session_name'],
300
+ 'session_id': r['session_key'],
301
+ 'js_file': r['js_file'],
302
+ 'total_pairs': r['total_pairs'],
303
+ 'first_time': r['first_time'],
304
+ 'last_time': r['last_time'],
305
+ 'work_url': r['work_url'],
306
+ } for r in results]
240
307
 
241
308
  index_content = f'''// Chat History Index - {len(sessions)} session(s)
242
309
  // Generated by chat-history-extractor skill
@@ -249,24 +316,82 @@ const chatIndex = {json.dumps(sessions, ensure_ascii=False, indent=2)};
249
316
  print(f"\nIndex: {index_path} ({len(sessions)} sessions)")
250
317
 
251
318
 
319
+ def run_scan(output_dir=None):
320
+ """Scan all agents, count conversations, show table. Optionally generate JS files."""
321
+ all_sessions = scan_all_agents()
322
+ print(f"Found {len(all_sessions)} session(s) across all agents.\n")
323
+
324
+ entries = []
325
+ for i, (session_key, agent_id, session_name, jsonl_path) in enumerate(all_sessions, 1):
326
+ try:
327
+ convs, first_ts, last_ts = count_conversations(jsonl_path)
328
+ entries.append({
329
+ 'index': i,
330
+ 'session_key': session_key,
331
+ 'session_name': session_name,
332
+ 'agent_id': agent_id,
333
+ 'total_pairs': len(convs),
334
+ 'work_url': '',
335
+ 'first_time': parse_time(first_ts) if first_ts else '',
336
+ 'last_time': parse_time(last_ts) if last_ts else '',
337
+ '_jsonl_path': jsonl_path,
338
+ '_first_ts': first_ts,
339
+ })
340
+ except Exception as e:
341
+ print(f" [SKIP] {session_key}: {e}")
342
+
343
+ print_summary_table(entries)
344
+
345
+ # Generate JS files if output_dir specified
346
+ if output_dir:
347
+ os.makedirs(output_dir, exist_ok=True)
348
+ results = []
349
+ for e in entries:
350
+ js_filename = build_filename(e['session_key'], e['_first_ts'])
351
+ js_path = os.path.join(output_dir, js_filename)
352
+ convs, e_first, e_last = count_conversations(e['_jsonl_path'])
353
+ generate_js(convs, e['session_key'], e_first, e_last, js_path)
354
+ results.append({**e, 'js_file': js_filename})
355
+
356
+ if len(results) > 1:
357
+ generate_index_js(results, output_dir)
358
+
359
+ # Backward compat
360
+ first_js = os.path.join(output_dir, results[0]['js_file'])
361
+ compat_js = os.path.join(output_dir, 'chat_history.js')
362
+ if first_js != compat_js:
363
+ import shutil
364
+ shutil.copy2(first_js, compat_js)
365
+
366
+ total_pairs = sum(e['total_pairs'] for e in entries)
367
+ print(f"\n合计:{len(entries)} 个会话,{total_pairs} 对对话。")
368
+ return entries
369
+
370
+
252
371
  def main():
253
372
  if len(sys.argv) < 2:
254
373
  print("Usage:")
255
374
  print(" python3 extract_chat.py <url-or-key> [output-dir]")
256
375
  print(" python3 extract_chat.py 'url1,url2,url3' [output-dir]")
257
376
  print(' python3 extract_chat.py \'["url1","url2"]\' [output-dir]')
377
+ print(" python3 extract_chat.py --scan [output-dir]")
258
378
  sys.exit(1)
259
379
 
260
- raw_input = sys.argv[1]
261
- output_dir = sys.argv[2] if len(sys.argv) > 2 else os.getcwd()
380
+ arg1 = sys.argv[1]
381
+
382
+ # --scan mode
383
+ if arg1 == '--scan':
384
+ output_dir = sys.argv[2] if len(sys.argv) > 2 else None
385
+ run_scan(output_dir)
386
+ return
262
387
 
263
- # Parse input into list
264
- items = parse_input(raw_input)
388
+ # Normal mode
389
+ output_dir = sys.argv[2] if len(sys.argv) > 2 else os.getcwd()
390
+ items = parse_input(arg1)
265
391
  print(f"Input: {len(items)} session(s)")
266
392
 
267
393
  os.makedirs(output_dir, exist_ok=True)
268
394
 
269
- # Process each session
270
395
  results = []
271
396
  for i, item in enumerate(items, 1):
272
397
  r = process_one(item, output_dir, i)
@@ -277,11 +402,9 @@ def main():
277
402
  print("\nERROR: No sessions processed successfully.")
278
403
  sys.exit(1)
279
404
 
280
- # Generate index.js if multiple sessions
281
405
  if len(results) > 1:
282
406
  generate_index_js(results, output_dir)
283
407
 
284
- # Backward compat: copy first session as chat_history.js
285
408
  first_js = os.path.join(output_dir, results[0]['js_file'])
286
409
  compat_js = os.path.join(output_dir, 'chat_history.js')
287
410
  if first_js != compat_js:
@@ -289,11 +412,10 @@ def main():
289
412
  shutil.copy2(first_js, compat_js)
290
413
  print(f"Compat: chat_history.js -> {results[0]['js_file']}")
291
414
 
292
- # Summary
293
- print(f"\n{'='*50}")
294
- print(f"Done! {len(results)}/{len(items)} session(s) processed.")
295
- for r in results:
296
- print(f" [{r['index']}] {r['session_name']} ({r['total_pairs']} pairs) -> {r['js_file']}")
415
+ # Summary table
416
+ print_summary_table(results)
417
+ total_pairs = sum(r['total_pairs'] for r in results)
418
+ print(f"\n合计:{len(results)} 个会话,{total_pairs} 对对话。")
297
419
 
298
420
 
299
421
  if __name__ == '__main__':