@aiyiran/myclaw 1.1.39 → 1.1.41

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.
@@ -2182,6 +2182,7 @@
2182
2182
  // ═══ 作品模板列表弹框 ═══
2183
2183
  // 使用通用文件接口 GET /api/file?path=(路径相对 .openclaw 根)
2184
2184
  var TEMPLATE_ROOT = 'skills/yiran-playground-template-use';
2185
+ var TEMPLATE_CDN_BASE = 'https://cdn.yiranlaoshi.com/myclaw/live/yiran/skills/yiran-playground-template-use/templates';
2185
2186
 
2186
2187
  function openTemplateModal() {
2187
2188
  if (document.querySelector('#myclaw-template-modal')) return;
@@ -2422,9 +2423,7 @@
2422
2423
  rightHeader.appendChild(infoSpan);
2423
2424
  rightHeader.appendChild(openNewBtn);
2424
2425
  rightHeader.appendChild(useBtn);
2425
- previewIframe.src = MYCLAW_API_BASE + '/api/file?path='
2426
- + encodeURIComponent(TEMPLATE_ROOT + '/templates/' + tpl['文件夹名'] + '/__student-view__.html')
2427
- + '&t=' + Date.now();
2426
+ previewIframe.src = TEMPLATE_CDN_BASE + '/' + encodeURIComponent(tpl['文件夹名']) + '/__student-view__.html?t=' + Date.now();
2428
2427
  }
2429
2428
 
2430
2429
  row.onclick = setActive;
@@ -2463,11 +2462,60 @@
2463
2462
  }
2464
2463
 
2465
2464
  // ── 并行:加载本地 index + 检查 CDN 更新 ──────────────────────────────
2465
+ var _pollTimer = null; // 提到外层,doSync 守卫用
2466
+
2466
2467
  function doSync() {
2468
+ // 轮询进行中时点击无效,避免重复起 timer
2469
+ if (_pollTimer) return;
2467
2470
  headSyncBtn.disabled = true;
2468
2471
  headSyncBtn.style.opacity = '0.5';
2469
2472
  syncStatus.style.color = 'rgba(205,214,244,0.4)';
2470
2473
  syncStatus.textContent = '⟳ 检查更新...';
2474
+
2475
+ var _pollDeadline = Date.now() + 10 * 60 * 1000; // 最多轮询 10 分钟
2476
+
2477
+ function _reloadList(onDone) {
2478
+ fetch(MYCLAW_API_BASE + '/api/file?path=' + encodeURIComponent(TEMPLATE_ROOT + '/template-index.json'))
2479
+ .then(function (r) { return r.json(); })
2480
+ .then(function (idx) { renderTemplateList(flattenTemplates(idx)); if (onDone) onDone(); })
2481
+ .catch(function () { if (onDone) onDone(); });
2482
+ }
2483
+
2484
+ function _stopPolling() {
2485
+ if (_pollTimer) { clearInterval(_pollTimer); _pollTimer = null; }
2486
+ }
2487
+
2488
+ function _startPolling() {
2489
+ _pollTimer = setInterval(function () {
2490
+ if (Date.now() > _pollDeadline) {
2491
+ _stopPolling();
2492
+ syncStatus.textContent = '⚠ 同步超时';
2493
+ setTimeout(function () { syncStatus.textContent = ''; }, 4000);
2494
+ return;
2495
+ }
2496
+ fetch(MYCLAW_API_BASE + '/api/sync-status')
2497
+ .then(function (r) { return r.json(); })
2498
+ .then(function (s) {
2499
+ if (s.running) {
2500
+ syncStatus.textContent = '⟳ 同步中 ' + s.completed.length + '/' + s.total;
2501
+ return;
2502
+ }
2503
+ // 完成
2504
+ _stopPolling();
2505
+ var result = s.result || {};
2506
+ var msg = '';
2507
+ if (result.added && result.added.length) msg += '新增 ' + result.added.length + ' 个';
2508
+ if (result.updated && result.updated.length) msg += (msg ? ',' : '') + '更新 ' + result.updated.length + ' 个';
2509
+ syncStatus.style.color = '#10b981';
2510
+ syncStatus.textContent = '✓ ' + (msg || '已完成');
2511
+ _reloadList(function () {
2512
+ setTimeout(function () { syncStatus.textContent = ''; syncStatus.style.color = 'rgba(205,214,244,0.4)'; }, 4000);
2513
+ });
2514
+ })
2515
+ .catch(function () { /* 网络抖动,下次再试 */ });
2516
+ }, 3000);
2517
+ }
2518
+
2471
2519
  fetch(MYCLAW_API_BASE + '/api/sync-templates')
2472
2520
  .then(function (r) { return r.json(); })
2473
2521
  .then(function (sync) {
@@ -2479,30 +2527,21 @@
2479
2527
  return;
2480
2528
  }
2481
2529
  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);
2530
+ // index 已写好,立刻刷新列表让用户看到模板
2531
+ _reloadList(null);
2532
+ syncStatus.textContent = '⟳ 同步中 ' + (sync.completed || []).length + '/' + (sync.total || '?');
2533
+ _startPolling();
2494
2534
  return;
2495
2535
  }
