@jlcpcb/core 0.1.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/CHANGELOG.md +10 -0
- package/README.md +474 -0
- package/package.json +48 -0
- package/src/api/easyeda-community.ts +259 -0
- package/src/api/easyeda.ts +153 -0
- package/src/api/index.ts +7 -0
- package/src/api/jlc.ts +185 -0
- package/src/constants/design-rules.ts +119 -0
- package/src/constants/footprints.ts +68 -0
- package/src/constants/index.ts +7 -0
- package/src/constants/kicad.ts +147 -0
- package/src/converter/category-router.ts +638 -0
- package/src/converter/footprint-mapper.ts +236 -0
- package/src/converter/footprint.ts +949 -0
- package/src/converter/global-lib-table.ts +394 -0
- package/src/converter/index.ts +46 -0
- package/src/converter/lib-table.ts +181 -0
- package/src/converter/svg-arc.ts +179 -0
- package/src/converter/symbol-templates.ts +214 -0
- package/src/converter/symbol.ts +1682 -0
- package/src/converter/value-normalizer.ts +262 -0
- package/src/index.ts +25 -0
- package/src/parsers/easyeda-shapes.ts +628 -0
- package/src/parsers/http-client.ts +96 -0
- package/src/parsers/index.ts +38 -0
- package/src/parsers/utils.ts +29 -0
- package/src/services/component-service.ts +100 -0
- package/src/services/fix-service.ts +50 -0
- package/src/services/index.ts +9 -0
- package/src/services/library-service.ts +696 -0
- package/src/types/component.ts +61 -0
- package/src/types/easyeda-community.ts +78 -0
- package/src/types/easyeda.ts +356 -0
- package/src/types/index.ts +12 -0
- package/src/types/jlc.ts +84 -0
- package/src/types/kicad.ts +136 -0
- package/src/types/mcp.ts +77 -0
- package/src/types/project.ts +60 -0
- package/src/utils/conversion.ts +104 -0
- package/src/utils/file-system.ts +143 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.ts +96 -0
- package/src/utils/validation.ts +110 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EasyEDA Community Library API client
|
|
3
|
+
* For searching and fetching user-contributed components
|
|
4
|
+
*
|
|
5
|
+
* Uses shared parsers from common/parsers for all shape types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
EasyEDACommunitySearchParams,
|
|
10
|
+
EasyEDACommunitySearchResult,
|
|
11
|
+
EasyEDACommunityComponent,
|
|
12
|
+
} from '../types/index.js'
|
|
13
|
+
import { createLogger } from '../utils/index.js'
|
|
14
|
+
import {
|
|
15
|
+
fetchWithCurlFallback,
|
|
16
|
+
parseSymbolShapes,
|
|
17
|
+
parseFootprintShapes,
|
|
18
|
+
} from '../parsers/index.js'
|
|
19
|
+
|
|
20
|
+
const logger = createLogger('easyeda-community-api')
|
|
21
|
+
|
|
22
|
+
const API_SEARCH_ENDPOINT = 'https://easyeda.com/api/components/search'
|
|
23
|
+
const API_COMPONENT_ENDPOINT = 'https://easyeda.com/api/components'
|
|
24
|
+
const API_VERSION = '6.5.51'
|
|
25
|
+
|
|
26
|
+
// Reuse 3D model endpoints from existing easyeda client
|
|
27
|
+
const ENDPOINT_3D_MODEL_STEP = 'https://modules.easyeda.com/qAxj6KHrDKw4blvCG8QJPs7Y/{uuid}'
|
|
28
|
+
const ENDPOINT_3D_MODEL_OBJ = 'https://modules.easyeda.com/3dmodel/{uuid}'
|
|
29
|
+
|
|
30
|
+
export class EasyEDACommunityClient {
|
|
31
|
+
/**
|
|
32
|
+
* Search the EasyEDA community library
|
|
33
|
+
*/
|
|
34
|
+
async search(
|
|
35
|
+
params: EasyEDACommunitySearchParams
|
|
36
|
+
): Promise<EasyEDACommunitySearchResult[]> {
|
|
37
|
+
const formData = new URLSearchParams()
|
|
38
|
+
formData.append('type', '3') // Component type
|
|
39
|
+
formData.append('doctype[]', '2') // Symbol+footprint
|
|
40
|
+
formData.append('uid', params.source || 'user')
|
|
41
|
+
formData.append('returnListStyle', 'classifyarr')
|
|
42
|
+
formData.append('wd', params.query)
|
|
43
|
+
formData.append('version', API_VERSION)
|
|
44
|
+
|
|
45
|
+
logger.debug(`Searching EasyEDA community: ${params.query}`)
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const responseText = (await fetchWithCurlFallback(API_SEARCH_ENDPOINT, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
body: formData.toString(),
|
|
51
|
+
contentType: 'application/x-www-form-urlencoded',
|
|
52
|
+
})) as string
|
|
53
|
+
|
|
54
|
+
const data = JSON.parse(responseText)
|
|
55
|
+
|
|
56
|
+
if (!data.success || !data.result) {
|
|
57
|
+
logger.warn('EasyEDA search returned no results')
|
|
58
|
+
return []
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return this.parseSearchResults(data.result.lists, params.limit)
|
|
62
|
+
} catch (error) {
|
|
63
|
+
logger.error('EasyEDA search failed:', error)
|
|
64
|
+
throw error
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get full component details by UUID
|
|
70
|
+
*/
|
|
71
|
+
async getComponent(uuid: string): Promise<EasyEDACommunityComponent | null> {
|
|
72
|
+
const url = `${API_COMPONENT_ENDPOINT}/${uuid}?version=${API_VERSION}&uuid=${uuid}`
|
|
73
|
+
|
|
74
|
+
logger.debug(`Fetching component: ${uuid}`)
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const responseText = (await fetchWithCurlFallback(url)) as string
|
|
78
|
+
const data = JSON.parse(responseText)
|
|
79
|
+
|
|
80
|
+
if (!data.success || !data.result) {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return this.parseComponent(data.result)
|
|
85
|
+
} catch (error) {
|
|
86
|
+
logger.error(`Failed to fetch component ${uuid}:`, error)
|
|
87
|
+
throw error
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Download 3D model for a component
|
|
93
|
+
*/
|
|
94
|
+
async get3DModel(
|
|
95
|
+
uuid: string,
|
|
96
|
+
format: 'step' | 'obj' = 'step'
|
|
97
|
+
): Promise<Buffer | null> {
|
|
98
|
+
const url =
|
|
99
|
+
format === 'step'
|
|
100
|
+
? ENDPOINT_3D_MODEL_STEP.replace('{uuid}', uuid)
|
|
101
|
+
: ENDPOINT_3D_MODEL_OBJ.replace('{uuid}', uuid)
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const result = await fetchWithCurlFallback(url, { binary: true })
|
|
105
|
+
return result as Buffer
|
|
106
|
+
} catch {
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Parse search results from the API response
|
|
113
|
+
*/
|
|
114
|
+
private parseSearchResults(
|
|
115
|
+
lists: Record<string, unknown[]>,
|
|
116
|
+
limit?: number
|
|
117
|
+
): EasyEDACommunitySearchResult[] {
|
|
118
|
+
const results: EasyEDACommunitySearchResult[] = []
|
|
119
|
+
|
|
120
|
+
// Process all source lists (user, lcsc, easyeda, etc.)
|
|
121
|
+
for (const [_source, items] of Object.entries(lists)) {
|
|
122
|
+
if (!Array.isArray(items)) continue
|
|
123
|
+
|
|
124
|
+
for (const item of items) {
|
|
125
|
+
const result = this.parseSearchItem(item)
|
|
126
|
+
if (result) {
|
|
127
|
+
results.push(result)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (limit && results.length >= limit) {
|
|
131
|
+
return results
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return results
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Parse a single search result item
|
|
141
|
+
*/
|
|
142
|
+
private parseSearchItem(item: any): EasyEDACommunitySearchResult | null {
|
|
143
|
+
try {
|
|
144
|
+
const cPara = item.dataStr?.head?.c_para || {}
|
|
145
|
+
const puuid = item.dataStr?.head?.puuid // Package/footprint UUID
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
uuid: item.uuid || '',
|
|
149
|
+
title: item.title || '',
|
|
150
|
+
thumb: item.thumb || '',
|
|
151
|
+
description: item.description || '',
|
|
152
|
+
tags: item.tags || [],
|
|
153
|
+
package: cPara.package || '',
|
|
154
|
+
packageUuid: puuid || undefined,
|
|
155
|
+
manufacturer: cPara.Manufacturer || cPara.BOM_Manufacturer || undefined,
|
|
156
|
+
owner: {
|
|
157
|
+
uuid: item.owner?.uuid || '',
|
|
158
|
+
username: item.owner?.username || '',
|
|
159
|
+
nickname: item.owner?.nickname || '',
|
|
160
|
+
avatar: item.owner?.avatar,
|
|
161
|
+
team: item.owner?.team,
|
|
162
|
+
},
|
|
163
|
+
contributor: cPara.Contributor,
|
|
164
|
+
has3DModel: false, // Will be determined when fetching full component
|
|
165
|
+
docType: item.docType || 2,
|
|
166
|
+
updateTime: item.dataStr?.head?.utime,
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Parse full component data from the API response
|
|
175
|
+
* Uses shared parsers for consistent shape parsing with LCSC client.
|
|
176
|
+
*/
|
|
177
|
+
private parseComponent(result: any): EasyEDACommunityComponent {
|
|
178
|
+
const dataStr = result.dataStr || {}
|
|
179
|
+
const packageDetail = result.packageDetail || {}
|
|
180
|
+
const cPara = dataStr.head?.c_para || {}
|
|
181
|
+
|
|
182
|
+
// Parse symbol shapes using shared parser
|
|
183
|
+
const symbolData = parseSymbolShapes(dataStr.shape || [])
|
|
184
|
+
|
|
185
|
+
// Parse footprint shapes using shared parser
|
|
186
|
+
const fpDataStr = packageDetail.dataStr || {}
|
|
187
|
+
const fpCPara = fpDataStr.head?.c_para || {}
|
|
188
|
+
const footprintData = parseFootprintShapes(fpDataStr.shape || [])
|
|
189
|
+
|
|
190
|
+
// Get origins for coordinate normalization
|
|
191
|
+
const symbolOrigin = {
|
|
192
|
+
x: parseFloat(dataStr.head?.x) || 0,
|
|
193
|
+
y: parseFloat(dataStr.head?.y) || 0,
|
|
194
|
+
}
|
|
195
|
+
const footprintOrigin = {
|
|
196
|
+
x: parseFloat(fpDataStr.head?.x) || 0,
|
|
197
|
+
y: parseFloat(fpDataStr.head?.y) || 0,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
uuid: result.uuid || '',
|
|
202
|
+
title: result.title || cPara.name || '',
|
|
203
|
+
description: result.description || '',
|
|
204
|
+
tags: result.tags || [],
|
|
205
|
+
owner: {
|
|
206
|
+
uuid: result.owner?.uuid || '',
|
|
207
|
+
username: result.owner?.username || '',
|
|
208
|
+
nickname: result.owner?.nickname || '',
|
|
209
|
+
avatar: result.owner?.avatar,
|
|
210
|
+
team: result.owner?.team,
|
|
211
|
+
},
|
|
212
|
+
creator: result.creator
|
|
213
|
+
? {
|
|
214
|
+
uuid: result.creator.uuid || '',
|
|
215
|
+
username: result.creator.username || '',
|
|
216
|
+
nickname: result.creator.nickname || '',
|
|
217
|
+
avatar: result.creator.avatar,
|
|
218
|
+
team: result.creator.team,
|
|
219
|
+
}
|
|
220
|
+
: undefined,
|
|
221
|
+
updateTime: result.updateTime || fpDataStr.head?.utime || 0,
|
|
222
|
+
docType: result.docType || 2,
|
|
223
|
+
verify: result.verify || false,
|
|
224
|
+
symbol: {
|
|
225
|
+
pins: symbolData.pins,
|
|
226
|
+
rectangles: symbolData.rectangles,
|
|
227
|
+
circles: symbolData.circles,
|
|
228
|
+
ellipses: symbolData.ellipses,
|
|
229
|
+
arcs: symbolData.arcs,
|
|
230
|
+
polylines: symbolData.polylines,
|
|
231
|
+
polygons: symbolData.polygons,
|
|
232
|
+
paths: symbolData.paths,
|
|
233
|
+
origin: symbolOrigin,
|
|
234
|
+
head: dataStr.head || {},
|
|
235
|
+
},
|
|
236
|
+
footprint: {
|
|
237
|
+
uuid: packageDetail.uuid || '',
|
|
238
|
+
name: fpCPara.package || packageDetail.title || 'Unknown',
|
|
239
|
+
type: footprintData.type,
|
|
240
|
+
pads: footprintData.pads,
|
|
241
|
+
tracks: footprintData.tracks,
|
|
242
|
+
holes: footprintData.holes,
|
|
243
|
+
circles: footprintData.circles,
|
|
244
|
+
arcs: footprintData.arcs,
|
|
245
|
+
rects: footprintData.rects,
|
|
246
|
+
texts: footprintData.texts,
|
|
247
|
+
vias: footprintData.vias,
|
|
248
|
+
solidRegions: footprintData.solidRegions,
|
|
249
|
+
model3d: footprintData.model3d,
|
|
250
|
+
origin: footprintOrigin,
|
|
251
|
+
head: fpDataStr.head || {},
|
|
252
|
+
},
|
|
253
|
+
model3d: footprintData.model3d,
|
|
254
|
+
rawData: result,
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export const easyedaCommunityClient = new EasyEDACommunityClient()
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EasyEDA API client for component library fetching
|
|
3
|
+
*
|
|
4
|
+
* Uses shared parsers from common/parsers for all shape types.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { EasyEDAComponentData } from '../types/index.js';
|
|
8
|
+
import { createLogger } from '../utils/index.js';
|
|
9
|
+
import {
|
|
10
|
+
fetchWithCurlFallback,
|
|
11
|
+
parseSymbolShapes,
|
|
12
|
+
parseFootprintShapes,
|
|
13
|
+
} from '../parsers/index.js';
|
|
14
|
+
|
|
15
|
+
const logger = createLogger('easyeda-api');
|
|
16
|
+
|
|
17
|
+
const API_ENDPOINT = 'https://easyeda.com/api/products/{lcsc_id}/components?version=6.4.19.5';
|
|
18
|
+
const ENDPOINT_3D_MODEL_STEP = 'https://modules.easyeda.com/qAxj6KHrDKw4blvCG8QJPs7Y/{uuid}';
|
|
19
|
+
const ENDPOINT_3D_MODEL_OBJ = 'https://modules.easyeda.com/3dmodel/{uuid}';
|
|
20
|
+
|
|
21
|
+
export class EasyEDAClient {
|
|
22
|
+
private userAgent = 'ai-eda-lcsc-mcp/1.0.0';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Fetch component data from EasyEDA API
|
|
26
|
+
*/
|
|
27
|
+
async getComponentData(lcscPartNumber: string): Promise<EasyEDAComponentData | null> {
|
|
28
|
+
const url = API_ENDPOINT.replace('{lcsc_id}', lcscPartNumber);
|
|
29
|
+
|
|
30
|
+
logger.debug(`Fetching component data for: ${lcscPartNumber}`);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const responseText = await fetchWithCurlFallback(url) as string;
|
|
34
|
+
const data = JSON.parse(responseText);
|
|
35
|
+
|
|
36
|
+
if (!data.result) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return this.parseComponentData(data.result, lcscPartNumber);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
logger.error(`Failed to fetch component ${lcscPartNumber}:`, error);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Download 3D model for a component
|
|
49
|
+
*/
|
|
50
|
+
async get3DModel(uuid: string, format: 'step' | 'obj' = 'step'): Promise<Buffer | null> {
|
|
51
|
+
const url = format === 'step'
|
|
52
|
+
? ENDPOINT_3D_MODEL_STEP.replace('{uuid}', uuid)
|
|
53
|
+
: ENDPOINT_3D_MODEL_OBJ.replace('{uuid}', uuid);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const result = await fetchWithCurlFallback(url, { binary: true });
|
|
57
|
+
return result as Buffer;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse raw EasyEDA API response into structured data
|
|
65
|
+
*/
|
|
66
|
+
private parseComponentData(result: any, lcscId: string): EasyEDAComponentData {
|
|
67
|
+
const dataStr = result.dataStr;
|
|
68
|
+
const packageDetail = result.packageDetail;
|
|
69
|
+
const lcscInfo = result.lcsc || {};
|
|
70
|
+
const cPara = dataStr?.head?.c_para || {};
|
|
71
|
+
|
|
72
|
+
// Parse symbol shapes using shared parser
|
|
73
|
+
const symbolData = parseSymbolShapes(dataStr?.shape || []);
|
|
74
|
+
|
|
75
|
+
// Parse footprint shapes using shared parser
|
|
76
|
+
const fpDataStr = packageDetail?.dataStr;
|
|
77
|
+
const fpCPara = fpDataStr?.head?.c_para || {};
|
|
78
|
+
const footprintData = parseFootprintShapes(fpDataStr?.shape || []);
|
|
79
|
+
|
|
80
|
+
// Get origins for coordinate normalization
|
|
81
|
+
const symbolOrigin = {
|
|
82
|
+
x: parseFloat(dataStr?.head?.x) || 0,
|
|
83
|
+
y: parseFloat(dataStr?.head?.y) || 0,
|
|
84
|
+
};
|
|
85
|
+
const footprintOrigin = {
|
|
86
|
+
x: parseFloat(fpDataStr?.head?.x) || 0,
|
|
87
|
+
y: parseFloat(fpDataStr?.head?.y) || 0,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Extract attributes from c_para (BOM_ prefixed fields)
|
|
91
|
+
const attributes: Record<string, string> = {};
|
|
92
|
+
for (const [key, value] of Object.entries(cPara)) {
|
|
93
|
+
if (key.startsWith('BOM_') && value && typeof value === 'string') {
|
|
94
|
+
// Clean up key name: "BOM_Resistance" -> "Resistance"
|
|
95
|
+
const cleanKey = key.replace(/^BOM_/, '');
|
|
96
|
+
if (cleanKey !== 'Manufacturer' && cleanKey !== 'JLCPCB Part Class') {
|
|
97
|
+
attributes[cleanKey] = value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
info: {
|
|
104
|
+
name: cPara.name || lcscId,
|
|
105
|
+
prefix: cPara.pre || 'U',
|
|
106
|
+
package: cPara.package || fpCPara?.package,
|
|
107
|
+
manufacturer: cPara.BOM_Manufacturer || cPara.Manufacturer,
|
|
108
|
+
datasheet: lcscInfo.url,
|
|
109
|
+
lcscId: lcscInfo.number || lcscId,
|
|
110
|
+
jlcId: cPara['BOM_JLCPCB Part Class'],
|
|
111
|
+
description: result.title || cPara.name,
|
|
112
|
+
category: result.category || undefined,
|
|
113
|
+
attributes: Object.keys(attributes).length > 0 ? attributes : undefined,
|
|
114
|
+
// CDFER parity fields
|
|
115
|
+
stock: lcscInfo.stock,
|
|
116
|
+
price: lcscInfo.price,
|
|
117
|
+
minOrderQty: lcscInfo.min,
|
|
118
|
+
process: result.SMT ? 'SMT' : 'THT',
|
|
119
|
+
partClass: cPara['JLCPCB Part Class'],
|
|
120
|
+
partNumber: cPara['Manufacturer Part'],
|
|
121
|
+
},
|
|
122
|
+
symbol: {
|
|
123
|
+
pins: symbolData.pins,
|
|
124
|
+
rectangles: symbolData.rectangles,
|
|
125
|
+
circles: symbolData.circles,
|
|
126
|
+
ellipses: symbolData.ellipses,
|
|
127
|
+
arcs: symbolData.arcs,
|
|
128
|
+
polylines: symbolData.polylines,
|
|
129
|
+
polygons: symbolData.polygons,
|
|
130
|
+
paths: symbolData.paths,
|
|
131
|
+
origin: symbolOrigin,
|
|
132
|
+
},
|
|
133
|
+
footprint: {
|
|
134
|
+
name: fpCPara?.package || 'Unknown',
|
|
135
|
+
type: result.SMT && !packageDetail?.title?.includes('-TH_') ? 'smd' : 'tht',
|
|
136
|
+
pads: footprintData.pads,
|
|
137
|
+
tracks: footprintData.tracks,
|
|
138
|
+
holes: footprintData.holes,
|
|
139
|
+
circles: footprintData.circles,
|
|
140
|
+
arcs: footprintData.arcs,
|
|
141
|
+
rects: footprintData.rects,
|
|
142
|
+
texts: footprintData.texts,
|
|
143
|
+
vias: footprintData.vias,
|
|
144
|
+
solidRegions: footprintData.solidRegions,
|
|
145
|
+
origin: footprintOrigin,
|
|
146
|
+
},
|
|
147
|
+
model3d: footprintData.model3d,
|
|
148
|
+
rawData: result,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const easyedaClient = new EasyEDAClient();
|
package/src/api/index.ts
ADDED
package/src/api/jlc.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JLC API client for component search and details
|
|
3
|
+
* Uses JLCPCB's parts library API which provides LCSC component data
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { LCSCSearchOptions, ComponentSearchResult } from '../types/index.js';
|
|
7
|
+
import { createLogger } from '../utils/index.js';
|
|
8
|
+
|
|
9
|
+
const logger = createLogger('jlc-api');
|
|
10
|
+
|
|
11
|
+
// JLCPCB parts API - provides LCSC component data with better reliability
|
|
12
|
+
const JLCPCB_SEARCH_API =
|
|
13
|
+
'https://jlcpcb.com/api/overseas-pcb-order/v1/shoppingCart/smtGood/selectSmtComponentList/v2';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* JLCPCB component structure from API response
|
|
17
|
+
*/
|
|
18
|
+
interface JLCPCBComponent {
|
|
19
|
+
componentCode: string; // LCSC part number (e.g., "C82899")
|
|
20
|
+
componentModelEn: string; // Part model (e.g., "ESP32-WROOM-32-N4")
|
|
21
|
+
componentBrandEn: string; // Manufacturer
|
|
22
|
+
componentSpecificationEn: string; // Package type
|
|
23
|
+
stockCount: number;
|
|
24
|
+
componentPrices: Array<{
|
|
25
|
+
startNumber: number;
|
|
26
|
+
endNumber: number;
|
|
27
|
+
productPrice: number;
|
|
28
|
+
}>;
|
|
29
|
+
describe: string;
|
|
30
|
+
dataManualUrl?: string;
|
|
31
|
+
lcscGoodsUrl?: string;
|
|
32
|
+
componentTypeEn?: string;
|
|
33
|
+
componentLibraryType?: string; // "base" = basic part (no setup fee), "expand" = extended (setup fee required)
|
|
34
|
+
attributes?: Array<{
|
|
35
|
+
attribute_name_en: string;
|
|
36
|
+
attribute_value_name: string;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface JLCPCBSearchResponse {
|
|
41
|
+
code: number;
|
|
42
|
+
data: {
|
|
43
|
+
componentPageInfo: {
|
|
44
|
+
total: number;
|
|
45
|
+
list: JLCPCBComponent[] | null;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
message: string | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface LCSCProduct {
|
|
52
|
+
productCode: string;
|
|
53
|
+
productModel: string;
|
|
54
|
+
brandNameEn: string;
|
|
55
|
+
encapStandard: string;
|
|
56
|
+
productPriceList: Array<{
|
|
57
|
+
ladder: number;
|
|
58
|
+
productPrice: number;
|
|
59
|
+
currencySymbol: string;
|
|
60
|
+
}>;
|
|
61
|
+
stockNumber: number;
|
|
62
|
+
productIntroEn: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class JLCClient {
|
|
66
|
+
/**
|
|
67
|
+
* Search for components via JLCPCB API
|
|
68
|
+
*/
|
|
69
|
+
async search(
|
|
70
|
+
query: string,
|
|
71
|
+
options: LCSCSearchOptions = {}
|
|
72
|
+
): Promise<ComponentSearchResult[]> {
|
|
73
|
+
const { limit = 10, page = 1, inStock = false, basicOnly = false } = options;
|
|
74
|
+
|
|
75
|
+
logger.debug(`Searching LCSC (via JLCPCB) for: ${query} (inStock=${inStock}, basicOnly=${basicOnly})`);
|
|
76
|
+
|
|
77
|
+
const requestBody: Record<string, unknown> = {
|
|
78
|
+
currentPage: page,
|
|
79
|
+
pageSize: Math.min(limit, 50),
|
|
80
|
+
keyword: query,
|
|
81
|
+
searchType: 2, // Better search relevance matching
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Add in-stock filter
|
|
85
|
+
if (inStock) {
|
|
86
|
+
requestBody.presaleType = 'stock';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Add basic/preferred library filter (JLCPCB basic parts = lower assembly cost)
|
|
90
|
+
if (basicOnly) {
|
|
91
|
+
requestBody.componentLibTypes = ['base'];
|
|
92
|
+
requestBody.preferredComponentFlag = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const body = JSON.stringify(requestBody);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const response = await fetch(JLCPCB_SEARCH_API, {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: {
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
Accept: 'application/json',
|
|
103
|
+
},
|
|
104
|
+
body,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
throw new Error(`JLCPCB API returned ${response.status}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const data: JLCPCBSearchResponse = await response.json();
|
|
112
|
+
|
|
113
|
+
if (data.code !== 200) {
|
|
114
|
+
throw new Error(`JLCPCB API error: ${data.message || 'Unknown error'}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const components = data.data?.componentPageInfo?.list || [];
|
|
118
|
+
|
|
119
|
+
logger.debug(`Found ${components.length} components`);
|
|
120
|
+
|
|
121
|
+
return components.map((c) => ({
|
|
122
|
+
lcscId: c.componentCode,
|
|
123
|
+
name: c.componentModelEn,
|
|
124
|
+
manufacturer: c.componentBrandEn,
|
|
125
|
+
package: c.componentSpecificationEn || '',
|
|
126
|
+
price: c.componentPrices?.[0]?.productPrice,
|
|
127
|
+
stock: c.stockCount,
|
|
128
|
+
description: c.describe,
|
|
129
|
+
productUrl: c.lcscGoodsUrl, // LCSC product page
|
|
130
|
+
datasheetPdf: c.dataManualUrl, // Actual PDF datasheet
|
|
131
|
+
category: c.componentTypeEn,
|
|
132
|
+
// JLCPCB assembly part type: "basic" = no setup fee, "extended" = setup fee required
|
|
133
|
+
libraryType: c.componentLibraryType === 'base' ? 'basic' : 'extended',
|
|
134
|
+
// Component specifications as key-value pairs
|
|
135
|
+
attributes: c.attributes?.reduce(
|
|
136
|
+
(acc, attr) => {
|
|
137
|
+
if (attr.attribute_value_name && attr.attribute_value_name !== '-') {
|
|
138
|
+
acc[attr.attribute_name_en] = attr.attribute_value_name;
|
|
139
|
+
}
|
|
140
|
+
return acc;
|
|
141
|
+
},
|
|
142
|
+
{} as Record<string, string>
|
|
143
|
+
),
|
|
144
|
+
}));
|
|
145
|
+
} catch (error) {
|
|
146
|
+
logger.error(`Search failed: ${error}`);
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get stock and pricing information for a component
|
|
153
|
+
*/
|
|
154
|
+
async getStock(lcscPartNumber: string): Promise<{
|
|
155
|
+
stock: number;
|
|
156
|
+
priceBreaks: Array<{ quantity: number; price: number }>;
|
|
157
|
+
}> {
|
|
158
|
+
// Re-search to get current stock info
|
|
159
|
+
const results = await this.search(lcscPartNumber, { limit: 1 });
|
|
160
|
+
|
|
161
|
+
if (results.length === 0) {
|
|
162
|
+
throw new Error(`Component ${lcscPartNumber} not found`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const product = results[0];
|
|
166
|
+
return {
|
|
167
|
+
stock: product.stock,
|
|
168
|
+
priceBreaks: [{ quantity: 1, price: product.price || 0 }],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get detailed component information including attributes
|
|
174
|
+
* Used to enrich EasyEDA data with JLC-specific attributes
|
|
175
|
+
*/
|
|
176
|
+
async getComponentDetails(lcscPartNumber: string): Promise<ComponentSearchResult | null> {
|
|
177
|
+
const results = await this.search(lcscPartNumber, { limit: 1 });
|
|
178
|
+
if (results.length === 0) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
return results[0];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export const jlcClient = new JLCClient();
|