@aiyiran/myclaw 1.1.38 → 1.1.39

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.
@@ -2208,7 +2208,15 @@
2208
2208
  syncStatus.style.cssText = 'font-size:11px;color:rgba(205,214,244,0.4);transition:opacity 0.3s;';
2209
2209
  syncStatus.textContent = '⟳ 检查更新...';
2210
2210
 
2211
+ var headSyncBtn = document.createElement('button');
2212
+ headSyncBtn.textContent = '↺';
2213
+ headSyncBtn.title = '检查并同步最新模板';
2214
+ headSyncBtn.style.cssText = 'padding:3px 9px;background:rgba(255,255,255,0.07);border:1px solid rgba(255,255,255,0.15);border-radius:4px;color:#cdd6f4;font-size:13px;font-family:monospace;cursor:pointer;transition:all 0.15s;line-height:1;';
2215
+ headSyncBtn.onmouseenter = function () { headSyncBtn.style.background = 'rgba(255,255,255,0.14)'; };
2216
+ headSyncBtn.onmouseleave = function () { headSyncBtn.style.background = 'rgba(255,255,255,0.07)'; };
2217
+
2211
2218
  headLeft.appendChild(headTitle);
2219
+ headLeft.appendChild(headSyncBtn);
2212
2220
  headLeft.appendChild(syncStatus);
2213
2221
 
2214
2222
  var headClose = document.createElement('span');
@@ -2387,22 +2395,6 @@
2387
2395
  var infoSpan = document.createElement('span');
2388
2396
  infoSpan.style.cssText = 'font-size:12px;color:#cdd6f4;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
2389
2397
  infoSpan.textContent = tpl['系列'] + tpl['编号'] + ' ' + tpl['名称'];
2390
- // 刷新按钮
2391
- var refreshBtn = document.createElement('button');
2392
- refreshBtn.textContent = '↺';
2393
- refreshBtn.title = '刷新预览';
2394
- refreshBtn.style.cssText = 'flex-shrink:0;padding:5px 10px;background:rgba(255,255,255,0.07);border:1px solid rgba(255,255,255,0.15);border-radius:4px;color:#cdd6f4;font-size:14px;font-family:monospace;cursor:pointer;transition:all 0.15s;white-space:nowrap;line-height:1;';
2395
- refreshBtn.onmouseenter = function () { refreshBtn.style.background = 'rgba(255,255,255,0.14)'; };
2396
- refreshBtn.onmouseleave = function () { refreshBtn.style.background = 'rgba(255,255,255,0.07)'; };
2397
- refreshBtn.onclick = function () {
2398
- var src = previewIframe.src;
2399
- previewIframe.src = '';
2400
- setTimeout(function () { previewIframe.src = src; }, 50);
2401
- refreshBtn.style.transform = 'rotate(360deg)';
2402
- refreshBtn.style.transition = 'transform 0.4s ease';
2403
- setTimeout(function () { refreshBtn.style.transform = ''; refreshBtn.style.transition = 'all 0.15s'; }, 450);
2404
- };
2405
-
2406
2398
  // 新窗口打开按钮
2407
2399
  var openNewBtn = document.createElement('button');
2408
2400
  openNewBtn.textContent = '↗';
@@ -2428,11 +2420,11 @@
2428
2420
  });
2429
2421
  };
2430
2422
  rightHeader.appendChild(infoSpan);
2431
- rightHeader.appendChild(refreshBtn);
2432
2423
  rightHeader.appendChild(openNewBtn);
2433
2424
  rightHeader.appendChild(useBtn);
2434
2425
  previewIframe.src = MYCLAW_API_BASE + '/api/file?path='
2435
- + encodeURIComponent(TEMPLATE_ROOT + '/templates/' + tpl['文件夹名'] + '/__student-view__.html');
2426
+ + encodeURIComponent(TEMPLATE_ROOT + '/templates/' + tpl['文件夹名'] + '/__student-view__.html')
2427
+ + '&t=' + Date.now();
2436
2428
  }