2536
+ // 极少数情况:同步在返回前已完成
2496
2537
  var msg = '';
2497
2538
  if (sync.added && sync.added.length) msg += '新增 ' + sync.added.length + ' 个';
2498
2539
  if (sync.updated && sync.updated.length) msg += (msg ? ',' : '') + '更新 ' + sync.updated.length + ' 个';
2499
- syncStatus.textContent = '✓ ' + (msg || '已更新');
2500
2540
  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);
2541
+ syncStatus.textContent = '' + (msg || '已更新');
2542
+ _reloadList(function () {
2543
+ setTimeout(function () { syncStatus.textContent = ''; syncStatus.style.color = 'rgba(205,214,244,0.4)'; }, 4000);
2544
+ });
2506
2545
  })
2507
2546
  .catch(function () {
2508
2547
  headSyncBtn.disabled = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiyiran/myclaw",
3
- "version": "1.1.39",
3
+ "version": "1.1.41",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -479,7 +479,12 @@ def sync_all(workspace_id):
479
479
  # 模板同步:从 CDN 增量下载到本地
480
480
  # ═══════════════════════════════════════════════════════════════
481
481
 
482
- _sync_state = {'running': False, 'last_result': None}
482
+ _sync_state = {
483
+ 'running': False,
484
+ 'last_result': None,
485
+ 'completed': [], # 已下载完成的 folder 名列表
486
+ 'total': 0, # 本次需要下载的 folder 总数
487
+ }
483
488
  _sync_lock = threading.Lock()
484
489
 
485
490
  _SYNC_LOG = os.path.join(get_openclaw_path(), 'sync-templates.log')
@@ -499,55 +504,40 @@ TEMPLATE_LOCAL_INDEX = os.path.join(get_openclaw_path(), 'skills', 'yiran-playgr
499
504
  TEMPLATE_CDN_INDEX_URL = 'https://cdn.yiranlaoshi.com/myclaw/live/yiran/skills/yiran-playground-template-use/template-index.json'
500
505
 
501
506
 
502
- def _do_sync_templates(cdn_index, local_index):
503
- """后台线程:按 folder 全量下载有变化的模板文件"""
507
+ def _do_sync_templates(cdn_index, local_index, pending):
508
+ """后台线程:按 folder 全量下载有变化的模板文件
509
+ pending: list of (folder_name, is_new),已在调用方确定好
510
+ """
504
511
  try:
505
- cdn_templates = cdn_index.get('templates', {})
506
- local_templates = local_index.get('templates', {})
507
512
  added = []
508
513
  updated = []
509
514
 
510
- _sync_log('开始同步,CDN 模板系列: {}'.format(list(cdn_templates.keys())))
515
+ _sync_log('开始同步,共 {} 个 folder'.format(len(pending)))
511
516
 
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
+ for folder_name, is_new in pending:
518
+ _sync_log('{} {} 下载中...'.format('新增' if is_new else '更新', folder_name))
519
+ cdn_prefix = TEMPLATE_CDN_PREFIX_BASE + '/{}/'.format(folder_name)
520
+ folder_dir = os.path.join(TEMPLATE_LOCAL_DIR, folder_name)
521
+ os.makedirs(folder_dir, exist_ok=True)
517
522
 
518
- if not folder_name:
523
+ cdn_files = _list_files_from_qiniu(cdn_prefix)
524
+ _sync_log(' 列举到 {} 个文件'.format(len(cdn_files)))
525
+ for item in cdn_files:
526
+ key = item.get('key', '')
527
+ filename = key[len(cdn_prefix):]
528
+ if not filename:
519
529
  continue
530
+ ok = _download_from_cdn(key, os.path.join(folder_dir, filename))
531
+ _sync_log(' {} {}'.format('✓' if ok else '✗', filename))
520
532
 
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
533
+ if is_new:
534
+ added.append(folder_name)
535
+ else:
536
+ updated.append(folder_name)
527
537
 
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)
538
+ # 每完成一个 folder 就更新进度
539
+ with _sync_lock:
540
+ _sync_state['completed'].append(folder_name)
551
541
 
552
542
  _sync_log('完成。新增: {}, 更新: {}'.format(added, updated))
553
543
  with _sync_lock:
@@ -835,6 +825,8 @@ class MyclawAPIHandler(BaseHTTPRequestHandler):
835
825
  return self._handle_file(params)
836
826
  elif path == '/api/sync-templates':
837
827
  return self._handle_sync_templates()
828
+ elif path == '/api/sync-status':
829
+ return self._handle_sync_status()
838
830
  else:
839
831
  self._send_json({"error": "not found"}, 404)
840
832
 
@@ -1223,21 +1215,87 @@ class MyclawAPIHandler(BaseHTTPRequestHandler):
1223
1215
  self._send_json({'changed': False})
1224
1216
  return
1225
1217
 
1226
- # 有变化 启动后台线程,立即返回
1218
+ # 计算需要下载的 folder 列表
1219
+ cdn_templates = cdn_index.get('templates', {})
1220
+ local_templates = local_index.get('templates', {})
1221
+ pending = [] # list of (folder_name, is_new)
1222
+ for series, nums in cdn_templates.items():
1223
+ for num, record in nums.items():
1224
+ folder_name = record.get('文件夹名', '')
1225
+ if not folder_name:
1226
+ continue
1227
+ cdn_ua = record.get('updated_at', '')
1228
+ local_ua = local_templates.get(series, {}).get(num, {}).get('updated_at', '')
1229
+ is_new = series not in local_templates or num not in local_templates.get(series, {})
1230
+ is_changed = not is_new and cdn_ua != local_ua
1231
+ if is_new or is_changed:
1232
+ pending.append((folder_name, is_new))
1233
+
1234
+ if not pending:
1235
+ # index_updated_at 变了但所有 folder 都没变,直接更新 index 即可
1236
+ try:
1237
+ os.makedirs(os.path.dirname(TEMPLATE_LOCAL_INDEX), exist_ok=True)
1238
+ with open(TEMPLATE_LOCAL_INDEX, 'w', encoding='utf-8') as f:
1239
+ json.dump(cdn_index, f, ensure_ascii=False, indent=2)
1240
+ except Exception:
1241
+ pass
1242
+ self._send_json({'changed': False})
1243
+ return
1244
+
1245
+ # 已有同步在跑,直接返回当前进度
1227
1246
  with _sync_lock:
1228
1247
  if _sync_state['running']:
1229
- self._send_json({'changed': True, 'syncing': True})
1248
+ self._send_json({
1249
+ 'changed': True, 'syncing': True,
1250
+ 'completed': list(_sync_state['completed']),
1251
+ 'total': _sync_state['total'],
1252
+ })
1230
1253
  return
1254
+
1255
+ # 先写 index.json,让前端列表立刻出现
1256
+ try:
1257
+ os.makedirs(os.path.dirname(TEMPLATE_LOCAL_INDEX), exist_ok=True)
1258
+ with open(TEMPLATE_LOCAL_INDEX, 'w', encoding='utf-8') as f:
1259
+ json.dump(cdn_index, f, ensure_ascii=False, indent=2)
1260
+ except Exception as e:
1261
+ self._send_json({'error': 'index 写入失败: ' + str(e)}, 500)
1262
+ return
1263
+
1264
+ # 初始化状态,启动后台线程
1265
+ with _sync_lock:
1231
1266
  _sync_state['running'] = True
1232
1267
  _sync_state['last_result'] = None
1268
+ _sync_state['completed'] = []
1269
+ _sync_state['total'] = len(pending)
1233
1270
 
1234
1271
  t = threading.Thread(
1235
1272
  target=_do_sync_templates,
1236
- args=(cdn_index, local_index),
1273
+ args=(cdn_index, local_index, pending),
1237
1274
  daemon=True,
1238
1275
  )
1239
1276
  t.start()
1240
- self._send_json({'changed': True, 'syncing': True})
1277
+ self._send_json({
1278
+ 'changed': True, 'syncing': True,
1279
+ 'completed': [],
1280
+ 'total': len(pending),
1281
+ })
1282
+
1283
+ def _handle_sync_status(self):
1284
+ """GET /api/sync-status
1285
+ 返回当前后台同步进度。
1286
+ 运行中: {running: true, completed: [...], total: N}
1287
+ 完成后: {running: false, result: {changed, added, updated}}
1288
+ """
1289
+ with _sync_lock:
1290
+ running = _sync_state['running']
1291
+ completed = list(_sync_state['completed'])
1292
+ total = _sync_state['total']
1293
+ last_result = _sync_state['last_result']
1294
+
1295
+ if running:
1296
+ self._send_json({'running': True, 'completed': completed, 'total': total})
1297
+ else:
1298
+ self._send_json({'running': False, 'result': last_result})
1241
1299
 
1242
1300
  def log_message(self, format, *args):
1243
1301
  # 静默日志,避免轮询刷屏