@yinuo-ngm/server 1.0.21 → 1.0.23

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.
@@ -10,15 +10,16 @@ const deps_route_1 = __importDefault(require("./deps.route"));
10
10
  const fs_routes_1 = __importDefault(require("./fs.routes"));
11
11
  const project_routes_1 = __importDefault(require("./project.routes"));
12
12
  const rss_routes_1 = __importDefault(require("./rss.routes"));
13
- const sprite_routes_1 = require("./sprite.routes");
13
+ const sprite_routes_1 = require("./sprite/sprite.routes");
14
14
  const system_routes_1 = __importDefault(require("./system.routes"));
15
15
  const task_routes_1 = __importDefault(require("./task.routes"));
16
16
  const api_client_1 = require("./api-client");
17
17
  const nginx_routes_1 = require("./nginx.routes");
18
18
  const svn_routes_1 = __importDefault(require("./svn.routes"));
19
- const sprite_browse_routes_1 = __importDefault(require("./sprite-browse.routes"));
19
+ const sprite_browse_routes_1 = __importDefault(require("./sprite/sprite-browse.routes"));
20
20
  const static_files_routes_1 = __importDefault(require("./static-files.routes"));
21
21
  const hub_routes_1 = __importDefault(require("./hub.routes"));
22
+ const node_runtime_routes_1 = __importDefault(require("./node-runtime.routes"));
22
23
  const node_version_routes_1 = __importDefault(require("./node-version.routes"));
23
24
  async function routes(fastify) {
24
25
  await fastify.register(system_routes_1.default);
@@ -29,8 +30,8 @@ async function routes(fastify) {
29
30
  await fastify.register(config_routes_1.default, { prefix: '/api/config' });
30
31
  await fastify.register(dashboard_routes_1.default, { prefix: '/api/dashboard' });
31
32
  await fastify.register(rss_routes_1.default, { prefix: '/api/rss' });
32
- await fastify.register(sprite_routes_1.spriteRoutes, { prefix: '/api/sprite' });
33
33
  await fastify.register(svn_routes_1.default, { prefix: '/api/svn' });
34
+ await fastify.register(sprite_routes_1.spriteRoutes, { prefix: '/api/sprite' });
34
35
  await fastify.register(sprite_browse_routes_1.default, { prefix: '/api/sprite/browse' });
35
36
  await fastify.register(static_files_routes_1.default, { prefix: '/api/static' });
36
37
  await fastify.register(hub_routes_1.default, { prefix: '/api/hub' });
@@ -42,4 +43,5 @@ async function routes(fastify) {
42
43
  await fastify.register(api_client_1.apiClientHubTokenRoutes, { prefix: '/api/client/hub-token' });
43
44
  await fastify.register(nginx_routes_1.nginxRoutes, { prefix: '/api/nginx' });
44
45
  await fastify.register(node_version_routes_1.default, { prefix: '/api/node-version' });
46
+ await fastify.register(node_runtime_routes_1.default, { prefix: '/api/node-runtimes' });
45
47
  }
@@ -0,0 +1,2 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ export default function nodeRuntimeRoutes(fastify: FastifyInstance): Promise<void>;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = nodeRuntimeRoutes;
4
+ function toNodeRuntimeConfig(input) {
5
+ return input;
6
+ }
7
+ function toNodeRuntimeRecordDto(record) {
8
+ return {
9
+ id: record.id,
10
+ name: record.name,
11
+ version: record.version,
12
+ platform: String(record.platform),
13
+ arch: String(record.arch),
14
+ rootDir: record.rootDir,
15
+ nodePath: record.nodePath,
16
+ npmPath: record.npmPath,
17
+ npxPath: record.npxPath,
18
+ pnpmPath: record.pnpmPath,
19
+ yarnPath: record.yarnPath,
20
+ npmCliPath: record.npmCliPath,
21
+ npxCliPath: record.npxCliPath,
22
+ source: record.source,
23
+ };
24
+ }
25
+ function toResolvedNodeRuntimeDto(runtime) {
26
+ return {
27
+ type: runtime.type,
28
+ name: runtime.name,
29
+ version: runtime.version,
30
+ packageManager: runtime.packageManager,
31
+ rootDir: runtime.rootDir,
32
+ binDir: runtime.binDir,
33
+ nodePath: runtime.nodePath,
34
+ npmPath: runtime.npmPath,
35
+ npxPath: runtime.npxPath,
36
+ pnpmPath: runtime.pnpmPath,
37
+ yarnPath: runtime.yarnPath,
38
+ npmCliPath: runtime.npmCliPath,
39
+ npxCliPath: runtime.npxCliPath,
40
+ env: runtime.env,
41
+ source: runtime.source,
42
+ };
43
+ }
44
+ function toNodeRuntimeTestResultDto(result) {
45
+ return {
46
+ ok: result.ok,
47
+ nodeVersion: result.nodeVersion,
48
+ npmVersion: result.npmVersion,
49
+ nodePath: result.nodePath,
50
+ npmLaunchCommand: result.npmLaunchCommand,
51
+ errors: result.errors,
52
+ };
53
+ }
54
+ async function nodeRuntimeRoutes(fastify) {
55
+ fastify.get("/", async () => {
56
+ const runtimes = await fastify.core.nodeRuntime.listRuntimes();
57
+ return runtimes.map(toNodeRuntimeRecordDto);
58
+ });
59
+ fastify.post("/resolve", async (req) => {
60
+ const body = req.body;
61
+ const runtime = await fastify.core.nodeRuntime.resolveRuntime(toNodeRuntimeConfig(body?.runtime));
62
+ return toResolvedNodeRuntimeDto(runtime);
63
+ });
64
+ fastify.post("/test", async (req) => {
65
+ const body = req.body;
66
+ const result = await fastify.core.nodeRuntime.testRuntime(toNodeRuntimeConfig(body?.runtime) || { type: "system" });
67
+ return toNodeRuntimeTestResultDto(result);
68
+ });
69
+ }
@@ -65,6 +65,8 @@ function toProjectDto(project) {
65
65
  updatedAt: project.updatedAt,
66
66
  scripts: project.scripts,
67
67
  packageManager: project.packageManager,
68
+ runtime: project.runtime,
69
+ nodeVersion: project.nodeVersion,
68
70
  framework: project.framework,
69
71
  env: project.env,
70
72
  isFavorite: project.isFavorite,
@@ -126,6 +128,7 @@ async function projectRoutes(fastify) {
126
128
  ...(body.name !== undefined ? { name: body.name } : {}),
127
129
  ...(body.env !== undefined ? { env: body.env } : {}),
128
130
  ...(body.scripts !== undefined ? { scripts: body.scripts } : {}),
131
+ ...(body.runtime !== undefined ? { runtime: body.runtime } : {}),
129
132
  });
130
133
  return toProjectDto(updated);
131
134
  }
@@ -141,6 +144,30 @@ async function projectRoutes(fastify) {
141
144
  const project = await fastify.core.project.get(id);
142
145
  return toProjectDto(project);
143
146
  });