2437
2429
 
2438
2430
  row.onclick = setActive;
@@ -2471,27 +2463,56 @@
2471
2463
  }
2472
2464
 
2473
2465
  // ── 并行:加载本地 index + 检查 CDN 更新 ──────────────────────────────
2474
- // 检查 CDN 更新(不阻塞列表加载)
2475
- fetch(MYCLAW_API_BASE + '/api/sync-templates')
2476
- .then(function (r) { return r.json(); })
2477
- .then(function (sync) {
2478
- if (!sync.changed) {
2466
+ function doSync() {
2467
+ headSyncBtn.disabled = true;
2468
+ headSyncBtn.style.opacity = '0.5';
2469
+ syncStatus.style.color = 'rgba(205,214,244,0.4)';
2470
+ syncStatus.textContent = '⟳ 检查更新...';
2471
+ fetch(MYCLAW_API_BASE + '/api/sync-templates')
2472
+ .then(function (r) { return r.json(); })
2473
+ .then(function (sync) {
2474
+ headSyncBtn.disabled = false;
2475
+ headSyncBtn.style.opacity = '1';
2476
+ if (!sync.changed) {
2477
+ syncStatus.textContent = '已是最新';
2478
+ setTimeout(function () { syncStatus.textContent = ''; }, 2000);
2479
+ return;
2480
+ }
2481
+ if (sync.syncing) {
2482
+ syncStatus.textContent = '⟳ 同步中...';
2483
+ setTimeout(function () {
2484
+ fetch(MYCLAW_API_BASE + '/api/file?path=' + encodeURIComponent(TEMPLATE_ROOT + '/template-index.json'))
2485
+ .then(function (r) { return r.json(); })
2486
+ .then(function (newIndex) {
2487
+ renderTemplateList(flattenTemplates(newIndex));
2488
+ syncStatus.textContent = '✓ 已更新';
2489
+ syncStatus.style.color = '#10b981';
2490
+ setTimeout(function () { syncStatus.textContent = ''; }, 3000);
2491
+ })
2492
+ .catch(function () { syncStatus.textContent = ''; });
2493
+ }, 5000);
2494
+ return;
2495
+ }
2496
+ var msg = '';
2497
+ if (sync.added && sync.added.length) msg += '新增 ' + sync.added.length + ' 个';
2498
+ if (sync.updated && sync.updated.length) msg += (msg ? ',' : '') + '更新 ' + sync.updated.length + ' 个';
2499
+ syncStatus.textContent = '✓ ' + (msg || '已更新');
2500
+ syncStatus.style.color = '#10b981';
2501
+ fetch(MYCLAW_API_BASE + '/api/file?path=' + encodeURIComponent(TEMPLATE_ROOT + '/template-index.json'))
2502
+ .then(function (r) { return r.json(); })
2503
+ .then(function (newIndex) { renderTemplateList(flattenTemplates(newIndex)); })
2504
+ .catch(function () {});
2505
+ setTimeout(function () { syncStatus.textContent = ''; }, 4000);
2506
+ })
2507
+ .catch(function () {
2508
+ headSyncBtn.disabled = false;
2509
+ headSyncBtn.style.opacity = '1';
2479
2510
  syncStatus.textContent = '';
2480
- return;
2481
- }
2482
- var msg = '';
2483
- if (sync.added && sync.added.length) msg += '新增 ' + sync.added.length + ' 个';
2484
- if (sync.updated && sync.updated.length) msg += (msg ? ',' : '') + '更新 ' + sync.updated.length + ' 个';
2485
- syncStatus.textContent = '✓ ' + msg;
2486
- syncStatus.style.color = '#10b981';
2487
- // 重新加载列表
2488
- fetch(MYCLAW_API_BASE + '/api/file?path=' + encodeURIComponent(TEMPLATE_ROOT + '/template-index.json'))
2489
- .then(function (r) { return r.json(); })
2490
- .then(function (newIndex) { renderTemplateList(flattenTemplates(newIndex)); })
2491
- .catch(function () {});
2492
- setTimeout(function () { syncStatus.textContent = ''; }, 4000);
2493
- })
2494
- .catch(function () { syncStatus.textContent = ''; });
2511
+ });
2512
+ }
2513
+
2514
+ headSyncBtn.onclick = doSync;
2515
+ doSync();
2495
2516
 
