This commit is contained in:
tianmo committed 2026-09-10 14:04:53 +08:00
1 parent 9635428f78
commit ef01cdd6ee
6 files changed
+172 -66

No files matched your search

+33 -62
View File
@@ -17,12 +17,11 @@ function log(msg) {
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const discoveryJobs = new Map();
const backgroundJobs = new Map();
const renderJobs = new Map();
const refreshJobs = new Map();
/** 插件代码版本:每次改动递增,用于确认 Eagle 是否加载到最新代码 */
const VERSION = 'v13';
const VERSION = 'v14';
function coordinationKey(src) {
const root = utils.findLibraryRoot(src) || path.dirname(path.dirname(src));
@@ -66,6 +65,7 @@ async function runDiscovery(src, retries, delay) {
assets = utils.discoverAssets(src, {
strictStem: true,
allowCrossBatch: false,
refreshIndex: i > 0,
});
const signature = assetSignature(assets);
if (signature && signature === previousSignature) stableCount += 1;
@@ -143,15 +143,9 @@ module.exports = async (options) => {
}
if (changed && item.id) {
log('复合后缀元数据已规范化: name=' + item.name + ' ext=' + item.ext);
// The item does not enter Eagle's item map until this callback returns.
// Reconcile afterwards, using the correctly named persistent cache.
setTimeout(() => {
try {
const assets = utils.discoverAssets(src, { strictStem: true, allowCrossBatch: true });
if (assetSignature(assets)) refreshRelatedThumbnails(assets, canonical, item.id).catch(e => log(String(e)));
} catch (e) { log('复合后缀补刷失败: ' + String(e)); }
}, 1500);
}
const ext = utils.extOf(path.basename(src));
if (!item.isDeleted && (utils.isAtlasFile(src) || (utils.isSkeletonFile(src) && (ext !== '.json' || utils.classifySkeletonJson(src) === 'spine')))) scheduleBackgroundReconcile(src);
return result;
};
@@ -210,7 +204,6 @@ async function generate({ src, dest, item, deferRefresh }) {
// 找不到完整 Spine 资源链(骨骼 + atlas + atlas 引用的全部贴图)
if (!assets.skeletonPath || !assets.atlasPath || !assetSignature(assets)) {
log('未找到配套资源: ' + src + ' (skeleton=' + !!assets.skeletonPath + ', atlas=' + !!assets.atlasPath + ')');
scheduleBackgroundReconcile(src);
if (assets.atlasPath && await writeAtlasFallback(assets, dest)) {
item.width = 400; item.height = 400;
return resolve(item);
@@ -358,9 +351,9 @@ async function createWebpCopy(sourcePath, key) {
/** 将一份已验证的缩略图主动应用到同一资源链的 atlas/json/skel 条目。 */
async function refreshRelatedThumbnails(assets, thumbnailPath, currentItemId) {
const signature = assetSignature(assets);
if (!signature) return;
if (!signature) return false;
const ids = [...new Set((assets.relatedDataPaths || []).map(itemIdFromAssetPath).concat(currentItemId || []).filter(Boolean))];
if (ids.length === 0) return;
if (ids.length === 0) return true;
const key = signature + '|' + ids.slice().sort().join(',');
if (refreshJobs.has(key)) return refreshJobs.get(key);
const job = (async () => {
@@ -374,8 +367,10 @@ async function refreshRelatedThumbnails(assets, thumbnailPath, currentItemId) {
const failed = results.filter((result) => result.status === 'rejected');
if (failed.length) throw failed[0].reason;
log('统一补刷完成: ' + ids.length + ' 个条目 [' + ids.join(',') + ']');
return true;
} catch (e) {
log('统一补刷失败: ' + ((e && e.message) || String(e)));
return false;
} finally {
if (apiPath !== thumbnailPath) {
try { fs.unlinkSync(apiPath); } catch (e) { /* ignore */ }
@@ -384,7 +379,7 @@ async function refreshRelatedThumbnails(assets, thumbnailPath, currentItemId) {
})();
refreshJobs.set(key, job);
try {
await job;
return await job;
} finally {
setTimeout(() => { if (refreshJobs.get(key) === job) refreshJobs.delete(key); }, 2000);
}
@@ -395,56 +390,32 @@ async function refreshRelatedThumbnails(assets, thumbnailPath, currentItemId) {
* 在链路补齐后渲染一次,并统一刷新此前已生成占位图的所有数据条目。
*/
function scheduleBackgroundReconcile(src) {
const key = coordinationKey(src);
if (backgroundJobs.has(key)) return;
const job = (async () => {
let previousSignature = null;
let stableCount = 0;
// 前几秒快速观察,随后降低频率;总计约两分钟。若更晚补拖第三个文件,
// 新文件自己的缩略图回调会再次触发同一资源链发现,无需常驻轮询十分钟。
for (let i = 0; i < 36; i++) {
const delay = i < 8 ? 1000 : (i < 20 ? 2500 : 5000);
await sleep(delay);
const assets = utils.discoverAssets(src, {
strictStem: true,
allowCrossBatch: true,
});
const signature = assetSignature(assets);
if (signature && signature === previousSignature) stableCount += 1;
else stableCount = signature ? 1 : 0;
previousSignature = signature;
if (stableCount < 3) continue;
const tempPng = path.join(os.tmpdir(), 'spineviewer-reconcile-' + process.pid + '-' + Date.now() + '.png');
try {
const version = utils.detectVersion(assets.skeletonPath);
const runtimeDir = utils.chooseRuntimeDir(version, assets.isBinary);
let wrote = false;
if (runtimeDir) {
try {
const png = await renderThumbnailShared(assets, { ...assets, version, runtimeDir, size: 400 });
fs.writeFileSync(tempPng, png);
wrote = true;
} catch (e) {
wrote = await writeAtlasFallback(assets, tempPng);
}
}
if (wrote) {
await refreshRelatedThumbnails(assets, tempPng);
log('后台识别到完整资源链并补刷: ' + key);
}
} finally {
try { if (fs.existsSync(tempPng)) fs.unlinkSync(tempPng); } catch (e) { /* ignore */ }
}
return;
}
log('后台等待资源链超时: ' + key);
})();
backgroundJobs.set(key, job);
job.catch((e) => log('后台补刷异常: ' + ((e && e.message) || String(e))))
.finally(() => { if (backgroundJobs.get(key) === job) backgroundJobs.delete(key); });
try { require('../js/spine-reconcile-worker.js').enqueue(src); }
catch (error) { log('无法启动后台补刷: ' + String(error)); }
}
// Called by a separate Node worker; Eagle closes the thumbnail window when
// its callback resolves, so timers in that window cannot finish this work.
module.exports.reconcileOnce = async (src, previousSignature) => {
if (!fs.existsSync(src) || utils.isItemDeleted(src)) return { complete: true };
const assets = utils.discoverAssets(src, { strictStem: true, allowCrossBatch: true, refreshIndex: true });
const signature = assetSignature(assets);
if (!signature || signature !== previousSignature) return { signature, complete: false };
const version = utils.detectVersion(assets.skeletonPath);
const runtimeDir = utils.chooseRuntimeDir(version, assets.isBinary);
if (!runtimeDir) return { signature, complete: true };
const temp = path.join(os.tmpdir(), 'spineviewer-reconcile-' + crypto.randomBytes(12).toString('hex') + '.png');
try {
const png = await renderThumbnailShared(assets, { ...assets, version, runtimeDir, size: 400 });
fs.writeFileSync(temp, png);
const complete = await refreshRelatedThumbnails(assets, temp);
return { signature, complete };
} catch (error) {
log('后台补刷重试: ' + String(error));
return { signature, complete: false };
} finally { try { fs.unlinkSync(temp); } catch (error) { if (error.code !== 'ENOENT') log(String(error)); } }
};
/** 普通 JSON 使用中性文件图形,避免错误显示 Spine badge。 */
async function writeNeutralJsonPlaceholder(dest) {
const size = 400;