sa2kit 1.2.0 → 1.3.0

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.
Files changed (49) hide show
  1. package/dist/{UniversalFileService-CEZRJ87g.d.mts → UniversalFileService-BuHN-jrR.d.ts} +47 -259
  2. package/dist/{UniversalFileService-CEZRJ87g.d.ts → UniversalFileService-CGGzYeeF.d.mts} +47 -259
  3. package/dist/{chunk-3XG5OHFD.mjs → chunk-CIVO4R6N.mjs} +2 -2
  4. package/dist/{chunk-3XG5OHFD.mjs.map → chunk-CIVO4R6N.mjs.map} +1 -1
  5. package/dist/chunk-EV6BCVOQ.mjs +204 -0
  6. package/dist/chunk-EV6BCVOQ.mjs.map +1 -0
  7. package/dist/chunk-W35VTQAW.js +211 -0
  8. package/dist/chunk-W35VTQAW.js.map +1 -0
  9. package/dist/{chunk-HWJ34NL6.js → chunk-ZRAW3HXA.js} +2 -2
  10. package/dist/{chunk-HWJ34NL6.js.map → chunk-ZRAW3HXA.js.map} +1 -1
  11. package/dist/drizzle-schema-BNhqj2AZ.d.mts +1114 -0
  12. package/dist/drizzle-schema-BNhqj2AZ.d.ts +1114 -0
  13. package/dist/mmd/admin/index.d.mts +487 -0
  14. package/dist/mmd/admin/index.d.ts +487 -0
  15. package/dist/mmd/admin/index.js +871 -0
  16. package/dist/mmd/admin/index.js.map +1 -0
  17. package/dist/mmd/admin/index.mjs +822 -0
  18. package/dist/mmd/admin/index.mjs.map +1 -0
  19. package/dist/mmd/index.d.mts +4 -193
  20. package/dist/mmd/index.d.ts +4 -193
  21. package/dist/mmd/server/index.d.mts +138 -0
  22. package/dist/mmd/server/index.d.ts +138 -0
  23. package/dist/mmd/server/index.js +245 -0
  24. package/dist/mmd/server/index.js.map +1 -0
  25. package/dist/mmd/server/index.mjs +207 -0
  26. package/dist/mmd/server/index.mjs.map +1 -0
  27. package/dist/testYourself/index.d.mts +145 -0
  28. package/dist/testYourself/index.d.ts +145 -0
  29. package/dist/testYourself/index.js +1004 -0
  30. package/dist/testYourself/index.js.map +1 -0
  31. package/dist/testYourself/index.mjs +993 -0
  32. package/dist/testYourself/index.mjs.map +1 -0
  33. package/dist/types-Bc_p-zAR.d.mts +194 -0
  34. package/dist/types-Bc_p-zAR.d.ts +194 -0
  35. package/dist/types-CK4We_aI.d.mts +270 -0
  36. package/dist/types-CK4We_aI.d.ts +270 -0
  37. package/dist/universalFile/index.d.mts +3 -2
  38. package/dist/universalFile/index.d.ts +3 -2
  39. package/dist/universalFile/index.js +48 -10
  40. package/dist/universalFile/index.js.map +1 -1
  41. package/dist/universalFile/index.mjs +43 -5
  42. package/dist/universalFile/index.mjs.map +1 -1
  43. package/dist/universalFile/server/index.d.mts +3 -2
  44. package/dist/universalFile/server/index.d.ts +3 -2
  45. package/dist/universalFile/server/index.js +239 -7
  46. package/dist/universalFile/server/index.js.map +1 -1
  47. package/dist/universalFile/server/index.mjs +234 -2
  48. package/dist/universalFile/server/index.mjs.map +1 -1
  49. package/package.json +19 -1