147
+ async function updateProjectRuntime(projectId, body) {
148
+ const updated = await fastify.core.project.update(projectId, {
149
+ runtime: body.runtime,
150
+ });
151
+ return toProjectDto(updated);
152
+ }
153
+ fastify.patch("/:id/runtime", async (req) => {
154
+ const { id } = req.params;
155
+ const body = req.body;
156
+ return updateProjectRuntime(id, body);
157
+ });
158
+ fastify.post("/:id/runtime", async (req) => {
159
+ const { id } = req.params;
160
+ const body = req.body;
161
+ return updateProjectRuntime(id, body);
162
+ });
163
+ fastify.post("/:id/runtime/test", async (req) => {
164
+ const { id } = req.params;
165
+ const project = await fastify.core.project.get(id);
166
+ const runtimeConfig = project.runtime ?? (project.nodeVersion
167
+ ? { type: "managed", version: project.nodeVersion, packageManager: project.packageManager === "pnpm" || project.packageManager === "yarn" ? project.packageManager : "npm" }
168
+ : { type: "system", packageManager: project.packageManager === "pnpm" || project.packageManager === "yarn" ? project.packageManager : "npm" });
169
+ return await fastify.core.nodeRuntime.testRuntime(runtimeConfig);
170
+ });
144
171
  fastify.delete("/delete/:id", async (req) => {
145
172
  const { id } = req.params;
146
173
  await fastify.core.project.remove(id);
@@ -0,0 +1,2 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ export default function spriteBrowseRoutes(fastify: FastifyInstance): Promise<void>;
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = spriteBrowseRoutes;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const errors_1 = require("@yinuo-ngm/errors");
10
+ const sprite_quick_utils_1 = require("./sprite-quick.utils");
11
+ const SKIP = new Set([
12
+ ".svn",
13
+ ".git",
14
+ ".hg",
15
+ ".DS_Store",
16
+ "__MACOSX",
17
+ "Thumbs.db",
18
+ ]);
19
+ function safeJoin(root, sub) {
20
+ const resolved = node_path_1.default.resolve(root, sub);
21
+ const rootResolved = node_path_1.default.resolve(root);
22
+ if (!resolved.startsWith(rootResolved)) {
23
+ throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.BAD_REQUEST, "INVALID_PATH");
24
+ }
25
+ return resolved;
26
+ }
27
+ function listDir(root) {
28
+ if (!node_fs_1.default.existsSync(root))
29
+ return [];
30
+ return node_fs_1.default
31
+ .readdirSync(root, { withFileTypes: true })
32
+ .filter(d => !SKIP.has(d.name) && !d.name.startsWith("."))
33
+ .map(d => ({
34
+ name: d.name,
35
+ kind: (d.isDirectory() ? "dir" : "file"),
36
+ ext: d.isDirectory() ? undefined : node_path_1.default.extname(d.name).toLowerCase(),
37
+ })).sort((a, b) => {
38
+ if (a.kind === b.kind) {
39
+ return a.name.localeCompare(b.name, "zh-Hans-CN", { numeric: true });
40
+ }
41
+ return a.kind === "dir" ? -1 : 1;
42
+ });
43
+ }
44
+ function countFiles(dir) {
45
+ if (!node_fs_1.default.existsSync(dir) || !node_fs_1.default.statSync(dir).isDirectory())
46
+ return 0;
47
+ return node_fs_1.default.readdirSync(dir, { withFileTypes: true }).filter(d => d.isFile()).length;
48
+ }
49
+ function toBrowseEntriesDto(root, entries) {
50
+ return { root, entries };
51
+ }
52
+ function toBrowseFilesDto(root, dir, entries) {
53
+ return { root, dir, entries };
54
+ }
55
+ async function spriteBrowseRoutes(fastify) {
56
+ fastify.get("/icons/groups/:projectId", async (req) => {
57
+ const { projectId } = req.params;
58
+ const project = await fastify.core.project.get(projectId);
59
+ const cfg = await fastify.core.sprite.getConfig(projectId);
60
+ const localRoot = String(cfg?.localImageRoot ?? "").trim();
61
+ const root = localRoot || project?.assets?.iconsSvn?.localDir;
62
+ if (!root) {
63
+ return toBrowseEntriesDto("", []);
64
+ }
65
+ const entries = listDir(root).filter(e => e.kind === "dir");
66
+ if (localRoot) {
67
+ const rootFiles = listDir(root).filter((e) => e.kind === "file" && (e.ext === ".png" || e.ext === ".svg"));
68
+ if (rootFiles.length > 0) {
69
+ entries.unshift({ name: "root", kind: "dir", ext: undefined });
70
+ }
71
+ }
72
+ return toBrowseEntriesDto(root, entries);
73
+ });
74
+ fastify.get("/icons/files/:projectId", async (req) => {
75
+ const { projectId } = req.params;
76
+ const group = req.query?.group;
77
+ const project = await fastify.core.project.get(projectId);
78
+ const cfg = await fastify.core.sprite.getConfig(projectId);
79
+ const localRoot = String(cfg?.localImageRoot ?? "").trim();
80
+ const root = localRoot || project?.assets?.iconsSvn?.localDir;
81
+ if (!root) {
82
+ return toBrowseEntriesDto("", []);
83
+ }
84
+ const isRootGroup = !!localRoot && (group === "root");
85
+ const groupDir = isRootGroup ? root : safeJoin(root, group || "");
86
+ const entries = listDir(groupDir)
87
+ .filter(e => e.kind === "file")
88
+ .map(e => ({
89
+ ...e,
90
+ url: localRoot
91
+ ? `/api/static/local/${projectId}/${isRootGroup ? e.name : `${group}/${e.name}`}`
92
+ : `/api/static/svn/${projectId}/icons/${group}/${e.name}`,
93
+ }));
94
+ return toBrowseEntriesDto(groupDir, entries);
95
+ });
96
+ fastify.get("/images/list/:projectId", async (req) => {
97
+ const { projectId } = req.params;
98
+ const quickProjectId = await (0, sprite_quick_utils_1.resolveEnabledRemoteProjectId)(fastify, projectId);
99
+ if (quickProjectId) {
100
+ const cfg = await fastify.core.sprite.getConfig(projectId);
101
+ const baseUrl = (0, sprite_quick_utils_1.getBaseUrl)(cfg);
102
+ const dir = String(req.query?.dir ?? "").trim();
103
+ const cache = await (0, sprite_quick_utils_1.fetchAndCacheRemoteMiscImages)(fastify, baseUrl, quickProjectId);
104
+ return (0, sprite_quick_utils_1.buildBrowseFromCache)(quickProjectId, cache, dir || undefined);
105
+ }
106
+ const project = await fastify.core.project.get(projectId);
107
+ const root = project?.assets?.cutImageSvn?.localDir;
108
+ if (!root) {
109
+ return toBrowseEntriesDto("", []);
110
+ }
111
+ const dir = String(req.query?.dir ?? "").trim();
112
+ if (dir.split(/[\\/]/g).some(seg => seg === "..")) {
113
+ return toBrowseFilesDto(root, "", []);
114
+ }
115
+ const absDir = dir ? safeJoin(root, dir) : root;
116
+ if (!node_fs_1.default.existsSync(absDir))
117
+ return toBrowseFilesDto(root, dir, []);
118
+ const entries = listDir(absDir).map(e => {
119
+ if (e.kind === "file") {
120
+ const rel = dir ? `${dir}/${e.name}` : e.name;
121
+ return {
122
+ ...e,
123
+ url: `/api/static/svn/${projectId}/images/${rel}`,
124
+ };
125
+ }
126
+ else if (e.kind === "dir") {
127
+ const rel = dir ? `${dir}/${e.name}` : e.name;
128
+ return {
129
+ ...e,
130
+ fileCount: countFiles(safeJoin(root, rel)),
131
+ };
132
+ }
133
+ return e;
134
+ });
135
+ return toBrowseFilesDto(root, dir, entries);
136
+ });
137
+ }
@@ -0,0 +1,19 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type { QuickSpriteProjectDto, QuickGenerateResponseDto, QuickImagesResponseDto, SpriteSnapshotDto, SpriteGroupItemDto, BrowseFilesDto } from "@yinuo-ngm/protocol";
3
+ import type { SpriteConfig } from "@yinuo-ngm/sprite";
4
+ export declare function getBaseUrl(cfg?: {
5
+ quickSpriteBaseUrl?: string;
6
+ } | null): string;
7
+ export declare const QUICK_SPRITE_PROXY_PREFIX = "/api/sprite/proxy";
8
+ export declare function buildQuickPreviewUrl(localProjectId: string, group: string): string;
9
+ export declare function quickFetch<T>(fastify: FastifyInstance, baseUrl: string, path: string, init?: RequestInit): Promise<T>;
10
+ export declare function resolveEnabledRemoteProjectId(fastify: FastifyInstance, projectId: string): Promise<string | null>;
11
+ export declare function fetchRemoteProject(fastify: FastifyInstance, baseUrl: string, quickProjectId: string): Promise<QuickSpriteProjectDto | null>;
12
+ export declare function mapToGroup(result: QuickGenerateResponseDto, localProjectId: string, prefix: string, spriteUrlTemplate: string): SpriteGroupItemDto;
13
+ export declare function mapQuickGroupsToSnapshot(projectId: string, results: QuickGenerateResponseDto[], remoteProj: QuickSpriteProjectDto | null, localCfg: SpriteConfig | null): SpriteSnapshotDto;
14
+ export declare function copyRawResponseHeaders(response: Response, reply: any): void;
15
+ export declare function hasQuickSprite(fastify: FastifyInstance, projectId: string): Promise<boolean>;
16
+ export declare function fetchAndCacheRemoteMiscImages(fastify: FastifyInstance, baseUrl: string, quickProjectId: string, forceRefresh?: boolean): Promise<QuickImagesResponseDto>;
17
+ export declare function buildBrowseFromCache(quickProjectId: string, cache: QuickImagesResponseDto, dir?: string): BrowseFilesDto;
18
+ export declare const MISC_PROXY_PREFIX = "/api/sprite/misc-proxy";
19
+ export declare function buildRemoteMiscUrl(baseUrl: string, quickProjectId: string, filename: string): string;
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MISC_PROXY_PREFIX = exports.QUICK_SPRITE_PROXY_PREFIX = void 0;
4
+ exports.getBaseUrl = getBaseUrl;
5
+ exports.buildQuickPreviewUrl = buildQuickPreviewUrl;
6
+ exports.quickFetch = quickFetch;
7
+ exports.resolveEnabledRemoteProjectId = resolveEnabledRemoteProjectId;
8
+ exports.fetchRemoteProject = fetchRemoteProject;
9
+ exports.mapToGroup = mapToGroup;
10
+ exports.mapQuickGroupsToSnapshot = mapQuickGroupsToSnapshot;
11
+ exports.copyRawResponseHeaders = copyRawResponseHeaders;
12
+ exports.hasQuickSprite = hasQuickSprite;
13
+ exports.fetchAndCacheRemoteMiscImages = fetchAndCacheRemoteMiscImages;
14
+ exports.buildBrowseFromCache = buildBrowseFromCache;
15
+ exports.buildRemoteMiscUrl = buildRemoteMiscUrl;
16
+ const errors_1 = require("@yinuo-ngm/errors");
17
+ const sprite_1 = require("@yinuo-ngm/sprite");
18
+ function getBaseUrl(cfg) {
19
+ return (cfg?.quickSpriteBaseUrl?.trim() ||
20
+ "http://192.168.1.31:7010").replace(/\/+$/, "");
21
+ }
22
+ exports.QUICK_SPRITE_PROXY_PREFIX = "/api/sprite/proxy";
23
+ function buildQuickPreviewUrl(localProjectId, group) {
24
+ return `${exports.QUICK_SPRITE_PROXY_PREFIX}/${encodeURIComponent(localProjectId)}/${encodeURIComponent(group)}.png`;
25
+ }
26
+ async function quickFetch(fastify, baseUrl, path, init) {
27
+ const url = `${baseUrl}${path}`;
28
+ fastify.log.info(`[sprite-quick] → ${init?.method || "GET"} ${url}`);
29
+ let res;
30
+ try {
31
+ res = await fetch(url, {
32
+ ...init,
33
+ headers: {
34
+ "Content-Type": "application/json",
35
+ ...init?.headers,
36
+ },
37
+ });
38
+ }
39
+ catch (err) {
40
+ fastify.log.error(`[sprite-quick] ✗ 无法连接远端 ${url}: ${err?.message || err}`);
41
+ throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.INTERNAL_ERROR, `无法连接远端雪碧图服务 (${baseUrl}):${err?.message || err}`);
42
+ }
43
+ if (!res.ok) {
44
+ const text = await res.text().catch(() => "");
45
+ fastify.log.error(`[sprite-quick] ← ${res.status} ${url}: ${text}`);
46
+ throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.INTERNAL_ERROR, `远端服务返回 ${res.status}: ${text || res.statusText}`);
47
+ }
48
+ const data = (await res.json());
49
+ fastify.log.info(`[sprite-quick] ← ${res.status} ${url}`);
50
+ return data;
51
+ }
52
+ async function resolveEnabledRemoteProjectId(fastify, projectId) {
53
+ const cfg = await fastify.core.sprite.getConfig(projectId);
54
+ if (!cfg?.quickSpriteEnabled || !cfg?.quickSpriteProjectId)
55
+ return null;
56
+ return cfg.quickSpriteProjectId;
57
+ }
58
+ async function fetchRemoteProject(fastify, baseUrl, quickProjectId) {
59
+ try {
60
+ return await quickFetch(fastify, baseUrl, `/api/projects/${encodeURIComponent(quickProjectId)}`);
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ function mapToGroup(result, localProjectId, prefix, spriteUrlTemplate) {
67
+ const classes = (result.classes || []).map((c) => ({
68
+ name: c.name,
69
+ className: `${prefix}-${result.group.split("-")[0]}-${c.name}`,
70
+ x: c.x,
71
+ y: c.y,
72
+ width: c.width,
73
+ height: c.height,
74
+ }));
75
+ const derived = deriveSpriteSize(classes);
76
+ const spriteWidth = derived.width > 0 ? derived.width : result.width || 0;
77
+ const spriteHeight = derived.height > 0 ? derived.height : result.height || 0;
78
+ const [tileW, tileH] = parseTileSize(result.group);
79
+ const resolvedSpriteUrl = spriteUrlTemplate
80
+ ? spriteUrlTemplate.replace(/{group}/g, result.group).replace(/{size}/g, String(tileW))
81
+ : result.spriteUrl;
82
+ const cssOpts = {
83
+ prefix,
84
+ spriteUrlResolver: ({ spriteUrl }) => spriteUrl,
85
+ };
86
+ const lessText = (0, sprite_1.buildLessForSprite)({ group: result.group, spriteUrl: resolvedSpriteUrl, classes }, cssOpts);
87
+ return {
88
+ group: result.group,
89
+ kind: "png",
90
+ spriteUrl: resolvedSpriteUrl,
91
+ previewSpriteUrl: buildQuickPreviewUrl(localProjectId, result.group),
92
+ lessText,
93
+ status: "ok",
94
+ meta: {
95
+ mode: result.mode,
96
+ group: result.group,
97
+ tileWidth: tileW,
98
+ tileHeight: tileH,
99
+ spriteWidth,
100
+ spriteHeight,
101
+ classes,
102
+ },
103
+ };
104
+ }
105
+ function deriveSpriteSize(classes) {
106
+ if (!classes.length)
107
+ return { width: 0, height: 0 };
108
+ let maxW = 0;
109
+ let maxH = 0;
110
+ for (const c of classes) {
111
+ const r = c.x + c.width;
112
+ const b = c.y + c.height;
113
+ if (r > maxW)
114
+ maxW = r;
115
+ if (b > maxH)
116
+ maxH = b;
117
+ }
118
+ return { width: maxW, height: maxH };
119
+ }
120
+ function parseTileSize(group) {
121
+ if (!group)
122
+ return [0, 0];
123
+ const parts = group.split("-");
124
+ if (parts.length === 0)
125
+ return [0, 0];
126
+ if (parts.length === 1) {
127
+ const n = parseInt(parts[0], 10);
128
+ return Number.isFinite(n) ? [n, n] : [0, 0];
129
+ }
130
+ const w = parseInt(parts[0], 10);
131
+ const h = parseInt(parts[1], 10);
132
+ return [Number.isFinite(w) ? w : 0, Number.isFinite(h) ? h : 0];
133
+ }
134
+ function mapQuickGroupsToSnapshot(projectId, results, remoteProj, localCfg) {
135
+ const cssPrefix = localCfg?.prefix ?? "sl";
136
+ const spriteUrlTpl = localCfg?.spriteUrl ?? "";
137
+ const groups = (results || []).map((r) => mapToGroup(r, projectId, cssPrefix, spriteUrlTpl));
138
+ return {
139
+ projectId,
140
+ sourceId: "",
141
+ iconsRoot: remoteProj?.iconsPath ?? "",
142
+ cacheOutDir: remoteProj?.exportSpritesDir ?? "",
143
+ config: {
144
+ projectId,
145
+ enabled: true,
146
+ sourceId: "",
147
+ localDir: "",
148
+ prefix: cssPrefix,
149
+ algorithm: "binary-tree",
150
+ persistLess: false,
151
+ updatedAt: remoteProj?.updatedAt
152
+ ? new Date(remoteProj.updatedAt).getTime()
153
+ : Date.now(),
154
+ template: localCfg?.template ?? '<i class="{base} {class}"></i>',
155
+ spriteUrl: localCfg?.spriteUrl ?? "",
156
+ spriteExportDir: localCfg?.spriteExportDir ?? "",
157
+ lessExportDir: localCfg?.lessExportDir ?? "",
158
+ localImageRoot: localCfg?.localImageRoot ?? "",
159
+ localCacheDir: localCfg?.localCacheDir ?? "",
160
+ },
161
+ total: groups.length,
162
+ success: groups.length,
163
+ failed: 0,
164
+ groups,
165
+ };
166
+ }
167
+ function copyRawResponseHeaders(response, reply) {
168
+ const contentType = response.headers.get("content-type");
169
+ if (contentType)
170
+ reply.header("content-type", contentType);
171
+ const contentLength = response.headers.get("content-length");
172
+ if (contentLength)
173
+ reply.header("content-length", contentLength);
174
+ const cacheControl = response.headers.get("cache-control");
175
+ if (cacheControl)
176
+ reply.header("cache-control", cacheControl);
177
+ const etag = response.headers.get("etag");
178
+ if (etag)
179
+ reply.header("etag", etag);
180
+ const lastModified = response.headers.get("last-modified");
181
+ if (lastModified)
182
+ reply.header("last-modified", lastModified);
183
+ }
184
+ async function hasQuickSprite(fastify, projectId) {
185
+ const cfg = await fastify.core.sprite.getConfig(projectId);
186
+ return !!cfg?.quickSpriteProjectId;
187
+ }
188
+ const miscCache = new Map();
189
+ const MISC_CACHE_TTL = 5 * 60 * 1000;
190
+ async function fetchAndCacheRemoteMiscImages(fastify, baseUrl, quickProjectId, forceRefresh = false) {
191
+ const cached = miscCache.get(quickProjectId);
192
+ if (!forceRefresh && cached && Date.now() - cached.time < MISC_CACHE_TTL) {
193
+ return cached.data;
194
+ }
195
+ const data = await quickFetch(fastify, baseUrl, `/api/misc/list?projectId=${encodeURIComponent(quickProjectId)}`);
196
+ miscCache.set(quickProjectId, { time: Date.now(), data });
197
+ return data;
198
+ }
199
+ function buildBrowseFromCache(quickProjectId, cache, dir) {
200
+ const list = cache.list || [];
201
+ const quickDir = dir ? `${dir}` : ".";
202
+ const filtered = list.filter((item) => item.dir === quickDir);
203
+ const sorted = [...filtered].sort((a, b) => a.name.localeCompare(b.name, "zh-Hans-CN", { numeric: true }));
204
+ const fileEntries = sorted.map((item) => {
205
+ const encodedName = encodeURIComponent(item.name);
206
+ return {
207
+ name: item.name,
208
+ kind: "file",
209
+ ext: item.name.split(".").pop()?.toLowerCase(),
210
+ url: `/api/sprite/misc-proxy/${encodeURIComponent(quickProjectId)}/${encodeURIComponent(item.dir)}/${encodedName}`,
211
+ };
212
+ });
213
+ const dirSet = new Set();
214
+ for (const item of list) {
215
+ if (item.dir)
216
+ dirSet.add(item.dir);
217
+ }
218
+ const subDirSet = new Set();
219
+ for (const d of dirSet) {
220
+ if (d === quickDir)
221
+ continue;
222
+ if (d.startsWith(quickDir) || quickDir === ".") {
223
+ const dirName = quickDir === "."
224
+ ? d.split("/")[0]
225
+ : d.slice(quickDir.length).split("/")[1];
226
+ subDirSet.add(dirName);
227
+ }
228
+ }
229
+ const dirEntries = Array.from(subDirSet)
230
+ .sort((a, b) => a.localeCompare(b, "zh-Hans-CN", { numeric: true }))
231
+ .map((d) => ({
232
+ name: d,
233
+ kind: "dir",
234
+ ext: undefined,
235
+ fileCount: list.filter((item) => item.dir === `${quickDir === "." ? "" : quickDir + "/"}${d}`).length,
236
+ }));
237
+ const entries = [...dirEntries, ...fileEntries];
238
+ return {
239
+ root: `remote:${quickProjectId}`,
240
+ dir,
241
+ entries,
242
+ };
243
+ }
244
+ exports.MISC_PROXY_PREFIX = "/api/sprite/misc-proxy";
245
+ function buildRemoteMiscUrl(baseUrl, quickProjectId, filename) {
246
+ return `${baseUrl}/misc/${encodeURIComponent(quickProjectId)}/misc/${encodeURIComponent(filename)}`;
247
+ }
@@ -0,0 +1,2 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ export declare function spriteRoutes(fastify: FastifyInstance): Promise<void>;
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.spriteRoutes = spriteRoutes;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const node_stream_1 = require("node:stream");
9
+ const errors_1 = require("@yinuo-ngm/errors");
10
+ const sprite_quick_utils_1 = require("./sprite-quick.utils");
11
+ function toSpriteConfigDto(cfg) {
12
+ return {
13
+ projectId: cfg.projectId,
14
+ enabled: cfg.enabled,
15
+ sourceId: cfg.sourceId,
16
+ localDir: cfg.localDir,
17
+ prefix: cfg.prefix,
18
+ template: cfg.template,
19
+ spriteUrl: cfg.spriteUrl,
20
+ spriteExportDir: cfg.spriteExportDir,
21
+ lessExportDir: cfg.lessExportDir,
22
+ algorithm: cfg.algorithm,
23
+ persistLess: cfg.persistLess,
24
+ updatedAt: cfg.updatedAt,
25
+ localImageRoot: cfg.localImageRoot,
26
+ localCacheDir: cfg.localCacheDir,
27
+ quickSpriteProjectId: cfg.quickSpriteProjectId,
28
+ quickSpriteEnabled: cfg.quickSpriteEnabled,
29
+ quickSpriteBaseUrl: cfg.quickSpriteBaseUrl,
30
+ };
31
+ }
32
+ function toSpriteGroupItemDto(item) {
33
+ return {
34
+ group: item.group,
35
+ kind: item.kind,
36
+ previewSpriteUrl: item.previewSpriteUrl,
37
+ spriteUrl: item.spriteUrl,
38
+ meta: item.meta,
39
+ lessText: item.lessText,
40
+ exported: item.exported,
41
+ status: item.status,
42
+ error: item.error,
43
+ };
44
+ }
45
+ function toSpriteSnapshotDto(snapshot) {
46
+ return {
47
+ projectId: snapshot.projectId,
48
+ sourceId: snapshot.sourceId,
49
+ iconsRoot: snapshot.iconsRoot,
50
+ cacheOutDir: snapshot.cacheOutDir,
51
+ config: toSpriteConfigDto(snapshot.config),
52
+ total: snapshot.total,
53
+ success: snapshot.success,
54
+ failed: snapshot.failed,
55
+ groups: (snapshot.groups ?? []).map(toSpriteGroupItemDto),
56
+ };
57
+ }
58
+ function toGenerateSpriteResultDto(snapshot) {
59
+ return toSpriteSnapshotDto(snapshot);
60
+ }
61
+ async function spriteRoutes(fastify) {
62
+ fastify.get("/config/:projectId", async (req) => {
63
+ const { projectId } = req.params;
64
+ const cfg = await fastify.core.sprite.getConfig(projectId);
65
+ if (cfg && !cfg?.quickSpriteBaseUrl) {
66
+ const base = (0, sprite_quick_utils_1.getBaseUrl)(cfg);
67
+ cfg.quickSpriteBaseUrl = base;
68
+ }
69
+ return cfg ? toSpriteConfigDto(cfg) : null;
70
+ });
71
+ fastify.post("/config/:projectId", async (req) => {
72
+ const { projectId } = req.params;
73
+ const body = req.body;
74
+ if (!body || !body.config) {
75
+ throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.BAD_REQUEST, "Missing config in request body");
76
+ }
77
+ const nextCfg = body.config;
78
+ const nextAssets = (body.assets || {});
79
+ const hasLocalImageRoot = !!String(nextCfg.localImageRoot ?? "").trim();
80
+ const enableQuickSprite = nextCfg.quickSpriteProjectId && nextCfg.quickSpriteEnabled;
81
+ if (!enableQuickSprite && !nextAssets.iconsSvn && !hasLocalImageRoot) {
82
+ throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.BAD_REQUEST, "iconsSvn asset or localImageRoot is required");
83
+ }
84
+ if (nextCfg.localDir && nextAssets.iconsSvn) {
85
+ nextAssets.iconsSvn.localDir = node_path_1.default.join(nextCfg.localDir, nextAssets.iconsSvn.label || "icons");
86
+ if (nextAssets.cutImageSvn) {
87
+ nextAssets.cutImageSvn.localDir = node_path_1.default.join(nextCfg.localDir, nextAssets.cutImageSvn.label || "images");
88
+ }
89
+ }
90
+ const shouldUpdateAssets = !!(nextAssets.iconsSvn || nextAssets.cutImageSvn);
91
+ const project = shouldUpdateAssets
92
+ ? await fastify.core.project.updateAssets(projectId, nextAssets)
93
+ : await fastify.core.project.get(projectId);
94
+ if (project.assets?.iconsSvn) {
95
+ nextCfg.sourceId = project.assets.iconsSvn.id;
96
+ }
97
+ const cfg = await fastify.core.sprite.createConfig(projectId, nextCfg);
98
+ const assets = project.assets
99
+ ? {
100
+ iconsSvn: project.assets.iconsSvn,
101
+ cutImageSvn: project.assets.cutImageSvn,
102
+ }
103
+ : undefined;
104
+ return {
105
+ cfg: toSpriteConfigDto(cfg),
106
+ project: { id: project.id, name: project.name, root: project.root, assets },
107
+ };
108
+ });
109
+ fastify.post("/generate/:projectId", async (req) => {
110
+ const { projectId } = req.params;
111
+ const body = req.body || {};
112
+ const quickProjectId = await (0, sprite_quick_utils_1.resolveEnabledRemoteProjectId)(fastify, projectId);
113
+ if (quickProjectId) {
114
+ const localCfg = await fastify.core.sprite.getConfig(projectId);
115
+ const baseUrl = (0, sprite_quick_utils_1.getBaseUrl)(localCfg);
116
+ const [results, remoteProj] = await Promise.all([
117
+ (0, sprite_quick_utils_1.quickFetch)(fastify, baseUrl, `/api/project-sprites?projectId=${encodeURIComponent(quickProjectId)}`),
118
+ (0, sprite_quick_utils_1.fetchRemoteProject)(fastify, baseUrl, quickProjectId),
119
+ ]);
120
+ if (!results?.length) {
121
+ throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.NOT_FOUND, `远端项目「${quickProjectId}」尚未生成任何雪碧图,请先在远端服务中为该项目生成雪碧图后再试`);
122
+ }
123
+ return (0, sprite_quick_utils_1.mapQuickGroupsToSnapshot)(projectId, results, remoteProj, localCfg);
124
+ }
125
+ const result = await fastify.core.sprite.generate(projectId, {
126
+ groups: body.groups,
127
+ forceRefresh: !!body.forceRefresh,
128
+ concurrency: body.concurrency ?? 1,
129
+ continueOnError: body.continueOnError ?? true,
130
+ });
131
+ return toGenerateSpriteResultDto(result);
132
+ });
133
+ fastify.get("/list/:projectId", async (req) => {
134
+ const { projectId } = req.params;
135
+ const local = String(req.query?.local ?? "").toLowerCase() === "true";
136
+ const quickProjectId = await (0, sprite_quick_utils_1.resolveEnabledRemoteProjectId)(fastify, projectId);
137
+ if (quickProjectId) {
138
+ const localCfg = await fastify.core.sprite.getConfig(projectId);
139
+ const baseUrl = (0, sprite_quick_utils_1.getBaseUrl)(localCfg);
140
+ const [results, remoteProj] = await Promise.all([
141
+ (0, sprite_quick_utils_1.quickFetch)(fastify, baseUrl, `/api/project-sprites?projectId=${encodeURIComponent(quickProjectId)}`),
142
+ (0, sprite_quick_utils_1.fetchRemoteProject)(fastify, baseUrl, quickProjectId),
143
+ ]);
144
+ return (0, sprite_quick_utils_1.mapQuickGroupsToSnapshot)(projectId, results, remoteProj, localCfg);
145
+ }
146
+ const snapshot = await fastify.core.sprite.getSprites(projectId, local);
147
+ return toSpriteSnapshotDto(snapshot);
148
+ });
149
+ fastify.get("/quick/projects", async (_req) => {
150
+ const baseUrl = (0, sprite_quick_utils_1.getBaseUrl)();
151
+ const projects = await (0, sprite_quick_utils_1.quickFetch)(fastify, baseUrl, "/api/projects");
152
+ return projects;
153
+ });
154
+ fastify.get("/quick/groups/:projectId", async (req) => {
155
+ const { projectId } = req.params;
156
+ const baseUrl = (0, sprite_quick_utils_1.getBaseUrl)();
157
+ const groups = await (0, sprite_quick_utils_1.quickFetch)(fastify, baseUrl, `/api/groups?projectId=${encodeURIComponent(projectId)}`);
158
+ return groups;
159
+ });
160
+ fastify.get("/proxy/:projectId/:group.png", async (req, reply) => {
161
+ const { projectId, group } = req.params;
162
+ const cfg = await fastify.core.sprite.getConfig(projectId);
163
+ const quickProjectId = cfg?.quickSpriteProjectId;
164
+ if (!quickProjectId) {
165
+ throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.NOT_FOUND, `项目「${projectId}」未配置快捷雪碧图远端映射`);
166
+ }
167
+ const baseUrl = (0, sprite_quick_utils_1.getBaseUrl)(cfg);
168
+ const remoteUrl = `${baseUrl}/sprites/${encodeURIComponent(quickProjectId)}/${encodeURIComponent(group)}.png`;
169
+ fastify.log.info(`[sprite-proxy] → GET ${remoteUrl}`);
170
+ let response;
171
+ try {
172
+ response = await fetch(remoteUrl);
173
+ }
174
+ catch (err) {
175
+ fastify.log.error(`[sprite-proxy] ✗ ${remoteUrl}: ${err?.message || err}`);
176
+ throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.INTERNAL_ERROR, `无法连接远端雪碧图服务: ${err?.message || err}`);
177
+ }
178
+ if (!response.ok) {
179
+ throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.NOT_FOUND, `远端雪碧图不存在: ${response.status}`);
180
+ }
181
+ (0, sprite_quick_utils_1.copyRawResponseHeaders)(response, reply);
182
+ const body = response.body;
183
+ if (!body) {
184
+ return reply.status(response.status).send();
185
+ }
186
+ return reply.status(response.status).send(node_stream_1.Readable.fromWeb(body));
187
+ });
188
+ fastify.get("/misc-proxy/:quickProjectId/*", async (req, reply) => {
189
+ const { quickProjectId } = req.params;
190
+ const filename = req.params["*"];
191
+ const baseUrl = (0, sprite_quick_utils_1.getBaseUrl)();
192
+ const remoteUrl = (0, sprite_quick_utils_1.buildRemoteMiscUrl)(baseUrl, quickProjectId, filename);
193
+ fastify.log.info(`[sprite-misc-proxy] → GET ${remoteUrl}`);
194
+ let response;
195
+ try {
196
+ response = await fetch(remoteUrl);
197
+ }
198
+ catch (err) {
199
+ fastify.log.error(`[sprite-misc-proxy] ✗ ${remoteUrl}: ${err?.message || err}`);
200
+ throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.INTERNAL_ERROR, `无法连接远端切图服务: ${err?.message || err}`);
201
+ }
202
+ if (!response.ok) {
203
+ throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.NOT_FOUND, `远端切图不存在: ${response.status}`);
204
+ }
205
+ (0, sprite_quick_utils_1.copyRawResponseHeaders)(response, reply);
206
+ const body = response.body;
207
+ if (!body) {
208
+ return reply.status(response.status).send();
209
+ }
210
+ return reply.status(response.status).send(node_stream_1.Readable.fromWeb(body));
211
+ });
212
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yinuo-ngm/server",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "a server package for Yinuo NG Manager",
5
5
  "author": "ZhangJing <892295834@qq.com>",
