@aiyiran/myclaw 1.1.135 → 1.1.137
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.
|
@@ -808,8 +808,14 @@
|
|
|
808
808
|
'background: #252536',
|
|
809
809
|
].join(';');
|
|
810
810
|
|
|
811
|
-
//
|
|
811
|
+
// 远程环境:走 CDN(边缘缓存,速度快,不受本机出口带宽限制)
|
|
812
|
+
// 本地环境:走本地 API(文件尚未同步到 CDN)
|
|
813
|
+
// CSP img-src 限制后续在服务端统一放行,此处不做 blob 绕行
|
|
812
814
|
var img = document.createElement('img');
|
|
815
|
+
var _imgEnv = detectEnvironment();
|
|
816
|
+
img.src = _imgEnv.remote
|
|
817
|
+
? previewUrl + '?t=' + Date.now()
|
|
818
|
+
: MYCLAW_API_BASE + '/api/file?path=' + encodeURIComponent(getWorkspaceId() + '/' + asset.path) + '&t=' + Date.now();
|
|
813
819
|
img.alt = asset.name || asset.path;
|
|
814
820
|
img.style.cssText = [
|
|
815
821
|
'max-width: 100%',
|
|
@@ -820,28 +826,13 @@
|
|
|
820
826
|
'position: relative',
|
|
821
827
|
'z-index: 1',
|
|
822
828
|
].join(';');
|
|
823
|
-
|
|
824
|
-
function showImgError() {
|
|
829
|
+
img.onerror = function () {
|
|
825
830
|
img.style.display = 'none';
|
|
826
831
|
var errMsg = document.createElement('div');
|
|
827
832
|
errMsg.textContent = '加载失败';
|
|
828
833
|
errMsg.style.cssText = 'color:#666;font-size:13px;font-family:monospace;';
|
|
829
834
|
imgArea.appendChild(errMsg);
|
|
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);
|
|
835
|
+
};
|
|
845
836
|
|
|
846
837
|
imgArea.appendChild(img);
|
|
847
838
|
imgBox.appendChild(imgArea);
|
|
@@ -1179,11 +1170,17 @@
|
|
|
1179
1170
|
var isVideo = VIDEO_EXTS.indexOf(assetExt) !== -1;
|
|
1180
1171
|
|
|
1181
1172
|
if (isVideo) {
|
|
1182
|
-
//
|
|
1173
|
+
// <video> 元素支持 range 请求,流式加载,可拖拽进度条,不用 iframe
|
|
1174
|
+
// 远程环境:走 CDN(带宽快,不受本机出口限制)
|
|
1175
|
+
// 本地环境:走本地 API(文件尚未同步到 CDN)
|
|
1176
|
+
// CSP media-src 限制后续在服务端统一放行
|
|
1183
1177
|
var video = document.createElement('video');
|
|
1184
1178
|
video.controls = true;
|
|
1185
1179
|
video.style.cssText = 'flex:1;width:100%;max-height:100%;background:#000;outline:none;';
|
|
1186
|
-
|
|
1180
|
+
var _videoEnv = detectEnvironment();
|
|
1181
|
+
video.src = _videoEnv.remote
|
|
1182
|
+
? previewUrl + '?t=' + Date.now()
|
|
1183
|
+
: MYCLAW_API_BASE + '/api/file?path=' + encodeURIComponent(getWorkspaceId() + '/' + asset.path);
|
|
1187
1184
|
box.appendChild(video);
|
|
1188
1185
|
overlay.appendChild(box);
|
|
1189
1186
|
document.body.appendChild(overlay);
|
package/package.json
CHANGED
package/patches/patch.js
CHANGED
|
@@ -113,6 +113,19 @@ function getMyclawVersion() {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* 获取已安装的 OpenClaw 版本号
|
|
118
|
+
* uiDir 形如 <pkg>/dist/control-ui,包根目录为 uiDir/../..
|
|
119
|
+
*/
|
|
120
|
+
function getOpenclawVersion(uiDir) {
|
|
121
|
+
try {
|
|
122
|
+
const pkgRoot = path.resolve(uiDir, '..', '..');
|
|
123
|
+
return require(path.join(pkgRoot, 'package.json')).version;
|
|
124
|
+
} catch {
|
|
125
|
+
return 'unknown';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
116
129
|
/**
|
|
117
130
|
* 执行注入
|
|
118
131
|
*/
|
|
@@ -250,6 +263,13 @@ function patch() {
|
|
|
250
263
|
let patched = false;
|
|
251
264
|
let foundTarget = false;
|
|
252
265
|
|
|
266
|
+
// img-src / media-src 放宽仅针对特定 OpenClaw 版本(2026.6.9)。
|
|
267
|
+
// 该版本 CSP 中 img-src/media-src 为 'self' data: blob:,会拦截 CDN 图片/视频;
|
|
268
|
+
// 限定版本可避免误改其它版本(CSP 结构可能不同,盲目放宽有风险)。
|
|
269
|
+
const ocVersion = getOpenclawVersion(uiDir);
|
|
270
|
+
const needsMediaCspFix = ocVersion === '2026.6.9';
|
|
271
|
+
console.log('[myclaw-patch] → OpenClaw 版本: ' + ocVersion + (needsMediaCspFix ? ' (将额外放宽 img-src/media-src)' : ''));
|
|
272
|
+
|
|
253
273
|
for (const f of distFiles) {
|
|
254
274
|
// 兼容多版本:gateway-cli-*.js(旧)、server-*.js(中)、control-ui-*.js(新)
|
|
255
275
|
const isTarget = (f.startsWith('gateway-cli-') || f.startsWith('server-') || f.startsWith('control-ui-')) && f.endsWith('.js');
|
|
@@ -264,7 +284,9 @@ function patch() {
|
|
|
264
284
|
const needsFrameSrc = !content.includes('"frame-src *');
|
|
265
285
|
const needsConnectSrc = !content.includes("connect-src *");
|
|
266
286
|
const needsFrameAncestors = !content.includes("frame-ancestors *");
|
|
267
|
-
const
|
|
287
|
+
const needsImgSrc = needsMediaCspFix && !content.includes('"img-src *');
|
|
288
|
+
const needsMediaSrc = needsMediaCspFix && !content.includes('"media-src *');
|
|
289
|
+
const needsAnyCspPatch = needsFrameSrc || needsConnectSrc || needsFrameAncestors || needsImgSrc || needsMediaSrc;
|
|
268
290
|
|
|
269
291
|
if (needsMicrophonePatch || needsAnyCspPatch) {
|
|
270
292
|
// 备份
|
|
@@ -306,6 +328,16 @@ function patch() {
|
|
|
306
328
|
content = content.replace(/\"frame-ancestors[^"]*\"/g, '"frame-ancestors *"');
|
|
307
329
|
cspPatches.push('frame-ancestors *');
|
|
308
330
|
}
|
|
331
|
+
// img-src → 允许所有(仅 2026.6.9,放行 CDN 图片)
|
|
332
|
+
if (needsImgSrc) {
|
|
333
|
+
content = content.replace(/\"img-src[^"]*\"/g, '"img-src *"');
|
|
334
|
+
cspPatches.push('img-src *');
|
|
335
|
+
}
|
|
336
|
+
// media-src → 允许所有(仅 2026.6.9,放行 CDN 视频)
|
|
337
|
+
if (needsMediaSrc) {
|
|
338
|
+
content = content.replace(/\"media-src[^"]*\"/g, '"media-src *"');
|
|
339
|
+
cspPatches.push('media-src *');
|
|
340
|
+
}
|
|
309
341
|
if (cspPatches.length > 0) {
|
|
310
342
|
console.log('[myclaw-patch] ✅ 已修复 CSP (' + cspPatches.join(', ') + '): ' + f);
|
|
311
343
|
}
|
|
@@ -3,20 +3,25 @@
|
|
|
3
3
|
Extract chat history from OpenClaw session URL(s) and generate JS data file(s).
|
|
4
4
|
|
|
5
5
|
Usage:
|
|
6
|
-
#
|
|
7
|
-
python3 extract_chat.py <session-url-or-key>
|
|
6
|
+
# 单个会话(输出到当前 workspace)
|
|
7
|
+
python3 extract_chat.py <session-url-or-key>
|
|
8
8
|
|
|
9
9
|
# 多个会话(逗号分隔 / JSON 数组)
|
|
10
10
|
python3 extract_chat.py "url1,url2,url3" [output-dir]
|
|
11
|
-
python3 extract_chat.py '["url1","url2","url3"]' [output-dir]
|
|
11
|
+
python3 extract_chat.py '[ "url1","url2","url3"]' [output-dir]
|
|
12
12
|
|
|
13
13
|
# 扫描所有 agent 的所有会话,统计表格
|
|
14
|
-
python3 extract_chat.py --scan [
|
|
14
|
+
python3 extract_chat.py --scan [--html]
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
输出(默认到当前 workspace /root/.openclaw/workspace-teacher):
|
|
17
17
|
- 每个会话生成独立的 <agentId>_<sessionName>_<date>_<time>.js
|
|
18
18
|
- 生成 index.js,包含所有会话的元信息列表(含 work_url 字段)
|
|
19
19
|
- 生成 chat_history.js(向后兼容,指向第一个会话)
|
|
20
|
+
- --html 时额外生成 summary.html(含可点击链接)
|
|
21
|
+
|
|
22
|
+
Host 检测:
|
|
23
|
+
- 自动从 /root/.openclaw/myclaw/server/config.json 读取
|
|
24
|
+
- 可用 --host 覆盖
|
|
20
25
|
|
|
21
26
|
work_url 字段:
|
|
22
27
|
- 默认为空字符串
|
|
@@ -34,6 +39,31 @@ from datetime import datetime, timezone, timedelta
|
|
|
34
39
|
tz_beijing = timezone(timedelta(hours=8))
|
|
35
40
|
|
|
36
41
|
|
|
42
|
+
def detect_chat_host():
|
|
43
|
+
"""Auto-detect chat host from config.json."""
|
|
44
|
+
config_path = '/root/.openclaw/myclaw/server/config.json'
|
|
45
|
+
try:
|
|
46
|
+
with open(config_path) as f:
|
|
47
|
+
cfg = json.load(f)
|
|
48
|
+
claw = cfg.get('claw', 'claw5')
|
|
49
|
+
return f"{claw}.kekouen.cn"
|
|
50
|
+
except Exception:
|
|
51
|
+
return 'claw5.kekouen.cn'
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_default_output_dir():
|
|
55
|
+
"""Return the current workspace directory."""
|
|
56
|
+
# OPENCLAW_WORKSPACE is set to the workspace path
|
|
57
|
+
ws = os.environ.get('OPENCLAW_WORKSPACE', '')
|
|
58
|
+
if ws and os.path.isdir(ws):
|
|
59
|
+
return ws
|
|
60
|
+
# Fallback: derive from cwd (e.g. /root/.openclaw/workspace-teacher)
|
|
61
|
+
cwd = os.getcwd()
|
|
62
|
+
if '/workspace-' in cwd:
|
|
63
|
+
return cwd
|
|
64
|
+
return cwd
|
|
65
|
+
|
|
66
|
+
|
|
37
67
|
def parse_time(ts_str):
|
|
38
68
|
try:
|
|
39
69
|
ts_str = ts_str.replace('Z', '')
|
|
@@ -202,10 +232,12 @@ const chatData = {json.dumps(output_data, ensure_ascii=False, indent=2)};
|
|
|
202
232
|
return len(conversations), session_name
|
|
203
233
|
|
|
204
234
|
|
|
205
|
-
def session_key_to_url(session_key):
|
|
206
|
-
"""Build the
|
|
235
|
+
def session_key_to_url(session_key, host=None):
|
|
236
|
+
"""Build the chat URL from a session key. Host auto-detected from config.json."""
|
|
237
|
+
if host is None:
|
|
238
|
+
host = detect_chat_host()
|
|
207
239
|
encoded = quote(session_key, safe='')
|
|
208
|
-
return f"https://
|
|
240
|
+
return f"https://{host}/chat?session={encoded}&token=aiyiran"
|
|
209
241
|
|
|
210
242
|
|
|
211
243
|
def scan_all_agents():
|
|
@@ -238,21 +270,141 @@ def scan_all_agents():
|
|
|
238
270
|
return results
|
|
239
271
|
|
|
240
272
|
|
|
241
|
-
def print_summary_table(entries):
|
|
273
|
+
def print_summary_table(entries, host=None):
|
|
274
|
+
if host is None:
|
|
275
|
+
host = detect_chat_host()
|
|
242
276
|
"""
|
|
243
277
|
Print the summary table.
|
|
244
278
|
entries: list of dicts with keys: index, session_name, agent_id, total_pairs, work_url, session_key, first_time, last_time
|
|
245
279
|
"""
|
|
246
|
-
print(f"\n{'#':>3} {'workspace':<16} {'会话名':<28} {'对话数':>6} {'起始时间':<20} {'最后更新':<20} {'作品链接':<
|
|
247
|
-
print(f"{'-'*3} {'-'*16} {'-'*28} {'-'*6} {'-'*20} {'-'*20} {'-'*
|
|
280
|
+
print(f"\n{'#':>3} {'workspace':<16} {'会话名':<28} {'对话数':>6} {'起始时间':<20} {'最后更新':<20} {'作品链接':<14} {'对话链接'}")
|
|
281
|
+
print(f"{'-'*3} {'-'*16} {'-'*28} {'-'*6} {'-'*20} {'-'*20} {'-'*14} {'-'*50}")
|
|
248
282
|
for e in entries:
|
|
249
283
|
work = e.get('work_url') or '(待填)'
|
|
250
|
-
url = session_key_to_url(e['session_key'])
|
|
284
|
+
url = session_key_to_url(e['session_key'], host)
|
|
251
285
|
name = (e['session_name'] or '')[:26]
|
|
252
286
|
agent = (e.get('agent_id') or '')[:16]
|
|
253
287
|
first_t = (e.get('first_time') or '')[:19]
|
|
254
288
|
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:<
|
|
289
|
+
print(f"{e['index']:>3} {agent:<16} {name:<28} {e['total_pairs']:>6} {first_t:<20} {last_t:<20} {work:<14} {url}")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def generate_html_table(entries, host, output_path):
|
|
293
|
+
"""Generate an HTML file with a clickable table of all sessions."""
|
|
294
|
+
total_pairs = sum(e['total_pairs'] for e in entries)
|
|
295
|
+
rows_html = []
|
|
296
|
+
for e in entries:
|
|
297
|
+
work_url = e.get('work_url', '')
|
|
298
|
+
if work_url:
|
|
299
|
+
work_cell = f'<a href="{work_url}" target="_blank" class="link">🔗 查看作品</a>'
|
|
300
|
+
else:
|
|
301
|
+
work_cell = '<span class="pending">(待填)</span>'
|
|
302
|
+
|
|
303
|
+
chat_url = session_key_to_url(e['session_key'], host)
|
|
304
|
+
name = e.get('session_name', '') or ''
|
|
305
|
+
agent = e.get('agent_id', '') or ''
|
|
306
|
+
chat_cell = f'<a href="{chat_url}" target="_blank" class="link">💬 打开对话</a>'
|
|
307
|
+
|
|
308
|
+
rows_html.append(f"""
|
|
309
|
+
<tr>
|
|
310
|
+
<td class="num">{e['index']}</td>
|
|
311
|
+
<td>{agent}</td>
|
|
312
|
+
<td>{name}</td>
|
|
313
|
+
<td class="num">{e['total_pairs']}</td>
|
|
314
|
+
<td>{e.get('first_time', '')}</td>
|
|
315
|
+
<td>{e.get('last_time', '')}</td>
|
|
316
|
+
<td>{work_cell}</td>
|
|
317
|
+
<td>{chat_cell}</td>
|
|
318
|
+
</tr>""")
|
|
319
|
+
|
|
320
|
+
rows = '\n'.join(rows_html)
|
|
321
|
+
html = f'''<!DOCTYPE html>
|
|
322
|
+
<html lang="zh-CN">
|
|
323
|
+
<head>
|
|
324
|
+
<meta charset="UTF-8">
|
|
325
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
326
|
+
<title>作品扫描 - {len(entries)} sessions</title>
|
|
327
|
+
<style>
|
|
328
|
+
* {{ box-sizing: border-box; }}
|
|
329
|
+
body {{
|
|
330
|
+
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
331
|
+
background: linear-gradient(135deg, #f5f7fa 0%, #e9ecf3 100%);
|
|
332
|
+
margin: 0; padding: 24px; min-height: 100vh; color: #2d3748;
|
|
333
|
+
}}
|
|
334
|
+
.container {{ max-width: 1200px; margin: 0 auto; }}
|
|
335
|
+
.header {{
|
|
336
|
+
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
337
|
+
color: white; padding: 30px; border-radius: 16px; margin-bottom: 24px;
|
|
338
|
+
box-shadow: 0 10px 30px rgba(102,126,234,0.3);
|
|
339
|
+
}}
|
|
340
|
+
.header h1 {{ margin: 0 0 8px 0; font-size: 1.8em; }}
|
|
341
|
+
.header p {{ margin: 0; opacity: 0.9; }}
|
|
342
|
+
.summary-bar {{
|
|
343
|
+
display: flex; gap: 16px; margin-top: 16px; flex-wrap: wrap;
|
|
344
|
+
}}
|
|
345
|
+
.summary-bar .stat {{
|
|
346
|
+
background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 8px;
|
|
347
|
+
backdrop-filter: blur(10px);
|
|
348
|
+
}}
|
|
349
|
+
.stat strong {{ font-size: 1.3em; margin-right: 6px; }}
|
|
350
|
+
table {{
|
|
351
|
+
width: 100%; background: white; border-radius: 12px; overflow: hidden;
|
|
352
|
+
border-collapse: collapse; box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
|
353
|
+
}}
|
|
354
|
+
th {{
|
|
355
|
+
background: #f7fafc; padding: 14px 12px; text-align: left;
|
|
356
|
+
font-weight: 600; color: #4a5568; border-bottom: 2px solid #e2e8f0;
|
|
357
|
+
font-size: 0.9em;
|
|
358
|
+
}}
|
|
359
|
+
td {{ padding: 12px; border-bottom: 1px solid #edf2f7; font-size: 0.92em; }}
|
|
360
|
+
tr:hover td {{ background: #f7fafc; }}
|
|
361
|
+
.num {{ text-align: right; font-variant-numeric: tabular-nums; }}
|
|
362
|
+
.link {{
|
|
363
|
+
color: #667eea; text-decoration: none; font-weight: 500;
|
|
364
|
+
padding: 4px 10px; border-radius: 6px; background: #edf2f7;
|
|
365
|
+
transition: all 0.2s; display: inline-block;
|
|
366
|
+
}}
|
|
367
|
+
.link:hover {{ background: #667eea; color: white; transform: translateY(-1px); }}
|
|
368
|
+
.pending {{ color: #a0aec0; font-style: italic; }}
|
|
369
|
+
.footer {{
|
|
370
|
+
text-align: center; margin-top: 24px; color: #718096; font-size: 0.85em;
|
|
371
|
+
}}
|
|
372
|
+
</style>
|
|
373
|
+
</head>
|
|
374
|
+
<body>
|
|
375
|
+
<div class="container">
|
|
376
|
+
<div class="header">
|
|
377
|
+
<h1>📋 作品扫描</h1>
|
|
378
|
+
<p>Generated by chat-history-extractor</p>
|
|
379
|
+
<div class="summary-bar">
|
|
380
|
+
<div class="stat"><strong>{len(entries)}</strong> sessions</div>
|
|
381
|
+
<div class="stat"><strong>{total_pairs}</strong> conversation pairs</div>
|
|
382
|
+
<div class="stat"><strong>host:</strong> {host}</div>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
<table>
|
|
386
|
+
<thead>
|
|
387
|
+
<tr>
|
|
388
|
+
<th class="num">#</th>
|
|
389
|
+
<th>workspace</th>
|
|
390
|
+
<th>会话名</th>
|
|
391
|
+
<th class="num">对话数</th>
|
|
392
|
+
<th>起始时间</th>
|
|
393
|
+
<th>最后更新</th>
|
|
394
|
+
<th>作品链接</th>
|
|
395
|
+
<th>对话链接</th>
|
|
396
|
+
</tr>
|
|
397
|
+
</thead>
|
|
398
|
+
<tbody>{rows}
|
|
399
|
+
</tbody>
|
|
400
|
+
</table>
|
|
401
|
+
<p class="footer">提示:点击「作品链接」和「对话链接」在新页面打开。后续可在 JS 文件中编辑 work_url 字段补上作品链接。</p>
|
|
402
|
+
</div>
|
|
403
|
+
</body>
|
|
404
|
+
</html>
|
|
405
|
+
'''
|
|
406
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
407
|
+
f.write(html)
|
|
256
408
|
|
|
257
409
|
|
|
258
410
|
def process_one(url_or_key, output_dir, index):
|
|
@@ -316,8 +468,10 @@ const chatIndex = {json.dumps(sessions, ensure_ascii=False, indent=2)};
|
|
|
316
468
|
print(f"\nIndex: {index_path} ({len(sessions)} sessions)")
|
|
317
469
|
|
|
318
470
|
|
|
319
|
-
def run_scan(output_dir=None):
|
|
320
|
-
|
|
471
|
+
def run_scan(output_dir=None, host=None, html_output=False):
|
|
472
|
+
if host is None:
|
|
473
|
+
host = detect_chat_host()
|
|
474
|
+
"""Scan all agents, count conversations, show table. Optionally generate JS files + HTML."""
|
|
321
475
|
all_sessions = scan_all_agents()
|
|
322
476
|
print(f"Found {len(all_sessions)} session(s) across all agents.\n")
|
|
323
477
|
|
|
@@ -340,7 +494,13 @@ def run_scan(output_dir=None):
|
|
|
340
494
|
except Exception as e:
|
|
341
495
|
print(f" [SKIP] {session_key}: {e}")
|
|
342
496
|
|
|
343
|
-
|
|
497
|
+
# Sort by first_time descending (newest first)
|
|
498
|
+
entries.sort(key=lambda e: (e.get('first_time') or '') or '', reverse=True)
|
|
499
|
+
# Re-assign index after sorting
|
|
500
|
+
for i, e in enumerate(entries, 1):
|
|
501
|
+
e['index'] = i
|
|
502
|
+
|
|
503
|
+
print_summary_table(entries, host)
|
|
344
504
|
|
|
345
505
|
# Generate JS files if output_dir specified
|
|
346
506
|
if output_dir:
|
|
@@ -363,32 +523,71 @@ def run_scan(output_dir=None):
|
|
|
363
523
|
import shutil
|
|
364
524
|
shutil.copy2(first_js, compat_js)
|
|
365
525
|
|
|
526
|
+
# Generate HTML table with clickable links
|
|
527
|
+
if html_output:
|
|
528
|
+
target_dir = output_dir if output_dir else os.getcwd()
|
|
529
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
530
|
+
now_str = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
531
|
+
html_filename = f"作品扫描_{now_str}.html"
|
|
532
|
+
html_path = os.path.join(target_dir, html_filename)
|
|
533
|
+
generate_html_table(entries, host, html_path)
|
|
534
|
+
print(f"HTML: {html_path}")
|
|
535
|
+
|
|
366
536
|
total_pairs = sum(e['total_pairs'] for e in entries)
|
|
367
537
|
print(f"\n合计:{len(entries)} 个会话,{total_pairs} 对对话。")
|
|
368
538
|
return entries
|
|
369
539
|
|
|
370
540
|
|
|
371
541
|
def main():
|
|
372
|
-
|
|
542
|
+
# Parse flags: --host HOST, --html, --scan
|
|
543
|
+
# Position: positional args (no --) come first, flags can be interleaved
|
|
544
|
+
args = sys.argv[1:]
|
|
545
|
+
|
|
546
|
+
host = detect_chat_host()
|
|
547
|
+
html_output = False
|
|
548
|
+
scan_mode = False
|
|
549
|
+
positional = []
|
|
550
|
+
|
|
551
|
+
i = 0
|
|
552
|
+
while i < len(args):
|
|
553
|
+
a = args[i]
|
|
554
|
+
if a == '--host' and i + 1 < len(args):
|
|
555
|
+
host = args[i + 1]
|
|
556
|
+
i += 2
|
|
557
|
+
elif a == '--html':
|
|
558
|
+
html_output = True
|
|
559
|
+
i += 1
|
|
560
|
+
elif a == '--scan':
|
|
561
|
+
scan_mode = True
|
|
562
|
+
i += 1
|
|
563
|
+
else:
|
|
564
|
+
positional.append(a)
|
|
565
|
+
i += 1
|
|
566
|
+
|
|
567
|
+
if not positional and not scan_mode:
|
|
373
568
|
print("Usage:")
|
|
374
|
-
print(" python3 extract_chat.py <url-or-key> [output-dir]")
|
|
375
|
-
print(" python3 extract_chat.py 'url1,url2,url3' [output-dir]")
|
|
376
|
-
print(' python3 extract_chat.py \'["url1","url2"]\' [output-dir]')
|
|
377
|
-
print(" python3 extract_chat.py --scan [output-dir]")
|
|
569
|
+
print(" python3 extract_chat.py <url-or-key> [output-dir] [--host HOST] [--html]")
|
|
570
|
+
print(" python3 extract_chat.py 'url1,url2,url3' [output-dir] [--host HOST] [--html]")
|
|
571
|
+
print(' python3 extract_chat.py \'["url1","url2"]\' [output-dir] [--host HOST] [--html]')
|
|
572
|
+
print(" python3 extract_chat.py --scan [output-dir] [--host HOST] [--html]")
|
|
573
|
+
print("Flags:")
|
|
574
|
+
print(" --host HOST Override chat host (default: auto-detect from config.json)")
|
|
575
|
+
print(" --html Also generate summary.html with clickable links")
|
|
576
|
+
print(" --scan Scan all agents")
|
|
378
577
|
sys.exit(1)
|
|
379
578
|
|
|
380
|
-
arg1 = sys.argv[1]
|
|
381
|
-
|
|
382
579
|
# --scan mode
|
|
383
|
-
if
|
|
384
|
-
output_dir =
|
|
385
|
-
run_scan(output_dir)
|
|
580
|
+
if scan_mode:
|
|
581
|
+
output_dir = positional[0] if positional else get_default_output_dir()
|
|
582
|
+
run_scan(output_dir, host=host, html_output=html_output)
|
|
386
583
|
return
|
|
387
584
|
|
|
388
585
|
# Normal mode
|
|
389
|
-
|
|
586
|
+
arg1 = positional[0]
|
|
587
|
+
output_dir = positional[1] if len(positional) > 1 else get_default_output_dir()
|
|
390
588
|
items = parse_input(arg1)
|
|
391
589
|
print(f"Input: {len(items)} session(s)")
|
|
590
|
+
print(f"Host: {detect_chat_host()}")
|
|
392
591
|
|
|
393
592
|
os.makedirs(output_dir, exist_ok=True)
|
|
394
593
|
|
|
@@ -413,7 +612,14 @@ def main():
|
|
|
413
612
|
print(f"Compat: chat_history.js -> {results[0]['js_file']}")
|
|
414
613
|
|
|
415
614
|
# Summary table
|
|
416
|
-
print_summary_table(results)
|
|
615
|
+
print_summary_table(results, host)
|
|
616
|
+
|
|
617
|
+
# HTML output
|
|
618
|
+
if html_output:
|
|
619
|
+
html_path = os.path.join(output_dir, 'summary.html')
|
|
620
|
+
generate_html_table(results, host, html_path)
|
|
621
|
+
print(f"HTML: {html_path}")
|
|
622
|
+
|
|
417
623
|
total_pairs = sum(r['total_pairs'] for r in results)
|
|
418
624
|
print(f"\n合计:{len(results)} 个会话,{total_pairs} 对对话。")
|
|
419
625
|
|