@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.
- package/assets/myclaw-artifacts.js +60 -21
- package/package.json +1 -1
- package/server/sync_workspace.py +103 -45
|
@@ -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 =
|
|
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
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
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
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
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
package/server/sync_workspace.py
CHANGED
|
@@ -479,7 +479,12 @@ def sync_all(workspace_id):
|
|
|
479
479
|
# 模板同步:从 CDN 增量下载到本地
|
|
480
480
|
# ═══════════════════════════════════════════════════════════════
|
|
481
481
|
|
|
482
|
-
_sync_state = {
|
|
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('
|
|
515
|
+
_sync_log('开始同步,共 {} 个 folder'.format(len(pending)))
|
|
511
516
|
|
|
512
|
-
for
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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({
|
|
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({
|
|
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
|
# 静默日志,避免轮询刷屏
|