lanhu-layer-tree 1.0.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.
- package/.env.example +11 -0
- package/README.md +317 -0
- package/dist/assets.d.ts +38 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/assets.js +247 -0
- package/dist/assets.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +322 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +44 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +441 -0
- package/dist/client.js.map +1 -0
- package/dist/cookie.d.ts +32 -0
- package/dist/cookie.d.ts.map +1 -0
- package/dist/cookie.js +177 -0
- package/dist/cookie.js.map +1 -0
- package/dist/formatter.d.ts +6 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +225 -0
- package/dist/formatter.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/merger.d.ts +27 -0
- package/dist/merger.d.ts.map +1 -0
- package/dist/merger.js +141 -0
- package/dist/merger.js.map +1 -0
- package/dist/resolver.d.ts +27 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/resolver.js +196 -0
- package/dist/resolver.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +23 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +106 -0
- package/dist/utils.js.map +1 -0
- package/package.json +38 -0
- package/src/assets.ts +221 -0
- package/src/cli.ts +333 -0
- package/src/client.ts +490 -0
- package/src/cookie.ts +156 -0
- package/src/formatter.ts +251 -0
- package/src/index.ts +4 -0
- package/src/merger.ts +154 -0
- package/src/resolver.ts +195 -0
- package/src/types.ts +94 -0
- package/src/utils.ts +120 -0
- package/tsconfig.json +20 -0
package/src/assets.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 资源处理:图片尺寸读取、Cocos/Unity meta 文件处理
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as crypto from 'crypto';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 生成 UUID v4
|
|
11
|
+
*/
|
|
12
|
+
function generateUuid(): string {
|
|
13
|
+
return crypto.randomUUID();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 读取 PNG 图片尺寸
|
|
18
|
+
*/
|
|
19
|
+
export function readPngSize(filePath: string): [number, number] {
|
|
20
|
+
const fd = fs.openSync(filePath, 'r');
|
|
21
|
+
try {
|
|
22
|
+
const buffer = Buffer.alloc(24);
|
|
23
|
+
fs.readSync(fd, buffer, 0, 24, 0);
|
|
24
|
+
// PNG: 8 字节签名 + 4 字节长度 + 4 字节类型 + 4 字节宽 + 4 字节高
|
|
25
|
+
const width = buffer.readUInt32BE(16);
|
|
26
|
+
const height = buffer.readUInt32BE(20);
|
|
27
|
+
return [width, height];
|
|
28
|
+
} finally {
|
|
29
|
+
fs.closeSync(fd);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 读取 JPEG 图片尺寸
|
|
35
|
+
*/
|
|
36
|
+
export function readJpegSize(filePath: string): [number, number] {
|
|
37
|
+
const data = fs.readFileSync(filePath);
|
|
38
|
+
let i = 2;
|
|
39
|
+
while (i < data.length) {
|
|
40
|
+
if (data[i] !== 0xff) break;
|
|
41
|
+
const marker = data[i + 1];
|
|
42
|
+
const length = data.readUInt16BE(i + 2);
|
|
43
|
+
if (marker === 0xc0 || marker === 0xc1 || marker === 0xc2) {
|
|
44
|
+
const height = data.readUInt16BE(i + 5);
|
|
45
|
+
const width = data.readUInt16BE(i + 7);
|
|
46
|
+
return [width, height];
|
|
47
|
+
}
|
|
48
|
+
i += 2 + length;
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`无法读取 JPEG 尺寸: ${filePath}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 获取图片尺寸(支持 PNG/JPEG/WEBP)
|
|
55
|
+
*/
|
|
56
|
+
export function getImageSize(filePath: string): [number, number] {
|
|
57
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
58
|
+
if (ext === '.png') return readPngSize(filePath);
|
|
59
|
+
if (ext === '.jpg' || ext === '.jpeg') return readJpegSize(filePath);
|
|
60
|
+
|
|
61
|
+
// WEBP (VP8)
|
|
62
|
+
const data = Buffer.alloc(30);
|
|
63
|
+
const fd = fs.openSync(filePath, 'r');
|
|
64
|
+
try {
|
|
65
|
+
fs.readSync(fd, data, 0, 30, 0);
|
|
66
|
+
} finally {
|
|
67
|
+
fs.closeSync(fd);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (
|
|
71
|
+
data.slice(0, 4).toString() === 'RIFF' &&
|
|
72
|
+
data.slice(8, 12).toString() === 'WEBP' &&
|
|
73
|
+
data.slice(12, 16).toString() === 'VP8 '
|
|
74
|
+
) {
|
|
75
|
+
const width = data.readUInt16LE(26) & 0x3fff;
|
|
76
|
+
const height = data.readUInt16LE(28) & 0x3fff;
|
|
77
|
+
return [width, height];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
throw new Error(`不支持的图片格式: ${filePath}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 生成 Cocos Creator .meta 文件内容
|
|
85
|
+
*/
|
|
86
|
+
export function makeCocosMeta(imgPath: string): any {
|
|
87
|
+
const [w, h] = getImageSize(imgPath);
|
|
88
|
+
const textureUuid = generateUuid();
|
|
89
|
+
const spriteUuid = generateUuid();
|
|
90
|
+
const stem = path.basename(imgPath, path.extname(imgPath));
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
ver: '2.3.5',
|
|
94
|
+
uuid: textureUuid,
|
|
95
|
+
type: 'sprite',
|
|
96
|
+
wrapMode: 'clamp',
|
|
97
|
+
filterMode: 'bilinear',
|
|
98
|
+
premultiplyAlpha: false,
|
|
99
|
+
genMipmaps: false,
|
|
100
|
+
packable: true,
|
|
101
|
+
width: w,
|
|
102
|
+
height: h,
|
|
103
|
+
platformSettings: {},
|
|
104
|
+
subMetas: {
|
|
105
|
+
[stem]: {
|
|
106
|
+
ver: '1.0.4',
|
|
107
|
+
uuid: spriteUuid,
|
|
108
|
+
rawTextureUuid: textureUuid,
|
|
109
|
+
trimType: 'auto',
|
|
110
|
+
trimThreshold: 1,
|
|
111
|
+
rotated: false,
|
|
112
|
+
offsetX: 0,
|
|
113
|
+
offsetY: 0,
|
|
114
|
+
trimX: 0,
|
|
115
|
+
trimY: 0,
|
|
116
|
+
width: w,
|
|
117
|
+
height: h,
|
|
118
|
+
rawWidth: w,
|
|
119
|
+
rawHeight: h,
|
|
120
|
+
borderTop: 0,
|
|
121
|
+
borderBottom: 0,
|
|
122
|
+
borderLeft: 0,
|
|
123
|
+
borderRight: 0,
|
|
124
|
+
subMetas: {}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 从 Cocos .meta 文件中读取 spriteFrame UUID
|
|
132
|
+
*/
|
|
133
|
+
export function readCocosSpriteUuid(metaPath: string): string | null {
|
|
134
|
+
try {
|
|
135
|
+
const text = fs.readFileSync(metaPath, 'utf-8');
|
|
136
|
+
const data = JSON.parse(text);
|
|
137
|
+
const sub = data.subMetas || {};
|
|
138
|
+
const keys = Object.keys(sub);
|
|
139
|
+
if (keys.length > 0) {
|
|
140
|
+
return sub[keys[0]].uuid || null;
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// ignore
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface UnityMetaInfo {
|
|
149
|
+
guid: string;
|
|
150
|
+
sliced: boolean;
|
|
151
|
+
border: [number, number, number, number] | null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 从 Unity YAML .meta 文件中读取 guid 和 spriteBorder
|
|
156
|
+
* Unity spriteBorder 格式: x=left, y=bottom, z=right, w=top
|
|
157
|
+
*/
|
|
158
|
+
export function readUnityMeta(metaPath: string): UnityMetaInfo | null {
|
|
159
|
+
try {
|
|
160
|
+
const text = fs.readFileSync(metaPath, 'utf-8');
|
|
161
|
+
const guidMatch = text.match(/^guid:\s*([0-9a-f]+)/m);
|
|
162
|
+
if (!guidMatch) return null;
|
|
163
|
+
const guid = guidMatch[1];
|
|
164
|
+
|
|
165
|
+
let sliced = false;
|
|
166
|
+
let border: [number, number, number, number] | null = null;
|
|
167
|
+
|
|
168
|
+
const borderMatch = text.match(
|
|
169
|
+
/spriteBorder:\s*\{x:\s*([\d.]+),\s*y:\s*([\d.]+),\s*z:\s*([\d.]+),\s*w:\s*([\d.]+)\}/
|
|
170
|
+
);
|
|
171
|
+
if (borderMatch) {
|
|
172
|
+
const values = [
|
|
173
|
+
parseFloat(borderMatch[1]),
|
|
174
|
+
parseFloat(borderMatch[2]),
|
|
175
|
+
parseFloat(borderMatch[3]),
|
|
176
|
+
parseFloat(borderMatch[4])
|
|
177
|
+
];
|
|
178
|
+
sliced = values.some(v => v !== 0);
|
|
179
|
+
if (sliced) {
|
|
180
|
+
border = [
|
|
181
|
+
Math.round(values[0]),
|
|
182
|
+
Math.round(values[1]),
|
|
183
|
+
Math.round(values[2]),
|
|
184
|
+
Math.round(values[3])
|
|
185
|
+
];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { guid, sliced, border };
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 在 assetsDir 下递归搜索同名文件
|
|
197
|
+
*/
|
|
198
|
+
export function findLocalAsset(filename: string, assetsDir: string): string | null {
|
|
199
|
+
if (!fs.existsSync(assetsDir)) return null;
|
|
200
|
+
|
|
201
|
+
const stack: string[] = [assetsDir];
|
|
202
|
+
while (stack.length > 0) {
|
|
203
|
+
const current = stack.pop()!;
|
|
204
|
+
let entries: fs.Dirent[];
|
|
205
|
+
try {
|
|
206
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
207
|
+
} catch {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
const fullPath = path.join(current, entry.name);
|
|
213
|
+
if (entry.isDirectory()) {
|
|
214
|
+
stack.push(fullPath);
|
|
215
|
+
} else if (entry.isFile() && entry.name === filename) {
|
|
216
|
+
return fullPath;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { config } from 'dotenv';
|
|
5
|
+
import { LanhuClient } from './client';
|
|
6
|
+
import { formatLayersTree } from './formatter';
|
|
7
|
+
import { parseUrl } from './utils';
|
|
8
|
+
import { calculateLayerSimilarity, mergeLayers } from './merger';
|
|
9
|
+
import { refreshCookieFromChrome } from './cookie';
|
|
10
|
+
import * as fs from 'fs/promises';
|
|
11
|
+
import { existsSync, readFileSync } from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
|
|
14
|
+
// 加载 .env 文件
|
|
15
|
+
config();
|
|
16
|
+
|
|
17
|
+
const program = new Command();
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.name('lanhu')
|
|
21
|
+
.description('蓝湖设计图图层树提取工具')
|
|
22
|
+
.version('1.0.0');
|
|
23
|
+
|
|
24
|
+
// list 命令:列出项目中的所有设计图
|
|
25
|
+
program
|
|
26
|
+
.command('list')
|
|
27
|
+
.description('列出项目中的所有设计图')
|
|
28
|
+
.argument('<url>', '蓝湖项目 URL')
|
|
29
|
+
.option('-o, --output <file>', '输出文件路径')
|
|
30
|
+
.action(async (url: string, options: { output?: string }) => {
|
|
31
|
+
try {
|
|
32
|
+
const client = new LanhuClient();
|
|
33
|
+
const result = await client.getDesigns(url);
|
|
34
|
+
const output = JSON.stringify(result, null, 2);
|
|
35
|
+
if (options.output) {
|
|
36
|
+
await fs.writeFile(options.output, output, 'utf-8');
|
|
37
|
+
console.log(`✅ 设计图列表已保存到: ${options.output}`);
|
|
38
|
+
} else {
|
|
39
|
+
console.log(output);
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('❌ 错误:', error instanceof Error ? error.message : error);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// layers 命令:获取指定设计图的图层信息
|
|
48
|
+
program
|
|
49
|
+
.command('layers')
|
|
50
|
+
.description('获取指定设计图的图层信息')
|
|
51
|
+
.argument('<url>', '蓝湖设计图 URL(如果包含 image_id 参数,可省略 design-id)')
|
|
52
|
+
.argument('[design-id]', '设计图 ID(可选)')
|
|
53
|
+
.option('-o, --output <file>', '输出文件路径')
|
|
54
|
+
.option('-t, --tree', '以树形结构输出图层信息')
|
|
55
|
+
.option('-e, --engine <type>', '引擎类型(unity/cocos),转换为引擎坐标系')
|
|
56
|
+
.option('-a, --assets-dir <dir>', '资源目录路径,用于解析资源 UUID')
|
|
57
|
+
.action(async (
|
|
58
|
+
urlArg: string,
|
|
59
|
+
designId: string | undefined,
|
|
60
|
+
options: {
|
|
61
|
+
output?: string;
|
|
62
|
+
tree?: boolean;
|
|
63
|
+
engine?: 'unity' | 'cocos';
|
|
64
|
+
assetsDir?: string;
|
|
65
|
+
}
|
|
66
|
+
) => {
|
|
67
|
+
try {
|
|
68
|
+
if (options.engine && !['unity', 'cocos'].includes(options.engine)) {
|
|
69
|
+
console.error(`❌ 不支持的引擎类型: ${options.engine},仅支持 unity/cocos`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 支持 @file 语法读取 URL
|
|
74
|
+
let urlInput = urlArg;
|
|
75
|
+
if (urlInput.startsWith('@')) {
|
|
76
|
+
const urlFile = urlInput.slice(1);
|
|
77
|
+
if (!existsSync(urlFile)) {
|
|
78
|
+
console.error(`❌ URL 文件不存在: ${urlFile}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
urlInput = readFileSync(urlFile, 'utf-8').trim();
|
|
82
|
+
console.log(`✅ 从文件读取 URL: ${urlFile}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 支持多 URL(逗号分隔)
|
|
86
|
+
const urls = urlInput.split(',').map(u => u.trim()).filter(Boolean);
|
|
87
|
+
|
|
88
|
+
const client = new LanhuClient();
|
|
89
|
+
|
|
90
|
+
if (urls.length === 1) {
|
|
91
|
+
const singleUrl = urls[0];
|
|
92
|
+
let did = designId;
|
|
93
|
+
if (!did) {
|
|
94
|
+
const params = parseUrl(singleUrl);
|
|
95
|
+
did = params.doc_id;
|
|
96
|
+
if (!did) {
|
|
97
|
+
console.error('❌ URL 中未找到 image_id 参数,请手动指定设计图ID');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const result = await client.getDesignLayers(singleUrl, did);
|
|
103
|
+
let output: string;
|
|
104
|
+
if (options.tree) {
|
|
105
|
+
output = formatLayersTree(
|
|
106
|
+
result.layers,
|
|
107
|
+
result.design_name,
|
|
108
|
+
result.canvas_size.width,
|
|
109
|
+
result.canvas_size.height,
|
|
110
|
+
{ engine: options.engine, assetsDir: options.assetsDir }
|
|
111
|
+
);
|
|
112
|
+
} else {
|
|
113
|
+
output = JSON.stringify(result, null, 2);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (options.output) {
|
|
117
|
+
await fs.writeFile(options.output, output, 'utf-8');
|
|
118
|
+
console.log(`✅ 图层信息已保存到: ${options.output}`);
|
|
119
|
+
} else {
|
|
120
|
+
console.log(output);
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 多 URL:相似度 + 合并
|
|
126
|
+
console.log(`🔍 检测到 ${urls.length} 个 URL,开始获取图层信息...`);
|
|
127
|
+
const allResults = [];
|
|
128
|
+
const allLayers: any[][] = [];
|
|
129
|
+
const designNames: string[] = [];
|
|
130
|
+
|
|
131
|
+
for (let idx = 0; idx < urls.length; idx++) {
|
|
132
|
+
const u = urls[idx];
|
|
133
|
+
console.log(`\n📥 [${idx + 1}/${urls.length}] 获取图层: ${u.slice(0, 80)}...`);
|
|
134
|
+
const params = parseUrl(u);
|
|
135
|
+
const did = params.doc_id;
|
|
136
|
+
if (!did) {
|
|
137
|
+
console.error(`❌ URL ${idx + 1} 中未找到 image_id 参数`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
const r = await client.getDesignLayers(u, did);
|
|
141
|
+
allResults.push(r);
|
|
142
|
+
allLayers.push(r.layers);
|
|
143
|
+
designNames.push(r.design_name || `Design${idx + 1}`);
|
|
144
|
+
console.log(`✅ 获取成功: ${r.design_name} (${r.total_layers} 个图层)`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`\n🔍 计算图层相似度...`);
|
|
148
|
+
const similarities: number[] = [];
|
|
149
|
+
for (let i = 0; i < allLayers.length - 1; i++) {
|
|
150
|
+
const sim = calculateLayerSimilarity(allLayers[i], allLayers[i + 1]);
|
|
151
|
+
similarities.push(sim);
|
|
152
|
+
console.log(` ${designNames[i]} vs ${designNames[i + 1]}: ${sim.toFixed(2)}%`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const minSim = similarities.length > 0 ? Math.min(...similarities) : 100;
|
|
156
|
+
console.log(`\n📊 最低相似度: ${minSim.toFixed(2)}%`);
|
|
157
|
+
if (minSim < 80) {
|
|
158
|
+
console.error(`\n❌ 错误: 图层相似度 (${minSim.toFixed(2)}%) 低于阈值 (80%),无法合并`);
|
|
159
|
+
console.error('提示: 请确保所有设计图具有相似的图层结构');
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(`\n🔄 相似度检查通过,开始合并图层...`);
|
|
164
|
+
const merged = mergeLayers(allLayers, designNames);
|
|
165
|
+
console.log(`✅ 合并完成: ${merged.layers.length} 个图层`);
|
|
166
|
+
|
|
167
|
+
const canvasSize = allResults[0].canvas_size;
|
|
168
|
+
const mergedResult = {
|
|
169
|
+
design_id: 'merged',
|
|
170
|
+
design_name: merged.name,
|
|
171
|
+
version: 'merged',
|
|
172
|
+
canvas_size: canvasSize,
|
|
173
|
+
total_layers: merged.layers.length,
|
|
174
|
+
layers: merged.layers,
|
|
175
|
+
source_designs: allResults.map(r => ({
|
|
176
|
+
name: r.design_name,
|
|
177
|
+
id: r.design_id,
|
|
178
|
+
total_layers: r.total_layers
|
|
179
|
+
})),
|
|
180
|
+
similarities: similarities.map((s, i) => ({
|
|
181
|
+
pair: `${designNames[i]} vs ${designNames[i + 1]}`,
|
|
182
|
+
similarity: s
|
|
183
|
+
}))
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
let output: string;
|
|
187
|
+
if (options.tree) {
|
|
188
|
+
output = formatLayersTree(
|
|
189
|
+
merged.layers,
|
|
190
|
+
merged.name,
|
|
191
|
+
canvasSize.width,
|
|
192
|
+
canvasSize.height,
|
|
193
|
+
{ engine: options.engine, assetsDir: options.assetsDir }
|
|
194
|
+
);
|
|
195
|
+
} else {
|
|
196
|
+
output = JSON.stringify(mergedResult, null, 2);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (options.output) {
|
|
200
|
+
await fs.writeFile(options.output, output, 'utf-8');
|
|
201
|
+
console.log(`\n✅ 合并后的图层信息已保存到: ${options.output}`);
|
|
202
|
+
} else {
|
|
203
|
+
console.log(`\n${output}`);
|
|
204
|
+
}
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error('❌ 错误:', error instanceof Error ? error.message : error);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// slices 命令:获取设计图切图列表
|
|
212
|
+
program
|
|
213
|
+
.command('slices')
|
|
214
|
+
.description('获取指定设计图的切图列表')
|
|
215
|
+
.argument('<url>', '蓝湖设计图 URL')
|
|
216
|
+
.argument('[design-id]', '设计图 ID(可选)')
|
|
217
|
+
.option('-o, --output <file>', '输出文件路径(JSON)')
|
|
218
|
+
.option('-d, --download-dir <dir>', '指定目录则批量下载切图到该目录')
|
|
219
|
+
.action(async (
|
|
220
|
+
url: string,
|
|
221
|
+
designId: string | undefined,
|
|
222
|
+
options: { output?: string; downloadDir?: string }
|
|
223
|
+
) => {
|
|
224
|
+
try {
|
|
225
|
+
if (!designId) {
|
|
226
|
+
const params = parseUrl(url);
|
|
227
|
+
designId = params.doc_id;
|
|
228
|
+
if (!designId) {
|
|
229
|
+
console.error('❌ URL 中未找到 image_id 参数,请手动指定设计图ID');
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const client = new LanhuClient();
|
|
235
|
+
const result = await client.getDesignSlices(url, designId);
|
|
236
|
+
|
|
237
|
+
const output = JSON.stringify(result, null, 2);
|
|
238
|
+
if (options.output) {
|
|
239
|
+
await fs.writeFile(options.output, output, 'utf-8');
|
|
240
|
+
console.log(`✅ 切图列表已保存到: ${options.output}`);
|
|
241
|
+
} else if (!options.downloadDir) {
|
|
242
|
+
console.log(output);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (options.downloadDir) {
|
|
246
|
+
console.log(`\n📥 开始批量下载 ${result.total_slices} 个切图到: ${options.downloadDir}`);
|
|
247
|
+
let okCount = 0;
|
|
248
|
+
for (const slice of result.slices) {
|
|
249
|
+
const ext = slice.format || 'png';
|
|
250
|
+
const safeName = slice.name.replace(/[\\/:*?"<>|]/g, '_') || 'slice';
|
|
251
|
+
const outPath = path.join(options.downloadDir, `${safeName}.${ext}`);
|
|
252
|
+
const ok = await client.downloadSlice(slice, outPath);
|
|
253
|
+
if (ok) okCount++;
|
|
254
|
+
}
|
|
255
|
+
console.log(`✅ 下载完成: ${okCount}/${result.total_slices}`);
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error('❌ 错误:', error instanceof Error ? error.message : error);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// download 命令:下载设计图截图
|
|
264
|
+
program
|
|
265
|
+
.command('download')
|
|
266
|
+
.description('下载设计图截图')
|
|
267
|
+
.argument('<url>', '蓝湖设计图 URL')
|
|
268
|
+
.argument('[design-id]', '设计图 ID(可选)')
|
|
269
|
+
.argument('[design-name]', '设计图名称(可选)')
|
|
270
|
+
.option('-d, --output-dir <dir>', '输出目录', '.')
|
|
271
|
+
.action(async (
|
|
272
|
+
url: string,
|
|
273
|
+
designId: string | undefined,
|
|
274
|
+
designName: string | undefined,
|
|
275
|
+
options: { outputDir: string }
|
|
276
|
+
) => {
|
|
277
|
+
try {
|
|
278
|
+
if (!designId) {
|
|
279
|
+
const params = parseUrl(url);
|
|
280
|
+
designId = params.doc_id;
|
|
281
|
+
if (!designId) {
|
|
282
|
+
console.error('❌ URL 中未找到 image_id 参数,请手动指定设计图ID');
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const client = new LanhuClient();
|
|
288
|
+
const designsResult = await client.getDesigns(url);
|
|
289
|
+
|
|
290
|
+
const design = designsResult.designs?.find(d => d.id === designId);
|
|
291
|
+
if (!design) {
|
|
292
|
+
console.error(`❌ 未找到设计图 ID: ${designId}`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const name = designName || design.name;
|
|
297
|
+
const outputPath = path.join(options.outputDir, `${name}.png`);
|
|
298
|
+
|
|
299
|
+
const success = await client.downloadScreenshot(design, outputPath);
|
|
300
|
+
if (success) {
|
|
301
|
+
console.log(`✅ 截图已下载到: ${outputPath}`);
|
|
302
|
+
} else {
|
|
303
|
+
console.error('❌ 下载失败');
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
} catch (error) {
|
|
307
|
+
console.error('❌ 错误:', error instanceof Error ? error.message : error);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// refresh-cookie 命令:从 Chrome 刷新蓝湖 cookie
|
|
313
|
+
program
|
|
314
|
+
.command('refresh-cookie')
|
|
315
|
+
.description('从 Chrome 浏览器刷新蓝湖 cookie 并写入 .env 文件')
|
|
316
|
+
.argument('[env-file]', '.env 文件路径(默认为当前目录的 .env)')
|
|
317
|
+
.action(async (envFile: string | undefined) => {
|
|
318
|
+
try {
|
|
319
|
+
const envPath = envFile ? path.resolve(envFile) : path.resolve(process.cwd(), '.env');
|
|
320
|
+
const cookie = await refreshCookieFromChrome(envPath);
|
|
321
|
+
if (cookie) {
|
|
322
|
+
console.log('✅ Cookie 刷新成功');
|
|
323
|
+
} else {
|
|
324
|
+
console.error('❌ Cookie 刷新失败');
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.error('❌ 错误:', error instanceof Error ? error.message : error);
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
program.parse();
|