@aiyiran/myclaw 1.1.131 → 1.1.133
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
|
-
|
|
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
|
-
|
|
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 (
|
|
1185
|
-
|
|
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
|
-
|
|
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 = '✕';
|
|
@@ -179,9 +179,32 @@ function run(cliArgs) {
|
|
|
179
179
|
if (agent.agentDir) agentDirs.set(agent.agentDir, agent.id);
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
// 检测 openclaw 版本,决定写入目标
|
|
183
|
+
// 6.X 使用 node:sqlite (DatabaseSync),5.X 使用 auth-profiles.json
|
|
184
|
+
let DatabaseSync = null;
|
|
185
|
+
try {
|
|
186
|
+
({ DatabaseSync } = require('node:sqlite'));
|
|
187
|
+
} catch { /* Node < 22,无内置 sqlite,用 JSON 方式 */ }
|
|
188
|
+
|
|
189
|
+
const SQLITE_SCHEMA = `
|
|
190
|
+
CREATE TABLE IF NOT EXISTS auth_profile_store (
|
|
191
|
+
store_key TEXT NOT NULL PRIMARY KEY, store_json TEXT NOT NULL, updated_at INTEGER NOT NULL
|
|
192
|
+
);
|
|
193
|
+
CREATE TABLE IF NOT EXISTS auth_profile_state (
|
|
194
|
+
state_key TEXT NOT NULL PRIMARY KEY, state_json TEXT NOT NULL, updated_at INTEGER NOT NULL
|
|
195
|
+
);`;
|
|
196
|
+
|
|
197
|
+
const sqliteStorePayload = { version: 1, profiles: { "minimax:cn": { type: "api_key", provider: "minimax", key: apiKey } } };
|
|
198
|
+
const sqliteStatePayload = { lastGood: { "minimax": "minimax:cn" } };
|
|
199
|
+
|
|
182
200
|
let okCount = 0, skipCount = 0;
|
|
183
201
|
|
|
184
202
|
for (const [agentDir, agentId] of agentDirs) {
|
|
203
|
+
if (!fs.existsSync(agentDir)) {
|
|
204
|
+
try { fs.mkdirSync(agentDir, { recursive: true }); } catch { /* ignore */ }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── 5.X:始终写 auth-profiles.json(兼容旧版) ──
|
|
185
208
|
const profilesPath = path.join(agentDir, 'auth-profiles.json');
|
|
186
209
|
let profiles = { version: 1, profiles: {}, lastGood: {} };
|
|
187
210
|
if (fs.existsSync(profilesPath)) {
|
|
@@ -191,14 +214,32 @@ function run(cliArgs) {
|
|
|
191
214
|
if (!profiles.lastGood) profiles.lastGood = {};
|
|
192
215
|
} catch { /* 解析失败用空骨架 */ }
|
|
193
216
|
}
|
|
194
|
-
|
|
195
217
|
profiles.profiles["minimax:cn"] = { type: "api_key", provider: "minimax", key: apiKey };
|
|
196
218
|
profiles.lastGood["minimax"] = "minimax:cn";
|
|
219
|
+
try { fs.writeFileSync(profilesPath, JSON.stringify(profiles, null, 2) + '\n', 'utf8'); } catch { /* ignore */ }
|
|
220
|
+
|
|
221
|
+
// ── 6.X:写入 openclaw-agent.sqlite ──
|
|
222
|
+
let sqliteOk = false;
|
|
223
|
+
if (DatabaseSync) {
|
|
224
|
+
const dbPath = path.join(agentDir, 'openclaw-agent.sqlite');
|
|
225
|
+
try {
|
|
226
|
+
const db = new DatabaseSync(dbPath);
|
|
227
|
+
db.exec('PRAGMA journal_mode = WAL;');
|
|
228
|
+
db.exec(SQLITE_SCHEMA);
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
db.prepare('INSERT OR REPLACE INTO auth_profile_store (store_key, store_json, updated_at) VALUES (?,?,?)')
|
|
231
|
+
.run('primary', JSON.stringify(sqliteStorePayload), now);
|
|
232
|
+
db.prepare('INSERT OR REPLACE INTO auth_profile_state (state_key, state_json, updated_at) VALUES (?,?,?)')
|
|
233
|
+
.run('primary', JSON.stringify(sqliteStatePayload), now);
|
|
234
|
+
db.close();
|
|
235
|
+
sqliteOk = true;
|
|
236
|
+
} catch (err) {
|
|
237
|
+
console.log(' ⚠ ' + agentId + ' (sqlite): ' + err.message);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
197
240
|
|
|
198
241
|
try {
|
|
199
|
-
|
|
200
|
-
fs.writeFileSync(profilesPath, JSON.stringify(profiles, null, 2) + '\n', 'utf8');
|
|
201
|
-
console.log(' ✅ ' + agentId);
|
|
242
|
+
console.log(' ✅ ' + agentId + (sqliteOk ? ' (json+sqlite)' : ' (json)'));
|
|
202
243
|
okCount++;
|
|
203
244
|
} catch (err) {
|
|
204
245
|
console.log(' ⚠ ' + agentId + ': ' + err.message);
|
package/package.json
CHANGED
|
@@ -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
|
-
- 每个会话生成独立的 <
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
99
|
-
"""Parse JSONL
|
|
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
|
-
#
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
271
|
+
print(f"[{index}] ERROR: JSONL not found: {jsonl_path}")
|
|
207
272
|
return None
|
|
208
273
|
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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,
|
|
217
|
-
print(f"[{index}] Generated: {js_filename}
|
|
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
|
-
'
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
#
|
|
264
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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__':
|