@aiyiran/myclaw 1.1.167 → 1.1.169
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
|
*/
|
|
@@ -317,22 +327,32 @@
|
|
|
317
327
|
|
|
318
328
|
function buildTestDonePrompt() {
|
|
319
329
|
var rec = window.myclawLastDebugRecord;
|
|
320
|
-
var lines = [
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (rec
|
|
326
|
-
|
|
330
|
+
var lines = ["我刚才已经完成了测试。"];
|
|
331
|
+
|
|
332
|
+
// 调试数据落在 debug-data.js(window.__DEBUG_DATA__ = {...}),AI 直接读这个
|
|
333
|
+
// 数据文件即可,不要读同目录的 debug-record.html(那只是给人看的渲染壳)。
|
|
334
|
+
var dataFile = null;
|
|
335
|
+
if (rec) {
|
|
336
|
+
if (rec.debugRecordPath) {
|
|
337
|
+
dataFile = rec.debugRecordPath.replace(/debug-record\.html$/, "debug-data.js");
|
|
338
|
+
} else if (rec.workspace) {
|
|
339
|
+
dataFile = rec.workspace + "/debug/debug-data.js";
|
|
340
|
+
}
|
|
327
341
|
}
|
|
342
|
+
if (dataFile) {
|
|
343
|
+
lines.push("请你读取调试数据文件:" + dataFile);
|
|
344
|
+
} else {
|
|
345
|
+
lines.push("请你读取本次测试的调试数据文件 debug/debug-data.js。");
|
|
346
|
+
}
|
|
347
|
+
lines.push("它是一行 `window.__DEBUG_DATA__ = {...}` 的赋值,里面的 events 数组就是完整运行现场;不要去读同目录的 debug-record.html。");
|
|
328
348
|
if (rec && rec.entryFile) {
|
|
329
349
|
lines.push("我刚才测试的文件是:" + rec.entryFile);
|
|
330
350
|
}
|
|
331
351
|
|
|
332
352
|
lines.push("");
|
|
333
353
|
lines.push("请帮我判断:我们之前的问题是否已经修复。");
|
|
334
|
-
lines.push("
|
|
335
|
-
lines.push("
|
|
354
|
+
lines.push("如果已经修复,请简单说明你从 events 里看到了什么证据。");
|
|
355
|
+
lines.push("如果还没有修复,请指出 events 里最关键的问题,并建议下一步最小修改。");
|
|
336
356
|
|
|
337
357
|
return lines.join("\n");
|
|
338
358
|
}
|
|
@@ -368,8 +388,7 @@
|
|
|
368
388
|
menu.appendChild(makeItem("查报错", showDebugModal));
|
|
369
389
|
menu.appendChild(makeItem("加日志", showLogModal));
|
|
370
390
|
menu.appendChild(makeItem("看结果", function () {
|
|
371
|
-
|
|
372
|
-
trySend();
|
|
391
|
+
sendGeneratedPrompt(buildTestDonePrompt());
|
|
373
392
|
}));
|
|
374
393
|
|
|
375
394
|
wrap.appendChild(btn);
|
|
@@ -639,9 +658,8 @@
|
|
|
639
658
|
submitBtn.textContent = "➤ 发给 AI 帮我查";
|
|
640
659
|
submitBtn.addEventListener("click", function () {
|
|
641
660
|
if (!hasDebugInput()) return;
|
|
642
|
-
setTextareaValue(buildDebugPrompt());
|
|
643
661
|
closeDebugModal();
|
|
644
|
-
|
|
662
|
+
sendGeneratedPrompt(buildDebugPrompt());
|
|
645
663
|
});
|
|
646
664
|
|
|
647
665
|
function hasDebugInput() {
|
|
@@ -743,9 +761,8 @@
|
|
|
743
761
|
submitBtn.textContent = "➤ 发给 AI 加提示";
|
|
744
762
|
submitBtn.addEventListener("click", function () {
|
|
745
763
|
if (!hasLogInput()) return;
|
|
746
|
-
setTextareaValue(buildLogPrompt());
|
|
747
764
|
closeLogModal();
|
|
748
|
-
|
|
765
|
+
sendGeneratedPrompt(buildLogPrompt());
|
|
749
766
|
});
|
|
750
767
|
|
|
751
768
|
function hasLogInput() {
|
|
@@ -1225,15 +1242,20 @@ btn.addEventListener("click", function () {
|
|
|
1225
1242
|
return;
|
|
1226
1243
|
}
|
|
1227
1244
|
|
|
1228
|
-
// 2)
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1245
|
+
// 2) 复制到剪贴板。自动生成的流程提示词不写剪贴板。
|
|
1246
|
+
if (suppressNextClipboardCopy) {
|
|
1247
|
+
suppressNextClipboardCopy = false;
|
|
1248
|
+
console.log("[myclaw-send] 已跳过自动提示词的剪贴板复制");
|
|
1249
|
+
} else {
|
|
1250
|
+
try {
|
|
1251
|
+
navigator.clipboard.writeText(text).then(function () {
|
|
1252
|
+
console.log("[myclaw-send] 📋 已复制到剪贴板:", text.substring(0, 50) + (text.length > 50 ? "..." : ""));
|
|
1253
|
+
}).catch(function () {
|
|
1254
|
+
fallbackCopy(text);
|
|
1255
|
+
});
|
|
1256
|
+
} catch (ex) {
|
|
1233
1257
|
fallbackCopy(text);
|
|
1234
|
-
}
|
|
1235
|
-
} catch (ex) {
|
|
1236
|
-
fallbackCopy(text);
|
|
1258
|
+
}
|
|
1237
1259
|
}
|
|
1238
1260
|
|
|
1239
1261
|
// 3) 让原生 click 继续走(发送消息)
|
package/package.json
CHANGED
|
@@ -27,38 +27,39 @@ python3 scripts/extract_chat.py '["url1","url2","url3"]' <output-dir>
|
|
|
27
27
|
|
|
28
28
|
## 输出文件
|
|
29
29
|
|
|
30
|
+
**只产出两个文件**,不再为每个会话单独落 js(所有会话的完整数据都汇入 `index.js`):
|
|
31
|
+
|
|
30
32
|
| 文件 | 说明 |
|
|
31
33
|
|---|---|
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
| `...` | |
|
|
35
|
-
| `index.js` | 所有会话的索引列表(仅批量模式生成) |
|
|
36
|
-
| `chat_history.js` | 向后兼容,指向第一个会话 |
|
|
37
|
-
|
|
38
|
-
每个 JS 文件结构:
|
|
39
|
-
|
|
40
|
-
```javascript
|
|
41
|
-
const chatData = {
|
|
42
|
-
"session": "session-name",
|
|
43
|
-
"session_id": "agent:xxx:yyy",
|
|
44
|
-
"total_pairs": N,
|
|
45
|
-
"initiator": "...",
|
|
46
|
-
"conversations": [
|
|
47
|
-
{ "user": "...", "user_time": "...", "ai": "...", "ai_time": "..." },
|
|
48
|
-
...
|
|
49
|
-
]
|
|
50
|
-
};
|
|
51
|
-
```
|
|
34
|
+
| `index.js` | 唯一数据产物:`chatIndex`(会话元数据列表)+ `chatAll`(每个会话的完整对话) |
|
|
35
|
+
| `index.html` | 查看器,复制自 `assets/chat-history-template.html`,只加载 `index.js` |
|
|
52
36
|
|
|
53
37
|
`index.js` 结构:
|
|
54
38
|
|
|
55
39
|
```javascript
|
|
40
|
+
// chatIndex:用于会话切换下拉框的元数据
|
|
56
41
|
const chatIndex = [
|
|
57
|
-
{ "
|
|
58
|
-
|
|
42
|
+
{ "session_id": "agent:xxx:main", "workspace_name": "xxx", "session_name": "main",
|
|
43
|
+
"total_pairs": 10, "first_time": "...", "last_time": "...", "work_url": "" },
|
|
44
|
+
...
|
|
59
45
|
];
|
|
46
|
+
|
|
47
|
+
// chatAll:按 session_id 索引的完整会话数据(HTML 渲染的真正数据源)
|
|
48
|
+
const chatAll = {
|
|
49
|
+
"agent:xxx:main": {
|
|
50
|
+
"workspace_name": "xxx", "session_name": "main", "session_id": "agent:xxx:main",
|
|
51
|
+
"total_pairs": 10, "first_time": "...", "last_time": "...", "work_url": "",
|
|
52
|
+
"conversations": [
|
|
53
|
+
{ "user": "...", "user_time": "...", "ai": "...", "ai_time": "..." },
|
|
54
|
+
...
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
...
|
|
58
|
+
};
|
|
60
59
|
```
|
|
61
60
|
|
|
61
|
+
> 单会话和批量模式产物一致,区别只是 `chatIndex` / `chatAll` 里的会话数量。
|
|
62
|
+
|
|
62
63
|
## 工作流
|
|
63
64
|
|
|
64
65
|
### Step 1: 解析输入
|
|
@@ -85,14 +86,14 @@ URL 示例:`https://claw1.kekouen.cn/chat?session=agent%3Ac108-v1-1811%3Amain`
|
|
|
85
86
|
3. 多条 AI 回复合并为一条
|
|
86
87
|
4. 跳过 `toolResult` 事件和空文本消息
|
|
87
88
|
|
|
88
|
-
### Step 4: 生成
|
|
89
|
+
### Step 4: 生成 index.js
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
所有会话的数据在内存中汇总,一次性写入唯一的 `index.js`(`chatIndex` + `chatAll`),不为单个会话单独落文件。
|
|
91
92
|
|
|
92
93
|
### Step 5: 渲染
|
|
93
94
|
|
|
94
|
-
将模板 `assets/chat-history-template.html`
|
|
95
|
-
|
|
95
|
+
将模板 `assets/chat-history-template.html` 复制到输出目录为 `index.html`。
|
|
96
|
+
模板只加载 `index.js`,通过 `chatIndex` 渲染会话切换下拉框、从 `chatAll` 取完整对话。
|
|
96
97
|
|
|
97
98
|
模板展示三种时间指标:
|
|
98
99
|
- **课程流逝** — 距首条消息的时间
|
|
@@ -116,8 +117,8 @@ python3 scripts/extract_chat.py "https://claw1.kekouen.cn/chat?session=agent%3Ac
|
|
|
116
117
|
```
|
|
117
118
|
|
|
118
119
|
输出:
|
|
119
|
-
- `
|
|
120
|
-
- `
|
|
120
|
+
- `index.js`(含 1 个会话)
|
|
121
|
+
- `index.html`
|
|
121
122
|
|
|
122
123
|
### 批量处理
|
|
123
124
|
|
|
@@ -131,9 +132,5 @@ python3 scripts/extract_chat.py '[
|
|
|
131
132
|
```
|
|
132
133
|
|
|
133
134
|
输出:
|
|
134
|
-
- `
|
|
135
|
-
- `
|
|
136
|
-
- `03-每天一个小习惯·宠物养成_1813.js`
|
|
137
|
-
- `04-我的金币任务板_1810.js`
|
|
138
|
-
- `index.js`
|
|
139
|
-
- `chat_history.js`(= 01-main.js 的副本)
|
|
135
|
+
- `index.js`(含 4 个会话,全部数据在 `chatAll` 里)
|
|
136
|
+
- `index.html`
|
|
@@ -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>
|
|
@@ -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>
|
|
@@ -14,10 +23,11 @@ Usage:
|
|
|
14
23
|
python3 extract_chat.py --scan [--html]
|
|
15
24
|
|
|
16
25
|
输出(默认到当前 workspace /root/.openclaw/workspace-teacher):
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
26
|
+
- index.js:唯一数据产物,含 chatIndex(会话元信息列表,含 work_url 字段)
|
|
27
|
+
+ chatAll(每个会话的完整对话,按 session_id 索引)
|
|
28
|
+
- index.html:查看器,复制自 assets/chat-history-template.html,只加载 index.js
|
|
29
|
+
- 不再为每个会话单独落 js 文件
|
|
30
|
+
- --scan 模式额外生成 作品扫描_<时间>.html(含可点击链接)
|
|
21
31
|
|
|
22
32
|
Host 检测:
|
|
23
33
|
- 自动从 /root/.openclaw/myclaw/server/config.json 读取
|
|
@@ -25,7 +35,7 @@ Host 检测:
|
|
|
25
35
|
|
|
26
36
|
work_url 字段:
|
|
27
37
|
- 默认为空字符串
|
|
28
|
-
- AI
|
|
38
|
+
- AI 可后续编辑 index.js 中对应会话的 work_url 填入学生作品链接
|
|
29
39
|
"""
|
|
30
40
|
|
|
31
41
|
import json
|
|
@@ -35,6 +45,7 @@ import re
|
|
|
35
45
|
import glob
|
|
36
46
|
from urllib.parse import urlparse, parse_qs, quote
|
|
37
47
|
from datetime import datetime, timezone, timedelta
|
|
48
|
+
import time
|
|
38
49
|
|
|
39
50
|
tz_beijing = timezone(timedelta(hours=8))
|
|
40
51
|
|
|
@@ -156,7 +167,8 @@ def count_conversations(jsonl_path):
|
|
|
156
167
|
messages.append({
|
|
157
168
|
'role': role,
|
|
158
169
|
'text': text.strip(),
|
|
159
|
-
'has_text': bool(text.strip())
|
|
170
|
+
'has_text': bool(text.strip()),
|
|
171
|
+
'timestamp': timestamp
|
|
160
172
|
})
|
|
161
173
|
|
|
162
174
|
# Count conversation pairs (user messages with at least one AI reply)
|
|
@@ -173,9 +185,13 @@ def count_conversations(jsonl_path):
|
|
|
173
185
|
ai_messages.append(messages[j])
|
|
174
186
|
j += 1
|
|
175
187
|
ai_text = '\n\n'.join([m['text'] for m in ai_messages if m['has_text']])
|
|
188
|
+
user_time = msg.get('timestamp', '')
|
|
189
|
+
ai_time = ai_messages[0].get('timestamp', '') if ai_messages else ''
|
|
176
190
|
conversations.append({
|
|
177
191
|
'user': user_text,
|
|
178
192
|
'ai': ai_text,
|
|
193
|
+
'user_time': user_time,
|
|
194
|
+
'ai_time': ai_time,
|
|
179
195
|
})
|
|
180
196
|
i += 1
|
|
181
197
|
else:
|
|
@@ -196,22 +212,30 @@ def sanitize_filename(name):
|
|
|
196
212
|
return safe or 'session'
|
|
197
213
|
|
|
198
214
|
|
|
199
|
-
def build_filename(session_key
|
|
215
|
+
def build_filename(session_key):
|
|
200
216
|
parts = session_key.split(':')
|
|
201
217
|
agent_id = parts[1] if len(parts) > 1 else 'unknown'
|
|
202
218
|
session_name = parts[2] if len(parts) > 2 else 'main'
|
|
219
|
+
# Use last part (UUID or 'main') as unique suffix to avoid collisions
|
|
220
|
+
unique_id = parts[-1] if len(parts) > 3 else session_name
|
|
203
221
|
agent_safe = sanitize_filename(agent_id)
|
|
204
222
|
name_safe = sanitize_filename(session_name)
|
|
205
|
-
|
|
206
|
-
|
|
223
|
+
unique_safe = sanitize_filename(unique_id)
|
|
224
|
+
if unique_safe == name_safe:
|
|
225
|
+
return f"{agent_safe}_{name_safe}.js"
|
|
226
|
+
return f"{agent_safe}_{name_safe}_{unique_safe}.js"
|
|
207
227
|
|
|
208
228
|
|
|
209
|
-
def
|
|
229
|
+
def build_chat_data(conversations, session_key, first_ts, last_ts):
|
|
230
|
+
"""构建单个会话的完整数据对象(仅内存,不写盘)。
|
|
231
|
+
|
|
232
|
+
数据最终全部汇入 index.js 的 chatAll,不再为每个会话单独落 js 文件。
|
|
233
|
+
"""
|
|
210
234
|
parts = session_key.split(':')
|
|
211
235
|
workspace_name = parts[1] if len(parts) > 1 else 'unknown'
|
|
212
236
|
session_name = parts[2] if len(parts) > 2 else 'main'
|
|
213
237
|
|
|
214
|
-
|
|
238
|
+
return {
|
|
215
239
|
'workspace_name': workspace_name,
|
|
216
240
|
'session_name': session_name,
|
|
217
241
|
'session_id': session_key,
|
|
@@ -221,15 +245,6 @@ def generate_js(conversations, session_key, first_ts, last_ts, output_path):
|
|
|
221
245
|
'work_url': '',
|
|
222
246
|
'conversations': conversations
|
|
223
247
|
}
|
|
224
|
-
js_content = f'''// Chat History - {session_name}
|
|
225
|
-
// Session Key: {session_key}
|
|
226
|
-
// Generated by chat-history-extractor skill
|
|
227
|
-
|
|
228
|
-
const chatData = {json.dumps(output_data, ensure_ascii=False, indent=2)};
|
|
229
|
-
'''
|
|
230
|
-
with open(output_path, 'w', encoding='utf-8') as f:
|
|
231
|
-
f.write(js_content)
|
|
232
|
-
return len(conversations), session_name
|
|
233
248
|
|
|
234
249
|
|
|
235
250
|
def session_key_to_url(session_key, host=None):
|
|
@@ -289,124 +304,76 @@ def print_summary_table(entries, host=None):
|
|
|
289
304
|
print(f"{e['index']:>3} {agent:<16} {name:<28} {e['total_pairs']:>6} {first_t:<20} {last_t:<20} {work:<14} {url}")
|
|
290
305
|
|
|
291
306
|
|
|
307
|
+
def agent_to_color(agent_id):
|
|
308
|
+
"""Map agent_id to a consistent pastel RGB color via HSV hash."""
|
|
309
|
+
import hashlib, colorsys
|
|
310
|
+
h = hashlib.md5(agent_id.encode()).hexdigest()
|
|
311
|
+
hue = int(h[:2], 16) / 255.0
|
|
312
|
+
r, g, b = colorsys.hsv_to_rgb(hue, 0.45, 0.92)
|
|
313
|
+
bg = "rgb(%d, %d, %d)" % (int(r*255), int(g*255), int(b*255))
|
|
314
|
+
r2, g2, b2 = colorsys.hsv_to_rgb(hue, 0.50, 0.82)
|
|
315
|
+
border = "rgb(%d, %d, %d)" % (int(r2*255), int(g2*255), int(b2*255))
|
|
316
|
+
return bg, border
|
|
317
|
+
|
|
318
|
+
|
|
292
319
|
def generate_html_table(entries, host, output_path):
|
|
293
|
-
"""Generate
|
|
294
|
-
|
|
295
|
-
rows_html = []
|
|
320
|
+
"""Generate HTML from scan_template.html with checkbox + colored rows."""
|
|
321
|
+
agent_colors = {}
|
|
296
322
|
for e in entries:
|
|
297
|
-
|
|
298
|
-
if
|
|
299
|
-
|
|
300
|
-
else:
|
|
301
|
-
work_cell = '<span class="pending">(待填)</span>'
|
|
323
|
+
agent = e.get('agent_id', '') or 'unknown'
|
|
324
|
+
if agent not in agent_colors:
|
|
325
|
+
agent_colors[agent] = agent_to_color(agent)
|
|
302
326
|
|
|
327
|
+
rows_html = []
|
|
328
|
+
for e in entries:
|
|
303
329
|
chat_url = session_key_to_url(e['session_key'], host)
|
|
304
330
|
name = e.get('session_name', '') or ''
|
|
305
331
|
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
|
-
|
|
332
|
+
chat_cell = '<a href="' + chat_url + '" target="_blank" class="link">💬 打开对话</a>'
|
|
333
|
+
bg, border = agent_colors[agent]
|
|
334
|
+
work_val = e.get('work_url', '') or ''
|
|
335
|
+
|
|
336
|
+
row = '\n <tr style="background: ' + bg + '; border-left: 3px solid ' + border + ';">'
|
|
337
|
+
row += '\n <td><input type="checkbox" class="row-check" data-session="' + e['session_key'] + '" data-agent="' + agent + '" data-name="' + name + '"></td>'
|
|
338
|
+
row += '\n <td class="num">' + str(e['index']) + '</td>'
|
|
339
|
+
row += '\n <td>' + agent + '</td>'
|
|
340
|
+
row += '\n <td>' + name + '</td>'
|
|
341
|
+
row += '\n <td class="num">' + str(e['total_pairs']) + '</td>'
|
|
342
|
+
row += '\n <td>' + e.get('first_time', '') + '</td>'
|
|
343
|
+
row += '\n <td>' + e.get('last_time', '') + '</td>'
|
|
344
|
+
row += '\n <td><input type="text" class="work-input" placeholder="填入作品链接" value="' + work_val + '"></td>'
|
|
345
|
+
row += '\n <td>' + chat_cell + '</td>'
|
|
346
|
+
row += '\n </tr>'
|
|
347
|
+
rows_html.append(row)
|
|
319
348
|
|
|
320
349
|
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
|
-
'''
|
|
350
|
+
|
|
351
|
+
legend_items = ''.join([
|
|
352
|
+
'<span class="legend-item" style="background: ' + bg + '; border: 1px solid ' + bd + ';">' + agent + '</span>'
|
|
353
|
+
for agent, (bg, bd) in sorted(agent_colors.items())
|
|
354
|
+
])
|
|
355
|
+
|
|
356
|
+
storage_key = 'scan_' + os.path.basename(output_path).replace('.html', '')
|
|
357
|
+
html = _build_html(len(entries), sum(e['total_pairs'] for e in entries), host, legend_items, rows, storage_key)
|
|
358
|
+
|
|
406
359
|
with open(output_path, 'w', encoding='utf-8') as f:
|
|
407
360
|
f.write(html)
|
|
408
361
|
|
|
409
362
|
|
|
363
|
+
def _build_html(n, total_pairs, host, legend_items, rows, storage_key):
|
|
364
|
+
"""Read scan_template.html and fill in placeholders."""
|
|
365
|
+
tpl_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'assets', 'scan_template.html')
|
|
366
|
+
with open(tpl_dir, 'r') as f:
|
|
367
|
+
tpl = f.read()
|
|
368
|
+
tpl = tpl.replace('__SESSIONS_COUNT__', str(n))
|
|
369
|
+
tpl = tpl.replace('__TOTAL_PAIRS__', str(total_pairs))
|
|
370
|
+
tpl = tpl.replace('__HOST__', host)
|
|
371
|
+
tpl = tpl.replace('__LEGEND__', legend_items)
|
|
372
|
+
tpl = tpl.replace('__ROWS__', rows)
|
|
373
|
+
tpl = tpl.replace('___STORAGE_KEY___', storage_key)
|
|
374
|
+
return tpl
|
|
375
|
+
|
|
376
|
+
|
|
410
377
|
def process_one(url_or_key, output_dir, index):
|
|
411
378
|
session_key = extract_session_key(url_or_key)
|
|
412
379
|
parts = session_key.split(':')
|
|
@@ -426,54 +393,63 @@ def process_one(url_or_key, output_dir, index):
|
|
|
426
393
|
conversations, first_ts, last_ts = parse_jsonl_to_conversations(jsonl_path)
|
|
427
394
|
print(f"[{index}] {len(conversations)} pairs")
|
|
428
395
|
|
|
429
|
-
|
|
430
|
-
js_path = os.path.join(output_dir, js_filename)
|
|
431
|
-
count, _ = generate_js(conversations, session_key, first_ts, last_ts, js_path)
|
|
432
|
-
print(f"[{index}] Generated: {js_filename}")
|
|
396
|
+
data = build_chat_data(conversations, session_key, first_ts, last_ts)
|
|
433
397
|
|
|
434
398
|
return {
|
|
435
399
|
'index': index,
|
|
436
400
|
'session_key': session_key,
|
|
437
401
|
'session_name': session_name,
|
|
438
402
|
'agent_id': agent_id,
|
|
439
|
-
'
|
|
440
|
-
'
|
|
441
|
-
'
|
|
442
|
-
'last_time': parse_time(last_ts) if last_ts else '',
|
|
403
|
+
'total_pairs': data['total_pairs'],
|
|
404
|
+
'first_time': data['first_time'],
|
|
405
|
+
'last_time': data['last_time'],
|
|
443
406
|
'work_url': '',
|
|
407
|
+
'data': data, # 完整会话数据,汇入 index.js 的 chatAll
|
|
444
408
|
}
|
|
445
409
|
|
|
446
410
|
|
|
447
411
|
def generate_index_js(results, output_dir):
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
'''
|
|
412
|
+
"""直接用内存中的 results 拼出唯一产物 index.js(chatIndex + chatAll)。
|
|
413
|
+
|
|
414
|
+
不再为每个会话单独落 js 文件——每个会话的完整对话都进 chatAll,
|
|
415
|
+
HTML 只加载 index.js 即可渲染全部会话。
|
|
416
|
+
"""
|
|
417
|
+
all_entries = []
|
|
418
|
+
all_data = {}
|
|
419
|
+
|
|
420
|
+
for r in results:
|
|
421
|
+
data = r['data']
|
|
422
|
+
sid = data['session_id']
|
|
423
|
+
all_data[sid] = data
|
|
424
|
+
all_entries.append({
|
|
425
|
+
'workspace_name': data.get('workspace_name', ''),
|
|
426
|
+
'session_name': data.get('session_name', ''),
|
|
427
|
+
'session_id': sid,
|
|
428
|
+
'total_pairs': data.get('total_pairs', 0),
|
|
429
|
+
'first_time': data.get('first_time', ''),
|
|
430
|
+
'last_time': data.get('last_time', ''),
|
|
431
|
+
'work_url': data.get('work_url', ''),
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
now_ts = int(time.time())
|
|
435
|
+
idx = '// Chat History Index - %d session(s)\n' % len(all_entries)
|
|
436
|
+
idx += '// Generated by chat-history-extractor skill\n'
|
|
437
|
+
idx += '// timestamp: %d\n\n' % now_ts
|
|
438
|
+
idx += 'const chatIndex = %s;\n\n' % json.dumps(all_entries, ensure_ascii=False, indent=2)
|
|
439
|
+
idx += 'const chatAll = %s;\n' % json.dumps(all_data, ensure_ascii=False)
|
|
440
|
+
|
|
465
441
|
index_path = os.path.join(output_dir, 'index.js')
|
|
466
442
|
with open(index_path, 'w', encoding='utf-8') as f:
|
|
467
|
-
f.write(
|
|
468
|
-
print(
|
|
443
|
+
f.write(idx)
|
|
444
|
+
print('\nIndex: %s (%d sessions, ts=%d)' % (index_path, len(all_entries), now_ts))
|
|
469
445
|
|
|
470
446
|
|
|
471
|
-
def run_scan(output_dir=None, host=None
|
|
447
|
+
def run_scan(output_dir=None, host=None):
|
|
448
|
+
"""Scan all agents, count conversations, show table, generate HTML."""
|
|
472
449
|
if host is None:
|
|
473
450
|
host = detect_chat_host()
|
|
474
|
-
"""Scan all agents, count conversations, show table. Optionally generate JS files + HTML."""
|
|
475
451
|
all_sessions = scan_all_agents()
|
|
476
|
-
print(
|
|
452
|
+
print("Found %d session(s) across all agents.\n" % len(all_sessions))
|
|
477
453
|
|
|
478
454
|
entries = []
|
|
479
455
|
for i, (session_key, agent_id, session_name, jsonl_path) in enumerate(all_sessions, 1):
|
|
@@ -488,53 +464,27 @@ def run_scan(output_dir=None, host=None, html_output=False):
|
|
|
488
464
|
'work_url': '',
|
|
489
465
|
'first_time': parse_time(first_ts) if first_ts else '',
|
|
490
466
|
'last_time': parse_time(last_ts) if last_ts else '',
|
|
491
|
-
'_jsonl_path': jsonl_path,
|
|
492
|
-
'_first_ts': first_ts,
|
|
493
467
|
})
|
|
494
468
|
except Exception as e:
|
|
495
|
-
print(
|
|
469
|
+
print(" [SKIP] %s: %s" % (session_key, e))
|
|
496
470
|
|
|
497
|
-
# Sort by first_time descending (newest first)
|
|
498
471
|
entries.sort(key=lambda e: (e.get('first_time') or '') or '', reverse=True)
|
|
499
|
-
# Re-assign index after sorting
|
|
500
472
|
for i, e in enumerate(entries, 1):
|
|
501
473
|
e['index'] = i
|
|
502
474
|
|
|
503
475
|
print_summary_table(entries, host)
|
|
504
476
|
|
|
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}")
|
|
477
|
+
# Always generate HTML
|
|
478
|
+
target_dir = output_dir if output_dir else os.getcwd()
|
|
479
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
480
|
+
now_str = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
481
|
+
html_filename = "作品扫描_%s.html" % now_str
|
|
482
|
+
html_path = os.path.join(target_dir, html_filename)
|
|
483
|
+
generate_html_table(entries, host, html_path)
|
|
484
|
+
print("HTML: %s" % html_path)
|
|
535
485
|
|
|
536
486
|
total_pairs = sum(e['total_pairs'] for e in entries)
|
|
537
|
-
print(
|
|
487
|
+
print("\n合计:%d 个会话,%d 对对话。" % (len(entries), total_pairs))
|
|
538
488
|
return entries
|
|
539
489
|
|
|
540
490
|
|
|
@@ -544,7 +494,6 @@ def main():
|
|
|
544
494
|
args = sys.argv[1:]
|
|
545
495
|
|
|
546
496
|
host = detect_chat_host()
|
|
547
|
-
html_output = False
|
|
548
497
|
scan_mode = False
|
|
549
498
|
positional = []
|
|
550
499
|
|
|
@@ -554,9 +503,7 @@ def main():
|
|
|
554
503
|
if a == '--host' and i + 1 < len(args):
|
|
555
504
|
host = args[i + 1]
|
|
556
505
|
i += 2
|
|
557
|
-
|
|
558
|
-
html_output = True
|
|
559
|
-
i += 1
|
|
506
|
+
|
|
560
507
|
elif a == '--scan':
|
|
561
508
|
scan_mode = True
|
|
562
509
|
i += 1
|
|
@@ -579,18 +526,26 @@ def main():
|
|
|
579
526
|
# --scan mode
|
|
580
527
|
if scan_mode:
|
|
581
528
|
output_dir = positional[0] if positional else get_default_output_dir()
|
|
582
|
-
run_scan(output_dir, host=host
|
|
529
|
+
run_scan(output_dir, host=host)
|
|
583
530
|
return
|
|
584
531
|
|
|
585
532
|
# Normal mode
|
|
586
533
|
arg1 = positional[0]
|
|
587
534
|
output_dir = positional[1] if len(positional) > 1 else get_default_output_dir()
|
|
588
535
|
items = parse_input(arg1)
|
|
589
|
-
print(
|
|
590
|
-
print(
|
|
536
|
+
print("Input: %d session(s)" % len(items))
|
|
537
|
+
print("Host: %s" % detect_chat_host())
|
|
591
538
|
|
|
592
539
|
os.makedirs(output_dir, exist_ok=True)
|
|
593
540
|
|
|
541
|
+
# 清理旧产物(含历史遗留的单会话 js)后再重新生成
|
|
542
|
+
for old_f in os.listdir(output_dir):
|
|
543
|
+
if old_f.endswith('.js') or old_f == 'index.html':
|
|
544
|
+
try:
|
|
545
|
+
os.remove(os.path.join(output_dir, old_f))
|
|
546
|
+
except Exception:
|
|
547
|
+
pass
|
|
548
|
+
|
|
594
549
|
results = []
|
|
595
550
|
for i, item in enumerate(items, 1):
|
|
596
551
|
r = process_one(item, output_dir, i)
|
|
@@ -601,27 +556,21 @@ def main():
|
|
|
601
556
|
print("\nERROR: No sessions processed successfully.")
|
|
602
557
|
sys.exit(1)
|
|
603
558
|
|
|
604
|
-
|
|
605
|
-
|
|
559
|
+
# 唯一数据产物:index.js(chatIndex + chatAll)
|
|
560
|
+
generate_index_js(results, output_dir)
|
|
606
561
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
shutil.copy2(
|
|
612
|
-
print(
|
|
562
|
+
# Copy chat-history-template.html as index.html
|
|
563
|
+
import shutil
|
|
564
|
+
tpl_path = os.path.join(_get_script_dir(), '..', 'assets', 'chat-history-template.html')
|
|
565
|
+
if os.path.exists(tpl_path):
|
|
566
|
+
shutil.copy2(tpl_path, os.path.join(output_dir, 'index.html'))
|
|
567
|
+
print("View: %s" % os.path.join(output_dir, 'index.html'))
|
|
613
568
|
|
|
614
569
|
# Summary table
|
|
615
570
|
print_summary_table(results, host)
|
|
616
571
|
|
|
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
572
|
total_pairs = sum(r['total_pairs'] for r in results)
|
|
624
|
-
print(
|
|
573
|
+
print("\n合计:%d 个会话,%d 对对话。" % (len(results), total_pairs))
|
|
625
574
|
|
|
626
575
|
|
|
627
576
|
if __name__ == '__main__':
|