@aiyiran/myclaw 1.1.166 → 1.1.168
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/myclaw-inject.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
var cursorOffset = 0; // 录音开始时光标在 textarea 中的位置
|
|
29
29
|
var injected = false;
|
|
30
30
|
var stopping = false; // 正在等待最终识别结果(stopVoice 的 2 秒窗口)
|
|
31
|
+
var suppressNextClipboardCopy = false;
|
|
31
32
|
|
|
32
33
|
// ═══ 1. 右下角升级按钮 ═══
|
|
33
34
|
function createVersionBar() {
|
|
@@ -273,6 +274,15 @@
|
|
|
273
274
|
ta.dispatchEvent(new Event("input", { bubbles: true }));
|
|
274
275
|
}
|
|
275
276
|
|
|
277
|
+
function sendGeneratedPrompt(text) {
|
|
278
|
+
suppressNextClipboardCopy = true;
|
|
279
|
+
setTextareaValue(text);
|
|
280
|
+
trySend();
|
|
281
|
+
setTimeout(function () {
|
|
282
|
+
suppressNextClipboardCopy = false;
|
|
283
|
+
}, 3000);
|
|
284
|
+
}
|
|
285
|
+
|
|
276
286
|
/**
|
|
277
287
|
* 获取 textarea 当前值
|
|
278
288
|
*/
|
|
@@ -368,8 +378,7 @@
|
|
|
368
378
|
menu.appendChild(makeItem("查报错", showDebugModal));
|
|
369
379
|
menu.appendChild(makeItem("加日志", showLogModal));
|
|
370
380
|
menu.appendChild(makeItem("看结果", function () {
|
|
371
|
-
|
|
372
|
-
trySend();
|
|
381
|
+
sendGeneratedPrompt(buildTestDonePrompt());
|
|
373
382
|
}));
|
|
374
383
|
|
|
375
384
|
wrap.appendChild(btn);
|
|
@@ -639,9 +648,8 @@
|
|
|
639
648
|
submitBtn.textContent = "➤ 发给 AI 帮我查";
|
|
640
649
|
submitBtn.addEventListener("click", function () {
|
|
641
650
|
if (!hasDebugInput()) return;
|
|
642
|
-
setTextareaValue(buildDebugPrompt());
|
|
643
651
|
closeDebugModal();
|
|
644
|
-
|
|
652
|
+
sendGeneratedPrompt(buildDebugPrompt());
|
|
645
653
|
});
|
|
646
654
|
|
|
647
655
|
function hasDebugInput() {
|
|
@@ -743,9 +751,8 @@
|
|
|
743
751
|
submitBtn.textContent = "➤ 发给 AI 加提示";
|
|
744
752
|
submitBtn.addEventListener("click", function () {
|
|
745
753
|
if (!hasLogInput()) return;
|
|
746
|
-
setTextareaValue(buildLogPrompt());
|
|
747
754
|
closeLogModal();
|
|
748
|
-
|
|
755
|
+
sendGeneratedPrompt(buildLogPrompt());
|
|
749
756
|
});
|
|
750
757
|
|
|
751
758
|
function hasLogInput() {
|
|
@@ -1225,15 +1232,20 @@ btn.addEventListener("click", function () {
|
|
|
1225
1232
|
return;
|
|
1226
1233
|
}
|
|
1227
1234
|
|
|
1228
|
-
// 2)
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1235
|
+
// 2) 复制到剪贴板。自动生成的流程提示词不写剪贴板。
|
|
1236
|
+
if (suppressNextClipboardCopy) {
|
|
1237
|
+
suppressNextClipboardCopy = false;
|
|
1238
|
+
console.log("[myclaw-send] 已跳过自动提示词的剪贴板复制");
|
|
1239
|
+
} else {
|
|
1240
|
+
try {
|
|
1241
|
+
navigator.clipboard.writeText(text).then(function () {
|
|
1242
|
+
console.log("[myclaw-send] 📋 已复制到剪贴板:", text.substring(0, 50) + (text.length > 50 ? "..." : ""));
|
|
1243
|
+
}).catch(function () {
|
|
1244
|
+
fallbackCopy(text);
|
|
1245
|
+
});
|
|
1246
|
+
} catch (ex) {
|
|
1233
1247
|
fallbackCopy(text);
|
|
1234
|
-
}
|
|
1235
|
-
} catch (ex) {
|
|
1236
|
-
fallbackCopy(text);
|
|
1248
|
+
}
|
|
1237
1249
|
}
|
|
1238
1250
|
|
|
1239
1251
|
// 3) 让原生 click 继续走(发送消息)
|
package/package.json
CHANGED
|
@@ -22,6 +22,40 @@
|
|
|
22
22
|
color: #1a1a2e;
|
|
23
23
|
margin-bottom: 10px;
|
|
24
24
|
}
|
|
25
|
+
.session-selector {
|
|
26
|
+
background: white;
|
|
27
|
+
border-radius: 12px;
|
|
28
|
+
padding: 15px 20px;
|
|
29
|
+
margin-bottom: 20px;
|
|
30
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: 12px;
|
|
34
|
+
flex-wrap: wrap;
|
|
35
|
+
}
|
|
36
|
+
.session-selector label {
|
|
37
|
+
font-weight: bold;
|
|
38
|
+
color: #333;
|
|
39
|
+
white-space: nowrap;
|
|
40
|
+
}
|
|
41
|
+
.session-selector select {
|
|
42
|
+
flex: 1;
|
|
43
|
+
min-width: 300px;
|
|
44
|
+
padding: 8px 12px;
|
|
45
|
+
border: 2px solid #e0e0e0;
|
|
46
|
+
border-radius: 8px;
|
|
47
|
+
font-size: 14px;
|
|
48
|
+
background: #fafafa;
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
}
|
|
51
|
+
.session-selector select:focus {
|
|
52
|
+
border-color: #667eea;
|
|
53
|
+
outline: none;
|
|
54
|
+
}
|
|
55
|
+
.session-meta {
|
|
56
|
+
font-size: 12px;
|
|
57
|
+
color: #888;
|
|
58
|
+
}
|
|
25
59
|
.info-box {
|
|
26
60
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
27
61
|
color: white;
|
|
@@ -116,6 +150,12 @@
|
|
|
116
150
|
<div class="container">
|
|
117
151
|
<h1>📋 聊天记录</h1>
|
|
118
152
|
|
|
153
|
+
<div class="session-selector" id="sessionSelector" style="display:none;">
|
|
154
|
+
<label>🔍 切换会话:</label>
|
|
155
|
+
<select id="sessionSelect"></select>
|
|
156
|
+
<span class="session-meta" id="sessionMeta"></span>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
119
159
|
<div class="info-box">
|
|
120
160
|
<p><strong>会话ID:</strong> <span id="sessionId">--</span></p>
|
|
121
161
|
<p><strong>会话名称:</strong> <span id="sessionName">--</span></p>
|
|
@@ -143,20 +183,15 @@
|
|
|
143
183
|
<div id="conversationList"></div>
|
|
144
184
|
</div>
|
|
145
185
|
|
|
146
|
-
<!-- Load
|
|
147
|
-
<script src="
|
|
186
|
+
<!-- Load index.js (contains chatIndex + chatAll) -->
|
|
187
|
+
<script src="index.js"></script>
|
|
148
188
|
<script>
|
|
149
|
-
|
|
150
|
-
if (typeof chatData === 'undefined') {
|
|
151
|
-
document.getElementById('sessionId').textContent = '数据未加载';
|
|
152
|
-
document.getElementById('conversationList').innerHTML = '<p style="text-align:center;color:#999;">请确保 chat_history.js 文件存在且可访问</p>';
|
|
153
|
-
} else {
|
|
154
|
-
init();
|
|
155
|
-
}
|
|
189
|
+
var currentSession = null;
|
|
156
190
|
|
|
191
|
+
// parseTime, formatDuration, escapeHtml
|
|
157
192
|
function parseTime(timeStr) {
|
|
158
193
|
if (!timeStr) return null;
|
|
159
|
-
|
|
194
|
+
var parts = timeStr.split(/[\s:\-]/);
|
|
160
195
|
if (parts.length >= 6) {
|
|
161
196
|
return new Date(parts[0], parts[1]-1, parts[2], parts[3], parts[4], parts[5]);
|
|
162
197
|
}
|
|
@@ -165,107 +200,174 @@
|
|
|
165
200
|
|
|
166
201
|
function formatDuration(ms) {
|
|
167
202
|
if (ms === null || ms === undefined || ms < 0) return '--';
|
|
168
|
-
|
|
203
|
+
var seconds = Math.floor(ms / 1000);
|
|
169
204
|
if (seconds < 60) return seconds + '秒';
|
|
170
|
-
|
|
171
|
-
|
|
205
|
+
var minutes = Math.floor(seconds / 60);
|
|
206
|
+
var remainingSeconds = seconds % 60;
|
|
172
207
|
if (minutes < 60) {
|
|
173
208
|
return remainingSeconds > 0 ? minutes + '分' + remainingSeconds + '秒' : minutes + '分';
|
|
174
209
|
}
|
|
175
|
-
|
|
176
|
-
|
|
210
|
+
var hours = Math.floor(minutes / 60);
|
|
211
|
+
var remainingMinutes = minutes % 60;
|
|
177
212
|
return hours + '时' + remainingMinutes + '分';
|
|
178
213
|
}
|
|
179
214
|
|
|
180
215
|
function escapeHtml(text) {
|
|
181
|
-
|
|
216
|
+
var div = document.createElement('div');
|
|
182
217
|
div.textContent = text;
|
|
183
218
|
return div.innerHTML;
|
|
184
219
|
}
|
|
185
220
|
|
|
186
|
-
|
|
221
|
+
// Build dropdown from chatIndex
|
|
222
|
+
function buildSelector() {
|
|
223
|
+
if (typeof chatIndex === 'undefined' || !chatIndex || chatIndex.length === 0) {
|
|
224
|
+
return; // No index data, hide selector
|
|
225
|
+
}
|
|
226
|
+
var sel = document.getElementById('sessionSelector');
|
|
227
|
+
var select = document.getElementById('sessionSelect');
|
|
228
|
+
sel.style.display = 'flex';
|
|
229
|
+
|
|
230
|
+
chatIndex.forEach(function(item, i) {
|
|
231
|
+
var opt = document.createElement('option');
|
|
232
|
+
opt.value = item.session_id;
|
|
233
|
+
var label = item.workspace_name + ' / ' + item.session_name + ' (' + item.total_pairs + '对)';
|
|
234
|
+
if (item.first_time) label += ' [' + item.first_time + ']';
|
|
235
|
+
opt.textContent = label;
|
|
236
|
+
select.appendChild(opt);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
select.addEventListener('change', function() {
|
|
240
|
+
switchTo(this.value);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Switch to a specific session
|
|
245
|
+
function switchTo(sessionId) {
|
|
246
|
+
if (typeof chatAll === 'undefined' || !chatAll) return;
|
|
247
|
+
|
|
248
|
+
var data = chatAll[sessionId];
|
|
249
|
+
if (!data) {
|
|
250
|
+
document.getElementById('conversationList').innerHTML = '<p style="text-align:center;color:#999;">该会话数据未找到</p>';
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
currentSession = sessionId;
|
|
255
|
+
renderSession(data);
|
|
256
|
+
|
|
257
|
+
// Update meta
|
|
258
|
+
var metaEl = document.getElementById('sessionMeta');
|
|
259
|
+
if (data.first_time && data.last_time) {
|
|
260
|
+
metaEl.textContent = data.first_time + ' ~ ' + data.last_time;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Render a session's conversations
|
|
265
|
+
function renderSession(chatData) {
|
|
187
266
|
document.getElementById('sessionId').textContent = chatData.session_id || '--';
|
|
188
|
-
document.getElementById('sessionName').textContent = chatData.
|
|
267
|
+
document.getElementById('sessionName').textContent = (chatData.workspace_name || '') + ' / ' + (chatData.session_name || '--');
|
|
189
268
|
document.getElementById('initiator').textContent = chatData.initiator || '--';
|
|
190
269
|
document.getElementById('totalPairs').textContent = chatData.total_pairs || 0;
|
|
191
270
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
271
|
+
var userCount = 0;
|
|
272
|
+
var aiCount = 0;
|
|
273
|
+
var totalDuration = 0;
|
|
274
|
+
var list = document.getElementById('conversationList');
|
|
275
|
+
list.innerHTML = '';
|
|
196
276
|
|
|
197
|
-
chatData.conversations
|
|
198
|
-
|
|
277
|
+
var conversations = chatData.conversations || [];
|
|
278
|
+
conversations.forEach(function(c, i) {
|
|
279
|
+
var hasReply = c.ai && c.ai.trim();
|
|
199
280
|
if (hasReply) aiCount++;
|
|
200
281
|
|
|
201
|
-
|
|
202
|
-
let responseDuration = null;
|
|
282
|
+
var responseDuration = null;
|
|
203
283
|
if (hasReply) {
|
|
204
|
-
|
|
205
|
-
|
|
284
|
+
var userTime = parseTime(c.user_time);
|
|
285
|
+
var aiTime = parseTime(c.ai_time);
|
|
206
286
|
if (userTime && aiTime) {
|
|
207
287
|
responseDuration = aiTime - userTime;
|
|
208
288
|
if (responseDuration > 0) totalDuration += responseDuration;
|
|
209
289
|
}
|
|
210
290
|
}
|
|
211
291
|
|
|
212
|
-
|
|
213
|
-
if (i <
|
|
214
|
-
|
|
215
|
-
|
|
292
|
+
var intervalDuration = null;
|
|
293
|
+
if (i < conversations.length - 1) {
|
|
294
|
+
var nextUserTime = parseTime(conversations[i + 1].user_time);
|
|
295
|
+
var currentUserTime = parseTime(c.user_time);
|
|
216
296
|
if (currentUserTime && nextUserTime) {
|
|
217
297
|
intervalDuration = nextUserTime - currentUserTime;
|
|
218
298
|
}
|
|
219
299
|
}
|
|
220
300
|
|
|
221
|
-
|
|
301
|
+
var elapsedDuration = null;
|
|
222
302
|
if (i > 0) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (firstUserTime &&
|
|
226
|
-
elapsedDuration =
|
|
303
|
+
var firstUserTime = parseTime(conversations[0].user_time);
|
|
304
|
+
var currentUserTime2 = parseTime(c.user_time);
|
|
305
|
+
if (firstUserTime && currentUserTime2) {
|
|
306
|
+
elapsedDuration = currentUserTime2 - firstUserTime;
|
|
227
307
|
}
|
|
228
308
|
}
|
|
229
309
|
|
|
230
|
-
|
|
310
|
+
var pair = document.createElement('div');
|
|
231
311
|
pair.className = 'pair';
|
|
232
|
-
pair.innerHTML =
|
|
233
|
-
<div class="pair-header">
|
|
234
|
-
<div class="pair-left">
|
|
235
|
-
<span class="pair-num">第
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
</div>
|
|
239
|
-
</div>
|
|
240
|
-
<div class="message-section user-section">
|
|
241
|
-
<div class="msg-label user-label">
|
|
242
|
-
<span>👤 学生</span>
|
|
243
|
-
<span class="msg-time"
|
|
244
|
-
</div>
|
|
245
|
-
<div class="msg-content">
|
|
246
|
-
<pre
|
|
247
|
-
</div>
|
|
248
|
-
</div>
|
|
249
|
-
|
|
250
|
-
<div class="message-section ai-section">
|
|
251
|
-
<div class="msg-label ai-label">
|
|
252
|
-
<span>🤖 AI</span>
|
|
253
|
-
<span class="msg-time"
|
|
254
|
-
<span class="pair-duration">⏱️ 回复耗时:
|
|
255
|
-
</div>
|
|
256
|
-
<div class="msg-content">
|
|
257
|
-
<pre
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
|
|
261
|
-
`;
|
|
312
|
+
pair.innerHTML =
|
|
313
|
+
'<div class="pair-header">' +
|
|
314
|
+
'<div class="pair-left">' +
|
|
315
|
+
'<span class="pair-num">第 ' + (i + 1) + ' 轮</span>' +
|
|
316
|
+
(elapsedDuration !== null ? '<span class="pair-duration elapsed">⏰ 课程流逝: ' + formatDuration(elapsedDuration) + '</span>' : '<span class="pair-duration elapsed">⏰ 课程流逝: 0分</span>') +
|
|
317
|
+
(intervalDuration !== null ? '<span class="pair-duration interval">⏸️ 间隔耗时: ' + formatDuration(intervalDuration) + '</span>' : '') +
|
|
318
|
+
'</div>' +
|
|
319
|
+
'</div>' +
|
|
320
|
+
'<div class="message-section user-section">' +
|
|
321
|
+
'<div class="msg-label user-label">' +
|
|
322
|
+
'<span>👤 学生</span>' +
|
|
323
|
+
'<span class="msg-time">' + (c.user_time || '未知时间') + '</span>' +
|
|
324
|
+
'</div>' +
|
|
325
|
+
'<div class="msg-content">' +
|
|
326
|
+
'<pre>' + escapeHtml(c.user) + '</pre>' +
|
|
327
|
+
'</div>' +
|
|
328
|
+
'</div>' +
|
|
329
|
+
(hasReply ?
|
|
330
|
+
'<div class="message-section ai-section">' +
|
|
331
|
+
'<div class="msg-label ai-label">' +
|
|
332
|
+
'<span>🤖 AI</span>' +
|
|
333
|
+
'<span class="msg-time">' + (c.ai_time || '未知时间') + '</span>' +
|
|
334
|
+
'<span class="pair-duration">⏱️ 回复耗时: ' + formatDuration(responseDuration) + '</span>' +
|
|
335
|
+
'</div>' +
|
|
336
|
+
'<div class="msg-content">' +
|
|
337
|
+
'<pre>' + escapeHtml(c.ai) + '</pre>' +
|
|
338
|
+
'</div>' +
|
|
339
|
+
'</div>' : '');
|
|
340
|
+
|
|
262
341
|
list.appendChild(pair);
|
|
263
342
|
});
|
|
264
343
|
|
|
265
|
-
document.getElementById('userCount').textContent =
|
|
344
|
+
document.getElementById('userCount').textContent = conversations.length;
|
|
266
345
|
document.getElementById('aiCount').textContent = aiCount;
|
|
267
346
|
document.getElementById('totalTime').textContent = formatDuration(totalDuration);
|
|
268
347
|
}
|
|
348
|
+
|
|
349
|
+
// Init
|
|
350
|
+
(function() {
|
|
351
|
+
// Strategy 1: Use index.js with chatIndex + chatAll
|
|
352
|
+
if (typeof chatIndex !== 'undefined' && chatIndex && chatIndex.length > 0 &&
|
|
353
|
+
typeof chatAll !== 'undefined' && chatAll) {
|
|
354
|
+
buildSelector();
|
|
355
|
+
// Select first session
|
|
356
|
+
document.getElementById('sessionSelect').value = chatIndex[0].session_id;
|
|
357
|
+
switchTo(chatIndex[0].session_id);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Strategy 2: Fallback to single chatData (old format)
|
|
362
|
+
if (typeof chatData !== 'undefined' && chatData) {
|
|
363
|
+
renderSession(chatData);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// No data
|
|
368
|
+
document.getElementById('sessionId').textContent = '数据未加载';
|
|
369
|
+
document.getElementById('conversationList').innerHTML = '<p style="text-align:center;color:#999;">请确保 index.js 或 chat_data.js 文件存在且可访问</p>';
|
|
370
|
+
})();
|
|
269
371
|
</script>
|
|
270
372
|
</body>
|
|
271
373
|
</html>
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>作品扫描 - __SESSIONS_COUNT__ sessions</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
11
|
+
background: linear-gradient(135deg, #f5f7fa 0%, #e9ecf3 100%);
|
|
12
|
+
margin: 0; padding: 24px; min-height: 100vh; color: #2d3748;
|
|
13
|
+
}
|
|
14
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
15
|
+
.header {
|
|
16
|
+
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
17
|
+
color: white; padding: 30px; border-radius: 16px; margin-bottom: 24px;
|
|
18
|
+
box-shadow: 0 10px 30px rgba(102,126,234,0.3);
|
|
19
|
+
}
|
|
20
|
+
.header h1 { margin: 0 0 8px 0; font-size: 1.8em; }
|
|
21
|
+
.header p { margin: 0; opacity: 0.9; }
|
|
22
|
+
.summary-bar { display: flex; gap: 16px; margin-top: 16px; flex-wrap: wrap; }
|
|
23
|
+
.summary-bar .stat {
|
|
24
|
+
background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 8px;
|
|
25
|
+
backdrop-filter: blur(10px);
|
|
26
|
+
}
|
|
27
|
+
.stat strong { font-size: 1.3em; margin-right: 6px; }
|
|
28
|
+
.legend { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 16px; }
|
|
29
|
+
.legend-item { padding: 4px 12px; border-radius: 6px; font-size: 0.85em; font-weight: 500; }
|
|
30
|
+
table {
|
|
31
|
+
width: 100%; background: white; border-radius: 12px; overflow: hidden;
|
|
32
|
+
border-collapse: collapse; box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
|
33
|
+
}
|
|
34
|
+
th {
|
|
35
|
+
background: #f7fafc; padding: 14px 12px; text-align: left;
|
|
36
|
+
font-weight: 600; color: #4a5568; border-bottom: 2px solid #e2e8f0;
|
|
37
|
+
font-size: 0.9em;
|
|
38
|
+
}
|
|
39
|
+
td { padding: 10px 12px; border-bottom: 1px solid rgba(0,0,0,0.05); font-size: 0.92em; }
|
|
40
|
+
tr:hover { filter: brightness(0.96); }
|
|
41
|
+
.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
42
|
+
.link {
|
|
43
|
+
color: #667eea; text-decoration: none; font-weight: 500;
|
|
44
|
+
padding: 4px 10px; border-radius: 6px; background: rgba(255,255,255,0.6);
|
|
45
|
+
transition: all 0.2s; display: inline-block;
|
|
46
|
+
}
|
|
47
|
+
.link:hover { background: #667eea; color: white; transform: translateY(-1px); }
|
|
48
|
+
.footer { text-align: center; margin-top: 24px; color: #718096; font-size: 0.85em; }
|
|
49
|
+
.export-bar {
|
|
50
|
+
position: sticky; bottom: 20px; margin-top: 20px;
|
|
51
|
+
background: white; border-radius: 12px; padding: 16px 24px;
|
|
52
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.12);
|
|
53
|
+
display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
|
|
54
|
+
}
|
|
55
|
+
.export-bar .count { font-weight: 600; color: #667eea; }
|
|
56
|
+
.btn {
|
|
57
|
+
background: #667eea; color: white; border: none; padding: 8px 20px;
|
|
58
|
+
border-radius: 8px; font-size: 0.95em; cursor: pointer; transition: all 0.2s;
|
|
59
|
+
}
|
|
60
|
+
.btn:hover { background: #5a67d8; transform: translateY(-1px); }
|
|
61
|
+
.btn:disabled { background: #cbd5e0; cursor: not-allowed; transform: none; }
|
|
62
|
+
input[type=checkbox] { width: 18px; height: 18px; cursor: pointer; }
|
|
63
|
+
.work-input {
|
|
64
|
+
width: 100%; max-width: 180px; padding: 4px 8px; border: 1px solid #e2e8f0;
|
|
65
|
+
border-radius: 6px; font-size: 0.85em; background: rgba(255,255,255,0.7);
|
|
66
|
+
}
|
|
67
|
+
.work-input:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 2px rgba(102,126,234,0.2); }
|
|
68
|
+
.work-input.saved { border-color: #48bb78; }
|
|
69
|
+
</style>
|
|
70
|
+
</head>
|
|
71
|
+
<body>
|
|
72
|
+
<div class="container">
|
|
73
|
+
<div class="header">
|
|
74
|
+
<h1>📋 作品扫描</h1>
|
|
75
|
+
<p>Generated by chat-history-extractor</p>
|
|
76
|
+
<div class="summary-bar">
|
|
77
|
+
<div class="stat"><strong>__SESSIONS_COUNT__</strong> sessions</div>
|
|
78
|
+
<div class="stat"><strong>__TOTAL_PAIRS__</strong> conversation pairs</div>
|
|
79
|
+
<div class="stat"><strong>host:</strong> __HOST__</div>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="legend">__LEGEND__</div>
|
|
82
|
+
</div>
|
|
83
|
+
<table>
|
|
84
|
+
<thead>
|
|
85
|
+
<tr>
|
|
86
|
+
<th><input type="checkbox" id="checkAll"></th>
|
|
87
|
+
<th class="num">#</th>
|
|
88
|
+
<th>workspace</th>
|
|
89
|
+
<th>会话名</th>
|
|
90
|
+
<th class="num">对话数</th>
|
|
91
|
+
<th>起始时间</th>
|
|
92
|
+
<th>最后更新</th>
|
|
93
|
+
<th>作品链接</th>
|
|
94
|
+
<th>对话链接</th>
|
|
95
|
+
</tr>
|
|
96
|
+
</thead>
|
|
97
|
+
<tbody>__ROWS__
|
|
98
|
+
</tbody>
|
|
99
|
+
</table>
|
|
100
|
+
<div class="export-bar">
|
|
101
|
+
<span>已选择 <span class="count" id="selectedCount">0</span> 个会话</span>
|
|
102
|
+
<button class="btn" id="exportBtn" disabled>导出所选数据</button>
|
|
103
|
+
<span style="color:#a0aec0;font-size:0.85em;">勾选后点击导出,复制到剪贴板</span>
|
|
104
|
+
</div>
|
|
105
|
+
<p class="footer">提示:相同颜色 = 同一项目。作品链接会自动保存到浏览器,刷新不丢失。</p>
|
|
106
|
+
<script>
|
|
107
|
+
var STORAGE_KEY = '___STORAGE_KEY___';
|
|
108
|
+
|
|
109
|
+
// --- Restore state on load ---
|
|
110
|
+
function loadState() {
|
|
111
|
+
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); }
|
|
112
|
+
catch(e) { return {}; }
|
|
113
|
+
}
|
|
114
|
+
function saveState(state) {
|
|
115
|
+
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); }
|
|
116
|
+
catch(e) {}
|
|
117
|
+
}
|
|
118
|
+
var state = loadState();
|
|
119
|
+
|
|
120
|
+
// --- Checkbox: restore + persist ---
|
|
121
|
+
document.querySelectorAll('.row-check').forEach(function(cb) {
|
|
122
|
+
var sk = cb.dataset.session;
|
|
123
|
+
// restore checked
|
|
124
|
+
if (state[sk] && state[sk].checked) cb.checked = true;
|
|
125
|
+
// restore work_url
|
|
126
|
+
var input = cb.closest('tr').querySelector('.work-input');
|
|
127
|
+
if (input) {
|
|
128
|
+
if (state[sk] && state[sk].work) {
|
|
129
|
+
input.value = state[sk].work;
|
|
130
|
+
input.classList.add('saved');
|
|
131
|
+
}
|
|
132
|
+
// persist on edit
|
|
133
|
+
input.addEventListener('input', function() {
|
|
134
|
+
if (!state[sk]) state[sk] = {};
|
|
135
|
+
state[sk].work = input.value;
|
|
136
|
+
saveState(state);
|
|
137
|
+
input.classList.add('saved');
|
|
138
|
+
});
|
|
139
|
+
input.addEventListener('blur', function() {
|
|
140
|
+
input.classList.remove('saved');
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// persist checkbox
|
|
144
|
+
cb.addEventListener('change', function() {
|
|
145
|
+
if (!state[sk]) state[sk] = {};
|
|
146
|
+
state[sk].checked = cb.checked;
|
|
147
|
+
saveState(state);
|
|
148
|
+
updateCount();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// --- Check all ---
|
|
153
|
+
var checkAll = document.getElementById('checkAll');
|
|
154
|
+
// restore checkAll based on all checked
|
|
155
|
+
function syncCheckAll() {
|
|
156
|
+
var cbs = document.querySelectorAll('.row-check');
|
|
157
|
+
var allChecked = Array.from(cbs).every(function(c) { return c.checked; });
|
|
158
|
+
checkAll.checked = cbs.length > 0 && allChecked;
|
|
159
|
+
}
|
|
160
|
+
checkAll.addEventListener('change', function() {
|
|
161
|
+
document.querySelectorAll('.row-check').forEach(function(cb) {
|
|
162
|
+
cb.checked = checkAll.checked;
|
|
163
|
+
var sk = cb.dataset.session;
|
|
164
|
+
if (!state[sk]) state[sk] = {};
|
|
165
|
+
state[sk].checked = cb.checked;
|
|
166
|
+
});
|
|
167
|
+
saveState(state);
|
|
168
|
+
updateCount();
|
|
169
|
+
});
|
|
170
|
+
syncCheckAll();
|
|
171
|
+
|
|
172
|
+
function updateCount() {
|
|
173
|
+
var checked = document.querySelectorAll('.row-check:checked');
|
|
174
|
+
document.getElementById('selectedCount').textContent = checked.length;
|
|
175
|
+
document.getElementById('exportBtn').disabled = checked.length === 0;
|
|
176
|
+
}
|
|
177
|
+
updateCount();
|
|
178
|
+
|
|
179
|
+
// --- Export: copy text to clipboard ---
|
|
180
|
+
document.getElementById('exportBtn').addEventListener('click', function() {
|
|
181
|
+
var checked = document.querySelectorAll('.row-check:checked');
|
|
182
|
+
var lines = [];
|
|
183
|
+
checked.forEach(function(cb, idx) {
|
|
184
|
+
var tr = cb.closest('tr');
|
|
185
|
+
var linkEl = tr.querySelector('.link');
|
|
186
|
+
var workEl = tr.querySelector('.work-input');
|
|
187
|
+
var workUrl = workEl ? workEl.value.trim() : '';
|
|
188
|
+
lines.push('编号' + (idx + 1) + ':');
|
|
189
|
+
lines.push('workspace名:' + cb.dataset.agent);
|
|
190
|
+
lines.push('session名:' + cb.dataset.name);
|
|
191
|
+
lines.push('对话链接:' + (linkEl ? linkEl.href : ''));
|
|
192
|
+
lines.push('作品链接:' + (workUrl || '暂无'));
|
|
193
|
+
lines.push('');
|
|
194
|
+
});
|
|
195
|
+
var text = lines.join('\n');
|
|
196
|
+
var ta = document.createElement('textarea');
|
|
197
|
+
ta.value = text;
|
|
198
|
+
ta.style.position = 'fixed';
|
|
199
|
+
ta.style.left = '-9999px';
|
|
200
|
+
document.body.appendChild(ta);
|
|
201
|
+
ta.select();
|
|
202
|
+
try {
|
|
203
|
+
document.execCommand('copy');
|
|
204
|
+
var btn = document.getElementById('exportBtn');
|
|
205
|
+
btn.textContent = '✅ 已复制 ' + checked.length + ' 条到剪贴板';
|
|
206
|
+
btn.style.background = '#48bb78';
|
|
207
|
+
setTimeout(function() {
|
|
208
|
+
btn.textContent = '导出所选数据';
|
|
209
|
+
btn.style.background = '';
|
|
210
|
+
}, 2000);
|
|
211
|
+
} catch(e) {
|
|
212
|
+
alert('复制失败,请手动复制:\n\n' + text);
|
|
213
|
+
}
|
|
214
|
+
document.body.removeChild(ta);
|
|
215
|
+
});
|
|
216
|
+
</script>
|
|
217
|
+
</div>
|
|
218
|
+
</body>
|
|
219
|
+
</html>
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
3
|
Extract chat history from OpenClaw session URL(s) and generate JS data file(s).
|
|
4
|
+
"""
|
|
5
|
+
import os as _os
|
|
6
|
+
def _get_script_dir():
|
|
7
|
+
try:
|
|
8
|
+
return os.path.dirname(os.path.abspath(__file__))
|
|
9
|
+
except NameError:
|
|
10
|
+
return os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
4
11
|
|
|
12
|
+
|
|
13
|
+
"""
|
|
5
14
|
Usage:
|
|
6
15
|
# 单个会话(输出到当前 workspace)
|
|
7
16
|
python3 extract_chat.py <session-url-or-key>
|
|
@@ -35,6 +44,7 @@ import re
|
|
|
35
44
|
import glob
|
|
36
45
|
from urllib.parse import urlparse, parse_qs, quote
|
|
37
46
|
from datetime import datetime, timezone, timedelta
|
|
47
|
+
import time
|
|
38
48
|
|
|
39
49
|
tz_beijing = timezone(timedelta(hours=8))
|
|
40
50
|
|
|
@@ -196,14 +206,13 @@ def sanitize_filename(name):
|
|
|
196
206
|
return safe or 'session'
|
|
197
207
|
|
|
198
208
|
|
|
199
|
-
def build_filename(session_key
|
|
209
|
+
def build_filename(session_key):
|
|
200
210
|
parts = session_key.split(':')
|
|
201
211
|
agent_id = parts[1] if len(parts) > 1 else 'unknown'
|
|
202
212
|
session_name = parts[2] if len(parts) > 2 else 'main'
|
|
203
213
|
agent_safe = sanitize_filename(agent_id)
|
|
204
214
|
name_safe = sanitize_filename(session_name)
|
|
205
|
-
|
|
206
|
-
return f"{agent_safe}_{name_safe}_{time_str}.js"
|
|
215
|
+
return f"{agent_safe}_{name_safe}.js"
|
|
207
216
|
|
|
208
217
|
|
|
209
218
|
def generate_js(conversations, session_key, first_ts, last_ts, output_path):
|
|
@@ -289,124 +298,76 @@ def print_summary_table(entries, host=None):
|
|
|
289
298
|
print(f"{e['index']:>3} {agent:<16} {name:<28} {e['total_pairs']:>6} {first_t:<20} {last_t:<20} {work:<14} {url}")
|
|
290
299
|
|
|
291
300
|
|
|
301
|
+
def agent_to_color(agent_id):
|
|
302
|
+
"""Map agent_id to a consistent pastel RGB color via HSV hash."""
|
|
303
|
+
import hashlib, colorsys
|
|
304
|
+
h = hashlib.md5(agent_id.encode()).hexdigest()
|
|
305
|
+
hue = int(h[:2], 16) / 255.0
|
|
306
|
+
r, g, b = colorsys.hsv_to_rgb(hue, 0.45, 0.92)
|
|
307
|
+
bg = "rgb(%d, %d, %d)" % (int(r*255), int(g*255), int(b*255))
|
|
308
|
+
r2, g2, b2 = colorsys.hsv_to_rgb(hue, 0.50, 0.82)
|
|
309
|
+
border = "rgb(%d, %d, %d)" % (int(r2*255), int(g2*255), int(b2*255))
|
|
310
|
+
return bg, border
|
|
311
|
+
|
|
312
|
+
|
|
292
313
|
def generate_html_table(entries, host, output_path):
|
|
293
|
-
"""Generate
|
|
294
|
-
|
|
295
|
-
rows_html = []
|
|
314
|
+
"""Generate HTML from scan_template.html with checkbox + colored rows."""
|
|
315
|
+
agent_colors = {}
|
|
296
316
|
for e in entries:
|
|
297
|
-
|
|
298
|
-
if
|
|
299
|
-
|
|
300
|
-
else:
|
|
301
|
-
work_cell = '<span class="pending">(待填)</span>'
|
|
317
|
+
agent = e.get('agent_id', '') or 'unknown'
|
|
318
|
+
if agent not in agent_colors:
|
|
319
|
+
agent_colors[agent] = agent_to_color(agent)
|
|
302
320
|
|
|
321
|
+
rows_html = []
|
|
322
|
+
for e in entries:
|
|
303
323
|
chat_url = session_key_to_url(e['session_key'], host)
|
|
304
324
|
name = e.get('session_name', '') or ''
|
|
305
325
|
agent = e.get('agent_id', '') or ''
|
|
306
|
-
chat_cell =
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
<td
|
|
312
|
-
<td>
|
|
313
|
-
<td
|
|
314
|
-
<td>
|
|
315
|
-
<td>
|
|
316
|
-
<td>
|
|
317
|
-
<td>
|
|
318
|
-
|
|
326
|
+
chat_cell = '<a href="' + chat_url + '" target="_blank" class="link">💬 打开对话</a>'
|
|
327
|
+
bg, border = agent_colors[agent]
|
|
328
|
+
work_val = e.get('work_url', '') or ''
|
|
329
|
+
|
|
330
|
+
row = '\n <tr style="background: ' + bg + '; border-left: 3px solid ' + border + ';">'
|
|
331
|
+
row += '\n <td><input type="checkbox" class="row-check" data-session="' + e['session_key'] + '" data-agent="' + agent + '" data-name="' + name + '"></td>'
|
|
332
|
+
row += '\n <td class="num">' + str(e['index']) + '</td>'
|
|
333
|
+
row += '\n <td>' + agent + '</td>'
|
|
334
|
+
row += '\n <td>' + name + '</td>'
|
|
335
|
+
row += '\n <td class="num">' + str(e['total_pairs']) + '</td>'
|
|
336
|
+
row += '\n <td>' + e.get('first_time', '') + '</td>'
|
|
337
|
+
row += '\n <td>' + e.get('last_time', '') + '</td>'
|
|
338
|
+
row += '\n <td><input type="text" class="work-input" placeholder="填入作品链接" value="' + work_val + '"></td>'
|
|
339
|
+
row += '\n <td>' + chat_cell + '</td>'
|
|
340
|
+
row += '\n </tr>'
|
|
341
|
+
rows_html.append(row)
|
|
319
342
|
|
|
320
343
|
rows = '\n'.join(rows_html)
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
<
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
'''
|
|
344
|
+
|
|
345
|
+
legend_items = ''.join([
|
|
346
|
+
'<span class="legend-item" style="background: ' + bg + '; border: 1px solid ' + bd + ';">' + agent + '</span>'
|
|
347
|
+
for agent, (bg, bd) in sorted(agent_colors.items())
|
|
348
|
+
])
|
|
349
|
+
|
|
350
|
+
storage_key = 'scan_' + os.path.basename(output_path).replace('.html', '')
|
|
351
|
+
html = _build_html(len(entries), sum(e['total_pairs'] for e in entries), host, legend_items, rows, storage_key)
|
|
352
|
+
|
|
406
353
|
with open(output_path, 'w', encoding='utf-8') as f:
|
|
407
354
|
f.write(html)
|
|
408
355
|
|
|
409
356
|
|
|
357
|
+
def _build_html(n, total_pairs, host, legend_items, rows, storage_key):
|
|
358
|
+
"""Read scan_template.html and fill in placeholders."""
|
|
359
|
+
tpl_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'assets', 'scan_template.html')
|
|
360
|
+
with open(tpl_dir, 'r') as f:
|
|
361
|
+
tpl = f.read()
|
|
362
|
+
tpl = tpl.replace('__SESSIONS_COUNT__', str(n))
|
|
363
|
+
tpl = tpl.replace('__TOTAL_PAIRS__', str(total_pairs))
|
|
364
|
+
tpl = tpl.replace('__HOST__', host)
|
|
365
|
+
tpl = tpl.replace('__LEGEND__', legend_items)
|
|
366
|
+
tpl = tpl.replace('__ROWS__', rows)
|
|
367
|
+
tpl = tpl.replace('___STORAGE_KEY___', storage_key)
|
|
368
|
+
return tpl
|
|
369
|
+
|
|
370
|
+
|
|
410
371
|
def process_one(url_or_key, output_dir, index):
|
|
411
372
|
session_key = extract_session_key(url_or_key)
|
|
412
373
|
parts = session_key.split(':')
|
|
@@ -426,7 +387,7 @@ def process_one(url_or_key, output_dir, index):
|
|
|
426
387
|
conversations, first_ts, last_ts = parse_jsonl_to_conversations(jsonl_path)
|
|
427
388
|
print(f"[{index}] {len(conversations)} pairs")
|
|
428
389
|
|
|
429
|
-
js_filename = build_filename(session_key
|
|
390
|
+
js_filename = build_filename(session_key)
|
|
430
391
|
js_path = os.path.join(output_dir, js_filename)
|
|
431
392
|
count, _ = generate_js(conversations, session_key, first_ts, last_ts, js_path)
|
|
432
393
|
print(f"[{index}] Generated: {js_filename}")
|
|
@@ -444,36 +405,59 @@ def process_one(url_or_key, output_dir, index):
|
|
|
444
405
|
}
|
|
445
406
|
|
|
446
407
|
|
|
447
|
-
def generate_index_js(
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
'
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
408
|
+
def generate_index_js(output_dir):
|
|
409
|
+
"""Scan output_dir for all .js files, merge into index.js with full data."""
|
|
410
|
+
all_entries = []
|
|
411
|
+
all_data = {}
|
|
412
|
+
|
|
413
|
+
for fname in sorted(os.listdir(output_dir)):
|
|
414
|
+
if not fname.endswith('.js') or fname in ('index.js', 'chat_history.js'):
|
|
415
|
+
continue
|
|
416
|
+
fpath = os.path.join(output_dir, fname)
|
|
417
|
+
mtime = int(os.path.getmtime(fpath))
|
|
418
|
+
try:
|
|
419
|
+
with open(fpath, 'r', encoding='utf-8') as f:
|
|
420
|
+
fcontent = f.read()
|
|
421
|
+
m = re.search(r'const chatData = (\{.*\});?\s*$', fcontent, re.DOTALL)
|
|
422
|
+
if not m:
|
|
423
|
+
continue
|
|
424
|
+
data = json.loads(m.group(1))
|
|
425
|
+
sid = data.get('session_id', fname.replace('.js', ''))
|
|
426
|
+
all_data[sid] = data
|
|
427
|
+
all_entries.append({
|
|
428
|
+
'workspace_name': data.get('workspace_name', ''),
|
|
429
|
+
'session_name': data.get('session_name', ''),
|
|
430
|
+
'session_id': sid,
|
|
431
|
+
'js_file': fname,
|
|
432
|
+
'total_pairs': data.get('total_pairs', 0),
|
|
433
|
+
'first_time': data.get('first_time', ''),
|
|
434
|
+
'last_time': data.get('last_time', ''),
|
|
435
|
+
'work_url': data.get('work_url', ''),
|
|
436
|
+
'mtime': mtime,
|
|
437
|
+
})
|
|
438
|
+
except Exception as ex:
|
|
439
|
+
print(' [WARN] Skip %s: %s' % (fname, ex))
|
|
440
|
+
continue
|
|
441
|
+
|
|
442
|
+
now_ts = int(time.time())
|
|
443
|
+
idx = '// Chat History Index - %d session(s)\n' % len(all_entries)
|
|
444
|
+
idx += '// Generated by chat-history-extractor skill\n'
|
|
445
|
+
idx += '// timestamp: %d\n\n' % now_ts
|
|
446
|
+
idx += 'const chatIndex = %s;\n\n' % json.dumps(all_entries, ensure_ascii=False, indent=2)
|
|
447
|
+
idx += 'const chatAll = %s;\n' % json.dumps(all_data, ensure_ascii=False)
|
|
462
448
|
|
|
463
|
-
const chatIndex = {json.dumps(sessions, ensure_ascii=False, indent=2)};
|
|
464
|
-
'''
|
|
465
449
|
index_path = os.path.join(output_dir, 'index.js')
|
|
466
450
|
with open(index_path, 'w', encoding='utf-8') as f:
|
|
467
|
-
f.write(
|
|
468
|
-
print(
|
|
451
|
+
f.write(idx)
|
|
452
|
+
print('\nIndex: %s (%d sessions, ts=%d)' % (index_path, len(all_entries), now_ts))
|
|
469
453
|
|
|
470
454
|
|
|
471
|
-
def run_scan(output_dir=None, host=None
|
|
455
|
+
def run_scan(output_dir=None, host=None):
|
|
456
|
+
"""Scan all agents, count conversations, show table, generate HTML."""
|
|
472
457
|
if host is None:
|
|
473
458
|
host = detect_chat_host()
|
|
474
|
-
"""Scan all agents, count conversations, show table. Optionally generate JS files + HTML."""
|
|
475
459
|
all_sessions = scan_all_agents()
|
|
476
|
-
print(
|
|
460
|
+
print("Found %d session(s) across all agents.\n" % len(all_sessions))
|
|
477
461
|
|
|
478
462
|
entries = []
|
|
479
463
|
for i, (session_key, agent_id, session_name, jsonl_path) in enumerate(all_sessions, 1):
|
|
@@ -488,53 +472,27 @@ def run_scan(output_dir=None, host=None, html_output=False):
|
|
|
488
472
|
'work_url': '',
|
|
489
473
|
'first_time': parse_time(first_ts) if first_ts else '',
|
|
490
474
|
'last_time': parse_time(last_ts) if last_ts else '',
|
|
491
|
-
'_jsonl_path': jsonl_path,
|
|
492
|
-
'_first_ts': first_ts,
|
|
493
475
|
})
|
|
494
476
|
except Exception as e:
|
|
495
|
-
print(
|
|
477
|
+
print(" [SKIP] %s: %s" % (session_key, e))
|
|
496
478
|
|
|
497
|
-
# Sort by first_time descending (newest first)
|
|
498
479
|
entries.sort(key=lambda e: (e.get('first_time') or '') or '', reverse=True)
|
|
499
|
-
# Re-assign index after sorting
|
|
500
480
|
for i, e in enumerate(entries, 1):
|
|
501
481
|
e['index'] = i
|
|
502
482
|
|
|
503
483
|
print_summary_table(entries, host)
|
|
504
484
|
|
|
505
|
-
#
|
|
506
|
-
if output_dir
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
generate_js(convs, e['session_key'], e_first, e_last, js_path)
|
|
514
|
-
results.append({**e, 'js_file': js_filename})
|
|
515
|
-
|
|
516
|
-
if len(results) > 1:
|
|
517
|
-
generate_index_js(results, output_dir)
|
|
518
|
-
|
|
519
|
-
# Backward compat
|
|
520
|
-
first_js = os.path.join(output_dir, results[0]['js_file'])
|
|
521
|
-
compat_js = os.path.join(output_dir, 'chat_history.js')
|
|
522
|
-
if first_js != compat_js:
|
|
523
|
-
import shutil
|
|
524
|
-
shutil.copy2(first_js, compat_js)
|
|
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}")
|
|
485
|
+
# Always generate HTML
|
|
486
|
+
target_dir = output_dir if output_dir else os.getcwd()
|
|
487
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
488
|
+
now_str = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
489
|
+
html_filename = "作品扫描_%s.html" % now_str
|
|
490
|
+
html_path = os.path.join(target_dir, html_filename)
|
|
491
|
+
generate_html_table(entries, host, html_path)
|
|
492
|
+
print("HTML: %s" % html_path)
|
|
535
493
|
|
|
536
494
|
total_pairs = sum(e['total_pairs'] for e in entries)
|
|
537
|
-
print(
|
|
495
|
+
print("\n合计:%d 个会话,%d 对对话。" % (len(entries), total_pairs))
|
|
538
496
|
return entries
|
|
539
497
|
|
|
540
498
|
|
|
@@ -544,7 +502,6 @@ def main():
|
|
|
544
502
|
args = sys.argv[1:]
|
|
545
503
|
|
|
546
504
|
host = detect_chat_host()
|
|
547
|
-
html_output = False
|
|
548
505
|
scan_mode = False
|
|
549
506
|
positional = []
|
|
550
507
|
|
|
@@ -554,9 +511,7 @@ def main():
|
|
|
554
511
|
if a == '--host' and i + 1 < len(args):
|
|
555
512
|
host = args[i + 1]
|
|
556
513
|
i += 2
|
|
557
|
-
|
|
558
|
-
html_output = True
|
|
559
|
-
i += 1
|
|
514
|
+
|
|
560
515
|
elif a == '--scan':
|
|
561
516
|
scan_mode = True
|
|
562
517
|
i += 1
|
|
@@ -579,18 +534,26 @@ def main():
|
|
|
579
534
|
# --scan mode
|
|
580
535
|
if scan_mode:
|
|
581
536
|
output_dir = positional[0] if positional else get_default_output_dir()
|
|
582
|
-
run_scan(output_dir, host=host
|
|
537
|
+
run_scan(output_dir, host=host)
|
|
583
538
|
return
|
|
584
539
|
|
|
585
540
|
# Normal mode
|
|
586
541
|
arg1 = positional[0]
|
|
587
542
|
output_dir = positional[1] if len(positional) > 1 else get_default_output_dir()
|
|
588
543
|
items = parse_input(arg1)
|
|
589
|
-
print(
|
|
590
|
-
print(
|
|
544
|
+
print("Input: %d session(s)" % len(items))
|
|
545
|
+
print("Host: %s" % detect_chat_host())
|
|
591
546
|
|
|
592
547
|
os.makedirs(output_dir, exist_ok=True)
|
|
593
548
|
|
|
549
|
+
# Clean old JS + index.html before fresh extraction
|
|
550
|
+
for old_f in os.listdir(output_dir):
|
|
551
|
+
if old_f.endswith('.js') or old_f == 'index.html':
|
|
552
|
+
try:
|
|
553
|
+
os.remove(os.path.join(output_dir, old_f))
|
|
554
|
+
except Exception:
|
|
555
|
+
pass
|
|
556
|
+
|
|
594
557
|
results = []
|
|
595
558
|
for i, item in enumerate(items, 1):
|
|
596
559
|
r = process_one(item, output_dir, i)
|
|
@@ -601,27 +564,21 @@ def main():
|
|
|
601
564
|
print("\nERROR: No sessions processed successfully.")
|
|
602
565
|
sys.exit(1)
|
|
603
566
|
|
|
604
|
-
|
|
605
|
-
|
|
567
|
+
# Always regenerate index.js by scanning directory
|
|
568
|
+
generate_index_js(output_dir)
|
|
606
569
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
shutil.copy2(
|
|
612
|
-
print(
|
|
570
|
+
# Copy chat-history-template.html as index.html
|
|
571
|
+
import shutil
|
|
572
|
+
tpl_path = os.path.join(_get_script_dir(), '..', 'assets', 'chat-history-template.html')
|
|
573
|
+
if os.path.exists(tpl_path):
|
|
574
|
+
shutil.copy2(tpl_path, os.path.join(output_dir, 'index.html'))
|
|
575
|
+
print("View: %s" % os.path.join(output_dir, 'index.html'))
|
|
613
576
|
|
|
614
577
|
# Summary table
|
|
615
578
|
print_summary_table(results, host)
|
|
616
579
|
|
|
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
|
-
|
|
623
580
|
total_pairs = sum(r['total_pairs'] for r in results)
|
|
624
|
-
print(
|
|
581
|
+
print("\n合计:%d 个会话,%d 对对话。" % (len(results), total_pairs))
|
|
625
582
|
|
|
626
583
|
|
|
627
584
|
if __name__ == '__main__':
|