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.
Files changed (54) hide show
  1. package/.env.example +11 -0
  2. package/README.md +317 -0
  3. package/dist/assets.d.ts +38 -0
  4. package/dist/assets.d.ts.map +1 -0
  5. package/dist/assets.js +247 -0
  6. package/dist/assets.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +322 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/client.d.ts +44 -0
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/client.js +441 -0
  14. package/dist/client.js.map +1 -0
  15. package/dist/cookie.d.ts +32 -0
  16. package/dist/cookie.d.ts.map +1 -0
  17. package/dist/cookie.js +177 -0
  18. package/dist/cookie.js.map +1 -0
  19. package/dist/formatter.d.ts +6 -0
  20. package/dist/formatter.d.ts.map +1 -0
  21. package/dist/formatter.js +225 -0
  22. package/dist/formatter.js.map +1 -0
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +27 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/merger.d.ts +27 -0
  28. package/dist/merger.d.ts.map +1 -0
  29. package/dist/merger.js +141 -0
  30. package/dist/merger.js.map +1 -0
  31. package/dist/resolver.d.ts +27 -0
  32. package/dist/resolver.d.ts.map +1 -0
  33. package/dist/resolver.js +196 -0
  34. package/dist/resolver.js.map +1 -0
  35. package/dist/types.d.ts +85 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +6 -0
  38. package/dist/types.js.map +1 -0
  39. package/dist/utils.d.ts +23 -0
  40. package/dist/utils.d.ts.map +1 -0
  41. package/dist/utils.js +106 -0
  42. package/dist/utils.js.map +1 -0
  43. package/package.json +38 -0
  44. package/src/assets.ts +221 -0
  45. package/src/cli.ts +333 -0
  46. package/src/client.ts +490 -0
  47. package/src/cookie.ts +156 -0
  48. package/src/formatter.ts +251 -0
  49. package/src/index.ts +4 -0
  50. package/src/merger.ts +154 -0
  51. package/src/resolver.ts +195 -0
  52. package/src/types.ts +94 -0
  53. package/src/utils.ts +120 -0
  54. package/tsconfig.json +20 -0
