@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
|
-
|
|
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 = '✕';
|
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 +
|
|
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@' +
|
|
162
|
+
console.log('[5/8] 📦 安装 openclaw@' + targetVersion + '...');
|
|
161
163
|
try {
|
|
162
|
-
execSync('npm install -g openclaw@' +
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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__':
|