2496
2517
  fetch(MYCLAW_API_BASE + '/api/file?path=' + encodeURIComponent(TEMPLATE_ROOT + '/template-index.json'))
2497
2518
  .then(function (r) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiyiran/myclaw",
3
- "version": "1.1.38",
3
+ "version": "1.1.39",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -475,6 +475,92 @@ def sync_all(workspace_id):
475
475
  return True
476
476
 
477
477
 
478
+ # ═══════════════════════════════════════════════════════════════
479
+ # 模板同步:从 CDN 增量下载到本地
480
+ # ═══════════════════════════════════════════════════════════════
481
+
482
+ _sync_state = {'running': False, 'last_result': None}
483
+ _sync_lock = threading.Lock()
484
+
485
+ _SYNC_LOG = os.path.join(get_openclaw_path(), 'sync-templates.log')
486
+
487
+ def _sync_log(msg):
488
+ line = '[{}] {}\n'.format(datetime.now(timezone.utc).strftime('%H:%M:%S'), msg)
489
+ print(line, end='')
490
+ try:
491
+ with open(_SYNC_LOG, 'a', encoding='utf-8') as f:
492
+ f.write(line)
493
+ except Exception:
494
+ pass
495
+
496
+ TEMPLATE_CDN_PREFIX_BASE = 'myclaw/live/yiran/skills/yiran-playground-template-use/templates'
497
+ TEMPLATE_LOCAL_DIR = os.path.join(get_openclaw_path(), 'skills', 'yiran-playground-template-use', 'templates')
498
+ TEMPLATE_LOCAL_INDEX = os.path.join(get_openclaw_path(), 'skills', 'yiran-playground-template-use', 'template-index.json')
499
+ TEMPLATE_CDN_INDEX_URL = 'https://cdn.yiranlaoshi.com/myclaw/live/yiran/skills/yiran-playground-template-use/template-index.json'
500
+
501
+
502
+ def _do_sync_templates(cdn_index, local_index):
503
+ """后台线程:按 folder 全量下载有变化的模板文件"""
504
+ try:
505
+ cdn_templates = cdn_index.get('templates', {})
506
+ local_templates = local_index.get('templates', {})
507
+ added = []
508
+ updated = []
509
+
510
+ _sync_log('开始同步,CDN 模板系列: {}'.format(list(cdn_templates.keys())))
511
+
512
+ for series, nums in cdn_templates.items():
513
+ for num, record in nums.items():
514
+ folder_name = record.get('文件夹名', '')
515
+ cdn_ua = record.get('updated_at', '')
516
+ local_ua = local_templates.get(series, {}).get(num, {}).get('updated_at', '')
517
+
518
+ if not folder_name:
519
+ continue
520
+
521
+ is_new = series not in local_templates or num not in local_templates.get(series, {})
522
+ is_changed = not is_new and cdn_ua != local_ua
523
+
524
+ if not is_new and not is_changed:
525
+ _sync_log('跳过 {} (无变化)'.format(folder_name))
526
+ continue
527
+
528
+ _sync_log('{} {} → 下载中...'.format('新增' if is_new else '更新', folder_name))
529
+ cdn_prefix = TEMPLATE_CDN_PREFIX_BASE + '/{}/'.format(folder_name)
530
+ folder_dir = os.path.join(TEMPLATE_LOCAL_DIR, folder_name)
531
+ os.makedirs(folder_dir, exist_ok=True)
532
+ cdn_files = _list_files_from_qiniu(cdn_prefix)
533
+ _sync_log(' 列举到 {} 个文件'.format(len(cdn_files)))
534
+ for item in cdn_files:
535
+ key = item.get('key', '')
536
+ filename = key[len(cdn_prefix):]
537
+ if not filename:
538
+ continue
539
+ ok = _download_from_cdn(key, os.path.join(folder_dir, filename))
540
+ _sync_log(' {} {}'.format('✓' if ok else '✗', filename))
541
+
542
+ if is_new:
543
+ added.append(folder_name)
544
+ else:
545
+ updated.append(folder_name)
546
+
547
+ # 覆盖本地 index
548
+ os.makedirs(os.path.dirname(TEMPLATE_LOCAL_INDEX), exist_ok=True)
549
+ with open(TEMPLATE_LOCAL_INDEX, 'w', encoding='utf-8') as f:
550
+ json.dump(cdn_index, f, ensure_ascii=False, indent=2)
551
+
552
+ _sync_log('完成。新增: {}, 更新: {}'.format(added, updated))
553
+ with _sync_lock:
554
+ _sync_state['running'] = False
555
+ _sync_state['last_result'] = {'changed': True, 'added': added, 'updated': updated}
556
+
557
+ except Exception as e:
558
+ import traceback
559
+ _sync_log('异常: {}\n{}'.format(e, traceback.format_exc()))
560
+ with _sync_lock:
561
+ _sync_state['running'] = False
562
+
563
+
478
564
  # ═══════════════════════════════════════════════════════════════
479
565
  # Fork:从 CDN 克隆他人 workspace 到本地
480
566
  # ═══════════════════════════════════════════════════════════════
@@ -1105,22 +1191,17 @@ class MyclawAPIHandler(BaseHTTPRequestHandler):
1105
1191
 
1106
1192
  def _handle_sync_templates(self):
1107
1193
  """GET /api/sync-templates
1108
- 检查 CDN 上的 template-index.json 是否比本地新,若有变化则增量下载。
1109
- 返回 {changed: false} 或 {changed: true, added: [...], updated: [...]}
1194
+ 快速比对 CDN index_updated_at 与本地,若一致立即返回 {changed: false}。
1195
+ 若有变化,启动后台线程下载,立即返回 {changed: true, syncing: true}
1110
1196
  """
1111
- CDN_INDEX_URL = 'https://cdn.yiranlaoshi.com/myclaw/live/yiran/skills/yiran-playground-template-use/template-index.json'
1112
- CDN_TEMPLATE_BASE = 'https://cdn.yiranlaoshi.com/myclaw/live/yiran/skills/yiran-playground-template-use/templates'
1113
-
1114
- local_templates_dir = os.path.join(
1115
- get_openclaw_path(), 'skills', 'yiran-playground-template-use', 'templates'
1116
- )
1117
- local_index_path = os.path.join(
1118
- get_openclaw_path(), 'skills', 'yiran-playground-template-use', 'template-index.json'
1119
- )
1120
-
1197
+ # 快速拉取 CDN index(仅 JSON,通常 <1s)
1121
1198
  try:
1122
- # 拉取 CDN index
1123
- with urllib.request.urlopen(CDN_INDEX_URL, timeout=10) as resp:
1199
+ import ssl
1200
+ ctx = ssl.create_default_context()
1201
+ ctx.check_hostname = False
1202
+ ctx.verify_mode = ssl.CERT_NONE
1203
+ url_with_ts = TEMPLATE_CDN_INDEX_URL + '?t=' + str(int(time.time()))
1204
+ with urllib.request.urlopen(url_with_ts, timeout=10, context=ctx) as resp:
1124
1205
  cdn_index = json.loads(resp.read().decode('utf-8'))
1125
1206
  except Exception as e:
1126
1207
  self._send_json({'error': 'CDN index 获取失败: ' + str(e)}, 502)
@@ -1128,9 +1209,9 @@ class MyclawAPIHandler(BaseHTTPRequestHandler):
1128
1209
 
1129
1210
  # 读取本地 index
1130
1211
  local_index = {}
1131
- if os.path.isfile(local_index_path):
1212
+ if os.path.isfile(TEMPLATE_LOCAL_INDEX):
1132
1213
  try:
1133
- with open(local_index_path, 'r', encoding='utf-8') as f:
1214
+ with open(TEMPLATE_LOCAL_INDEX, 'r', encoding='utf-8') as f:
1134
1215
  local_index = json.load(f)
1135
1216
  except Exception:
1136
1217
  pass
@@ -1142,55 +1223,21 @@ class MyclawAPIHandler(BaseHTTPRequestHandler):
1142
1223
  self._send_json({'changed': False})
1143
1224
  return
1144
1225
 
1145
- # 找出新增和变更的模板
1146
- cdn_templates = cdn_index.get('templates', {})
1147
- local_templates = local_index.get('templates', {})
1148
- added = []
1149
- updated = []
1150
-
1151
- for series, nums in cdn_templates.items():
1152
- for num, record in nums.items():
1153
- folder_name = record.get('文件夹名', '')
1154
- cdn_ua = record.get('updated_at', '')
1155
- local_ua = local_templates.get(series, {}).get(num, {}).get('updated_at', '')
1156
-
1157
- if not folder_name:
1158
- continue
1159
-
1160
- is_new = series not in local_templates or num not in local_templates.get(series, {})
1161
- is_changed = not is_new and cdn_ua != local_ua
1162
-
1163
- if not is_new and not is_changed:
1164
- continue
1165
-
1166
- # 从七牛列举该文件夹下所有文件,全量下载
1167
- cdn_prefix = 'myclaw/live/yiran/skills/yiran-playground-template-use/templates/{}/'.format(folder_name)
1168
- folder_dir = os.path.join(local_templates_dir, folder_name)
1169
- os.makedirs(folder_dir, exist_ok=True)
1170
- cdn_files = _list_files_from_qiniu(cdn_prefix)
1171
- for item in cdn_files:
1172
- key = item.get('key', '')
1173
- filename = key[len(cdn_prefix):]
1174
- if not filename:
1175
- continue
1176
- dest = os.path.join(folder_dir, filename)
1177
- _download_from_cdn(key, dest)
1178
-
1179
- if is_new:
1180
- added.append(folder_name)
1181
- else:
1182
- updated.append(folder_name)
1183
-
1184
- # 覆盖本地 index
1185
- try:
1186
- os.makedirs(os.path.dirname(local_index_path), exist_ok=True)
1187
- with open(local_index_path, 'w', encoding='utf-8') as f:
1188
- json.dump(cdn_index, f, ensure_ascii=False, indent=2)
1189
- except Exception as e:
1190
- self._send_json({'error': 'index 写入失败: ' + str(e)}, 500)
1191
- return
1192
-
1193
- self._send_json({'changed': True, 'added': added, 'updated': updated})
1226
+ # 有变化 → 启动后台线程,立即返回
1227
+ with _sync_lock:
1228
+ if _sync_state['running']:
1229
+ self._send_json({'changed': True, 'syncing': True})
1230
+ return
1231
+ _sync_state['running'] = True
1232
+ _sync_state['last_result'] = None
1233
+
1234
+ t = threading.Thread(
1235
+ target=_do_sync_templates,
1236
+ args=(cdn_index, local_index),
1237
+ daemon=True,
1238
+ )
1239
+ t.start()
1240
+ self._send_json({'changed': True, 'syncing': True})
1194
1241
 
1195
1242
  def log_message(self, format, *args):
1196
1243
  # 静默日志,避免轮询刷屏
@@ -1,5 +1,5 @@
1
1
  {
2
- "index_updated_at": "2026-04-22T05:27:04Z",
2
+ "index_updated_at": "2026-04-22T06:16:49Z",
3
3
  "templates": {
4
4
  "A": {
5
5
  "100": {
@@ -133,7 +133,7 @@
133
133
  "迭代推进",
134
134
  "游戏机制"
135
135
  ],
136
- "updated_at": "2026-04-22T05:23:43Z"
136
+ "updated_at": "2026-04-22T06:16:45Z"
137
137
  }
138
138
  },
139
139
  "B": {