dsh-plugin-lcsc 0.3.5 → 0.3.6
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/README.md +7 -0
- package/data.js +16 -0
- package/index.js +403 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,6 +68,11 @@
|
|
|
68
68
|
| `suggest_alternatives` | true | 基础库替代 + 净节省(省上料费 − 元件价差;封装不同标注;替代带双库存) |
|
|
69
69
|
| `require_stock` | `any` | 替代建议的库存口径:`any` / `lcsc`(立创零售,DIY 配单)/ `smt`(嘉立创 SMT,PCBA);库存不满足的候选会列为 skipped |
|
|
70
70
|
| `export_bom` | false | 输出嘉立创 BOM CSV |
|
|
71
|
+
| `include_usd` | true | 是否输出 USD 列/合计;`false` = 纯 CNY 模式(无 `$` 列) |
|
|
72
|
+
| `smt_cost_basis` | `usd_jlc_global` | SMT 成本口径:`usd_jlc_global`(JLCPCB 海外 USD)/ `cny_retail`(立创零售参考)/ `cny_jlc_cn`(嘉立创国内 SMT 人民币计价) |
|
|
73
|
+
| `smt_service_type` | — | **`cny_jlc_cn` 必填**:`economic`(经济型)/ `standard`(标准型);两种产线费率不同,不能自动判断 |
|
|
74
|
+
| `smt_sides` | 1 | 贴装面数(1/2);经济型只支持 1 面 |
|
|
75
|
+
| `smt_joint_count` | — | 整单焊点总数;不给则按封装估算并标 `joint_count_estimated` |
|
|
71
76
|
| `region` | `global` | 费用口径标签(`cn` 会标注费率假设) |
|
|
72
77
|
|
|
73
78
|
输出:`summary`(含 USD/CNY 分列、`cost_table`、`dnp/hand` 统计)、`rows`(含 `place`/双价/双库存/`flags`)、`optimize`(净节省与 actions)、`bom_csv`、`skipped`(缺料号行不静默丢弃)。
|
|
@@ -176,6 +181,8 @@ node poc/test-improvements.mjs # 联网验收(BOM v0.6 100 片 + 人民币
|
|
|
176
181
|
6. 不做下单、不做比价大屏、不抓淘宝/1688;没有 LCSC 编号的料无法计价。
|
|
177
182
|
7. Chrome 进程按空闲/卸载退出;若宿主进程被强杀,可能留下一个 Chrome 残留进程(其 profile 缓存在下次启动前会先清理,不会持续膨胀)。
|
|
178
183
|
8. 中文/口语品类召回已按同义词表 + 关键词回退修复;但 JLC 关键词搜索对个别型号(如 LMA2718 系列 MEMS 麦)不返回,需用型号或 LCSC 编号精确查询(数据源限制,非插件解析问题)。
|
|
184
|
+
9. **SMT CNY 边界**:`cny_jlc_cn` 器件费按立创商城 **1pc 人民币价**估算(嘉立创系统按实际采购数量计价,可能更低);焊点费在未给 `smt_joint_count` 时按封装估算;经济型/标准型费率来自嘉立创官方「贴片、插件焊接收费模式」表(2026-09),但**插件工程费、AOI、X-RAY、大板费、包装/治具、整盘预订5元/种上料优惠**未计入,最终以嘉立创系统报价为准。
|
|
185
|
+
10. **产线选择**:插件无法判断某块板/某颗料是否符合经济型(单面、片式≥0402、IC pin间距≥0.4mm、BGA球径≥0.5mm 等),必须由用户显式传 `smt_service_type`;传 `economic` 时会给出已知限制提醒,但不会替用户做最终判断。
|
|
179
186
|
|
|
180
187
|
详细改进记录见仓库 [`docs/改进记录_0.3.0.md`](../../docs/改进记录_0.3.0.md)。
|
|
181
188
|
|
package/data.js
CHANGED
|
@@ -94,6 +94,15 @@ export const DEFAULT_CONFIG = Object.freeze({
|
|
|
94
94
|
extendedPartFeeUsd: 3.0,
|
|
95
95
|
setupFeeUsd: 0.0,
|
|
96
96
|
stencilFeeUsd: 0.0,
|
|
97
|
+
// 嘉立创国内 SMT(jlc-smt.com)人民币费率,来源:官方「贴片、插件焊接收费模式」表
|
|
98
|
+
// (server_guide_4025.html,2026-09 更新)。两种产线价格差异大,必须显式选择。
|
|
99
|
+
cnSmtEconomicEngineeringCny: 50.0,
|
|
100
|
+
cnSmtEconomicStencilCny: 0.0,
|
|
101
|
+
cnSmtEconomicExtendedCny: 20.0,
|
|
102
|
+
cnSmtStandardEngineeringCny: 150.0,
|
|
103
|
+
cnSmtStandardStencilCny: 50.0,
|
|
104
|
+
cnSmtStandardExtendedCny: 8.0,
|
|
105
|
+
cnSmtExtendedCapCny: 800.0,
|
|
97
106
|
})
|
|
98
107
|
|
|
99
108
|
// LCSC codes are normally C + 4–7 digits, but the catalogue also carries
|
|
@@ -1651,6 +1660,13 @@ export function normalizeConfig(raw, options = {}) {
|
|
|
1651
1660
|
extendedPartFeeUsd: float('extendedPartFeeUsd', DEFAULT_CONFIG.extendedPartFeeUsd, 0, 100),
|
|
1652
1661
|
setupFeeUsd: float('setupFeeUsd', DEFAULT_CONFIG.setupFeeUsd, 0, 10000),
|
|
1653
1662
|
stencilFeeUsd: float('stencilFeeUsd', DEFAULT_CONFIG.stencilFeeUsd, 0, 10000),
|
|
1663
|
+
cnSmtEconomicEngineeringCny: float('cnSmtEconomicEngineeringCny', DEFAULT_CONFIG.cnSmtEconomicEngineeringCny, 0, 100000),
|
|
1664
|
+
cnSmtEconomicStencilCny: float('cnSmtEconomicStencilCny', DEFAULT_CONFIG.cnSmtEconomicStencilCny, 0, 100000),
|
|
1665
|
+
cnSmtEconomicExtendedCny: float('cnSmtEconomicExtendedCny', DEFAULT_CONFIG.cnSmtEconomicExtendedCny, 0, 100000),
|
|
1666
|
+
cnSmtStandardEngineeringCny: float('cnSmtStandardEngineeringCny', DEFAULT_CONFIG.cnSmtStandardEngineeringCny, 0, 100000),
|
|
1667
|
+
cnSmtStandardStencilCny: float('cnSmtStandardStencilCny', DEFAULT_CONFIG.cnSmtStandardStencilCny, 0, 100000),
|
|
1668
|
+
cnSmtStandardExtendedCny: float('cnSmtStandardExtendedCny', DEFAULT_CONFIG.cnSmtStandardExtendedCny, 0, 100000),
|
|
1669
|
+
cnSmtExtendedCapCny: float('cnSmtExtendedCapCny', DEFAULT_CONFIG.cnSmtExtendedCapCny, 0, 1000000),
|
|
1654
1670
|
}
|
|
1655
1671
|
}
|
|
1656
1672
|
|
package/index.js
CHANGED
|
@@ -91,6 +91,135 @@ function money(value) {
|
|
|
91
91
|
return Math.round(value * 1e6) / 1e6
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* 嘉立创国内 SMT 官方收费模式(贴片、插件焊接收费模式表,server_guide_4025,
|
|
96
|
+
* 2026-09 更新)。经济型与标准型的工程费/钢网/上料费不同,必须显式选择,
|
|
97
|
+
* 绝不能把经济型价格当标准型输出。
|
|
98
|
+
*/
|
|
99
|
+
const CN_SMT_SERVICE_TYPES = ['economic', 'standard']
|
|
100
|
+
|
|
101
|
+
/** @param {number} totalJoints @param {'economic'|'standard'} serviceType */
|
|
102
|
+
function cnSmtJointFeeCny(totalJoints, serviceType) {
|
|
103
|
+
if (!(totalJoints > 0)) return 0
|
|
104
|
+
if (serviceType !== 'standard') return totalJoints * 0.01
|
|
105
|
+
if (totalJoints <= 50000) return totalJoints * 0.01
|
|
106
|
+
if (totalJoints <= 100000) return 50000 * 0.01 + (totalJoints - 50000) * 0.008
|
|
107
|
+
return 50000 * 0.01 + 50000 * 0.008 + (totalJoints - 100000) * 0.007
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Estimate solder pads/joints for one BOM line. The BOM has no pad data, so this
|
|
112
|
+
* is explicitly an estimate unless the user passes smt_joint_count.
|
|
113
|
+
* @param {Record<string, unknown>|undefined} item
|
|
114
|
+
* @returns {{ pads: number, estimated: boolean }}
|
|
115
|
+
*/
|
|
116
|
+
function estimatePadCount(item) {
|
|
117
|
+
if (item === undefined) return { pads: 2, estimated: true }
|
|
118
|
+
for (const attribute of attributesOf(item)) {
|
|
119
|
+
if (/number of (pins|contacts|positions)|pin count|pins|positions|contacts/i.test(attribute.name)) {
|
|
120
|
+
const count = Number(String(attribute.value).match(/\d+/)?.[0])
|
|
121
|
+
if (Number.isInteger(count) && count > 0 && count <= 2000) return { pads: count, estimated: false }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const text = `${asString(item.package) ?? ''} ${asString(item.model) ?? ''} ${asString(item.description) ?? ''}`
|
|
125
|
+
const packageCount = text.match(/\b(qfn|qfp|tqfp|lqfp|bga|sop|soic|ssop|tssop|msop|sot|dfn|wson|dip|sip|esop)[-_ ]?(\d{1,4})\b/i)
|
|
126
|
+
if (packageCount !== null) {
|
|
127
|
+
const count = Number(packageCount[2])
|
|
128
|
+
if (count >= 2 && count <= 2000) return { pads: count, estimated: false }
|
|
129
|
+
}
|
|
130
|
+
const category = `${asString(item.category) ?? ''} ${asString(item.description) ?? ''}`.toLowerCase()
|
|
131
|
+
if (/transistor|mosfet|三极管|mos/.test(category)) return { pads: 3, estimated: true }
|
|
132
|
+
return { pads: 2, estimated: true }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** @param {Record<string, unknown>[]} smtRows @param {Map<string, Record<string, unknown>>} jlcByCode */
|
|
136
|
+
function cnEconomicEligibilityWarnings(smtRows, jlcByCode) {
|
|
137
|
+
const warnings = []
|
|
138
|
+
for (const row of smtRows) {
|
|
139
|
+
const item = jlcByCode.get(row.lcsc)
|
|
140
|
+
const text = item === undefined
|
|
141
|
+
? ''
|
|
142
|
+
: `${asString(item.package) ?? ''} ${asString(item.description) ?? ''} ${attributesOf(item).map((a) => `${a.name} ${a.value}`).join(' ')}`.toLowerCase()
|
|
143
|
+
const packageText = `${asString(item?.package) ?? ''}`.toLowerCase()
|
|
144
|
+
if (/0201|01005|008004/.test(packageText)) {
|
|
145
|
+
warnings.push(`经济型不支持片式元件 <0402(${row.lcsc} ${packageText || '0201'}),请改用标准型或人工确认`)
|
|
146
|
+
}
|
|
147
|
+
const pitch = text.match(/pitch[^0-9]{0,8}(\d+(?:\.\d+)?)\s*mm/i) ?? text.match(/(\d+(?:\.\d+)?)\s*mm\s*(?:间距|pitch)/i)
|
|
148
|
+
if (pitch !== null && Number(pitch[1]) < 0.4) {
|
|
149
|
+
warnings.push(`经济型不支持 IC pin 间距 <0.4mm(${row.lcsc}:${pitch[1]}mm),请改用标准型或人工确认`)
|
|
150
|
+
}
|
|
151
|
+
if (/bga/.test(packageText) && /pitch[^0-9]{0,8}0?\.(?:[0-4]\d?)\s*mm/i.test(text)) {
|
|
152
|
+
warnings.push(`经济型不支持 BGA 球径间距 <0.5mm(${row.lcsc}),请改用标准型或人工确认`)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return [...new Set(warnings)]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 嘉立创国内 SMT 人民币成本(真实 CNY 元件价 + 官方费率)。
|
|
160
|
+
* @param {{
|
|
161
|
+
* smtRows: Record<string, unknown>[],
|
|
162
|
+
* boards: number,
|
|
163
|
+
* serviceType: 'economic'|'standard',
|
|
164
|
+
* sides: number,
|
|
165
|
+
* jointCount?: number,
|
|
166
|
+
* settings: Record<string, unknown>,
|
|
167
|
+
* jlcByCode: Map<string, Record<string, unknown>>,
|
|
168
|
+
* }} input
|
|
169
|
+
*/
|
|
170
|
+
function computeCnSmtCost(input) {
|
|
171
|
+
const { smtRows, boards, serviceType, sides, jointCount, settings, jlcByCode } = input
|
|
172
|
+
const economic = serviceType === 'economic'
|
|
173
|
+
const engineeringPerSide = asNumber(economic ? settings.cnSmtEconomicEngineeringCny : settings.cnSmtStandardEngineeringCny) ?? (economic ? 50 : 150)
|
|
174
|
+
const stencilPerSide = asNumber(economic ? settings.cnSmtEconomicStencilCny : settings.cnSmtStandardStencilCny) ?? (economic ? 0 : 50)
|
|
175
|
+
const extendedPerPart = asNumber(economic ? settings.cnSmtEconomicExtendedCny : settings.cnSmtStandardExtendedCny) ?? (economic ? 20 : 8)
|
|
176
|
+
const extendedCap = asNumber(settings.cnSmtExtendedCapCny) ?? 800
|
|
177
|
+
const componentCny = smtRows.reduce((sum, row) => sum + (asNumber(row.subtotal_cny) ?? 0), 0)
|
|
178
|
+
const estimatedRows = smtRows.filter((row) => row.subtotal_cny === undefined)
|
|
179
|
+
const componentEstimateCny = estimatedRows.reduce((sum, row) => sum + (asNumber(row.subtotal_cny_estimate) ?? 0), 0)
|
|
180
|
+
const componentsTotalCny = componentCny + componentEstimateCny
|
|
181
|
+
const extendedUnique = new Set(smtRows.filter((row) => row.library === 'expand').map((row) => row.lcsc))
|
|
182
|
+
const extendedRaw = extendedUnique.size * extendedPerPart
|
|
183
|
+
const extendedFee = Math.min(extendedRaw, extendedCap)
|
|
184
|
+
const engineeringFee = engineeringPerSide * sides
|
|
185
|
+
const stencilFee = stencilPerSide * sides
|
|
186
|
+
const estimatedJoints = smtRows.reduce((sum, row) => {
|
|
187
|
+
const { pads } = estimatePadCount(jlcByCode.get(row.lcsc))
|
|
188
|
+
return sum + pads * (asNumber(row.qty) ?? 0) * boards
|
|
189
|
+
}, 0)
|
|
190
|
+
const joints = Number.isFinite(jointCount) && jointCount > 0 ? Math.round(jointCount) : estimatedJoints
|
|
191
|
+
const jointFee = cnSmtJointFeeCny(joints, serviceType)
|
|
192
|
+
const totalCny = componentsTotalCny + extendedFee + engineeringFee + stencilFee + jointFee
|
|
193
|
+
const warnings = economic ? cnEconomicEligibilityWarnings(smtRows, jlcByCode) : []
|
|
194
|
+
return {
|
|
195
|
+
basis: 'cny_jlc_cn',
|
|
196
|
+
service_type: serviceType,
|
|
197
|
+
sides,
|
|
198
|
+
rules_source: '嘉立创官方「贴片、插件焊接收费模式」表(server_guide_4025,2026-09 更新)',
|
|
199
|
+
component_price_source: 'lcsc_cn_retail_1pc',
|
|
200
|
+
component_price_note: '器件费按立创商城国内站 1pc 人民币价估算;嘉立创系统按实际采购数量计价,最终以系统报价为准',
|
|
201
|
+
components_cny: money(componentsTotalCny),
|
|
202
|
+
components_actual_cny: money(componentCny),
|
|
203
|
+
component_estimated_lines: estimatedRows.length,
|
|
204
|
+
components_estimate_cny: money(componentEstimateCny),
|
|
205
|
+
extended_parts: extendedUnique.size,
|
|
206
|
+
extended_fee_per_part_cny: money(extendedPerPart),
|
|
207
|
+
extended_fee_cny: money(extendedFee),
|
|
208
|
+
extended_fee_cap_cny: money(extendedCap),
|
|
209
|
+
extended_fee_cap_applied: extendedRaw > extendedCap,
|
|
210
|
+
engineering_fee_cny: money(engineeringFee),
|
|
211
|
+
engineering_fee_per_side_cny: money(engineeringPerSide),
|
|
212
|
+
stencil_fee_cny: money(stencilFee),
|
|
213
|
+
stencil_fee_per_side_cny: money(stencilPerSide),
|
|
214
|
+
joint_count: joints,
|
|
215
|
+
joint_count_estimated: !(Number.isFinite(jointCount) && jointCount > 0),
|
|
216
|
+
joint_fee_cny: money(jointFee),
|
|
217
|
+
total_cny: money(totalCny),
|
|
218
|
+
per_board_cny: money(totalCny / Math.max(1, boards)),
|
|
219
|
+
warnings,
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
94
223
|
/**
|
|
95
224
|
* DSH validates tool results for lossless JSON: plain `undefined` properties,
|
|
96
225
|
* NaN and Infinity are rejected. Normalise any result before returning it.
|
|
@@ -1059,7 +1188,9 @@ export async function findParts(service, args, options = {}) {
|
|
|
1059
1188
|
const sort = ['cost', 'stock'].includes(args.sort) ? args.sort : 'balanced'
|
|
1060
1189
|
const baseOnly = args.base_only === true
|
|
1061
1190
|
const requireStock = ['lcsc', 'smt'].includes(args.require_stock) ? args.require_stock : 'any'
|
|
1191
|
+
const includeUsd = args.include_usd !== false
|
|
1062
1192
|
const includeCny = args.include_cny === true || (args.include_cny === undefined && assembly === 'hand') || requireStock === 'lcsc'
|
|
1193
|
+
if (!includeUsd && !includeCny) throw new Error('find_lcsc_parts: include_usd=false 且 include_cny=false 时没有可输出的价格;至少保留一种币种')
|
|
1063
1194
|
const exclude = new Set(
|
|
1064
1195
|
(Array.isArray(args.exclude_lcsc) ? args.exclude_lcsc : [])
|
|
1065
1196
|
.map((code) => normalizeCode(code))
|
|
@@ -1224,7 +1355,9 @@ export async function findParts(service, args, options = {}) {
|
|
|
1224
1355
|
}
|
|
1225
1356
|
const covered = [...cnyByCode.values()].filter((result) => result?.ok === true).length
|
|
1226
1357
|
if (covered > 0) {
|
|
1227
|
-
notes.push(
|
|
1358
|
+
notes.push(includeUsd
|
|
1359
|
+
? `人民币价来自立创商城国内站(Chrome 渲染,${covered}/${targets.length} 个候选取到 CNY,24h 缓存)。USD 与 CNY 分列,不混算。`
|
|
1360
|
+
: `人民币价来自立创商城国内站(Chrome 渲染,${covered}/${targets.length} 个候选取到 CNY,24h 缓存)。include_usd=false:只输出 CNY。`)
|
|
1228
1361
|
}
|
|
1229
1362
|
const cnyFailure = cnFailureHint(results, service.cn)
|
|
1230
1363
|
if (cnyFailure !== undefined) hints.push(cnyFailure)
|
|
@@ -1360,6 +1493,11 @@ export async function findParts(service, args, options = {}) {
|
|
|
1360
1493
|
notes.push(`assembly=smt:扩展库按每料号 $${feeUsd}/单上料费摊销;基础库优先度体现在排序里。`)
|
|
1361
1494
|
}
|
|
1362
1495
|
|
|
1496
|
+
if (!includeUsd) {
|
|
1497
|
+
for (const candidate of candidates) {
|
|
1498
|
+
for (const key of ['price', 'price_usd', 'used_tier', 'effective_unit_usd', 'fee_per_board_usd']) delete candidate[key]
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1363
1501
|
const allWarnings = [...new Set(candidates.flatMap((candidate) => candidate.data_warnings ?? []))]
|
|
1364
1502
|
if (allWarnings.includes('retail_out_of_stock_smt_ok')) {
|
|
1365
1503
|
notes.push('注意:部分候选「立创零售仓缺货、嘉立创 SMT 仓有货」——不是停产;DIY 配单需看 stock_retail,PCBA 可看 stock_smt。')
|
|
@@ -1372,10 +1510,16 @@ export async function findParts(service, args, options = {}) {
|
|
|
1372
1510
|
boards,
|
|
1373
1511
|
order_qty: orderQty,
|
|
1374
1512
|
require_stock: requireStock,
|
|
1513
|
+
include_usd: includeUsd,
|
|
1514
|
+
include_cny: includeCny,
|
|
1375
1515
|
stock_scope_note: assembly === 'smt'
|
|
1376
1516
|
? '默认按嘉立创 SMT 库存;DIY 配单请传 require_stock="lcsc"'
|
|
1377
1517
|
: '默认按立创商城零售库存;PCBA 请传 require_stock="smt"',
|
|
1378
|
-
currency:
|
|
1518
|
+
currency: [includeUsd ? 'USD' : undefined, includeCny ? 'CNY(retail)' : undefined].filter(Boolean).join(' + '),
|
|
1519
|
+
currency_info: {
|
|
1520
|
+
price_columns: [...(includeUsd ? ['usd'] : []), ...(includeCny ? ['cny'] : [])],
|
|
1521
|
+
note: 'USD 与 CNY 不互相换算;CNY 为立创商城零售 1pc 价。',
|
|
1522
|
+
},
|
|
1379
1523
|
relaxed,
|
|
1380
1524
|
relax_stage: relaxStage,
|
|
1381
1525
|
candidates,
|
|
@@ -1752,7 +1896,13 @@ export async function auditBom(service, args, options = {}) {
|
|
|
1752
1896
|
}
|
|
1753
1897
|
const assembly = args.assembly === 'smt' ? 'smt' : 'hand'
|
|
1754
1898
|
const boards = clampInteger(args.boards, 1, 100000, 1)
|
|
1755
|
-
|
|
1899
|
+
const includeUsd = args.include_usd !== false
|
|
1900
|
+
const smtCostBasis = ['usd_jlc_global', 'cny_retail', 'cny_jlc_cn'].includes(args.smt_cost_basis)
|
|
1901
|
+
? args.smt_cost_basis
|
|
1902
|
+
: 'usd_jlc_global'
|
|
1903
|
+
const smtServiceType = CN_SMT_SERVICE_TYPES.includes(args.smt_service_type) ? args.smt_service_type : undefined
|
|
1904
|
+
const smtSides = clampInteger(args.smt_sides, 1, 2, 1)
|
|
1905
|
+
let includeCny = args.include_cny === true || smtCostBasis === 'cny_jlc_cn'
|
|
1756
1906
|
const requireStock = ['lcsc', 'smt', 'any'].includes(args.require_stock) ? args.require_stock : 'any'
|
|
1757
1907
|
const stockScope = requireStock === 'any' ? (assembly === 'smt' ? 'smt' : 'lcsc') : requireStock
|
|
1758
1908
|
const suggestAlternatives = args.suggest_alternatives !== false
|
|
@@ -1957,6 +2107,50 @@ export async function auditBom(service, args, options = {}) {
|
|
|
1957
2107
|
}
|
|
1958
2108
|
})
|
|
1959
2109
|
|
|
2110
|
+
// ---- Domestic JLC SMT CNY cost (economic vs standard; explicit) ----------
|
|
2111
|
+
/** @type {Record<string, unknown>|undefined} */
|
|
2112
|
+
let cnSmt
|
|
2113
|
+
const currencyErrors = []
|
|
2114
|
+
if (!includeUsd && !includeCny) {
|
|
2115
|
+
currencyErrors.push('include_usd=false 且 include_cny=false:至少保留一种币种。')
|
|
2116
|
+
}
|
|
2117
|
+
if (smtCostBasis === 'cny_jlc_cn' && assembly === 'smt') {
|
|
2118
|
+
if (smtServiceType === undefined) {
|
|
2119
|
+
currencyErrors.push('smt_cost_basis=cny_jlc_cn 必须显式给 smt_service_type="economic" 或 "standard":两种产线工程费/钢网费/上料费不同,不能自动判断。')
|
|
2120
|
+
} else if (smtServiceType === 'economic' && smtSides > 1) {
|
|
2121
|
+
currencyErrors.push('经济型(economic)只支持单面焊接;smt_sides=2 请改用 standard。')
|
|
2122
|
+
} else {
|
|
2123
|
+
cnSmt = computeCnSmtCost({
|
|
2124
|
+
smtRows,
|
|
2125
|
+
boards,
|
|
2126
|
+
serviceType: smtServiceType,
|
|
2127
|
+
sides: smtSides,
|
|
2128
|
+
jointCount: args.smt_joint_count === undefined ? undefined : asNumber(args.smt_joint_count),
|
|
2129
|
+
settings,
|
|
2130
|
+
jlcByCode,
|
|
2131
|
+
})
|
|
2132
|
+
if (smtServiceType === 'economic') {
|
|
2133
|
+
notes.push('CNY SMT 按【经济型】计价:仅单面焊接,片式元件 ≥0402、IC pin 间距 ≥0.4mm;插件/特殊工艺需人工确认。')
|
|
2134
|
+
cnSmt.warnings = cnSmt.warnings ?? []
|
|
2135
|
+
if (cnSmt.warnings.length === 0) cnSmt.warnings.push('经济型仅支持单面焊接,BOM 未包含贴装面信息,请人工确认;精密器件可能需改用标准型。')
|
|
2136
|
+
} else {
|
|
2137
|
+
notes.push('CNY SMT 按【标准型】计价:可双面、工艺限制少;工程费/钢网费按面数计。')
|
|
2138
|
+
}
|
|
2139
|
+
notes.push('CNY SMT 器件费按立创商城 1pc 人民币价估算 + 嘉立创官方收费表(server_guide_4025,2026-09);焊点数为估算时,最终以嘉立创系统报价为准。')
|
|
2140
|
+
}
|
|
2141
|
+
if (currencyErrors.length > 0) {
|
|
2142
|
+
cnSmt = undefined
|
|
2143
|
+
}
|
|
2144
|
+
} else if (smtCostBasis === 'cny_retail' && assembly === 'smt') {
|
|
2145
|
+
cnSmt = {
|
|
2146
|
+
basis: 'cny_retail',
|
|
2147
|
+
service_type: undefined,
|
|
2148
|
+
note: '仅立创商城零售人民币参考价(元件费),不含嘉立创 SMT 加工费/上料费;如需组装报价请用 smt_cost_basis=cny_jlc_cn + smt_service_type。',
|
|
2149
|
+
components_cny: money(componentCny + componentCnyEstimate),
|
|
2150
|
+
total_cny: money(componentCny + componentCnyEstimate),
|
|
2151
|
+
per_board_cny: money((componentCny + componentCnyEstimate) / Math.max(1, boards)),
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
1960
2154
|
// ---- Alternatives & net-saving optimisation ------------------------------
|
|
1961
2155
|
const optimize = {
|
|
1962
2156
|
require_stock: requireStock,
|
|
@@ -2061,17 +2255,26 @@ export async function auditBom(service, args, options = {}) {
|
|
|
2061
2255
|
}
|
|
2062
2256
|
|
|
2063
2257
|
// ---- Notes ---------------------------------------------------------------
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2258
|
+
if (includeUsd) {
|
|
2259
|
+
notes.push(
|
|
2260
|
+
assembly === 'smt'
|
|
2261
|
+
? 'assembly=smt:价格与库存取嘉立创 SMT 元件库(USD 阶梯价按采购数量取档);上料费只计 smt 且为扩展库的唯一料号。'
|
|
2262
|
+
: 'assembly=hand:不计嘉立创上料费;价格优先取立创商城国内站人民币价(Chrome 渲染),未取到的行给 estimated 估算。',
|
|
2263
|
+
)
|
|
2264
|
+
notes.push('USD 与 CNY 分列展示、不互相换算;每行带 price_source / fetched_at / estimated。')
|
|
2265
|
+
notes.push(
|
|
2266
|
+
assembly === 'smt'
|
|
2267
|
+
? `USD 上料费按每唯一扩展库料号 $${feeUsd}/单估算;工程费 $${setupFeeUsd}、钢网费 $${stencilFeeUsd} 已单列(默认 0,可在插件 config 配置)。`
|
|
2268
|
+
: `上料费 0;工程费 $${setupFeeUsd}、钢网费 $${stencilFeeUsd} 可配置。`,
|
|
2269
|
+
)
|
|
2270
|
+
} else {
|
|
2271
|
+
notes.push('include_usd=false:USD 列与 USD 合计已隐藏,只输出 CNY。')
|
|
2272
|
+
if (cnSmt?.basis === 'cny_jlc_cn') {
|
|
2273
|
+
notes.push(`CNY SMT 采用嘉立创国内【${smtServiceType === 'economic' ? '经济型' : '标准型'}】费率:工程费/钢网费/上料费/焊点费均以人民币单列,最终以嘉立创系统报价为准。`)
|
|
2274
|
+
} else {
|
|
2275
|
+
notes.push('assembly=hand:只输出立创商城零售人民币价;SMT 加工费未计入。')
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2075
2278
|
if (region === 'cn') {
|
|
2076
2279
|
notes.push('region=cn:国内嘉立创 SMT 的费用规则以官方计价页为准;本工具仍按 USD 上料费估算并显式标注。')
|
|
2077
2280
|
}
|
|
@@ -2088,7 +2291,7 @@ export async function auditBom(service, args, options = {}) {
|
|
|
2088
2291
|
notes.push(`BOM 中有 ${skipped.length} 行没有可识别的 LCSC 编号,未计入成本:${list}${skipped.length > 30 ? ' 等' : ''}`)
|
|
2089
2292
|
hints.push('以上行需先选型(补上 LCSC 编号)再重新审计,否则单板成本会偏低。')
|
|
2090
2293
|
}
|
|
2091
|
-
if (costTable.length > 0) {
|
|
2294
|
+
if (includeUsd && costTable.length > 0) {
|
|
2092
2295
|
const scenario = costTable.find((row) => row.boards === 100) ?? costTable[costTable.length - 1]
|
|
2093
2296
|
notes.push(`批量摊销参考:${scenario.boards} 片时约 $${scenario.per_board_usd}/片(含上料费 $${scenario.extended_fee_per_board_usd}/片 + 工程/钢网 $${money(setupFees / scenario.boards)}/片)。`)
|
|
2094
2297
|
}
|
|
@@ -2131,6 +2334,110 @@ export async function auditBom(service, args, options = {}) {
|
|
|
2131
2334
|
stock_scope: stockScope,
|
|
2132
2335
|
}
|
|
2133
2336
|
|
|
2337
|
+
const cnSmtTable = []
|
|
2338
|
+
if (cnSmt?.basis === 'cny_jlc_cn') {
|
|
2339
|
+
for (const scenario of scenarioBoards) {
|
|
2340
|
+
const scaledComponents = (asNumber(cnSmt.components_cny) ?? 0) * scenario / Math.max(1, boards)
|
|
2341
|
+
const scaledJoints = (asNumber(cnSmt.joint_count) ?? 0) * scenario / Math.max(1, boards)
|
|
2342
|
+
const jointFee = cnSmtJointFeeCny(Math.round(scaledJoints), smtServiceType)
|
|
2343
|
+
const total = scaledComponents + (asNumber(cnSmt.extended_fee_cny) ?? 0) +
|
|
2344
|
+
(asNumber(cnSmt.engineering_fee_cny) ?? 0) + (asNumber(cnSmt.stencil_fee_cny) ?? 0) + jointFee
|
|
2345
|
+
cnSmtTable.push({
|
|
2346
|
+
boards: scenario,
|
|
2347
|
+
components_cny: money(scaledComponents),
|
|
2348
|
+
extended_fee_cny: money(cnSmt.extended_fee_cny),
|
|
2349
|
+
engineering_fee_cny: money(cnSmt.engineering_fee_cny),
|
|
2350
|
+
stencil_fee_cny: money(cnSmt.stencil_fee_cny),
|
|
2351
|
+
joint_fee_cny: money(jointFee),
|
|
2352
|
+
total_cny: money(total),
|
|
2353
|
+
per_board_cny: money(total / scenario),
|
|
2354
|
+
})
|
|
2355
|
+
}
|
|
2356
|
+
summary.cn_smt_table = cnSmtTable
|
|
2357
|
+
const scenario = cnSmtTable.find((row) => row.boards === 100) ?? cnSmtTable[cnSmtTable.length - 1]
|
|
2358
|
+
if (scenario !== undefined) {
|
|
2359
|
+
notes.push(`CNY SMT 批量摊销:${scenario.boards} 片时约 ¥${scenario.per_board_cny}/片(含工程/钢网/上料/焊点;最终以嘉立创系统报价为准)。`)
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
// ---- Currency separation: remove suppressed price columns ----------------
|
|
2364
|
+
if (!includeUsd) {
|
|
2365
|
+
for (const key of ['components_usd', 'components_per_board_usd', 'total_usd', 'total_per_board_usd',
|
|
2366
|
+
'hand_components_usd_reference', 'dnp_components_usd_excluded', 'extended_fee_per_order_usd',
|
|
2367
|
+
'setup_fee_usd', 'stencil_fee_usd']) {
|
|
2368
|
+
delete summary[key]
|
|
2369
|
+
}
|
|
2370
|
+
delete summary.cost_table
|
|
2371
|
+
}
|
|
2372
|
+
if (!includeCny) {
|
|
2373
|
+
for (const key of ['cny_covered_lines', 'cny_total_basis', 'cny_price_tier', 'components_cny',
|
|
2374
|
+
'components_cny_per_order', 'components_cny_per_board']) {
|
|
2375
|
+
delete summary[key]
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
summary.cn_smt = cnSmt
|
|
2379
|
+
summary.cn_smt_service_type = smtServiceType
|
|
2380
|
+
summary.smt_cost_basis = smtCostBasis
|
|
2381
|
+
summary.include_usd = includeUsd
|
|
2382
|
+
summary.include_cny = includeCny
|
|
2383
|
+
for (const row of auditRows) {
|
|
2384
|
+
if (!includeUsd) {
|
|
2385
|
+
for (const key of ['unit_usd', 'price_usd_tier', 'price_usd_ladder', 'subtotal_usd', 'price_source']) delete row[key]
|
|
2386
|
+
if (Array.isArray(row.flags)) {
|
|
2387
|
+
row.flags = row.flags.filter((flag) => !String(flag).includes('$'))
|
|
2388
|
+
if (cnSmt?.basis === 'cny_jlc_cn' && row.library === 'expand' && row.billable_smt) {
|
|
2389
|
+
row.flags.push(`扩展库:嘉立创国内 SMT 上料费(${smtServiceType === 'economic' ? '经济型 ¥20/种' : '标准型 ¥8/种'})`)
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
if (!includeCny) {
|
|
2394
|
+
for (const key of ['unit_cny', 'subtotal_cny', 'price_cny_source', 'price_cny_estimated']) delete row[key]
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
if (currencyErrors.length > 0) {
|
|
2398
|
+
for (const message of currencyErrors) errors.push(message)
|
|
2399
|
+
hints.push('CNY SMT 报价未生成;修正参数后重试。')
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
// ---- Explicit totals block (USD / CNY never mixed) -----------------------
|
|
2403
|
+
/** @type {Record<string, unknown>} */
|
|
2404
|
+
const totals = {}
|
|
2405
|
+
if (includeUsd) {
|
|
2406
|
+
totals.components_usd = money(componentUsd)
|
|
2407
|
+
totals.extended_fee_usd = money(extendedFee)
|
|
2408
|
+
totals.setup_fee_usd = money(setupFeeUsd)
|
|
2409
|
+
totals.stencil_fee_usd = money(stencilFeeUsd)
|
|
2410
|
+
totals.smt_total_usd = money(totalUsd)
|
|
2411
|
+
totals.smt_per_board_usd = money(totalUsd / Math.max(1, boards))
|
|
2412
|
+
}
|
|
2413
|
+
if (includeCny) {
|
|
2414
|
+
totals.retail_total_cny = money(componentCny)
|
|
2415
|
+
totals.retail_per_board_cny = money(componentCny / Math.max(1, boards))
|
|
2416
|
+
totals.retail_backed_lines_cny = cnyCovered
|
|
2417
|
+
}
|
|
2418
|
+
if (cnSmt !== undefined) {
|
|
2419
|
+
totals.smt_total_cny = cnSmt.total_cny
|
|
2420
|
+
totals.smt_per_board_cny = cnSmt.per_board_cny
|
|
2421
|
+
totals.smt_service_type = cnSmt.service_type
|
|
2422
|
+
}
|
|
2423
|
+
if (includeUsd || includeCny) {
|
|
2424
|
+
totals.per_board = {
|
|
2425
|
+
...(includeUsd ? { usd: money(totalUsd / Math.max(1, boards)) } : {}),
|
|
2426
|
+
...(includeCny ? { cny_retail: money(componentCny / Math.max(1, boards)) } : {}),
|
|
2427
|
+
...(cnSmt?.basis === 'cny_jlc_cn' ? { cny_smt: cnSmt.per_board_cny } : {}),
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
const currencyInfo = {
|
|
2431
|
+
price_columns: [...(includeUsd ? ['usd'] : []), ...(includeCny ? ['cny'] : [])],
|
|
2432
|
+
smt_cost_basis: smtCostBasis,
|
|
2433
|
+
smt_service_type: smtServiceType ?? null,
|
|
2434
|
+
smt_sides: smtSides,
|
|
2435
|
+
notes: [
|
|
2436
|
+
'USD = 嘉立创/LCSC 国际口径;CNY = 立创商城零售/嘉立创国内 SMT 口径,不互相换算。',
|
|
2437
|
+
'CNY SMT(cny_jlc_cn)必须显式选择 economic / standard,两种产线价格不同。',
|
|
2438
|
+
],
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2134
2441
|
const dataWarnings = [...new Set(auditRows.flatMap((row) => row.data_warnings ?? []))]
|
|
2135
2442
|
if (dataWarnings.includes('retail_out_of_stock_smt_ok')) {
|
|
2136
2443
|
notes.push('注意:有料号「立创零售仓缺货、嘉立创 SMT 仓有货」——不是停产;DIY 配单看 stock_retail,PCBA 看 stock_smt。')
|
|
@@ -2141,7 +2448,17 @@ export async function auditBom(service, args, options = {}) {
|
|
|
2141
2448
|
region,
|
|
2142
2449
|
require_stock: requireStock,
|
|
2143
2450
|
stock_scope: stockScope,
|
|
2144
|
-
|
|
2451
|
+
include_usd: includeUsd,
|
|
2452
|
+
include_cny: includeCny,
|
|
2453
|
+
smt_cost_basis: smtCostBasis,
|
|
2454
|
+
smt_service_type: smtServiceType,
|
|
2455
|
+
currency: [
|
|
2456
|
+
includeUsd ? 'USD' : undefined,
|
|
2457
|
+
includeCny ? (cnSmt?.basis === 'cny_jlc_cn' ? `CNY(${smtServiceType})` : 'CNY(retail)') : undefined,
|
|
2458
|
+
].filter(Boolean).join(' + ') || 'none',
|
|
2459
|
+
currency_info: currencyInfo,
|
|
2460
|
+
totals,
|
|
2461
|
+
cn_smt: cnSmt,
|
|
2145
2462
|
summary,
|
|
2146
2463
|
rows: auditRows,
|
|
2147
2464
|
optimize,
|
|
@@ -2163,20 +2480,23 @@ export async function auditBom(service, args, options = {}) {
|
|
|
2163
2480
|
export function renderFindText(value) {
|
|
2164
2481
|
const candidates = Array.isArray(value.candidates) ? value.candidates : []
|
|
2165
2482
|
const header =
|
|
2166
|
-
`Find "${value.need}" [${value.assembly}${value.require_stock && value.require_stock !== 'any' ? `/stock=${value.require_stock}` : ''}] qty ${value.quantity}/board x ${value.boards} board(s)` +
|
|
2483
|
+
`Find "${value.need}" [${value.assembly}${value.require_stock && value.require_stock !== 'any' ? `/stock=${value.require_stock}` : ''}] qty ${value.quantity}/board x ${value.boards} board(s) [${value.currency ?? 'USD + CNY'}]` +
|
|
2167
2484
|
(value.exact === true ? ' (exact part query)' : '') +
|
|
2168
2485
|
(value.relaxed === true ? ` (relaxed stage ${value.relax_stage})` : '') +
|
|
2169
2486
|
` => ${candidates.length} candidate(s)`
|
|
2170
2487
|
const lines = [header]
|
|
2171
2488
|
for (const candidate of candidates) {
|
|
2172
2489
|
if (!isRecord(candidate)) continue
|
|
2173
|
-
const
|
|
2174
|
-
const
|
|
2490
|
+
const suppressedUsd = value.include_usd === false
|
|
2491
|
+
const suppressedCny = value.include_cny === false
|
|
2492
|
+
const priceUsd = !suppressedUsd && isRecord(candidate.price_usd) ? `USD ${candidate.price_usd.unit} @${candidate.price_usd.tier}` : ''
|
|
2493
|
+
const priceCny = !suppressedCny && isRecord(candidate.price_cny)
|
|
2175
2494
|
? `CNY ${candidate.price_cny.unit}${candidate.price_cny.estimated === true ? '(est.)' : ''}`
|
|
2176
|
-
: '
|
|
2495
|
+
: ''
|
|
2177
2496
|
lines.push(
|
|
2178
2497
|
`- ${candidate.lcsc} | ${candidate.model ?? '?'} | ${candidate.package ?? '?'} | lib ${candidate.library} | ` +
|
|
2179
|
-
`stock smt ${candidate.stock_smt ?? '?'} / retail ${candidate.stock_retail ?? '?'} |
|
|
2498
|
+
`stock smt ${candidate.stock_smt ?? '?'} / retail ${candidate.stock_retail ?? '?'} | ` +
|
|
2499
|
+
[priceUsd, priceCny].filter(Boolean).join(' | ') + ` | score ${candidate.score}`,
|
|
2180
2500
|
)
|
|
2181
2501
|
if (Array.isArray(candidate.data_warnings) && candidate.data_warnings.length > 0) {
|
|
2182
2502
|
lines.push(` ⚠ ${candidate.data_warnings.join(', ')}`)
|
|
@@ -2196,37 +2516,65 @@ export function renderAuditText(value) {
|
|
|
2196
2516
|
const summary = isRecord(value.summary) ? value.summary : {}
|
|
2197
2517
|
const rows = Array.isArray(value.rows) ? value.rows : []
|
|
2198
2518
|
const optimize = isRecord(value.optimize) ? value.optimize : {}
|
|
2519
|
+
const includeUsd = summary.include_usd !== false
|
|
2520
|
+
const includeCny = summary.include_cny !== false
|
|
2199
2521
|
const lines = [
|
|
2200
|
-
`BOM audit [${value.assembly}] region=${value.region ?? 'global'} boards=${value.boards}: ` +
|
|
2522
|
+
`BOM audit [${value.assembly}] region=${value.region ?? 'global'} boards=${value.boards} currency=${value.currency ?? '?'}: ` +
|
|
2201
2523
|
`${summary.lines ?? 0} lines (${summary.smt_lines ?? 0} smt / ${summary.hand_lines ?? 0} hand / ${summary.dnp_lines ?? 0} dnp), ` +
|
|
2202
2524
|
`${summary.unique_parts ?? 0} unique | base ${summary.base_parts ?? 0} / expand ${summary.expand_parts ?? 0}`,
|
|
2203
|
-
`components USD ${summary.components_usd ?? 0} + extended fee USD ${summary.extended_fee_per_order_usd ?? 0}` +
|
|
2204
|
-
` + setup USD ${money((summary.setup_fee_usd ?? 0) + (summary.stencil_fee_usd ?? 0))} = USD ${summary.total_usd ?? 0}` +
|
|
2205
|
-
` (USD ${summary.total_per_board_usd ?? 0}/board)` +
|
|
2206
|
-
(summary.cny_covered_lines > 0
|
|
2207
|
-
? ` | CNY retail: ¥${summary.components_cny_per_order ?? summary.components_cny ?? 0}/order = ¥${summary.components_cny_per_board ?? '?'}/board (${summary.cny_covered_lines} lines)`
|
|
2208
|
-
: '') +
|
|
2209
|
-
(summary.components_cny_estimate > 0 ? ` | CNY est. ≈ ${summary.components_cny_estimate}` : ''),
|
|
2210
2525
|
]
|
|
2526
|
+
if (includeUsd) {
|
|
2527
|
+
lines.push(
|
|
2528
|
+
`components USD ${summary.components_usd ?? 0} + extended fee USD ${summary.extended_fee_per_order_usd ?? 0}` +
|
|
2529
|
+
` + setup USD ${money((summary.setup_fee_usd ?? 0) + (summary.stencil_fee_usd ?? 0))} = USD ${summary.total_usd ?? 0}` +
|
|
2530
|
+
` (USD ${summary.total_per_board_usd ?? 0}/board)`,
|
|
2531
|
+
)
|
|
2532
|
+
}
|
|
2533
|
+
if (includeCny) {
|
|
2534
|
+
lines.push(
|
|
2535
|
+
`CNY retail: ¥${summary.components_cny_per_order ?? summary.components_cny ?? 0}/order = ¥${summary.components_cny_per_board ?? '?'}/board` +
|
|
2536
|
+
` (${summary.cny_covered_lines ?? 0} lines)` +
|
|
2537
|
+
(summary.components_cny_estimate > 0 ? ` | CNY est. ≈ ${summary.components_cny_estimate}` : ''),
|
|
2538
|
+
)
|
|
2539
|
+
}
|
|
2540
|
+
const cnSmt = isRecord(value.cn_smt) ? value.cn_smt : undefined
|
|
2541
|
+
if (cnSmt !== undefined && cnSmt.basis === 'cny_jlc_cn') {
|
|
2542
|
+
lines.push(
|
|
2543
|
+
`CNY SMT [${cnSmt.service_type}] 元件 ¥${cnSmt.components_cny} + 上料 ¥${cnSmt.extended_fee_cny}` +
|
|
2544
|
+
` + 工程 ¥${cnSmt.engineering_fee_cny} + 钢网 ¥${cnSmt.stencil_fee_cny} + 焊点 ¥${cnSmt.joint_fee_cny}` +
|
|
2545
|
+
` = ¥${cnSmt.total_cny}/单 = ¥${cnSmt.per_board_cny}/片` +
|
|
2546
|
+
` (焊点 ${cnSmt.joint_count}${cnSmt.joint_count_estimated === true ? ', 估算' : ''})`,
|
|
2547
|
+
)
|
|
2548
|
+
if (Array.isArray(cnSmt.warnings)) for (const warning of cnSmt.warnings) lines.push(`⚠ ${warning}`)
|
|
2549
|
+
} else if (cnSmt !== undefined && cnSmt.basis === 'cny_retail') {
|
|
2550
|
+
lines.push(`CNY retail SMT reference: ¥${cnSmt.total_cny}/单 = ¥${cnSmt.per_board_cny}/片(不含加工费/上料费)`)
|
|
2551
|
+
}
|
|
2211
2552
|
for (const row of rows) {
|
|
2212
2553
|
if (!isRecord(row)) continue
|
|
2213
|
-
const flags = Array.isArray(row.flags) && row.flags.length > 0
|
|
2554
|
+
const flags = Array.isArray(row.flags) && row.flags.length > 0
|
|
2555
|
+
? ` [${(includeUsd ? row.flags : row.flags.filter((flag) => !String(flag).includes('$'))).join('; ')}]`
|
|
2556
|
+
: ''
|
|
2214
2557
|
const designators = Array.isArray(row.designators) && row.designators.length > 0 ? ` {${row.designators.join(',')}}` : ''
|
|
2215
|
-
const priceUsd = row.unit_usd === undefined ? '
|
|
2558
|
+
const priceUsd = row.unit_usd === undefined ? '' : `USD ${row.unit_usd}@${row.price_usd_tier ?? '?'}`
|
|
2216
2559
|
const priceCny = row.unit_cny !== undefined
|
|
2217
2560
|
? `CNY ${row.unit_cny}`
|
|
2218
|
-
: row.unit_cny_estimate !== undefined ? `CNY≈${row.unit_cny_estimate}(est.)` : '
|
|
2219
|
-
const alternative = isRecord(row.alternative) && typeof row.alternative.lcsc === 'string'
|
|
2561
|
+
: row.unit_cny_estimate !== undefined ? `CNY≈${row.unit_cny_estimate}(est.)` : ''
|
|
2562
|
+
const alternative = includeUsd && isRecord(row.alternative) && typeof row.alternative.lcsc === 'string'
|
|
2220
2563
|
? ` -> base alt: ${row.alternative.lcsc} (${row.alternative.model ?? '?'}, net $${row.alternative.net_saving_usd ?? '?'})`
|
|
2221
2564
|
: ''
|
|
2222
2565
|
lines.push(
|
|
2223
2566
|
`${row.lcsc} x${row.qty}${designators} | ${row.place ?? '?'} | ${row.status} | lib ${row.library ?? '?'} | ` +
|
|
2224
|
-
`stock smt ${row.stock_smt ?? '?'} / retail ${row.stock_retail ?? '?'} |
|
|
2567
|
+
`stock smt ${row.stock_smt ?? '?'} / retail ${row.stock_retail ?? '?'} | ` +
|
|
2568
|
+
[priceUsd, priceCny].filter(Boolean).join(' | ') + `${flags}${alternative}`,
|
|
2225
2569
|
)
|
|
2226
2570
|
}
|
|
2227
2571
|
const costTable = Array.isArray(summary.cost_table) ? summary.cost_table : []
|
|
2228
|
-
if (costTable.length > 0) {
|
|
2229
|
-
lines.push('
|
|
2572
|
+
if (includeUsd && costTable.length > 0) {
|
|
2573
|
+
lines.push('批量摊销(USD): ' + costTable.map((row) => `${row.boards}片≈$${row.per_board_usd}/片`).join(' | '))
|
|
2574
|
+
}
|
|
2575
|
+
const cnSmtTable = Array.isArray(summary.cn_smt_table) ? summary.cn_smt_table : []
|
|
2576
|
+
if (cnSmtTable.length > 0) {
|
|
2577
|
+
lines.push('批量摊销(CNY SMT): ' + cnSmtTable.map((row) => `${row.boards}片≈¥${row.per_board_cny}/片`).join(' | '))
|
|
2230
2578
|
}
|
|
2231
2579
|
if (Array.isArray(value.data_warnings) && value.data_warnings.length > 0) {
|
|
2232
2580
|
lines.push(`⚠ data_warnings: ${value.data_warnings.join(', ')}`)
|
|
@@ -2234,7 +2582,7 @@ export function renderAuditText(value) {
|
|
|
2234
2582
|
if (isRecord(optimize) && (optimize.require_stock !== undefined || optimize.stock_scope !== undefined)) {
|
|
2235
2583
|
lines.push(`优化建议库存口径: require_stock=${optimize.require_stock ?? 'any'} / scope=${optimize.stock_scope ?? '?'}`)
|
|
2236
2584
|
}
|
|
2237
|
-
if (Array.isArray(optimize.actions) && optimize.actions.length > 0) {
|
|
2585
|
+
if (includeUsd && Array.isArray(optimize.actions) && optimize.actions.length > 0) {
|
|
2238
2586
|
lines.push(`优化建议(净省 $${optimize.saving_usd_per_order ?? 0}/单,扩展库 ${optimize.extended_parts_before}→${optimize.extended_parts_after}):`)
|
|
2239
2587
|
for (const action of optimize.actions) lines.push(` * ${action}`)
|
|
2240
2588
|
}
|
|
@@ -2269,7 +2617,7 @@ const AUDIT_DESCRIPTION =
|
|
|
2269
2617
|
'Default assembly="hand" (no feeder fees); set assembly="smt" for JLCPCB assembly cost planning. ' +
|
|
2270
2618
|
'Outputs a batch amortisation table (20/100/300/1000 boards), an optimisation summary of net savings, and optionally a JLCPCB BOM CSV for ordering (export_bom=true). ' +
|
|
2271
2619
|
'Real CNY retail prices are fetched via headless Chrome (24h cache) when include_cny=true; otherwise a reference estimate from bulk-tier USD × the configurable factor is reported and marked estimated=true. ' +
|
|
2272
|
-
'USD and CNY are never mixed into one total.'
|
|
2620
|
+
'USD and CNY are never mixed into one total; set include_usd=false for CNY-only output.'
|
|
2273
2621
|
|
|
2274
2622
|
/** Build the two DSH tool definitions bound to a service instance. */
|
|
2275
2623
|
export function createToolDefinitions(service) {
|
|
@@ -2295,7 +2643,8 @@ export function createToolDefinitions(service) {
|
|
|
2295
2643
|
boards: { type: 'integer', minimum: 1, maximum: 100000, description: 'Number of boards to build (default 1; used for price tiers and SMT fee amortization).' },
|
|
2296
2644
|
sort: { type: 'string', enum: ['balanced', 'cost', 'stock'], description: 'Ranking preference (default balanced).' },
|
|
2297
2645
|
limit: { type: 'integer', minimum: 1, maximum: 8, description: 'Candidates to return (default 5, max 8).' },
|
|
2298
|
-
|
|
2646
|
+
include_usd: { type: 'boolean', description: 'Include USD price columns (default true). Set false for CNY-only output; USD is then suppressed entirely (no $ columns).' },
|
|
2647
|
+
include_cny: { type: 'boolean', description: 'Fetch real LCSC domestic CNY retail prices for the top candidates via headless Chrome (default true in hand mode, false in smt mode; 24h cache). When unavailable, a marked reference estimate is shown. Set false for USD-only output.' },
|
|
2299
2648
|
exclude_lcsc: { type: 'array', items: { type: 'string' }, description: 'LCSC codes to exclude, e.g. when looking for an alternative to a part you already have.' },
|
|
2300
2649
|
base_only: { type: 'boolean', description: 'Only return JLCPCB basic-library parts (no feeder fee). If none exist the tool says so instead of silently returning extended parts.' },
|
|
2301
2650
|
require_stock: { type: 'string', enum: ['any', 'lcsc', 'smt'], description: 'Stock scope filter. any = no filter (default); lcsc = 立创商城零售仓有货 (DIY 配单); smt = 嘉立创 SMT 仓有货 (PCBA). lcsc forces a real CNY/stock lookup.' },
|
|
@@ -2312,7 +2661,10 @@ export function createToolDefinitions(service) {
|
|
|
2312
2661
|
quantity: { type: 'integer' },
|
|
2313
2662
|
boards: { type: 'integer' },
|
|
2314
2663
|
order_qty: { type: 'integer' },
|
|
2664
|
+
include_usd: { type: 'boolean' },
|
|
2665
|
+
include_cny: { type: 'boolean' },
|
|
2315
2666
|
currency: { type: 'string' },
|
|
2667
|
+
currency_info: { type: 'object' },
|
|
2316
2668
|
candidates: { type: 'array', items: { type: 'object' } },
|
|
2317
2669
|
hints: { type: 'array', items: { type: 'string' } },
|
|
2318
2670
|
notes: { type: 'array', items: { type: 'string' } },
|
|
@@ -2350,7 +2702,12 @@ export function createToolDefinitions(service) {
|
|
|
2350
2702
|
description: 'hand = no JLCPCB feeder fees (default); smt = include USD 3 per extended part number per order.',
|
|
2351
2703
|
},
|
|
2352
2704
|
boards: { type: 'integer', minimum: 1, maximum: 100000, description: 'Number of boards the BOM is for (default 1).' },
|
|
2705
|
+
include_usd: { type: 'boolean', description: 'Include USD columns/totals (default true). Set false for CNY-only output (no $ in rows/totals).' },
|
|
2353
2706
|
include_cny: { type: 'boolean', description: 'Fetch real LCSC domestic CNY prices via headless Chrome (24h cache, capped; default true in hand mode for small BOMs, false for large BOMs — pass true to force).' },
|
|
2707
|
+
smt_cost_basis: { type: 'string', enum: ['usd_jlc_global', 'cny_retail', 'cny_jlc_cn'], description: 'SMT cost basis: usd_jlc_global (default, JLCPCB international USD); cny_retail (立创商城零售人民币元件参考价,不含加工费); cny_jlc_cn (嘉立创国内 SMT 实际人民币计价,需同时给 smt_service_type).' },
|
|
2708
|
+
smt_service_type: { type: 'string', enum: ['economic', 'standard'], description: '嘉立创国内 SMT 产线:economic=经济型(单面,工程费50元/钢网免费/上料20元每种);standard=标准型(可双面,工程费150元/面/钢网50元/面/上料8元每种)。两种价格差距大,smt_cost_basis=cny_jlc_cn 时必须显式选择,不能自动判断。' },
|
|
2709
|
+
smt_sides: { type: 'integer', minimum: 1, maximum: 2, description: 'SMT 贴装面数(默认 1)。standard 可按面数计工程费/钢网费;economic 仅支持 1 面。' },
|
|
2710
|
+
smt_joint_count: { type: 'integer', minimum: 1, maximum: 100000000, description: '整单焊点总数(可选)。给定则 CNY SMT 焊点费按实际/指定焊点数计算;不给定则按元件封装估算并标 estimated。' },
|
|
2354
2711
|
require_stock: { type: 'string', enum: ['any', 'lcsc', 'smt'], description: 'Stock scope for basic-library substitution suggestions. any = no filter (default); lcsc = only suggest parts with 立创零售仓库存 (DIY 配单); smt = only suggest parts with 嘉立创 SMT 仓库存 (PCBA). Each suggestion always reports stock_retail and stock_smt.' },
|
|
2355
2712
|
suggest_alternatives: { type: 'boolean', description: 'For extended-library parts, suggest a basic-library substitute with net saving (default true; only in smt mode).' },
|
|
2356
2713
|
export_bom: { type: 'boolean', description: 'Also return a JLCPCB BOM CSV (Comment/Designator/Footprint/LCSC Part) for the smt-placed lines (default false).' },
|
|
@@ -2368,7 +2725,14 @@ export function createToolDefinitions(service) {
|
|
|
2368
2725
|
region: { type: 'string' },
|
|
2369
2726
|
require_stock: { type: 'string' },
|
|
2370
2727
|
stock_scope: { type: 'string' },
|
|
2728
|
+
include_usd: { type: 'boolean' },
|
|
2729
|
+
include_cny: { type: 'boolean' },
|
|
2730
|
+
smt_cost_basis: { type: 'string' },
|
|
2731
|
+
smt_service_type: { type: 'string' },
|
|
2371
2732
|
currency: { type: 'string' },
|
|
2733
|
+
currency_info: { type: 'object' },
|
|
2734
|
+
totals: { type: 'object' },
|
|
2735
|
+
cn_smt: { type: 'object' },
|
|
2372
2736
|
summary: { type: 'object' },
|
|
2373
2737
|
data_warnings: { type: 'array', items: { type: 'string' } },
|
|
2374
2738
|
optimize: { type: 'object' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-lcsc",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Requirement-driven LCSC/JLC part finder and BOM cost auditor for DeepSeek Harness agents: real CNY+USD dual pricing, basic-vs-extended library fees, DNP/hand placement, net-saving alternatives",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|