6
6
  "license": "ISC",
@@ -28,11 +28,11 @@
28
28
  "dependencies": {
29
29
  "@fastify/static": "^9.0.0",
30
30
  "@fastify/websocket": "^11.2.0",
31
- "@yinuo-ngm/api": "^0.1.7",
32
- "@yinuo-ngm/core": "^0.1.13",
31
+ "@yinuo-ngm/api": "^0.1.9",
32
+ "@yinuo-ngm/core": "^0.1.15",
33
33
  "@yinuo-ngm/errors": "^1.0.2",
34
34
  "@yinuo-ngm/nginx": "^0.1.4",
35
- "@yinuo-ngm/protocol": "^0.1.2",
35
+ "@yinuo-ngm/protocol": "^0.1.4",
36
36
  "fastify": "^5.6.2",
37
37
  "fastify-plugin": "^5.1.0",
38
38
  "launch-editor": "^2.12.0",
@@ -43,5 +43,5 @@
43
43
  "@types/mime-types": "^3.0.1",
44
44
  "tsx": "^4.21.0"
45
45
  },
46
- "gitHead": "3f4445714fe3483f9b92687b700100dcdd621614"
46
+ "gitHead": "9c45bd57798a539e3b6cf40dd56f7cdffdc02589"
47
47
  }