@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.
@@ -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
- "请你检查我的测试数据 debug/debug-record.html,重点查看里面的 JS 日志。",
323
- ];
324
-
325
- if (rec && rec.debugRecordPath) {
326
- lines.push("如果你能读取本地路径,也可以查看:" + rec.debugRecordPath);
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
- setTextareaValue(buildTestDonePrompt());
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
- trySend();
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
- trySend();
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
- try {
1230
- navigator.clipboard.writeText(text).then(function () {
1231
- console.log("[myclaw-send] 📋 已复制到剪贴板:", text.substring(0, 50) + (text.length > 50 ? "..." : ""));
1232
- }).catch(function () {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiyiran/myclaw",
3
- "version": "1.1.167",
3
+ "version": "1.1.169",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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
- | `01-<session-name>.js` | 1 个会话的数据 |
33
- | `02-<session-name>.js` | 2 个会话的数据 |
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
- { "index": 1, "session": "...", "js_file": "01-xxx.js", "total_pairs": 10 },
58
- { "index": 2, "session": "...", "js_file": "02-yyy.js", "total_pairs": 8 },
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: 生成 JS 文件
89
+ ### Step 4: 生成 index.js
89
90
 
90
- 批量模式下,每个会话生成独立的 `NN-<name>.js`,并额外生成 `index.js` 索引。
91
+ 所有会话的数据在内存中汇总,一次性写入唯一的 `index.js`(`chatIndex` + `chatAll`),不为单个会话单独落文件。
91
92
 
92
93
  ### Step 5: 渲染
93
94
 
94
- 将模板 `assets/chat-history-template.html` 复制到输出目录。
95
- 模板会加载 `chat_history.js`(单会话)或可通过 `index.js` 切换(多会话)。
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
- - `01-main.js`
120
- - `chat_history.js`(= 01-main.js 的副本)
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
- - `01-main.js`
135
- - `02-main.js`
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 chat data JS file -->
147
- <script src="chat_history.js"></script>
186
+ <!-- Load index.js (contains chatIndex + chatAll) -->
187
+ <script src="index.js"></script>
148
188
  <script>
149
- // Fallback if chatData not loaded
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
- const parts = timeStr.split(/[\s:\-]/);
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
- const seconds = Math.floor(ms / 1000);
203
+ var seconds = Math.floor(ms / 1000);
169
204
  if (seconds < 60) return seconds + '秒';
170
- const minutes = Math.floor(seconds / 60);
171
- const remainingSeconds = seconds % 60;
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
- const hours = Math.floor(minutes / 60);
176
- const remainingMinutes = minutes % 60;
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
- const div = document.createElement('div');
216
+ var div = document.createElement('div');
182
217
  div.textContent = text;
183
218
  return div.innerHTML;
184
219
  }
185
220
 
186
- function init() {
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.session || '--';
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
- let userCount = 0;
193
- let aiCount = 0;
194
- let totalDuration = 0;
195
- const list = document.getElementById('conversationList');
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.forEach((c, i) => {
198
- const hasReply = c.ai && c.ai.trim();
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
- // Compute metrics
202
- let responseDuration = null;
282
+ var responseDuration = null;
203
283
  if (hasReply) {
204
- const userTime = parseTime(c.user_time);
205
- const aiTime = parseTime(c.ai_time);
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
- let intervalDuration = null;
213
- if (i < chatData.conversations.length - 1) {
214
- const nextUserTime = parseTime(chatData.conversations[i + 1].user_time);
215
- const currentUserTime = parseTime(c.user_time);
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
- let elapsedDuration = null;
301
+ var elapsedDuration = null;
222
302
  if (i > 0) {
223
- const firstUserTime = parseTime(chatData.conversations[0].user_time);
224
- const currentUserTime = parseTime(c.user_time);
225
- if (firstUserTime && currentUserTime) {
226
- elapsedDuration = currentUserTime - firstUserTime;
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
- const pair = document.createElement('div');
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">第 ${i + 1} 轮</span>
236
- ${elapsedDuration !== null ? `<span class="pair-duration elapsed">⏰ 课程流逝: ${formatDuration(elapsedDuration)}</span>` : '<span class="pair-duration elapsed">⏰ 课程流逝: 0分</span>'}
237
- ${intervalDuration !== null ? `<span class="pair-duration interval">⏸️ 间隔耗时: ${formatDuration(intervalDuration)}</span>` : ''}
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">${c.user_time || '未知时间'}</span>
244
- </div>
245
- <div class="msg-content">
246
- <pre>${escapeHtml(c.user)}</pre>
247
- </div>
248
- </div>
249
- ${hasReply ? `
250
- <div class="message-section ai-section">
251
- <div class="msg-label ai-label">
252
- <span>🤖 AI</span>
253
- <span class="msg-time">${c.ai_time || '未知时间'}</span>
254
- <span class="pair-duration">⏱️ 回复耗时: ${formatDuration(responseDuration)}</span>
255
- </div>
256
- <div class="msg-content">
257
- <pre>${escapeHtml(c.ai)}</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 = chatData.conversations.length;
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
- - 每个会话生成独立的 <agentId>_<sessionName>_<date>_<time>.js
18
- - 生成 index.js,包含所有会话的元信息列表(含 work_url 字段)
19
- - 生成 chat_history.js(向后兼容,指向第一个会话)
20
- - --html 时额外生成 summary.html(含可点击链接)
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 可后续手动编辑 JS 文件填入学生作品链接
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, first_timestamp):
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
- time_str = parse_time_for_filename(first_timestamp) if first_timestamp else 'unknown'
206
- return f"{agent_safe}_{name_safe}_{time_str}.js"
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 generate_js(conversations, session_key, first_ts, last_ts, output_path):
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
- output_data = {
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 an HTML file with a clickable table of all sessions."""
294
- total_pairs = sum(e['total_pairs'] for e in entries)
295
- rows_html = []
320
+ """Generate HTML from scan_template.html with checkbox + colored rows."""
321
+ agent_colors = {}
296
322
  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>'
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 = 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>""")
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
- 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
- '''
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
- js_filename = build_filename(session_key, first_ts)
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
- 'js_file': js_filename,
440
- 'total_pairs': count,
441
- 'first_time': parse_time(first_ts) if first_ts else '',
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
- sessions = [{
449
- 'index': r['index'],
450
- 'workspace_name': r['agent_id'],
451
- 'session_name': r['session_name'],
452
- 'session_id': r['session_key'],
453
- 'js_file': r['js_file'],
454
- 'total_pairs': r['total_pairs'],
455
- 'first_time': r['first_time'],
456
- 'last_time': r['last_time'],
457
- 'work_url': r['work_url'],
458
- } for r in results]
459
-
460
- index_content = f'''// Chat History Index - {len(sessions)} session(s)
461
- // Generated by chat-history-extractor skill
462
-
463
- const chatIndex = {json.dumps(sessions, ensure_ascii=False, indent=2)};
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(index_content)
468
- print(f"\nIndex: {index_path} ({len(sessions)} sessions)")
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, html_output=False):
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(f"Found {len(all_sessions)} session(s) across all agents.\n")
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(f" [SKIP] {session_key}: {e}")
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
- # Generate JS files if output_dir specified
506
- if output_dir:
507
- os.makedirs(output_dir, exist_ok=True)
508
- results = []
509
- for e in entries:
510
- js_filename = build_filename(e['session_key'], e['_first_ts'])
511
- js_path = os.path.join(output_dir, js_filename)
512
- convs, e_first, e_last = count_conversations(e['_jsonl_path'])
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(f"\n合计:{len(entries)} 个会话,{total_pairs} 对对话。")
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
- elif a == '--html':
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, html_output=html_output)
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(f"Input: {len(items)} session(s)")
590
- print(f"Host: {detect_chat_host()}")
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
- if len(results) > 1:
605
- generate_index_js(results, output_dir)
559
+ # 唯一数据产物:index.js(chatIndex + chatAll)
560
+ generate_index_js(results, output_dir)
606
561
 
607
- first_js = os.path.join(output_dir, results[0]['js_file'])
608
- compat_js = os.path.join(output_dir, 'chat_history.js')
609
- if first_js != compat_js:
610
- import shutil
611
- shutil.copy2(first_js, compat_js)
612
- print(f"Compat: chat_history.js -> {results[0]['js_file']}")
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(f"\n合计:{len(results)} 个会话,{total_pairs} 对对话。")
573
+ print("\n合计:%d 个会话,%d 对对话。" % (len(results), total_pairs))
625
574
 
626
575
 
627
576
  if __name__ == '__main__':