@aiyiran/myclaw 1.1.167 → 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.
@@ -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
- setTextareaValue(buildTestDonePrompt());
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
- trySend();
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
- trySend();
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
- try {
1230
- navigator.clipboard.writeText(text).then(function () {
1231
- console.log("[myclaw-send] 📋 已复制到剪贴板:", text.substring(0, 50) + (text.length > 50 ? "..." : ""));
1232
- }).catch(function () {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiyiran/myclaw",
3
- "version": "1.1.167",
3
+ "version": "1.1.168",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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>
@@ -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, first_timestamp):
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
- time_str = parse_time_for_filename(first_timestamp) if first_timestamp else 'unknown'
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 an HTML file with a clickable table of all sessions."""
294
- total_pairs = sum(e['total_pairs'] for e in entries)
295
- rows_html = []
314
+ """Generate HTML from scan_template.html with checkbox + colored rows."""
315
+ agent_colors = {}
296
316
  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>'
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 = 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>""")
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
- 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
- '''
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, first_ts)
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(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
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(index_content)
468
- print(f"\nIndex: {index_path} ({len(sessions)} sessions)")
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, html_output=False):
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(f"Found {len(all_sessions)} session(s) across all agents.\n")
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(f" [SKIP] {session_key}: {e}")
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
- # 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}")
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(f"\n合计:{len(entries)} 个会话,{total_pairs} 对对话。")
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
- elif a == '--html':
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, html_output=html_output)
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(f"Input: {len(items)} session(s)")
590
- print(f"Host: {detect_chat_host()}")
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
- if len(results) > 1:
605
- generate_index_js(results, output_dir)
567
+ # Always regenerate index.js by scanning directory
568
+ generate_index_js(output_dir)
606
569
 
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']}")
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(f"\n合计:{len(results)} 个会话,{total_pairs} 对对话。")
581
+ print("\n合计:%d 个会话,%d 对对话。" % (len(results), total_pairs))
625
582
 
626
583
 
627
584
  if __name__ == '__main__':