@aiyiran/myclaw 1.1.38 → 1.1.40
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,94 @@
|
|
|
2471
2463
|
}
|
|
2472
2464
|
|
|
2473
2465
|
// ── 并行:加载本地 index + 检查 CDN 更新 ──────────────────────────────
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
.
|
|
2477
|
-
.
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
syncStatus.textContent = '✓ ' + msg;
|
|
2486
|
-
syncStatus.style.color = '#10b981';
|
|
2487
|
-
// 重新加载列表
|
|
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
|
+
|
|
2472
|
+
// 轮询句柄,用于清理
|
|
2473
|
+
var _pollTimer = null;
|
|
2474
|
+
var _pollDeadline = Date.now() + 10 * 60 * 1000; // 最多轮询 10 分钟
|
|
2475
|
+
|
|
2476
|
+
function _reloadList(onDone) {
|
|
2488
2477
|
fetch(MYCLAW_API_BASE + '/api/file?path=' + encodeURIComponent(TEMPLATE_ROOT + '/template-index.json'))
|
|
2489
2478
|
.then(function (r) { return r.json(); })
|
|
2490
|
-
.then(function (
|
|
2491
|
-
.catch(function () {});
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2479
|
+
.then(function (idx) { renderTemplateList(flattenTemplates(idx)); if (onDone) onDone(); })
|
|
2480
|
+
.catch(function () { if (onDone) onDone(); });
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
function _stopPolling() {
|
|
2484
|
+
if (_pollTimer) { clearInterval(_pollTimer); _pollTimer = null; }
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
function _startPolling() {
|
|
2488
|
+
_pollTimer = setInterval(function () {
|
|
2489
|
+
if (Date.now() > _pollDeadline) {
|
|
2490
|
+
_stopPolling();
|
|
2491
|
+
syncStatus.textContent = '⚠ 同步超时';
|
|
2492
|
+
setTimeout(function () { syncStatus.textContent = ''; }, 4000);
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
fetch(MYCLAW_API_BASE + '/api/sync-status')
|
|
2496
|
+
.then(function (r) { return r.json(); })
|
|
2497
|
+
.then(function (s) {
|
|
2498
|
+
if (s.running) {
|
|
2499
|
+
syncStatus.textContent = '⟳ 同步中 ' + s.completed.length + '/' + s.total;
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
// 完成
|
|
2503
|
+
_stopPolling();
|
|
2504
|
+
var result = s.result || {};
|
|
2505
|
+
var msg = '';
|
|
2506
|
+
if (result.added && result.added.length) msg += '新增 ' + result.added.length + ' 个';
|
|
2507
|
+
if (result.updated && result.updated.length) msg += (msg ? ',' : '') + '更新 ' + result.updated.length + ' 个';
|
|
2508
|
+
syncStatus.style.color = '#10b981';
|
|
2509
|
+
syncStatus.textContent = '✓ ' + (msg || '已完成');
|
|
2510
|
+
_reloadList(function () {
|
|
2511
|
+
setTimeout(function () { syncStatus.textContent = ''; syncStatus.style.color = 'rgba(205,214,244,0.4)'; }, 4000);
|
|
2512
|
+
});
|
|
2513
|
+
})
|
|
2514
|
+
.catch(function () { /* 网络抖动,下次再试 */ });
|
|
2515
|
+
}, 3000);
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
fetch(MYCLAW_API_BASE + '/api/sync-templates')
|
|
2519
|
+
.then(function (r) { return r.json(); })
|
|
2520
|
+
.then(function (sync) {
|
|
2521
|
+
headSyncBtn.disabled = false;
|
|
2522
|
+
headSyncBtn.style.opacity = '1';
|
|
2523
|
+
if (!sync.changed) {
|
|
2524
|
+
syncStatus.textContent = '已是最新';
|
|
2525
|
+
setTimeout(function () { syncStatus.textContent = ''; }, 2000);
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
if (sync.syncing) {
|
|
2529
|
+
// index 已写好,立刻刷新列表让用户看到模板
|
|
2530
|
+
_reloadList(null);
|
|
2531
|
+
syncStatus.textContent = '⟳ 同步中 ' + (sync.completed || []).length + '/' + (sync.total || '?');
|
|
2532
|
+
_startPolling();
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2535
|
+
// 极少数情况:同步在返回前已完成
|
|
2536
|
+
var msg = '';
|
|
2537
|
+
if (sync.added && sync.added.length) msg += '新增 ' + sync.added.length + ' 个';
|
|
2538
|
+
if (sync.updated && sync.updated.length) msg += (msg ? ',' : '') + '更新 ' + sync.updated.length + ' 个';
|
|
2539
|
+
syncStatus.style.color = '#10b981';
|
|
2540
|
+
syncStatus.textContent = '✓ ' + (msg || '已更新');
|
|
2541
|
+
_reloadList(function () {
|
|
2542
|
+
setTimeout(function () { syncStatus.textContent = ''; syncStatus.style.color = 'rgba(205,214,244,0.4)'; }, 4000);
|
|
2543
|
+
});
|
|
2544
|
+
})
|
|
2545
|
+
.catch(function () {
|
|
2546
|
+
headSyncBtn.disabled = false;
|
|
2547
|
+
headSyncBtn.style.opacity = '1';
|
|
2548
|
+
syncStatus.textContent = '';
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
headSyncBtn.onclick = doSync;
|
|
2553
|
+
doSync();
|
|
2495
2554
|
|
|
2496
2555
|
fetch(MYCLAW_API_BASE + '/api/file?path=' + encodeURIComponent(TEMPLATE_ROOT + '/template-index.json'))
|
|
2497
2556
|
.then(function (r) {
|
package/package.json
CHANGED
package/server/sync_workspace.py
CHANGED
|
@@ -475,6 +475,82 @@ def sync_all(workspace_id):
|
|
|
475
475
|
return True
|
|
476
476
|
|
|
477
477
|
|
|
478
|
+
# ═══════════════════════════════════════════════════════════════
|
|
479
|
+
# 模板同步:从 CDN 增量下载到本地
|
|
480
|
+
# ═══════════════════════════════════════════════════════════════
|
|
481
|
+
|
|
482
|
+
_sync_state = {
|
|
483
|
+
'running': False,
|
|
484
|
+
'last_result': None,
|
|
485
|
+
'completed': [], # 已下载完成的 folder 名列表
|
|
486
|
+
'total': 0, # 本次需要下载的 folder 总数
|
|
487
|
+
}
|
|
488
|
+
_sync_lock = threading.Lock()
|
|
489
|
+
|
|
490
|
+
_SYNC_LOG = os.path.join(get_openclaw_path(), 'sync-templates.log')
|
|
491
|
+
|
|
492
|
+
def _sync_log(msg):
|
|
493
|
+
line = '[{}] {}\n'.format(datetime.now(timezone.utc).strftime('%H:%M:%S'), msg)
|
|
494
|
+
print(line, end='')
|
|
495
|
+
try:
|
|
496
|
+
with open(_SYNC_LOG, 'a', encoding='utf-8') as f:
|
|
497
|
+
f.write(line)
|
|
498
|
+
except Exception:
|
|
499
|
+
pass
|
|
500
|
+
|
|
501
|
+
TEMPLATE_CDN_PREFIX_BASE = 'myclaw/live/yiran/skills/yiran-playground-template-use/templates'
|
|
502
|
+
TEMPLATE_LOCAL_DIR = os.path.join(get_openclaw_path(), 'skills', 'yiran-playground-template-use', 'templates')
|
|
503
|
+
TEMPLATE_LOCAL_INDEX = os.path.join(get_openclaw_path(), 'skills', 'yiran-playground-template-use', 'template-index.json')
|
|
504
|
+
TEMPLATE_CDN_INDEX_URL = 'https://cdn.yiranlaoshi.com/myclaw/live/yiran/skills/yiran-playground-template-use/template-index.json'
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _do_sync_templates(cdn_index, local_index, pending):
|
|
508
|
+
"""后台线程:按 folder 全量下载有变化的模板文件
|
|
509
|
+
pending: list of (folder_name, is_new),已在调用方确定好
|
|
510
|
+
"""
|
|
511
|
+
try:
|
|
512
|
+
added = []
|
|
513
|
+
updated = []
|
|
514
|
+
|
|
515
|
+
_sync_log('开始同步,共 {} 个 folder'.format(len(pending)))
|
|
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)
|
|
522
|
+
|
|
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:
|
|
529
|
+
continue
|
|
530
|
+
ok = _download_from_cdn(key, os.path.join(folder_dir, filename))
|
|
531
|
+
_sync_log(' {} {}'.format('✓' if ok else '✗', filename))
|
|
532
|
+
|
|
533
|
+
if is_new:
|
|
534
|
+
added.append(folder_name)
|
|
535
|
+
else:
|
|
536
|
+
updated.append(folder_name)
|
|
537
|
+
|
|
538
|
+
# 每完成一个 folder 就更新进度
|
|
539
|
+
with _sync_lock:
|
|
540
|
+
_sync_state['completed'].append(folder_name)
|
|
541
|
+
|
|
542
|
+
_sync_log('完成。新增: {}, 更新: {}'.format(added, updated))
|
|
543
|
+
with _sync_lock:
|
|
544
|
+
_sync_state['running'] = False
|
|
545
|
+
_sync_state['last_result'] = {'changed': True, 'added': added, 'updated': updated}
|
|
546
|
+
|
|
547
|
+
except Exception as e:
|
|
548
|
+
import traceback
|
|
549
|
+
_sync_log('异常: {}\n{}'.format(e, traceback.format_exc()))
|
|
550
|
+
with _sync_lock:
|
|
551
|
+
_sync_state['running'] = False
|
|
552
|
+
|
|
553
|
+
|
|
478
554
|
# ═══════════════════════════════════════════════════════════════
|
|
479
555
|
# Fork:从 CDN 克隆他人 workspace 到本地
|
|
480
556
|
# ═══════════════════════════════════════════════════════════════
|
|
@@ -749,6 +825,8 @@ class MyclawAPIHandler(BaseHTTPRequestHandler):
|
|
|
749
825
|
return self._handle_file(params)
|
|
750
826
|
elif path == '/api/sync-templates':
|
|
751
827
|
return self._handle_sync_templates()
|
|
828
|
+
elif path == '/api/sync-status':
|
|
829
|
+
return self._handle_sync_status()
|
|
752
830
|
else:
|
|
753
831
|
self._send_json({"error": "not found"}, 404)
|
|
754
832
|
|
|
@@ -1105,22 +1183,17 @@ class MyclawAPIHandler(BaseHTTPRequestHandler):
|
|
|
1105
1183
|
|
|
1106
1184
|
def _handle_sync_templates(self):
|
|
1107
1185
|
"""GET /api/sync-templates
|
|
1108
|
-
|
|
1109
|
-
|
|
1186
|
+
快速比对 CDN index_updated_at 与本地,若一致立即返回 {changed: false}。
|
|
1187
|
+
若有变化,启动后台线程下载,立即返回 {changed: true, syncing: true}。
|
|
1110
1188
|
"""
|
|
1111
|
-
|
|
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
|
-
|
|
1189
|
+
# 快速拉取 CDN index(仅 JSON,通常 <1s)
|
|
1121
1190
|
try:
|
|
1122
|
-
|
|
1123
|
-
|
|
1191
|
+
import ssl
|
|
1192
|
+
ctx = ssl.create_default_context()
|
|
1193
|
+
ctx.check_hostname = False
|
|
1194
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
1195
|
+
url_with_ts = TEMPLATE_CDN_INDEX_URL + '?t=' + str(int(time.time()))
|
|
1196
|
+
with urllib.request.urlopen(url_with_ts, timeout=10, context=ctx) as resp:
|
|
1124
1197
|
cdn_index = json.loads(resp.read().decode('utf-8'))
|
|
1125
1198
|
except Exception as e:
|
|
1126
1199
|
self._send_json({'error': 'CDN index 获取失败: ' + str(e)}, 502)
|
|
@@ -1128,9 +1201,9 @@ class MyclawAPIHandler(BaseHTTPRequestHandler):
|
|
|
1128
1201
|
|
|
1129
1202
|
# 读取本地 index
|
|
1130
1203
|
local_index = {}
|
|
1131
|
-
if os.path.isfile(
|
|
1204
|
+
if os.path.isfile(TEMPLATE_LOCAL_INDEX):
|
|
1132
1205
|
try:
|
|
1133
|
-
with open(
|
|
1206
|
+
with open(TEMPLATE_LOCAL_INDEX, 'r', encoding='utf-8') as f:
|
|
1134
1207
|
local_index = json.load(f)
|
|
1135
1208
|
except Exception:
|
|
1136
1209
|
pass
|
|
@@ -1142,55 +1215,87 @@ class MyclawAPIHandler(BaseHTTPRequestHandler):
|
|
|
1142
1215
|
self._send_json({'changed': False})
|
|
1143
1216
|
return
|
|
1144
1217
|
|
|
1145
|
-
#
|
|
1218
|
+
# 计算需要下载的 folder 列表
|
|
1146
1219
|
cdn_templates = cdn_index.get('templates', {})
|
|
1147
1220
|
local_templates = local_index.get('templates', {})
|
|
1148
|
-
|
|
1149
|
-
updated = []
|
|
1150
|
-
|
|
1221
|
+
pending = [] # list of (folder_name, is_new)
|
|
1151
1222
|
for series, nums in cdn_templates.items():
|
|
1152
1223
|
for num, record in nums.items():
|
|
1153
1224
|
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
1225
|
if not folder_name:
|
|
1158
1226
|
continue
|
|
1159
|
-
|
|
1227
|
+
cdn_ua = record.get('updated_at', '')
|
|
1228
|
+
local_ua = local_templates.get(series, {}).get(num, {}).get('updated_at', '')
|
|
1160
1229
|
is_new = series not in local_templates or num not in local_templates.get(series, {})
|
|
1161
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))
|
|
1162
1233
|
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
|
1165
1244
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
|
1245
|
+
# 已有同步在跑,直接返回当前进度
|
|
1246
|
+
with _sync_lock:
|
|
1247
|
+
if _sync_state['running']:
|
|
1248
|
+
self._send_json({
|
|
1249
|
+
'changed': True, 'syncing': True,
|
|
1250
|
+
'completed': list(_sync_state['completed']),
|
|
1251
|
+
'total': _sync_state['total'],
|
|
1252
|
+
})
|
|
1253
|
+
return
|
|
1254
|
+
|
|
1255
|
+
# 先写 index.json,让前端列表立刻出现
|
|
1185
1256
|
try:
|
|
1186
|
-
os.makedirs(os.path.dirname(
|
|
1187
|
-
with open(
|
|
1257
|
+
os.makedirs(os.path.dirname(TEMPLATE_LOCAL_INDEX), exist_ok=True)
|
|
1258
|
+
with open(TEMPLATE_LOCAL_INDEX, 'w', encoding='utf-8') as f:
|
|
1188
1259
|
json.dump(cdn_index, f, ensure_ascii=False, indent=2)
|
|
1189
1260
|
except Exception as e:
|
|
1190
1261
|
self._send_json({'error': 'index 写入失败: ' + str(e)}, 500)
|
|
1191
1262
|
return
|
|
1192
1263
|
|
|
1193
|
-
|
|
1264
|
+
# 初始化状态,启动后台线程
|
|
1265
|
+
with _sync_lock:
|
|
1266
|
+
_sync_state['running'] = True
|
|
1267
|
+
_sync_state['last_result'] = None
|
|
1268
|
+
_sync_state['completed'] = []
|
|
1269
|
+
_sync_state['total'] = len(pending)
|
|
1270
|
+
|
|
1271
|
+
t = threading.Thread(
|
|
1272
|
+
target=_do_sync_templates,
|
|
1273
|
+
args=(cdn_index, local_index, pending),
|
|
1274
|
+
daemon=True,
|
|
1275
|
+
)
|
|
1276
|
+
t.start()
|
|
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})
|
|
1194
1299
|
|
|
1195
1300
|
def log_message(self, format, *args):
|
|
1196
1301
|
# 静默日志,避免轮询刷屏
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"index_updated_at": "2026-04-
|
|
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-
|
|
136
|
+
"updated_at": "2026-04-22T06:16:45Z"
|
|
137
137
|
}
|
|
138
138
|
},
|
|
139
139
|
"B": {
|