@@ -0,0 +1,138 @@
1
+ export { M as MmdPlaylist, f as MmdPlaylistNode, j as MmdPresetItem, h as MmdResourceOption, N as NewMmdPlaylist, g as NewMmdPlaylistNode, k as NewMmdPresetItem, i as NewMmdResourceOption, a as mmdPlaylistNodes, e as mmdPlaylistNodesRelations, m as mmdPlaylists, d as mmdPlaylistsRelations, c as mmdPresetItems, b as mmdResourceOptions } from '../../drizzle-schema-BNhqj2AZ.mjs';
2
+ import { M as MMDPlaylistConfig } from '../../types-Bc_p-zAR.mjs';
3
+ import { U as UniversalFileService } from '../../UniversalFileService-CGGzYeeF.mjs';
4
+ import { F as FileMetadata } from '../../types-CK4We_aI.mjs';
5
+ import 'drizzle-orm';
6
+ import 'drizzle-orm/pg-core';
7
+ import 'three';
8
+ import 'events';
9
+
10
+ type SupportedModelFormat = 'pmx' | 'pmd';
11
+ interface ProcessMmdModelArchiveOptions {
12
+ /**
13
+ * 绝对路径,指向模型解压根目录,例如 /app/uploads/mmd/models
14
+ */
15
+ storageRoot: string;
16
+ /**
17
+ * 对外暴露的公共路径前缀,例如 /uploads/mmd/models
18
+ * 默认与 storageRoot 中的叶子目录一致
19
+ */
20
+ publicRoot?: string;
21
+ /**
22
+ * 自定义文件夹名称,默认使用随机 UUID
23
+ */
24
+ folderName?: string;
25
+ }
26
+ interface ProcessMmdModelArchiveResult {
27
+ /** 解压出来的目录(绝对路径) */
28
+ directory: string;
29
+ /** 与 storageRoot 相对的目录名 */
30
+ relativeDirectory: string;
31
+ /** 模型文件相对目录(包含子目录) */
32
+ modelRelativePath: string;
33
+ /** 可供前端使用的模型 URL */
34
+ modelUrl: string;
35
+ /** 模型格式 */
36
+ format: SupportedModelFormat;
37
+ /** 解压得到的文件数量 */
38
+ filesExtracted: number;
39
+ }
40
+ declare const MMD_MODEL_ARCHIVE_MIME_TYPES: readonly ["application/zip", "application/x-zip-compressed", "multipart/x-zip"];
41
+ /**
42
+ * 解析上传的 MMD 模型压缩包,保留目录结构并返回模型路径
43
+ */
44
+ declare function processMmdModelArchive(buffer: Buffer, options: ProcessMmdModelArchiveOptions): Promise<ProcessMmdModelArchiveResult>;
45
+
46
+ interface PlaylistModelSource {
47
+ id: string | number;
48
+ name: string;
49
+ filePath: string;
50
+ thumbnailPath?: string | null;
51
+ }
52
+ interface PlaylistMotionSource {
53
+ id: string | number;
54
+ name?: string;
55
+ filePath: string;
56
+ }
57
+ interface BuildMmdPlaylistOptions {
58
+ playlistId: string;
59
+ playlistName?: string;
60
+ models: PlaylistModelSource[];
61
+ motions?: PlaylistMotionSource[];
62
+ limit?: number;
63
+ loop?: boolean;
64
+ preload?: 'none' | 'next' | 'all';
65
+ autoPlay?: boolean;
66
+ nodeDuration?: number;
67
+ normalizeUrl?: (pathOrUrl: string) => string;
68
+ }
69
+ /**
70
+ * 根据数据库中的模型/动作记录快速构建 MMDPlaylistConfig
71
+ */
72
+ declare function buildMmdPlaylistFromSources(options: BuildMmdPlaylistOptions): MMDPlaylistConfig;
73
+
74
+ /**
75
+ * MMD 资源上传辅助函数
76
+ *
77
+ * 整合 UniversalFileService 用于 MMD 资源上传
78
+ */
79
+
80
+ declare const MMD_SUPPORTED_TYPES: {
81
+ model: ("application/zip" | "application/x-zip-compressed" | "multipart/x-zip")[];
82
+ animation: string[];
83
+ audio: string[];
84
+ };
85
+ declare const MMD_FILE_EXTENSIONS: {
86
+ model: string[];
87
+ animation: string[];
88
+ audio: string[];
89
+ };
90
+ interface MmdUploadOptions {
91
+ file: File;
92
+ resourceType: 'model' | 'animation' | 'audio';
93
+ name: string;
94
+ description?: string;
95
+ userId: string;
96
+ }
97
+ interface MmdUploadResult {
98
+ id: string;
99
+ name: string;
100
+ url: string;
101
+ filePath: string;
102
+ fileSize: number;
103
+ type: string;
104
+ format: string;
105
+ uploadTime: Date;
106
+ metadata?: FileMetadata;
107
+ }
108
+ /**
109
+ * 使用 UniversalFileService 上传 MMD 资源
110
+ *
111
+ * @param fileService - UniversalFileService 实例
112
+ * @param options - 上传选项
113
+ * @returns 上传结果
114
+ */
115
+ declare function uploadMmdResource(fileService: UniversalFileService, options: MmdUploadOptions): Promise<MmdUploadResult>;
116
+ /**
117
+ * 上传 MMD 模型 (ZIP 压缩包)
118
+ *
119
+ * @param fileService - UniversalFileService 实例
120
+ * @param options - 上传选项
121
+ * @returns 上传结果,包含解压后的模型路径
122
+ */
123
+ declare function uploadMmdModel(fileService: UniversalFileService, options: MmdUploadOptions & {
124
+ storageRoot: string;
125
+ publicRoot: string;
126
+ }): Promise<MmdUploadResult & {
127
+ extractedPath?: string;
128
+ }>;
129
+ /**
130
+ * 批量上传 MMD 资源
131
+ *
132
+ * @param fileService - UniversalFileService 实例
133
+ * @param uploads - 上传选项数组
134
+ * @returns 上传结果数组
135
+ */
136
+ declare function batchUploadMmdResources(fileService: UniversalFileService, uploads: MmdUploadOptions[]): Promise<MmdUploadResult[]>;
137
+
138
+ export { type BuildMmdPlaylistOptions, MMD_FILE_EXTENSIONS, MMD_MODEL_ARCHIVE_MIME_TYPES, MMD_SUPPORTED_TYPES, type MmdUploadOptions, type MmdUploadResult, type PlaylistModelSource, type PlaylistMotionSource, type ProcessMmdModelArchiveOptions, type ProcessMmdModelArchiveResult, type SupportedModelFormat, batchUploadMmdResources, buildMmdPlaylistFromSources, processMmdModelArchive, uploadMmdModel, uploadMmdResource };
@@ -0,0 +1,138 @@
1
+ export { M as MmdPlaylist, f as MmdPlaylistNode, j as MmdPresetItem, h as MmdResourceOption, N as NewMmdPlaylist, g as NewMmdPlaylistNode, k as NewMmdPresetItem, i as NewMmdResourceOption, a as mmdPlaylistNodes, e as mmdPlaylistNodesRelations, m as mmdPlaylists, d as mmdPlaylistsRelations, c as mmdPresetItems, b as mmdResourceOptions } from '../../drizzle-schema-BNhqj2AZ.js';
2
+ import { M as MMDPlaylistConfig } from '../../types-Bc_p-zAR.js';
3
+ import { U as UniversalFileService } from '../../UniversalFileService-BuHN-jrR.js';
4
+ import { F as FileMetadata } from '../../types-CK4We_aI.js';
5
+ import 'drizzle-orm';
6
+ import 'drizzle-orm/pg-core';
7
+ import 'three';
8
+ import 'events';
9
+
10
+ type SupportedModelFormat = 'pmx' | 'pmd';
11
+ interface ProcessMmdModelArchiveOptions {
12
+ /**
13
+ * 绝对路径,指向模型解压根目录,例如 /app/uploads/mmd/models
14
+ */
15
+ storageRoot: string;
16
+ /**
17
+ * 对外暴露的公共路径前缀,例如 /uploads/mmd/models
18
+ * 默认与 storageRoot 中的叶子目录一致
19
+ */
20
+ publicRoot?: string;
21
+ /**
22
+ * 自定义文件夹名称,默认使用随机 UUID
23
+ */
24
+ folderName?: string;
25
+ }
26
+ interface ProcessMmdModelArchiveResult {
27
+ /** 解压出来的目录(绝对路径) */
28
+ directory: string;
29
+ /** 与 storageRoot 相对的目录名 */
30
+ relativeDirectory: string;
31
+ /** 模型文件相对目录(包含子目录) */
32
+ modelRelativePath: string;
33
+ /** 可供前端使用的模型 URL */
34
+ modelUrl: string;
35
+ /** 模型格式 */
36
+ format: SupportedModelFormat;
37
+ /** 解压得到的文件数量 */
38
+ filesExtracted: number;
39
+ }
40
+ declare const MMD_MODEL_ARCHIVE_MIME_TYPES: readonly ["application/zip", "application/x-zip-compressed", "multipart/x-zip"];
41
+ /**
42
+ * 解析上传的 MMD 模型压缩包,保留目录结构并返回模型路径
43
+ */
44
+ declare function processMmdModelArchive(buffer: Buffer, options: ProcessMmdModelArchiveOptions): Promise<ProcessMmdModelArchiveResult>;
45
+
46
+ interface PlaylistModelSource {
47
+ id: string | number;
48
+ name: string;
49
+ filePath: string;
50
+ thumbnailPath?: string | null;
51
+ }
52
+ interface PlaylistMotionSource {
53
+ id: string | number;
54
+ name?: string;
55
+ filePath: string;
56
+ }
57
+ interface BuildMmdPlaylistOptions {
58
+ playlistId: string;
59
+ playlistName?: string;
60
+ models: PlaylistModelSource[];
61
+ motions?: PlaylistMotionSource[];
62
+ limit?: number;
63
+ loop?: boolean;
64
+ preload?: 'none' | 'next' | 'all';
65
+ autoPlay?: boolean;
66
+ nodeDuration?: number;
67
+ normalizeUrl?: (pathOrUrl: string) => string;
68
+ }
69
+ /**
70
+ * 根据数据库中的模型/动作记录快速构建 MMDPlaylistConfig
71
+ */
72
+ declare function buildMmdPlaylistFromSources(options: BuildMmdPlaylistOptions): MMDPlaylistConfig;
73
+
74
+ /**
75
+ * MMD 资源上传辅助函数
76
+ *
77
+ * 整合 UniversalFileService 用于 MMD 资源上传
78
+ */
79
+
80
+ declare const MMD_SUPPORTED_TYPES: {
81
+ model: ("application/zip" | "application/x-zip-compressed" | "multipart/x-zip")[];
82
+ animation: string[];
83
+ audio: string[];
84
+ };
85
+ declare const MMD_FILE_EXTENSIONS: {
86
+ model: string[];
87
+ animation: string[];
88
+ audio: string[];
89
+ };
90
+ interface MmdUploadOptions {
91
+ file: File;
92
+ resourceType: 'model' | 'animation' | 'audio';
93
+ name: string;
94
+ description?: string;
95
+ userId: string;
96
+ }
97
+ interface MmdUploadResult {
98
+ id: string;
99
+ name: string;
100
+ url: string;
101
+ filePath: string;
102
+ fileSize: number;
103
+ type: string;
104
+ format: string;
105
+ uploadTime: Date;
106
+ metadata?: FileMetadata;
107
+ }
108
+ /**
109
+ * 使用 UniversalFileService 上传 MMD 资源
110
+ *
111
+ * @param fileService - UniversalFileService 实例
112
+ * @param options - 上传选项
113
+ * @returns 上传结果
114
+ */
115
+ declare function uploadMmdResource(fileService: UniversalFileService, options: MmdUploadOptions): Promise<MmdUploadResult>;
116
+ /**
117
+ * 上传 MMD 模型 (ZIP 压缩包)
118
+ *
119
+ * @param fileService - UniversalFileService 实例
120
+ * @param options - 上传选项
121
+ * @returns 上传结果,包含解压后的模型路径
122
+ */
123
+ declare function uploadMmdModel(fileService: UniversalFileService, options: MmdUploadOptions & {
124
+ storageRoot: string;
125
+ publicRoot: string;
126
+ }): Promise<MmdUploadResult & {
127
+ extractedPath?: string;
128
+ }>;
129
+ /**
130
+ * 批量上传 MMD 资源
131
+ *
132
+ * @param fileService - UniversalFileService 实例
133
+ * @param uploads - 上传选项数组
134
+ * @returns 上传结果数组
135
+ */
136
+ declare function batchUploadMmdResources(fileService: UniversalFileService, uploads: MmdUploadOptions[]): Promise<MmdUploadResult[]>;
137
+
138
+ export { type BuildMmdPlaylistOptions, MMD_FILE_EXTENSIONS, MMD_MODEL_ARCHIVE_MIME_TYPES, MMD_SUPPORTED_TYPES, type MmdUploadOptions, type MmdUploadResult, type PlaylistModelSource, type PlaylistMotionSource, type ProcessMmdModelArchiveOptions, type ProcessMmdModelArchiveResult, type SupportedModelFormat, batchUploadMmdResources, buildMmdPlaylistFromSources, processMmdModelArchive, uploadMmdModel, uploadMmdResource };
@@ -0,0 +1,245 @@
1
+ 'use strict';
2
+
3
+ var chunkW35VTQAW_js = require('../../chunk-W35VTQAW.js');
4
+ require('../../chunk-DGUM43GV.js');
5
+ var AdmZip = require('adm-zip');
6
+ var crypto = require('crypto');
7
+ var promises = require('fs/promises');
8
+ var path = require('path');
9
+
10
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
+
12
+ var AdmZip__default = /*#__PURE__*/_interopDefault(AdmZip);
13
+ var path__default = /*#__PURE__*/_interopDefault(path);
14
+
15
+ var MMD_MODEL_ARCHIVE_MIME_TYPES = [
16
+ "application/zip",
17
+ "application/x-zip-compressed",
18
+ "multipart/x-zip"
19
+ ];
20
+ async function processMmdModelArchive(buffer, options) {
21
+ const folderName = options.folderName ?? crypto.randomUUID();
22
+ const targetDir = path__default.default.join(options.storageRoot, folderName);
23
+ await promises.mkdir(targetDir, { recursive: true });
24
+ const zip = new AdmZip__default.default(buffer);
25
+ const entries = zip.getEntries();
26
+ if (!entries.length) {
27
+ await promises.rm(targetDir, { recursive: true, force: true });
28
+ throw new Error("\u538B\u7F29\u5305\u4E3A\u7A7A\uFF0C\u8BF7\u68C0\u67E5\u4E0A\u4F20\u6587\u4EF6\u5185\u5BB9");
29
+ }
30
+ let modelRelativePath = null;
31
+ let filesExtracted = 0;
32
+ for (const entry of entries) {
33
+ if (!entry.entryName || entry.entryName.startsWith("__MACOSX") || entry.entryName.endsWith(".DS_Store")) {
34
+ continue;
35
+ }
36
+ const safeRelativePath = sanitizeEntryPath(entry.entryName);
37
+ if (!safeRelativePath) {
38
+ continue;
39
+ }
40
+ const destinationPath = path__default.default.join(targetDir, safeRelativePath);
41
+ if (entry.isDirectory) {
42
+ await promises.mkdir(destinationPath, { recursive: true });
43
+ continue;
44
+ }
45
+ await promises.mkdir(path__default.default.dirname(destinationPath), { recursive: true });
46
+ await promises.writeFile(destinationPath, entry.getData());
47
+ filesExtracted += 1;
48
+ const entryExt = path__default.default.extname(safeRelativePath).toLowerCase();
49
+ if (!modelRelativePath && (entryExt === ".pmx" || entryExt === ".pmd")) {
50
+ modelRelativePath = safeRelativePath.split(path__default.default.sep).join("/");
51
+ }
52
+ }
53
+ if (!modelRelativePath) {
54
+ await promises.rm(targetDir, { recursive: true, force: true });
55
+ throw new Error("\u538B\u7F29\u5305\u4E2D\u672A\u627E\u5230 PMX/PMD \u6A21\u578B\u6587\u4EF6\uFF0C\u8BF7\u786E\u8BA4\u76EE\u5F55\u7ED3\u6784\u662F\u5426\u6B63\u786E");
56
+ }
57
+ const format = path__default.default.extname(modelRelativePath).slice(1).toLowerCase();
58
+ const publicRoot = options.publicRoot ?? `/uploads/mmd/models`;
59
+ const modelUrl = joinPublicPath(publicRoot, folderName, modelRelativePath);
60
+ return {
61
+ directory: targetDir,
62
+ relativeDirectory: folderName,
63
+ modelRelativePath,
64
+ modelUrl,
65
+ format,
66
+ filesExtracted
67
+ };
68
+ }
69
+ function sanitizeEntryPath(entryName) {
70
+ const normalized = path__default.default.normalize(entryName).replace(/^(\.\.(\/|\\|$))+/, "");
71
+ if (!normalized || normalized === "." || normalized.startsWith("..") || path__default.default.isAbsolute(normalized)) {
72
+ return null;
73
+ }
74
+ return normalized;
75
+ }
76
+ function joinPublicPath(...segments) {
77
+ return segments.map((segment) => segment.replace(/\/+/g, "/").replace(/^\//, "").replace(/\/$/, "")).filter(Boolean).join("/").replace(/\/{2,}/g, "/").replace(/^/, "/");
78
+ }
79
+
80
+ // src/mmd/server/playlistBuilder.ts
81
+ var defaultNormalizer = (value) => {
82
+ if (!value) return "";
83
+ if (value.startsWith("http://") || value.startsWith("https://")) {
84
+ return value;
85
+ }
86
+ return value.startsWith("/") ? value : `/${value}`;
87
+ };
88
+ function buildMmdPlaylistFromSources(options) {
89
+ if (!options.models.length) {
90
+ throw new Error("\u6784\u5EFA MMD \u64AD\u653E\u5217\u8868\u5931\u8D25\uFF1Amodels \u4E3A\u7A7A");
91
+ }
92
+ const limit = Math.max(1, Math.min(options.limit ?? options.models.length, options.models.length));
93
+ const normalizeUrl = options.normalizeUrl ?? defaultNormalizer;
94
+ const motions = options.motions ?? [];
95
+ const hasMotions = motions.length > 0;
96
+ const duration = options.nodeDuration ?? 30;
97
+ const nodes = options.models.slice(0, limit).map((model, index) => {
98
+ const motion = hasMotions ? motions[index % motions.length] : void 0;
99
+ return {
100
+ id: String(model.id ?? index),
101
+ name: model.name,
102
+ loop: options.loop ?? true,
103
+ duration,
104
+ thumbnail: model.thumbnailPath ? normalizeUrl(model.thumbnailPath) : void 0,
105
+ resources: {
106
+ modelPath: normalizeUrl(model.filePath),
107
+ motionPath: motion ? normalizeUrl(motion.filePath) : void 0,
108
+ cameraPath: void 0,
109
+ audioPath: void 0,
110
+ stageModelPath: void 0,
111
+ additionalMotions: void 0
112
+ }
113
+ };
114
+ });
115
+ return {
116
+ id: options.playlistId,
117
+ name: options.playlistName ?? `MMD \u64AD\u653E\u5217\u8868 - ${options.playlistId}`,
118
+ nodes,
119
+ loop: options.loop ?? true,
120
+ preload: options.preload ?? "next",
121
+ autoPlay: options.autoPlay ?? true
122
+ };
123
+ }
124
+
125
+ // src/mmd/server/mmdUpload.ts
126
+ var MMD_SUPPORTED_TYPES = {
127
+ model: [...MMD_MODEL_ARCHIVE_MIME_TYPES],
128
+ animation: ["application/octet-stream", "animation/vmd"],
129
+ audio: ["audio/wav", "audio/mp3", "audio/mpeg", "audio/ogg"]
130
+ };
131
+ var MMD_FILE_EXTENSIONS = {
132
+ model: [".zip"],
133
+ animation: [".vmd"],
134
+ audio: [".wav", ".mp3", ".ogg"]
135
+ };
136
+ async function uploadMmdResource(fileService, options) {
137
+ const { file, resourceType, name, description, userId } = options;
138
+ const ext = `.${file.name.split(".").pop()?.toLowerCase()}`;
139
+ const allowedExtensions = MMD_FILE_EXTENSIONS[resourceType];
140
+ if (!allowedExtensions.includes(ext)) {
141
+ throw new Error(
142
+ `\u4E0D\u652F\u6301\u7684\u6587\u4EF6\u6269\u5C55\u540D: ${ext}\u3002\u652F\u6301\u7684\u683C\u5F0F: ${allowedExtensions.join(", ")}`
143
+ );
144
+ }
145
+ const moduleId = `mmd-${resourceType}s`;
146
+ const metadata = await fileService.uploadFile({
147
+ file,
148
+ moduleId,
149
+ businessId: "default",
150
+ permission: "public",
151
+ metadata: {
152
+ uploadedBy: userId,
153
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
154
+ originalFileName: file.name,
155
+ resourceType,
156
+ name,
157
+ description: description || ""
158
+ },
159
+ needsProcessing: false
160
+ });
161
+ const format = ext.slice(1).toUpperCase();
162
+ const fileUrl = metadata.cdnUrl || metadata.storagePath;
163
+ return {
164
+ id: metadata.id,
165
+ name,
166
+ url: fileUrl,
167
+ filePath: metadata.storagePath,
168
+ fileSize: metadata.size,
169
+ type: resourceType,
170
+ format,
171
+ uploadTime: metadata.uploadTime,
172
+ metadata
173
+ };
174
+ }
175
+ async function uploadMmdModel(fileService, options) {
176
+ const { file, storageRoot, publicRoot } = options;
177
+ const baseResult = await uploadMmdResource(fileService, options);
178
+ try {
179
+ const fileBuffer = Buffer.from(await file.arrayBuffer());
180
+ const archiveResult = await processMmdModelArchive(fileBuffer, {
181
+ storageRoot,
182
+ publicRoot,
183
+ folderName: baseResult.id
184
+ });
185
+ return {
186
+ ...baseResult,
187
+ url: archiveResult.modelUrl,
188
+ filePath: archiveResult.modelUrl,
189
+ format: archiveResult.format.toUpperCase(),
190
+ extractedPath: archiveResult.modelUrl
191
+ };
192
+ } catch (extractError) {
193
+ console.error("\u6A21\u578B\u538B\u7F29\u5305\u5904\u7406\u5931\u8D25:", extractError);
194
+ throw new Error(
195
+ extractError instanceof Error ? extractError.message : "\u6A21\u578B\u538B\u7F29\u5305\u5904\u7406\u5931\u8D25"
196
+ );
197
+ }
198
+ }
199
+ async function batchUploadMmdResources(fileService, uploads) {
200
+ const results = [];
201
+ for (const uploadOptions of uploads) {
202
+ try {
203
+ const result = await uploadMmdResource(fileService, uploadOptions);
204
+ results.push(result);
205
+ } catch (error) {
206
+ console.error(`\u4E0A\u4F20\u5931\u8D25: ${uploadOptions.file.name}`, error);
207
+ }
208
+ }
209
+ return results;
210
+ }
211
+
212
+ Object.defineProperty(exports, "mmdPlaylistNodes", {
213
+ enumerable: true,
214
+ get: function () { return chunkW35VTQAW_js.mmdPlaylistNodes; }
215
+ });
216
+ Object.defineProperty(exports, "mmdPlaylistNodesRelations", {
217
+ enumerable: true,
218
+ get: function () { return chunkW35VTQAW_js.mmdPlaylistNodesRelations; }
219
+ });
220
+ Object.defineProperty(exports, "mmdPlaylists", {
221
+ enumerable: true,
222
+ get: function () { return chunkW35VTQAW_js.mmdPlaylists; }
223
+ });
224
+ Object.defineProperty(exports, "mmdPlaylistsRelations", {
225
+ enumerable: true,
226
+ get: function () { return chunkW35VTQAW_js.mmdPlaylistsRelations; }
227
+ });
228
+ Object.defineProperty(exports, "mmdPresetItems", {
229
+ enumerable: true,
230
+ get: function () { return chunkW35VTQAW_js.mmdPresetItems; }
231
+ });
232
+ Object.defineProperty(exports, "mmdResourceOptions", {
233
+ enumerable: true,
234
+ get: function () { return chunkW35VTQAW_js.mmdResourceOptions; }
235
+ });
236
+ exports.MMD_FILE_EXTENSIONS = MMD_FILE_EXTENSIONS;
237
+ exports.MMD_MODEL_ARCHIVE_MIME_TYPES = MMD_MODEL_ARCHIVE_MIME_TYPES;
238
+ exports.MMD_SUPPORTED_TYPES = MMD_SUPPORTED_TYPES;
239
+ exports.batchUploadMmdResources = batchUploadMmdResources;
240
+ exports.buildMmdPlaylistFromSources = buildMmdPlaylistFromSources;
241
+ exports.processMmdModelArchive = processMmdModelArchive;
242
+ exports.uploadMmdModel = uploadMmdModel;
243
+ exports.uploadMmdResource = uploadMmdResource;
244
+ //# sourceMappingURL=index.js.map
245
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/mmd/server/modelArchive.ts","../../../src/mmd/server/playlistBuilder.ts","../../../src/mmd/server/mmdUpload.ts"],"names":["randomUUID","path","mkdir","AdmZip","rm","writeFile"],"mappings":"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","file":"index.js","sourcesContent":["import AdmZip from 'adm-zip';\nimport { randomUUID } from 'crypto';\nimport { mkdir, rm, writeFile } from 'fs/promises';\nimport path from 'path';\n\nexport type SupportedModelFormat = 'pmx' | 'pmd';\n\nexport interface ProcessMmdModelArchiveOptions {\n /**\n * 绝对路径,指向模型解压根目录,例如 /app/uploads/mmd/models\n */\n storageRoot: string;\n /**\n * 对外暴露的公共路径前缀,例如 /uploads/mmd/models\n * 默认与 storageRoot 中的叶子目录一致\n */\n publicRoot?: string;\n /**\n * 自定义文件夹名称,默认使用随机 UUID\n */\n folderName?: string;\n}\n\nexport interface ProcessMmdModelArchiveResult {\n /** 解压出来的目录(绝对路径) */\n directory: string;\n /** 与 storageRoot 相对的目录名 */\n relativeDirectory: string;\n /** 模型文件相对目录(包含子目录) */\n modelRelativePath: string;\n /** 可供前端使用的模型 URL */\n modelUrl: string;\n /** 模型格式 */\n format: SupportedModelFormat;\n /** 解压得到的文件数量 */\n filesExtracted: number;\n}\n\nexport const MMD_MODEL_ARCHIVE_MIME_TYPES = [\n 'application/zip',\n 'application/x-zip-compressed',\n 'multipart/x-zip',\n] as const;\n\n/**\n * 解析上传的 MMD 模型压缩包,保留目录结构并返回模型路径\n */\nexport async function processMmdModelArchive(\n buffer: Buffer,\n options: ProcessMmdModelArchiveOptions,\n): Promise<ProcessMmdModelArchiveResult> {\n const folderName = options.folderName ?? randomUUID();\n const targetDir = path.join(options.storageRoot, folderName);\n await mkdir(targetDir, { recursive: true });\n\n const zip = new AdmZip(buffer);\n const entries = zip.getEntries();\n if (!entries.length) {\n await rm(targetDir, { recursive: true, force: true });\n throw new Error('压缩包为空,请检查上传文件内容');\n }\n\n let modelRelativePath: string | null = null;\n let filesExtracted = 0;\n\n for (const entry of entries) {\n if (!entry.entryName || entry.entryName.startsWith('__MACOSX') || entry.entryName.endsWith('.DS_Store')) {\n continue;\n }\n\n const safeRelativePath = sanitizeEntryPath(entry.entryName);\n if (!safeRelativePath) {\n continue;\n }\n\n const destinationPath = path.join(targetDir, safeRelativePath);\n\n if (entry.isDirectory) {\n await mkdir(destinationPath, { recursive: true });\n continue;\n }\n\n await mkdir(path.dirname(destinationPath), { recursive: true });\n await writeFile(destinationPath, entry.getData());\n filesExtracted += 1;\n\n const entryExt = path.extname(safeRelativePath).toLowerCase();\n if (!modelRelativePath && (entryExt === '.pmx' || entryExt === '.pmd')) {\n modelRelativePath = safeRelativePath.split(path.sep).join('/');\n }\n }\n\n if (!modelRelativePath) {\n await rm(targetDir, { recursive: true, force: true });\n throw new Error('压缩包中未找到 PMX/PMD 模型文件,请确认目录结构是否正确');\n }\n\n const format = path.extname(modelRelativePath).slice(1).toLowerCase() as SupportedModelFormat;\n const publicRoot = options.publicRoot ?? `/uploads/mmd/models`;\n const modelUrl = joinPublicPath(publicRoot, folderName, modelRelativePath);\n\n return {\n directory: targetDir,\n relativeDirectory: folderName,\n modelRelativePath,\n modelUrl,\n format,\n filesExtracted,\n };\n}\n\nfunction sanitizeEntryPath(entryName: string): string | null {\n const normalized = path.normalize(entryName).replace(/^(\\.\\.(\\/|\\\\|$))+/, '');\n if (!normalized || normalized === '.' || normalized.startsWith('..') || path.isAbsolute(normalized)) {\n return null;\n }\n return normalized;\n}\n\nfunction joinPublicPath(...segments: string[]) {\n return segments\n .map((segment) => segment.replace(/\\/+/g, '/').replace(/^\\//, '').replace(/\\/$/, ''))\n .filter(Boolean)\n .join('/')\n .replace(/\\/{2,}/g, '/')\n .replace(/^/, '/');\n}\n\n","import type { MMDPlaylistConfig, MMDPlaylistNode } from '../types';\n\nexport interface PlaylistModelSource {\n id: string | number;\n name: string;\n filePath: string;\n thumbnailPath?: string | null;\n}\n\nexport interface PlaylistMotionSource {\n id: string | number;\n name?: string;\n filePath: string;\n}\n\nexport interface BuildMmdPlaylistOptions {\n playlistId: string;\n playlistName?: string;\n models: PlaylistModelSource[];\n motions?: PlaylistMotionSource[];\n limit?: number;\n loop?: boolean;\n preload?: 'none' | 'next' | 'all';\n autoPlay?: boolean;\n nodeDuration?: number;\n normalizeUrl?: (pathOrUrl: string) => string;\n}\n\nconst defaultNormalizer = (value: string) => {\n if (!value) return '';\n if (value.startsWith('http://') || value.startsWith('https://')) {\n return value;\n }\n return value.startsWith('/') ? value : `/${value}`;\n};\n\n/**\n * 根据数据库中的模型/动作记录快速构建 MMDPlaylistConfig\n */\nexport function buildMmdPlaylistFromSources(options: BuildMmdPlaylistOptions): MMDPlaylistConfig {\n if (!options.models.length) {\n throw new Error('构建 MMD 播放列表失败:models 为空');\n }\n\n const limit = Math.max(1, Math.min(options.limit ?? options.models.length, options.models.length));\n const normalizeUrl = options.normalizeUrl ?? defaultNormalizer;\n const motions = options.motions ?? [];\n const hasMotions = motions.length > 0;\n const duration = options.nodeDuration ?? 30;\n\n const nodes: MMDPlaylistNode[] = options.models.slice(0, limit).map((model, index) => {\n const motion = hasMotions ? motions[index % motions.length] : undefined;\n return {\n id: String(model.id ?? index),\n name: model.name,\n loop: options.loop ?? true,\n duration,\n thumbnail: model.thumbnailPath ? normalizeUrl(model.thumbnailPath) : undefined,\n resources: {\n modelPath: normalizeUrl(model.filePath),\n motionPath: motion ? normalizeUrl(motion.filePath) : undefined,\n cameraPath: undefined,\n audioPath: undefined,\n stageModelPath: undefined,\n additionalMotions: undefined,\n },\n };\n });\n\n return {\n id: options.playlistId,\n name: options.playlistName ?? `MMD 播放列表 - ${options.playlistId}`,\n nodes,\n loop: options.loop ?? true,\n preload: options.preload ?? 'next',\n autoPlay: options.autoPlay ?? true,\n };\n}\n\n","/**\n * MMD 资源上传辅助函数\n * \n * 整合 UniversalFileService 用于 MMD 资源上传\n */\n\nimport type { UniversalFileService } from '../../universalFile/server/UniversalFileService';\nimport type { FileMetadata } from '../../universalFile/types';\nimport { processMmdModelArchive, MMD_MODEL_ARCHIVE_MIME_TYPES } from './modelArchive';\n\nexport const MMD_SUPPORTED_TYPES = {\n model: [...MMD_MODEL_ARCHIVE_MIME_TYPES],\n animation: ['application/octet-stream', 'animation/vmd'],\n audio: ['audio/wav', 'audio/mp3', 'audio/mpeg', 'audio/ogg'],\n};\n\nexport const MMD_FILE_EXTENSIONS = {\n model: ['.zip'],\n animation: ['.vmd'],\n audio: ['.wav', '.mp3', '.ogg'],\n};\n\nexport interface MmdUploadOptions {\n file: File;\n resourceType: 'model' | 'animation' | 'audio';\n name: string;\n description?: string;\n userId: string;\n}\n\nexport interface MmdUploadResult {\n id: string;\n name: string;\n url: string;\n filePath: string;\n fileSize: number;\n type: string;\n format: string;\n uploadTime: Date;\n metadata?: FileMetadata;\n}\n\n/**\n * 使用 UniversalFileService 上传 MMD 资源\n * \n * @param fileService - UniversalFileService 实例\n * @param options - 上传选项\n * @returns 上传结果\n */\nexport async function uploadMmdResource(\n fileService: UniversalFileService,\n options: MmdUploadOptions\n): Promise<MmdUploadResult> {\n const { file, resourceType, name, description, userId } = options;\n\n // 验证文件扩展名\n const ext = `.${file.name.split('.').pop()?.toLowerCase()}`;\n const allowedExtensions = MMD_FILE_EXTENSIONS[resourceType];\n\n if (!allowedExtensions.includes(ext)) {\n throw new Error(\n `不支持的文件扩展名: ${ext}。支持的格式: ${allowedExtensions.join(', ')}`\n );\n }\n\n // 确定模块ID\n const moduleId = `mmd-${resourceType}s`;\n\n // 上传文件\n const metadata = await fileService.uploadFile({\n file,\n moduleId,\n businessId: 'default',\n permission: 'public',\n metadata: {\n uploadedBy: userId,\n uploadedAt: new Date().toISOString(),\n originalFileName: file.name,\n resourceType,\n name,\n description: description || '',\n },\n needsProcessing: false,\n });\n\n // 构建返回结果\n const format = ext.slice(1).toUpperCase();\n const fileUrl = metadata.cdnUrl || metadata.storagePath;\n\n return {\n id: metadata.id,\n name,\n url: fileUrl,\n filePath: metadata.storagePath,\n fileSize: metadata.size,\n type: resourceType,\n format,\n uploadTime: metadata.uploadTime,\n metadata,\n };\n}\n\n/**\n * 上传 MMD 模型 (ZIP 压缩包)\n * \n * @param fileService - UniversalFileService 实例\n * @param options - 上传选项\n * @returns 上传结果,包含解压后的模型路径\n */\nexport async function uploadMmdModel(\n fileService: UniversalFileService,\n options: MmdUploadOptions & { storageRoot: string; publicRoot: string }\n): Promise<MmdUploadResult & { extractedPath?: string }> {\n const { file, storageRoot, publicRoot } = options;\n\n // 首先上传 ZIP 文件\n const baseResult = await uploadMmdResource(fileService, options);\n\n // 处理 ZIP 压缩包\n try {\n const fileBuffer = Buffer.from(await file.arrayBuffer());\n const archiveResult = await processMmdModelArchive(fileBuffer, {\n storageRoot,\n publicRoot,\n folderName: baseResult.id,\n });\n\n return {\n ...baseResult,\n url: archiveResult.modelUrl,\n filePath: archiveResult.modelUrl,\n format: archiveResult.format.toUpperCase(),\n extractedPath: archiveResult.modelUrl,\n };\n } catch (extractError) {\n console.error('模型压缩包处理失败:', extractError);\n throw new Error(\n extractError instanceof Error ? extractError.message : '模型压缩包处理失败'\n );\n }\n}\n\n/**\n * 批量上传 MMD 资源\n * \n * @param fileService - UniversalFileService 实例\n * @param uploads - 上传选项数组\n * @returns 上传结果数组\n */\nexport async function batchUploadMmdResources(\n fileService: UniversalFileService,\n uploads: MmdUploadOptions[]\n): Promise<MmdUploadResult[]> {\n const results: MmdUploadResult[] = [];\n\n for (const uploadOptions of uploads) {\n try {\n const result = await uploadMmdResource(fileService, uploadOptions);\n results.push(result);\n } catch (error) {\n console.error(`上传失败: ${uploadOptions.file.name}`, error);\n // 继续处理其他文件\n }\n }\n\n return results;\n}\n\n"]}