@ninkch/dsh-infinite-canvas 0.19.0 → 0.19.2
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/director/engine.js +984 -39
- package/client.js +594 -89
- package/index.js +68 -4
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -421,6 +421,10 @@ function dbAssetDelete(id) {
|
|
|
421
421
|
const mapped = physicalRel(row.rel) || row.rel;
|
|
422
422
|
const { abs } = safeJoin(root, mapped);
|
|
423
423
|
unlinkSync(abs);
|
|
424
|
+
// 派生预览文件随原文件一起删(不存在则忽略)
|
|
425
|
+
for (const suffix of ['.thumb.jpg', '.poster.jpg']) {
|
|
426
|
+
try { unlinkSync(abs + suffix); } catch { /* 没有就忽略 */ }
|
|
427
|
+
}
|
|
424
428
|
} catch { /* 已不存在视为成功 */ }
|
|
425
429
|
}
|
|
426
430
|
return { ok: true, deleted: true };
|
|
@@ -880,7 +884,8 @@ function dataRootImport() {
|
|
|
880
884
|
}
|
|
881
885
|
try {
|
|
882
886
|
const list = JSON.parse(readFileSync(path.join(dir, 'assets.json'), 'utf8'));
|
|
883
|
-
|
|
887
|
+
// 派生预览文件不是资产:历史 data 里混入也要剔除
|
|
888
|
+
if (Array.isArray(list)) assets[id] = list.filter((a) => !isDerivedMedia(a && a.rel));
|
|
884
889
|
} catch { /* 无资产索引 */ }
|
|
885
890
|
// 多画布:画布文档镜像在 projects/<项目文件夹>/canvases/*.json(updatedAt 新者胜)
|
|
886
891
|
try {
|
|
@@ -975,6 +980,21 @@ function mimeOf(p) {
|
|
|
975
980
|
return m ? (MIME_BY_EXT[m[1].toLowerCase()] || 'application/octet-stream') : 'application/octet-stream';
|
|
976
981
|
}
|
|
977
982
|
|
|
983
|
+
// ============================================================
|
|
984
|
+
// 派生预览文件(系统内部用,**不是资产**)
|
|
985
|
+
// <rel>.thumb.jpg 节点展示用的极小压缩图(图片/截图/视频首帧)
|
|
986
|
+
// <rel>.poster.jpg 视频首帧(历史口径,视作同一类派生文件)
|
|
987
|
+
// 规则:不进资产库 / assets.json / 导入扫描;随原文件删除;不分配资产 ID。
|
|
988
|
+
// ============================================================
|
|
989
|
+
const THUMB_MAX_SIDE = 512; // 缩略图最长边(px)
|
|
990
|
+
function isDerivedMedia(rel) {
|
|
991
|
+
return /\.(?:thumb|poster)\.jpg$/i.test(String(rel || '').trim());
|
|
992
|
+
}
|
|
993
|
+
function thumbRelOf(rel) {
|
|
994
|
+
const t = String(rel || '').trim();
|
|
995
|
+
return t ? t + '.thumb.jpg' : null;
|
|
996
|
+
}
|
|
997
|
+
|
|
978
998
|
/**
|
|
979
999
|
* 弹出系统"选择目录"对话框(阻塞至用户选择/取消)。
|
|
980
1000
|
* Windows = PowerShell + WinForms FolderBrowserDialog(TopMost,可新建文件夹);
|
|
@@ -2775,7 +2795,7 @@ async function updateInstall() {
|
|
|
2775
2795
|
export const inject = ['webServer', 'tools'];
|
|
2776
2796
|
|
|
2777
2797
|
// 仅供诊断 / 自动化测试(不属于 RPC 面,运行时行为不受影响)
|
|
2778
|
-
export const _internals = { updateCheck, updateInstall, npmLatestVersion, cmpVer, findPnpm, getProfileDir: () => PROFILE_DIR, getVersion: () => VERSION, dataRoot, setDataRoot, dataRootMigrate, dataRootImport, ensureProjectFolder, physicalRel, mirrorProjectToDataRoot, writeAssetIndex, removeAssetIndexEntry, readDataRootMeta, removeDataRootProject, sqlDeleteProjectRows, deletedProjectIds, addDeletedProjectId, clearDeletedProjectId, fetchProxyForward, storyboardConsistencyWarnings, readAssetIndexNames, safeJoin, mimeOf, pipeFile, getIcConfigPath: () => CONFIG_PATH, canvasCreateProject, canvasWriteNode, canvasConnect, canvasDeleteProject, canvasDeleteNode, canvasRunNode, canvasNodeStatus, canvasGetBible, canvasUpdateBible, canvasCreateNode, canvasOpen, canvasesList, canvasCreate, canvasRename, canvasDelete, canvasDocId, isCanvasDocId, splitCanvasDocId, normalizeCanvasId, canvasRegistry, sanitizeCanvases, readCanvasDoc, writeCanvasDoc, deleteCanvasDocsForProject, projectsList, projectsPut, projectsGet, projectsDelete, projectsSync, sqliteAvailable: () => !!SQLITE, getDb, closeDb, dbAssetsList, dbAssetsGet, dbAssetNames, dbAssetCreate, dbAssetUpdate, dbAssetDelete, objectAbsFor, ftsReconcile, readDocOrNull, docExists, sqlActive, rpcMethods: () => _rpcMethods, canvasUpdateNode, canvasGenerateNode };
|
|
2798
|
+
export const _internals = { THUMB_MAX_SIDE, thumbRelOf, isDerivedMedia, updateCheck, updateInstall, npmLatestVersion, cmpVer, findPnpm, getProfileDir: () => PROFILE_DIR, getVersion: () => VERSION, dataRoot, setDataRoot, dataRootMigrate, dataRootImport, ensureProjectFolder, physicalRel, mirrorProjectToDataRoot, writeAssetIndex, removeAssetIndexEntry, readDataRootMeta, removeDataRootProject, sqlDeleteProjectRows, deletedProjectIds, addDeletedProjectId, clearDeletedProjectId, fetchProxyForward, storyboardConsistencyWarnings, readAssetIndexNames, safeJoin, mimeOf, pipeFile, getIcConfigPath: () => CONFIG_PATH, canvasCreateProject, canvasWriteNode, canvasConnect, canvasDeleteProject, canvasDeleteNode, canvasRunNode, canvasNodeStatus, canvasGetBible, canvasUpdateBible, canvasCreateNode, canvasOpen, canvasesList, canvasCreate, canvasRename, canvasDelete, canvasDocId, isCanvasDocId, splitCanvasDocId, normalizeCanvasId, canvasRegistry, sanitizeCanvases, readCanvasDoc, writeCanvasDoc, deleteCanvasDocsForProject, projectsList, projectsPut, projectsGet, projectsDelete, projectsSync, sqliteAvailable: () => !!SQLITE, getDb, closeDb, dbAssetsList, dbAssetsGet, dbAssetNames, dbAssetCreate, dbAssetUpdate, dbAssetDelete, objectAbsFor, ftsReconcile, readDocOrNull, docExists, sqlActive, rpcMethods: () => _rpcMethods, canvasUpdateNode, canvasGenerateNode };
|
|
2779
2799
|
|
|
2780
2800
|
function resolveModel(providers, want) {
|
|
2781
2801
|
const byProvider = want && want.provider
|
|
@@ -3926,7 +3946,8 @@ export function apply(ctx) {
|
|
|
3926
3946
|
if (!st.isFile()) { res.statusCode = 404; res.end('not found'); return; }
|
|
3927
3947
|
res.setHeader('content-type', mimeOf(abs));
|
|
3928
3948
|
res.setHeader('accept-ranges', 'bytes');
|
|
3929
|
-
|
|
3949
|
+
// 派生预览文件内容不变 → 长缓存;原文件保持 no-cache
|
|
3950
|
+
res.setHeader('cache-control', isDerivedMedia(rel) ? 'public, max-age=31536000, immutable' : 'no-cache');
|
|
3930
3951
|
// HEAD:只回应元信息(node fetch HEAD 探针用),不读文件、不占解码/内存
|
|
3931
3952
|
if (req.method === 'HEAD') {
|
|
3932
3953
|
res.statusCode = 200;
|
|
@@ -3999,7 +4020,50 @@ export function apply(ctx) {
|
|
|
3999
4020
|
}
|
|
4000
4021
|
},
|
|
4001
4022
|
}),
|
|
4002
|
-
//
|
|
4023
|
+
// 节点展示用缩略图落盘:body 为 jpeg/png,写入 `<rel>.thumb.jpg`。
|
|
4024
|
+
// 缩略图是系统派生文件:不进 assets.json、不出现在资产库;节点只加载它(几十 KB),
|
|
4025
|
+
// 点开看原图 / 点播放才加载原文件。
|
|
4026
|
+
ctx.webServer.register({
|
|
4027
|
+
kind: 'exact',
|
|
4028
|
+
path: `/${NAME}/api/thumb`,
|
|
4029
|
+
async handler(req, res) {
|
|
4030
|
+
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
4031
|
+
try {
|
|
4032
|
+
if (req.method !== 'POST' && req.method !== 'PUT') {
|
|
4033
|
+
res.statusCode = 405;
|
|
4034
|
+
res.end(JSON.stringify({ error: '仅接受 POST/PUT' }));
|
|
4035
|
+
return;
|
|
4036
|
+
}
|
|
4037
|
+
const root = dataRoot();
|
|
4038
|
+
if (!root) throw new Error('未设置数据根目录');
|
|
4039
|
+
const u = new URL(req.url, 'http://localhost');
|
|
4040
|
+
const rawRel = String(u.searchParams.get('rel') || '').trim();
|
|
4041
|
+
if (!rawRel) throw new Error('缺少 rel');
|
|
4042
|
+
if (/\.thumb\.jpg$/i.test(rawRel)) throw new Error('rel 不应包含 thumb 后缀');
|
|
4043
|
+
const ct = String(req.headers['content-type'] || '').toLowerCase();
|
|
4044
|
+
if (ct && !/^image\/(jpeg|png)/.test(ct)) throw new Error('缩略图只接受 image/jpeg 或 image/png');
|
|
4045
|
+
const rel = physicalRel(rawRel) || rawRel;
|
|
4046
|
+
const { abs, clean } = safeJoin(root, rel + '.thumb.jpg');
|
|
4047
|
+
mkdirSync(path.dirname(abs), { recursive: true });
|
|
4048
|
+
const chunks = [];
|
|
4049
|
+
let size = 0;
|
|
4050
|
+
const MAX_THUMB = 2 * 1024 * 1024;
|
|
4051
|
+
for await (const c of req) {
|
|
4052
|
+
size += c.length;
|
|
4053
|
+
if (size > MAX_THUMB) { res.statusCode = 413; res.end(JSON.stringify({ error: 'thumb 过大(上限 2MB)' })); return; }
|
|
4054
|
+
chunks.push(c);
|
|
4055
|
+
}
|
|
4056
|
+
if (!size) throw new Error('thumb 内容为空');
|
|
4057
|
+
await fs.writeFile(abs, Buffer.concat(chunks));
|
|
4058
|
+
res.end(JSON.stringify({ ok: true, rel: clean, size, maxSide: THUMB_MAX_SIDE }));
|
|
4059
|
+
} catch (e) {
|
|
4060
|
+
res.statusCode = 400;
|
|
4061
|
+
res.end(JSON.stringify({ error: String((e && e.message) || e) }));
|
|
4062
|
+
}
|
|
4063
|
+
},
|
|
4064
|
+
}),
|
|
4065
|
+
|
|
4066
|
+
// CAS 对象库读路径(0.18):0.18.0–0.18.4 上传的媒体仍在 objects/ 里,必须继续可读。
|
|
4003
4067
|
// IC-08 起写路径已退役(POST/PUT 一律 405),只有 GET。
|
|
4004
4068
|
ctx.webServer.register({
|
|
4005
4069
|
kind: 'exact',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ninkch/dsh-infinite-canvas",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"description": "Node-based short-drama studio for DeepSeek Harness: script → storyboard → assets → shots → voiced, subtitled final cut on an infinite canvas.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|