package/src/client.ts ADDED
@@ -0,0 +1,490 @@
1
+ import axios, { AxiosInstance } from 'axios';
2
+ import { LanhuConfig, ParsedUrl, DesignsResponse, LayersResponse, SlicesResponse, Design, Layer, Slice } from './types';
3
+ import { parseUrl, normalizeColor } from './utils';
4
+ import { buildCookie, refreshCookieFromChrome, AUTH_ERROR_CODES } from './cookie';
5
+
6
+ const BASE_URL = 'https://lanhuapp.com';
7
+ const HTTP_TIMEOUT = 60000;
8
+
9
+ /**
10
+ * 蓝湖 API 客户端
11
+ */
12
+ export class LanhuClient {
13
+ private cookie: string;
14
+ private baseUrl: string;
15
+ private timeout: number;
16
+ private client: AxiosInstance;
17
+ private defaultTid?: string;
18
+ private cookieRefreshAttempted: boolean = false;
19
+ private cookieInitialized: boolean = false;
20
+
21
+ constructor(config: LanhuConfig = {}) {
22
+ this.cookie = config.cookie || buildCookie();
23
+ this.baseUrl = config.baseUrl || BASE_URL;
24
+ this.timeout = config.timeout || HTTP_TIMEOUT;
25
+ this.defaultTid = process.env.TID;
26
+
27
+ // 如果构造时已有 cookie,标记为已初始化
28
+ if (this.cookie && this.cookie !== 'your_lanhu_cookie_here') {
29
+ this.cookieInitialized = true;
30
+ }
31
+
32
+ this.client = this.buildClient();
33
+ }
34
+
35
+ /**
36
+ * 异步初始化:当 cookie 缺失时自动尝试从 Chrome 读取(仅首次)
37
+ */
38
+ async ensureCookie(): Promise<void> {
39
+ if (this.cookieInitialized) return;
40
+
41
+ console.log('🔄 未找到蓝湖 cookie 配置,尝试从 Chrome 自动获取...');
42
+ const refreshed = await refreshCookieFromChrome();
43
+ if (!refreshed) {
44
+ throw new Error(
45
+ '❌ 无法获取蓝湖 cookie。请确保:\n' +
46
+ ' 1. Chrome 浏览器已登录 lanhuapp.com\n' +
47
+ ' 2. 已安装 chrome-cookies-secure: npm install chrome-cookies-secure\n' +
48
+ ' 或手动在 .env 文件中配置 SESSION 和 USER_TOKEN'
49
+ );
50
+ }
51
+ this.cookie = refreshed;
52
+ this.cookieInitialized = true;
53
+ this.client = this.buildClient();
54
+ }
55
+
56
+ private buildClient(): AxiosInstance {
57
+ return axios.create({
58
+ timeout: this.timeout,
59
+ headers: {
60
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
61
+ 'Referer': 'https://lanhuapp.com/web/',
62
+ 'Accept': 'application/json, text/plain, */*',
63
+ 'Cookie': this.cookie,
64
+ 'sec-ch-ua': '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"',
65
+ 'sec-ch-ua-mobile': '?0',
66
+ 'sec-ch-ua-platform': '"macOS"',
67
+ 'request-from': 'web',
68
+ 'real-path': '/item/project/product'
69
+ }
70
+ });
71
+ }
72
+
73
+ /**
74
+ * 带认证错误自动刷新的 GET 请求
75
+ */
76
+ private async requestGet(url: string, requestConfig: any = {}): Promise<any> {
77
+ await this.ensureCookie();
78
+ let response = await this.client.get(url, requestConfig);
79
+ const data = response.data;
80
+ const code = data && typeof data === 'object' ? String(data.code ?? '') : '';
81
+
82
+ if (AUTH_ERROR_CODES.has(code) && !this.cookieRefreshAttempted) {
83
+ this.cookieRefreshAttempted = true;
84
+ console.log(`🔄 蓝湖 API 认证错误 (code=${code}),尝试刷新 cookie...`);
85
+ const newCookie = await refreshCookieFromChrome();
86
+ if (newCookie) {
87
+ this.cookie = newCookie;
88
+ this.client = this.buildClient();
89
+ response = await this.client.get(url, requestConfig);
90
+ }
91
+ this.cookieRefreshAttempted = false;
92
+ }
93
+ return response;
94
+ }
95
+
96
+ /**
97
+ * 获取设计图列表
98
+ */
99
+ async getDesigns(url: string): Promise<DesignsResponse> {
100
+ const params = parseUrl(url, this.defaultTid);
101
+ const apiUrl = `${this.baseUrl}/api/project/images`;
102
+
103
+ const response = await this.requestGet(apiUrl, {
104
+ params: {
105
+ project_id: params.project_id,
106
+ team_id: params.team_id,
107
+ dds_status: 1,
108
+ position: 1,
109
+ show_cb_src: 1,
110
+ comment: 1
111
+ }
112
+ });
113
+
114
+ const data = response.data;
115
+
116
+ if (data.code !== '00000') {
117
+ return {
118
+ status: 'error',
119
+ message: data.msg || 'Unknown error'
120
+ };
121
+ }
122
+
123
+ const projectData = data.data || {};
124
+ const images = projectData.images || [];
125
+ const designList: Design[] = images.map((img: any, idx: number) => ({
126
+ index: idx + 1,
127
+ id: img.id,
128
+ name: img.name,
129
+ width: img.width,
130
+ height: img.height,
131
+ url: img.url,
132
+ update_time: img.update_time
133
+ }));
134
+
135
+ return {
136
+ status: 'success',
137
+ project_name: projectData.name,
138
+ total_designs: designList.length,
139
+ designs: designList
140
+ };
141
+ }
142
+
143
+ /**
144
+ * 获取设计图图层信息
145
+ */
146
+ async getDesignLayers(url: string, designId: string): Promise<LayersResponse> {
147
+ const params = parseUrl(url, this.defaultTid);
148
+ const apiUrl = `${this.baseUrl}/api/project/image`;
149
+
150
+ const response = await this.requestGet(apiUrl, {
151
+ params: {
152
+ dds_status: 1,
153
+ image_id: designId,
154
+ team_id: params.team_id,
155
+ project_id: params.project_id
156
+ }
157
+ });
158
+
159
+ const data = response.data;
160
+
161
+ if (data.code !== '00000') {
162
+ throw new Error(`Failed to get design: ${data.msg || 'Unknown error'}`);
163
+ }
164
+
165
+ const result = data.result;
166
+ const latestVersion = result.versions[0];
167
+ const jsonUrl = latestVersion.json_url;
168
+
169
+ // 获取 sketch 数据
170
+ const jsonResponse = await this.requestGet(jsonUrl);
171
+ const sketchData = jsonResponse.data;
172
+
173
+ // 解析画板尺寸
174
+ let abLeft = 0, abTop = 0, abWidth = 0, abHeight = 0;
175
+ const board = sketchData.board || {};
176
+ if (board.artboard?.artboardRect) {
177
+ const rect = board.artboard.artboardRect;
178
+ abLeft = rect.left || 0;
179
+ abTop = rect.top || 0;
180
+ abWidth = (rect.right || 0) - abLeft;
181
+ abHeight = (rect.bottom || 0) - abTop;
182
+ }
183
+
184
+ const layers: Layer[] = [];
185
+ const seenIds = new Set<string>();
186
+
187
+ // 递归提取图层
188
+ const extractLayer = (obj: any, depth: number = 0, parentPath: string = '') => {
189
+ if (!obj || typeof obj !== 'object') return;
190
+
191
+ const name = obj.name || '';
192
+ const ltype = obj.type || '';
193
+ const visible = obj.visible !== false;
194
+
195
+ if (!name || !visible) {
196
+ if (obj.layers) {
197
+ obj.layers.forEach((sub: any) => extractLayer(sub, depth, parentPath));
198
+ }
199
+ return;
200
+ }
201
+
202
+ const dedupKey = `${obj.id}_${name}_${depth}`;
203
+ if (seenIds.has(dedupKey)) return;
204
+ seenIds.add(dedupKey);
205
+
206
+ // 使用 ID 确保 path 唯一性
207
+ const layerId = obj.id || '';
208
+ const pathSegment = layerId ? `${name}#${layerId}` : name;
209
+ const currentPath = parentPath ? `${parentPath}/${pathSegment}` : pathSegment;
210
+
211
+ let w = obj.width || 0;
212
+ let h = obj.height || 0;
213
+ const bounds = obj._orgBounds || obj.bounds || obj.boundsWithFX || {};
214
+ let x = (bounds.left || 0) - abLeft;
215
+ let y = (bounds.top || 0) - abTop;
216
+
217
+ if (!w && bounds.right && bounds.left) {
218
+ w = bounds.right - bounds.left;
219
+ }
220
+ if (!h && bounds.bottom && bounds.top) {
221
+ h = bounds.bottom - bounds.top;
222
+ }
223
+
224
+ const layerInfo: Layer = {
225
+ name,
226
+ type: ltype,
227
+ width: Math.round(w),
228
+ height: Math.round(h),
229
+ x: Math.round(x),
230
+ y: Math.round(y),
231
+ path: currentPath,
232
+ depth
233
+ };
234
+
235
+ // smartObject.originalFileName
236
+ if (obj.smartObject) {
237
+ layerInfo.smart_object = obj.smartObject.originalFileName || name;
238
+ }
239
+
240
+ // 处理文本图层
241
+ if (ltype === 'textLayer') {
242
+ const textInfo = obj.textInfo || {};
243
+ const textData = obj.text;
244
+
245
+ if (textInfo.text) {
246
+ layerInfo.text = textInfo.text;
247
+ } else if (textData && typeof textData === 'object' && textData.textKey) {
248
+ layerInfo.text = textData.textKey;
249
+ }
250
+
251
+ const textStyle: any = {};
252
+ if (textInfo.color) textStyle.fill_color = normalizeColor(textInfo.color);
253
+ if (textInfo.size) textStyle.font_size = textInfo.size;
254
+ if (textInfo.fontName) textStyle.font_name = textInfo.fontName;
255
+
256
+ // 从 textStyleRange 获取样式
257
+ const textStyleRange = textInfo.textStyleRange || [];
258
+ if (textStyleRange.length > 0) {
259
+ const ts = textStyleRange[0].textStyle || {};
260
+ if (!textStyle.fill_color && ts.color) {
261
+ textStyle.fill_color = normalizeColor(ts.color);
262
+ }
263
+ if (!textStyle.font_size && ts.size) {
264
+ textStyle.font_size = typeof ts.size === 'object' ? ts.size.value : ts.size;
265
+ }
266
+ if (!textStyle.font_name && ts.fontName) {
267
+ textStyle.font_name = ts.fontName;
268
+ }
269
+ }
270
+
271
+ // layerEffects.frameFX → 描边
272
+ const layerFx = obj.layerEffects || {};
273
+ const frameFx = layerFx.frameFX || {};
274
+ if (frameFx.enabled && frameFx.color) {
275
+ textStyle.stroke_color = normalizeColor(frameFx.color);
276
+ if (frameFx.size) {
277
+ textStyle.stroke_width = frameFx.size;
278
+ }
279
+ }
280
+
281
+ // layerEffects.dropShadow → 阴影
282
+ const dropShadow = layerFx.dropShadow || {};
283
+ if (dropShadow.enabled && dropShadow.color) {
284
+ textStyle.shadow_color = normalizeColor(dropShadow.color);
285
+ }
286
+
287
+ // obj.textStyle 后备字段
288
+ const layerTs = obj.textStyle || {};
289
+ if (layerTs) {
290
+ if (!textStyle.fill_color && layerTs.color) {
291
+ textStyle.fill_color = normalizeColor(layerTs.color);
292
+ }
293
+ if (!textStyle.font_size && layerTs.fontSize) {
294
+ textStyle.font_size = layerTs.fontSize;
295
+ }
296
+ if (!textStyle.font_name && layerTs.fontName) {
297
+ textStyle.font_name = layerTs.fontName;
298
+ }
299
+ }
300
+
301
+ // obj.fills[*].color 作为 fill_color 后备
302
+ if (!textStyle.fill_color && Array.isArray(obj.fills)) {
303
+ for (const fill of obj.fills) {
304
+ if (fill && fill.color) {
305
+ textStyle.fill_color = normalizeColor(fill.color);
306
+ break;
307
+ }
308
+ }
309
+ }
310
+
311
+ // obj.strokes / obj.borders 作为 stroke 后备
312
+ if (!textStyle.stroke_color) {
313
+ const strokeList = obj.strokes || obj.borders || [];
314
+ for (const stroke of strokeList) {
315
+ if (!stroke) continue;
316
+ if (stroke.color) {
317
+ textStyle.stroke_color = normalizeColor(stroke.color);
318
+ }
319
+ if (stroke.width) {
320
+ textStyle.stroke_width = stroke.width;
321
+ }
322
+ break;
323
+ }
324
+ }
325
+
326
+ if (Object.keys(textStyle).length > 0) {
327
+ layerInfo.text_style = textStyle;
328
+ }
329
+ }
330
+
331
+ if (obj.opacity !== undefined && obj.opacity !== 255) {
332
+ layerInfo.opacity = Math.round((obj.opacity / 255) * 100);
333
+ }
334
+
335
+ layers.push(layerInfo);
336
+
337
+ // 递归处理子图层
338
+ if (obj.layers) {
339
+ obj.layers.forEach((sub: any) => extractLayer(sub, depth + 1, currentPath));
340
+ }
341
+ };
342
+
343
+ // 开始提取图层
344
+ if (board.layers) {
345
+ board.layers.forEach((item: any) => extractLayer(item, 0));
346
+ } else if (sketchData.artboard?.layers) {
347
+ sketchData.artboard.layers.forEach((item: any) => extractLayer(item, 0));
348
+ } else if (sketchData.info && sketchData.info.length > 0) {
349
+ const first = sketchData.info[0];
350
+ if (first.layers) {
351
+ first.layers.forEach((item: any) => extractLayer(item, 0));
352
+ } else {
353
+ sketchData.info.forEach((item: any) => extractLayer(item, 0));
354
+ }
355
+ }
356
+
357
+ return {
358
+ design_id: designId,
359
+ design_name: result.name,
360
+ version: latestVersion.version_info,
361
+ canvas_size: {
362
+ width: abWidth > 0 ? abWidth : result.width,
363
+ height: abHeight > 0 ? abHeight : result.height
364
+ },
365
+ total_layers: layers.length,
366
+ layers
367
+ };
368
+ }
369
+
370
+ /**
371
+ * 下载设计图截图
372
+ */
373
+ async downloadScreenshot(design: Design, outputPath: string): Promise<boolean> {
374
+ try {
375
+ const imgUrl = design.url.split('?')[0];
376
+ const response = await this.client.get(imgUrl, {
377
+ responseType: 'arraybuffer'
378
+ });
379
+
380
+ const fs = await import('fs/promises');
381
+ const path = await import('path');
382
+
383
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
384
+ await fs.writeFile(outputPath, response.data);
385
+
386
+ return true;
387
+ } catch (error) {
388
+ console.error(`⚠️ 下载截图失败 (${design.name}):`, error);
389
+ return false;
390
+ }
391
+ }
392
+
393
+ /**
394
+ * 获取设计图的切图/资源列表
395
+ */
396
+ async getDesignSlices(url: string, designId: string): Promise<SlicesResponse> {
397
+ const params = parseUrl(url, this.defaultTid);
398
+ const apiUrl = `${this.baseUrl}/api/project/image`;
399
+
400
+ const response = await this.requestGet(apiUrl, {
401
+ params: {
402
+ dds_status: 1,
403
+ image_id: designId,
404
+ team_id: params.team_id,
405
+ project_id: params.project_id
406
+ }
407
+ });
408
+
409
+ const data = response.data;
410
+ if (data.code !== '00000') {
411
+ throw new Error(`Failed to get design: ${data.msg || 'Unknown error'}`);
412
+ }
413
+
414
+ const result = data.result;
415
+ const latestVersion = result.versions[0];
416
+ const jsonResponse = await this.requestGet(latestVersion.json_url);
417
+ const sketchData = jsonResponse.data;
418
+
419
+ const slices: Slice[] = [];
420
+
421
+ const findSlices = (obj: any, layerPath: string = '') => {
422
+ if (!obj || typeof obj !== 'object') return;
423
+ const currentName = obj.name || '';
424
+ const currentPath = layerPath ? `${layerPath}/${currentName}` : currentName;
425
+
426
+ let downloadUrl: string | null = null;
427
+ let fmt = 'png';
428
+ let frame: any = {};
429
+
430
+ if (obj.image && (obj.image.imageUrl || obj.image.svgUrl)) {
431
+ downloadUrl = obj.image.imageUrl || obj.image.svgUrl;
432
+ frame = obj.frame || obj.bounds || {};
433
+ fmt = obj.image.imageUrl ? 'png' : 'svg';
434
+ } else if (obj.ddsImage && obj.ddsImage.imageUrl) {
435
+ downloadUrl = obj.ddsImage.imageUrl;
436
+ frame = obj;
437
+ fmt = 'png';
438
+ }
439
+
440
+ if (downloadUrl) {
441
+ const w = frame.width ?? ((frame.right || 0) - (frame.left || 0));
442
+ const h = frame.height ?? ((frame.bottom || 0) - (frame.top || 0));
443
+ slices.push({
444
+ name: currentName,
445
+ download_url: downloadUrl,
446
+ size: w && h ? `${Math.round(w)}x${Math.round(h)}` : 'unknown',
447
+ format: fmt,
448
+ layer_path: currentPath
449
+ });
450
+ }
451
+
452
+ if (obj.layers) {
453
+ obj.layers.forEach((layer: any) => findSlices(layer, currentPath));
454
+ }
455
+ };
456
+
457
+ if (sketchData.artboard?.layers) {
458
+ sketchData.artboard.layers.forEach((layer: any) => findSlices(layer));
459
+ } else if (sketchData.info) {
460
+ sketchData.info.forEach((item: any) => findSlices(item));
461
+ }
462
+
463
+ return {
464
+ design_id: designId,
465
+ design_name: result.name,
466
+ version: latestVersion.version_info,
467
+ canvas_size: { width: result.width, height: result.height },
468
+ total_slices: slices.length,
469
+ slices
470
+ };
471
+ }
472
+
473
+ /**
474
+ * 下载单个切图
475
+ */
476
+ async downloadSlice(slice: Slice, outputPath: string): Promise<boolean> {
477
+ try {
478
+ const url = slice.download_url.split('?')[0];
479
+ const response = await this.client.get(url, { responseType: 'arraybuffer' });
480
+ const fs = await import('fs/promises');
481
+ const path = await import('path');
482
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
483
+ await fs.writeFile(outputPath, response.data);
484
+ return true;
485
+ } catch (error) {
486
+ console.error(`⚠️ 下载切图失败 (${slice.name}):`, error);
487
+ return false;
488
+ }
489
+ }
490
+ }
package/src/cookie.ts ADDED
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Cookie 管理:从 Chrome 自动读取蓝湖 cookie,并维护 .env 配置
3
+ */
4
+
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+
8
+ export interface LanhuCookieFields {
9
+ SESSION: string;
10
+ USER_TOKEN: string;
11
+ HM_LVT_KEY: string;
12
+ HM_LVT_VALUE: string;
13
+ }
14
+
15
+ /**
16
+ * 从 SESSION/USER_TOKEN/HM_LVT 等环境变量拼装 Cookie 字符串
17
+ *
18
+ * 兼容两种格式:
19
+ * - SESSION 已是完整 cookie(包含 user_token=)→ 直接使用
20
+ * - 否则按字段拼装
21
+ */
22
+ export function buildCookie(): string {
23
+ const sessionValue = process.env.SESSION || '';
24
+
25
+ if (sessionValue.includes('user_token=')) {
26
+ return sessionValue;
27
+ }
28
+
29
+ const parts: string[] = [];
30
+ if (sessionValue) parts.push(`session=${sessionValue}`);
31
+ if (process.env.USER_TOKEN) parts.push(`user_token=${process.env.USER_TOKEN}`);
32
+
33
+ const hmKey = process.env.HM_LVT_KEY || '';
34
+ const hmVal = process.env.HM_LVT_VALUE || '';
35
+ if (hmKey && hmVal) parts.push(`${hmKey}=${hmVal}`);
36
+
37
+ return parts.join(';');
38
+ }
39
+
40
+ /**
41
+ * 从 Chrome 浏览器读取 lanhuapp.com 的 cookie
42
+ *
43
+ * 使用 chrome-cookies-secure 跨平台读取(macOS Keychain / Windows DPAPI / Linux GNOME Keyring)
44
+ */
45
+ export async function getLanhuCookiesFromChrome(): Promise<LanhuCookieFields | null> {
46
+ let chromeCookies: any;
47
+ try {
48
+ chromeCookies = require('chrome-cookies-secure');
49
+ } catch {
50
+ console.warn('⚠️ chrome-cookies-secure 未安装,无法自动刷新 cookie。请运行: npm install chrome-cookies-secure');
51
+ return null;
52
+ }
53
+
54
+ const cookies = await new Promise<Record<string, string>>((resolve, reject) => {
55
+ chromeCookies.getCookies(
56
+ 'https://lanhuapp.com/',
57
+ 'object',
58
+ (err: any, result: Record<string, string>) => {
59
+ if (err) reject(err);
60
+ else resolve(result || {});
61
+ }
62
+ );
63
+ }).catch((err: any) => {
64
+ console.warn(`⚠️ 从 Chrome 读取 cookie 失败: ${err?.message || err}`);
65
+ return {} as Record<string, string>;
66
+ });
67
+
68
+ const session = cookies['session'] || '';
69
+ const userToken = cookies['user_token'] || '';
70
+
71
+ if (!session && !userToken) {
72
+ console.warn('⚠️ Chrome 中未找到蓝湖 cookie,请确认 Chrome 中已登录 lanhuapp.com');
73
+ return null;
74
+ }
75
+
76
+ let hmKey = '';
77
+ let hmVal = '';
78
+ for (const [name, value] of Object.entries(cookies)) {
79
+ if (name.startsWith('Hm_lvt_')) {
80
+ hmKey = name;
81
+ hmVal = value;
82
+ break;
83
+ }
84
+ }
85
+
86
+ return {
87
+ SESSION: session,
88
+ USER_TOKEN: userToken,
89
+ HM_LVT_KEY: hmKey,
90
+ HM_LVT_VALUE: hmVal
91
+ };
92
+ }
93
+
94
+ /**
95
+ * 将 cookie 字段写入 .env 文件
96
+ */
97
+ function updateEnvFile(fields: LanhuCookieFields, envFile: string): void {
98
+ if (!fs.existsSync(envFile)) return;
99
+
100
+ let content = fs.readFileSync(envFile, 'utf-8');
101
+ for (const [key, val] of Object.entries(fields)) {
102
+ if (!val) continue;
103
+ const newLine = `${key}="${val}"`;
104
+ const re = new RegExp(`^${key}=.*$`, 'm');
105
+ if (re.test(content)) {
106
+ content = content.replace(re, newLine);
107
+ } else {
108
+ content += `\n${newLine}\n`;
109
+ }
110
+ }
111
+ fs.writeFileSync(envFile, content, 'utf-8');
112
+ }
113
+
114
+ /**
115
+ * 从 Chrome 读取蓝湖 cookie,更新 .env 文件,返回拼装好的 cookie 字符串
116
+ */
117
+ export async function refreshCookieFromChrome(envPath?: string): Promise<string> {
118
+ const fields = await getLanhuCookiesFromChrome();
119
+ if (!fields) return '';
120
+
121
+ // 同步到当前进程环境变量,让 buildCookie 后续调用也能读到
122
+ process.env.SESSION = fields.SESSION;
123
+ process.env.USER_TOKEN = fields.USER_TOKEN;
124
+ if (fields.HM_LVT_KEY) {
125
+ process.env.HM_LVT_KEY = fields.HM_LVT_KEY;
126
+ process.env.HM_LVT_VALUE = fields.HM_LVT_VALUE;
127
+ }
128
+
129
+ const envFile = envPath || path.resolve(process.cwd(), '.env');
130
+
131
+ // 如果 .env 不存在,创建它
132
+ if (!fs.existsSync(envFile)) {
133
+ const content = [
134
+ '# 蓝湖 Cookie 配置(自动生成)',
135
+ `SESSION="${fields.SESSION}"`,
136
+ `USER_TOKEN="${fields.USER_TOKEN}"`,
137
+ fields.HM_LVT_KEY ? `${fields.HM_LVT_KEY}="${fields.HM_LVT_VALUE}"` : '',
138
+ '# TID=your_default_team_id',
139
+ ''
140
+ ].filter(Boolean).join('\n');
141
+ fs.writeFileSync(envFile, content, 'utf-8');
142
+ console.log(`✅ 已从 Chrome 读取蓝湖 cookie 并创建 ${envFile}`);
143
+ } else {
144
+ updateEnvFile(fields, envFile);
145
+ console.log(`✅ 已从 Chrome 刷新蓝湖 cookie → ${envFile}`);
146
+ }
147
+
148
+ const parts = [`session=${fields.SESSION}`, `user_token=${fields.USER_TOKEN}`];
149
+ if (fields.HM_LVT_KEY) parts.push(`${fields.HM_LVT_KEY}=${fields.HM_LVT_VALUE}`);
150
+ return parts.join(';');
151
+ }
152
+
153
+ /**
154
+ * 蓝湖认证错误码
155
+ */
156
+ export const AUTH_ERROR_CODES = new Set(['10000', '10001', '10002', '99999', 'A0001']);