1.1.1版本审核修复
[spine-utils.js (line 35)](/Users/tianmokeji/Desktop/特效/SpineViewer/SpineViewer/js/spine-utils.js:35) 统一拦截绝对路径、..、Windows 盘符、UNC、URI、控制字符和符号链接越界。 跨素材贴图只能使用已枚举、验证且位于 Eagle 素材目录内的文件。 [spine-render.js (line 62)](/Users/tianmokeji/Desktop/特效/SpineViewer/SpineViewer/js/spine-render.js:62) 已删除未经验证的路径备用拼接。 自动缩略图只写当前回调提供的 dest,不再调用接口改写同组素材。 已删除后台整组补刷进程及 relatedDataPaths。 [viewer/spine.html (line 482)](/Users/tianmokeji/Desktop/特效/SpineViewer/SpineViewer/viewer/spine.html:482) 的手动缩略图只针对当前素材,并核对素材 ID、文件名和扩展名。 当前素材已有自定义缩略图时,会提示替换后无法自动恢复并要求确认。 手动生成的中间文件改存系统临时目录,API 失败时不再直接写 Eagle 素材目录。 八种语言文案及 README 已同步。 处理历史日志遗留问题
This commit is contained in:
1 parent
473a270510
commit
bb73cc9227
36 files changed
+499
-652
No files matched your search
+30
-26
@@ -25,7 +25,7 @@
|
||||
<input type="checkbox" id="backgroundCheck"> <span data-i18n="viewer.background">背景</span>
|
||||
</label>
|
||||
<div class="spacer"></div>
|
||||
<button id="thumbBtn" data-i18n="viewer.thumbnail" data-i18n-title="viewer.thumbnailTitle" title="使用当前帧生成 400×400 缩略图">生成缩略图</button>
|
||||
<button id="thumbBtn" data-i18n="viewer.thumbnail" data-i18n-title="viewer.thumbnailTitle" title="将当前帧设为当前素材的缩略图">设为当前素材缩略图</button>
|
||||
<label id="verLabel" style="color: var(--text-dim);" data-i18n-title="viewer.versionTitle" title="Spine 版本"></label>
|
||||
<button id="reloadBtn" title="重新加载目录" data-i18n-title="viewer.reloadTitle">↻</button>
|
||||
</div>
|
||||
@@ -64,6 +64,8 @@
|
||||
// 使用 Node 集成提供的模块
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const crypto = require('crypto');
|
||||
const utils = require('../js/spine-utils.js');
|
||||
const spineBadge = require('../js/spine-badge.js');
|
||||
const clippingRenderer = require('../js/spine-canvas-clipping.js');
|
||||
@@ -95,10 +97,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 简易日志(写入插件目录 thumbnails.log,便于诊断)
|
||||
// 诊断日志默认关闭;显式开启时只写系统临时目录,绝不写入插件包。
|
||||
function log(msg) {
|
||||
try {
|
||||
const logPath = path.join(__dirname, '..', 'thumbnails.log');
|
||||
if (process.env.SPINEVIEWER_DEBUG_LOG !== '1') return;
|
||||
const logDir = path.join(os.tmpdir(), 'SpineViewer');
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
const logPath = path.join(logDir, 'thumbnails.log');
|
||||
fs.appendFileSync(logPath, '[' + new Date().toISOString() + '] ' + msg + '\n');
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
@@ -477,14 +482,22 @@
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
async function setEagleCustomThumbnail(itemId, thumbnailPath) {
|
||||
if (/\.(atlas\.txt|skel\.bytes)$/i.test(filePath)) {
|
||||
const response = await fetch('http://localhost:41595/api/item/info?id=' + encodeURIComponent(itemId));
|
||||
const info = await response.json();
|
||||
if (!response.ok || info.status !== 'success' || info.data.name + '.' + info.data.ext !== path.basename(filePath))
|
||||
throw new Error(t('viewer.compoundName'));
|
||||
if (info.data.ext === 'txt') throw new Error(t('viewer.txtRegistered'));
|
||||
async function getCurrentEagleItem() {
|
||||
const pathItemId = eagleItemIdFromPath(filePath);
|
||||
const urlItemId = urlParams.get('id');
|
||||
if (!pathItemId || (urlItemId && urlItemId !== pathItemId)) throw new Error(t('viewer.itemMismatch'));
|
||||
const response = await fetch('http://localhost:41595/api/item/info?id=' + encodeURIComponent(pathItemId));
|
||||
let result = null;
|
||||
try { result = await response.json(); } catch (e) { /* use HTTP status below */ }
|
||||
if (!response.ok || !result || result.status !== 'success' || !result.data) {
|
||||
throw new Error(t('viewer.apiHttp', { status: response.status }));
|
||||
}
|
||||
if (result.data.name + '.' + result.data.ext !== path.basename(filePath)) throw new Error(t('viewer.itemMismatch'));
|
||||
if (result.data.ext === 'txt' && /\.atlas\.txt$/i.test(filePath)) throw new Error(t('viewer.txtRegistered'));
|
||||
return { id: pathItemId, data: result.data };
|
||||
}
|
||||
|
||||
async function setEagleCustomThumbnail(itemId, thumbnailPath) {
|
||||
const response = await fetch('http://localhost:41595/api/item/setCustomThumbnail', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -503,8 +516,10 @@
|
||||
if (!skeleton || !renderer) { setStatus(t('viewer.noSkeletonForThumb')); return; }
|
||||
const thumbBtn = document.getElementById('thumbBtn');
|
||||
thumbBtn.disabled = true;
|
||||
setStatus(t('viewer.generating'), Infinity);
|
||||
try {
|
||||
const currentItem = await getCurrentEagleItem();
|
||||
if (currentItem.data.customThumbnail === true && !window.confirm(t('viewer.replaceCustomThumbnailConfirm'))) return;
|
||||
setStatus(t('viewer.generating'), Infinity);
|
||||
// 暂停动画,冻结当前帧:先确保当前动画时间已被应用到骨骼
|
||||
playing = false;
|
||||
updatePlayButton();
|
||||
@@ -547,25 +562,14 @@
|
||||
const isWebp = out.startsWith('data:image/webp');
|
||||
if (!isWebp) out = off.toDataURL('image/png');
|
||||
const buf = Buffer.from(out.split(',')[1], 'base64');
|
||||
const stem = (utils.stemOf && utils.stemOf(path.basename(filePath))) || path.basename(filePath).replace(/\.[^.]*$/, '');
|
||||
const itemId = urlParams.get('id') || eagleItemIdFromPath(filePath);
|
||||
const outPath = path.join(path.dirname(filePath), stem + '_thumbnail.png');
|
||||
const tempPath = path.join(path.dirname(filePath), '.' + stem + '-thumbnail-' + Date.now() + (isWebp ? '.webp' : '.png'));
|
||||
const tempPath = path.join(os.tmpdir(), 'spineviewer-current-' + crypto.randomBytes(12).toString('hex') + (isWebp ? '.webp' : '.png'));
|
||||
try {
|
||||
if (!itemId) throw new Error(t('viewer.noItemId'));
|
||||
fs.writeFileSync(tempPath, buf);
|
||||
await setEagleCustomThumbnail(itemId, tempPath);
|
||||
await setEagleCustomThumbnail(currentItem.id, tempPath);
|
||||
setStatus(t('viewer.thumbUpdated', { format: isWebp ? 'WebP' : 'PNG', size: (buf.length / 1024).toFixed(1) }), 3500);
|
||||
log('通过 Eagle API 更新缩略图: item=' + itemId + ' (' + buf.length + 'B)');
|
||||
log('用户主动更新当前素材缩略图: item=' + currentItem.id + ' (' + buf.length + 'B)');
|
||||
} catch (e) {
|
||||
// 兼容旧版 Eagle:保留原先的磁盘写入方案,但明确提示可能需要手动刷新列表。
|
||||
try {
|
||||
fs.writeFileSync(outPath, buf);
|
||||
setStatus(t('viewer.thumbWrittenNoRefresh', { error: (e && e.message) || String(e) }), 6000);
|
||||
log('Eagle API 更新失败,已回退写入: ' + ((e && e.message) || String(e)));
|
||||
} catch (e2) {
|
||||
setStatus(t('viewer.thumbnailFailed', { error: (e2 && e2.message) || String(e2) }), 6000);
|
||||
}
|
||||
setStatus(t('viewer.thumbnailFailed', { error: (e && e.message) || String(e) }), 6000);
|
||||
} finally {
|
||||
try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user