yindie-erp-mcp 2.0.4 → 2.0.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/dist/client.js CHANGED
@@ -1,3 +1,7 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { basename, join } from 'node:path';
4
+ import { parseJsonSafe } from './tool-helpers.js';
1
5
  /** ERP API 客户端 */
2
6
  export class YindieClient {
3
7
  /** 后端根地址 */
@@ -63,6 +67,45 @@ export class YindieClient {
63
67
  async postForm(path, formFactory) {
64
68
  return this.requestForm(path, formFactory);
65
69
  }
70
+ /** 下载文件到临时目录,返回落盘路径 */
71
+ async download(method, path, fileName, options = {}, retried = false) {
72
+ const url = new URL(path, this.baseUrl);
73
+ for (const [key, value] of Object.entries(options.params ?? {})) {
74
+ if (value !== undefined && value !== '') {
75
+ url.searchParams.set(key, value);
76
+ }
77
+ }
78
+ const headers = {
79
+ Authorization: `Bearer ${this.token}`,
80
+ clientid: this.clientId
81
+ };
82
+ const init = { method, headers };
83
+ if (options.body !== undefined) {
84
+ headers['Content-Type'] = 'application/json';
85
+ init.body = JSON.stringify(options.body);
86
+ }
87
+ const response = await fetch(url, init);
88
+ const contentType = response.headers.get('content-type') ?? '';
89
+ if (contentType.includes('application/json')) {
90
+ const result = (await parseJsonSafe(await response.text()));
91
+ if (result.code === 401 && !retried) {
92
+ this.token = await this.relogin();
93
+ return this.download(method, path, fileName, options, true);
94
+ }
95
+ throw new Error(result.msg || `下载失败: ${path}`);
96
+ }
97
+ if (!response.ok) {
98
+ throw new Error(`下载失败: ${path}(HTTP ${response.status})`);
99
+ }
100
+ const headerName = response.headers.get('download-filename');
101
+ const name = fileName ?? (headerName ? decodeURIComponent(headerName) : basename(path));
102
+ const bytes = Buffer.from(await response.arrayBuffer());
103
+ const dir = join(tmpdir(), 'yindie-erp-exports');
104
+ await mkdir(dir, { recursive: true });
105
+ const filePath = join(dir, basename(name));
106
+ await writeFile(filePath, bytes);
107
+ return { filePath, size: bytes.length };
108
+ }
66
109
  /** 统一 HTTP 请求 */
67
110
  async request(method, path, body, params, retried = false) {
68
111
  const url = new URL(path, this.baseUrl);
@@ -83,7 +126,7 @@ export class YindieClient {
83
126
  init.body = JSON.stringify(body);
84
127
  }
85
128
  const response = await fetch(url, init);
86
- const result = (await response.json());
129
+ const result = (await parseJsonSafe(await response.text()));
87
130
  if (result.code === 401 && !retried) {
88
131
  this.token = await this.relogin();
89
132
  return this.request(method, path, body, params, true);
@@ -109,7 +152,7 @@ export class YindieClient {
109
152
  };
110
153
  const form = await formFactory();
111
154
  const response = await fetch(url, { method: 'POST', headers, body: form });
112
- const result = (await response.json());
155
+ const result = (await parseJsonSafe(await response.text()));
113
156
  if (result.code === 401 && !retried) {
114
157
  this.token = await this.relogin();
115
158
  return this.requestForm(path, formFactory, true);
@@ -1,9 +1,6 @@
1
1
  import { JACKET_STYLE_GROUPS, PANTS_STYLE_GROUPS } from './template.js';
2
2
  /** 拼字段示例 */
3
3
  function buildExample(item) {
4
- if (item.control === 'fixed') {
5
- return JSON.stringify({ input: item.fixedText || '' });
6
- }
7
4
  if (item.control === 'radio') {
8
5
  const first = item.options?.[0] || '选项';
9
6
  if (item.alwaysInput || item.inputWhen?.includes(first)) {
@@ -35,7 +32,7 @@ function buildExample(item) {
35
32
  return JSON.stringify({ checkbox: item.options?.slice(0, 2) || [] });
36
33
  }
37
34
  if (item.control === 'input') {
38
- return JSON.stringify({ input: '填写内容' });
35
+ return JSON.stringify({ input: item.defaultInput || '填写内容' });
39
36
  }
40
37
  if (item.control === 'combine' && item.multiFill) {
41
38
  const map = {};
@@ -60,9 +57,6 @@ function buildExample(item) {
60
57
  }
61
58
  /** 拼选择说明 */
62
59
  function buildHowTo(item) {
63
- if (item.control === 'fixed') {
64
- return `固定文案:${item.fixedText || ''}`;
65
- }
66
60
  if (item.control === 'radio') {
67
61
  let text = `单选其一:${(item.options || []).join('/')}`;
68
62
  if (item.allowAdd) {
@@ -114,9 +108,11 @@ function buildHowTo(item) {
114
108
  return text;
115
109
  }
116
110
  if (item.control === 'input') {
117
- return item.multiline
118
- ? `文本域自由填写${item.inputSuffix ? ',后缀 ' + item.inputSuffix : ''}`
119
- : `输入框填写${item.inputSuffix ? ',后缀 ' + item.inputSuffix : ''}`;
111
+ if (item.multiline) {
112
+ return `文本域自由填写${item.inputSuffix ? ',后缀 ' + item.inputSuffix : ''}`;
113
+ }
114
+ const suffix = item.inputSuffix ? `,后缀 ${item.inputSuffix}` : '';
115
+ return `输入框填写${suffix}${item.defaultInput ? `;默认「${item.defaultInput}」` : ''}`;
120
116
  }
121
117
  if (item.control === 'combine' && item.multiFill) {
122
118
  const parts = (item.combineOptions || []).map((o) => `${o.label}${o.needValue ? '(旁填值' + (o.suffix || '') + ')' : ''}`);
@@ -158,11 +154,7 @@ function flattenGroups(garmentPart, groups) {
158
154
  syncSizeOptions: item.syncSizeOptions,
159
155
  inputSuffix: item.inputSuffix,
160
156
  multiline: item.multiline,
161
- noteWhen: item.key === 'creaseHas'
162
- ? 'radio=有 → 填 note'
163
- : item.key === 'spareButton'
164
- ? 'radio=自己写 → 填 note'
165
- : undefined,
157
+ defaultInput: item.defaultInput,
166
158
  fieldValueExample: buildExample(item),
167
159
  howToSelect: buildHowTo(item)
168
160
  });
@@ -170,19 +162,44 @@ function flattenGroups(garmentPart, groups) {
170
162
  }
171
163
  return list;
172
164
  }
165
+ /** 色卡白名单字段(与后端 ErpColorCardCatalog 一致) */
166
+ const COLOR_CARD_FIELDS = [
167
+ { garmentPart: 'jacket', fieldKey: 'lining', titleCn: '里布' },
168
+ { garmentPart: 'jacket', fieldKey: 'handkerchiefShow', titleCn: '手巾袋露出' },
169
+ { garmentPart: 'jacket', fieldKey: 'fuse', titleCn: '粘衬' },
170
+ { garmentPart: 'jacket', fieldKey: 'sleeveButton', titleCn: '袖扣' },
171
+ { garmentPart: 'jacket', fieldKey: 'buttons', titleCn: '门襟大扣' },
172
+ { garmentPart: 'jacket', fieldKey: 'sleeveLining', titleCn: '袖里' },
173
+ { garmentPart: 'jacket', fieldKey: 'collarHang', titleCn: '领吊' },
174
+ { garmentPart: 'jacket', fieldKey: 'collarBottom', titleCn: '领底' },
175
+ { garmentPart: 'jacket', fieldKey: 'triangle', titleCn: '三角' },
176
+ { garmentPart: 'jacket', fieldKey: 'weltClimb', titleCn: '嵌线袋小攀' },
177
+ { garmentPart: 'jacket', fieldKey: 'innerPatch', titleCn: '里贴袋' },
178
+ { garmentPart: 'jacket', fieldKey: 'pocketCloth', titleCn: '上衣口袋布' },
179
+ { garmentPart: 'pants', fieldKey: 'crotchCloth', titleCn: '小裆布' },
180
+ { garmentPart: 'pants', fieldKey: 'liningHas', titleCn: '脚绸' },
181
+ { garmentPart: 'pants', fieldKey: 'backPocketStitch', titleCn: '后袋' },
182
+ { garmentPart: 'pants', fieldKey: 'backSeam', titleCn: '裤后缝' },
183
+ { garmentPart: 'pants', fieldKey: 'waistInnerStrip', titleCn: '腰里嵌条' },
184
+ { garmentPart: 'pants', fieldKey: 'waistInnerMaterial', titleCn: '腰里材质' },
185
+ { garmentPart: 'pants', fieldKey: 'pantsPocketCloth', titleCn: '裤子口袋布' },
186
+ { garmentPart: 'pants', fieldKey: 'button', titleCn: '裤子纽扣' },
187
+ { garmentPart: 'pants', fieldKey: 'hemTape', titleCn: '脚口织带' }
188
+ ];
173
189
  /** 工艺选择总指南 */
174
190
  export const CRAFT_SELECTION_GUIDE = {
175
191
  summary: '写工艺前先读本指南。fieldKey=指南里的 fieldKey;fieldValue=JSON 字符串,结构见各字段 fieldValueExample;craftModule=模块 key',
176
192
  controls: {
177
- radio: '单选 → {"radio":"选项"};特例 creaseHas/spareButton 可能再带 note',
193
+ radio: '单选 → {"radio":"选项"};howToSelect 提示可填备注时再带 note',
178
194
  checkbox: '多选 → {"checkbox":["a","b"]}',
179
195
  'checkbox+perItemSize': '多选+尺寸 → {"checkbox":["领"],"combineMap":{"领":"0.15"}}',
180
196
  'checkbox+gateNone': '先有无再多选 → 无:{"radio":"无","checkbox":["无"]};有:{"radio":"有","checkbox":["领"],"combineMap":{"领":"0.15"}}',
181
- combine: '结合单选 → {"combineKey":"三折光","combineValue":"3"};needValue=false 可不带 combineValue',
197
+ combine: '结合单选 → {"combineKey":"净脚口三折光压线","combineValue":"3"};needValue=false 可不带 combineValue',
182
198
  'combine+multiFill': '多填 → {"combineMap":{"28L*":"2","24L*":"1"}}',
183
- input: '输入 → {"input":"文案"}',
184
- fixed: '固定文案,一般可不传'
199
+ input: '输入 → {"input":"文案"};各模块末尾 xxNote 备注、人工填写项同此'
185
200
  },
201
+ colorCardFields: COLOR_CARD_FIELDS,
202
+ colorCardNote: '色卡选料写在 style_save colors[].colorCardJson(JSON 字符串,键=上面 fieldKey,值={materialCode,materialName,titleCn};pantsPocketCloth 列仅裤子前后袋工艺有值时可填)',
186
203
  jacket: flattenGroups('jacket', JACKET_STYLE_GROUPS),
187
204
  pants: flattenGroups('pants', PANTS_STYLE_GROUPS)
188
205
  };
@@ -31,6 +31,7 @@ const MARK_ITEMS = [
31
31
  { label: '无', needValue: false },
32
32
  ],
33
33
  },
34
+ { key: 'markNote', label: '备注', control: 'input' },
34
35
  ];
35
36
  /** 印花组字段 */
36
37
  const PRINT_ITEMS = [
@@ -41,9 +42,10 @@ const PRINT_ITEMS = [
41
42
  options: ['有', '无'],
42
43
  },
43
44
  { key: 'printDetail', label: '印花位置及颜色', control: 'input' },
45
+ { key: 'printNote', label: '备注', control: 'input' },
44
46
  ];
45
47
  /** 制边部位 */
46
- const EDGE_PARTS = ['领', '驳', '挂面止口', '手巾袋上', '袖口', '下摆', '开叉'];
48
+ const EDGE_PARTS = ['领', '驳', '挂面止口', '手巾袋上', '袋盖', '袖口', '下摆', '开叉'];
47
49
  /** 裤子工艺字段 */
48
50
  export const PANTS_STYLE_GROUPS = [
49
51
  {
@@ -132,10 +134,14 @@ export const PANTS_STYLE_GROUPS = [
132
134
  control: 'combine',
133
135
  combineOptions: [
134
136
  {
135
- label: '三折光',
137
+ label: '净脚口三折光压线',
138
+ needValue: true,
139
+ suffix: 'CM',
140
+ },
141
+ {
142
+ label: '净脚口内折撬边',
136
143
  needValue: true,
137
144
  suffix: 'CM',
138
- textPrefix: '脚口',
139
145
  },
140
146
  {
141
147
  label: '内撬',
@@ -166,6 +172,7 @@ export const PANTS_STYLE_GROUPS = [
166
172
  alwaysInput: true,
167
173
  inputPlaceholder: '特殊要求',
168
174
  },
175
+ { key: 'liningNote', label: '备注', control: 'input' },
169
176
  ],
170
177
  },
171
178
  {
@@ -192,8 +199,8 @@ export const PANTS_STYLE_GROUPS = [
192
199
  label: '后袋',
193
200
  control: 'combine',
194
201
  combineOptions: [
195
- { label: '拷边', needValue: false },
196
- { label: '折光', needValue: false },
202
+ { label: '拷边', needValue: true, suffix: '' },
203
+ { label: '折光', needValue: true, suffix: '' },
197
204
  { label: '包边', needValue: true, suffix: '' },
198
205
  ],
199
206
  },
@@ -214,6 +221,7 @@ export const PANTS_STYLE_GROUPS = [
214
221
  { label: '宽度', needValue: true, suffix: '' },
215
222
  ],
216
223
  },
224
+ { key: 'topstitchNote', label: '备注', control: 'input' },
217
225
  ],
218
226
  },
219
227
  {
@@ -237,6 +245,17 @@ export const PANTS_STYLE_GROUPS = [
237
245
  inputWhen: ['其他'],
238
246
  inputPlaceholder: '材质',
239
247
  },
248
+ {
249
+ key: 'beadEdge',
250
+ label: '珠边',
251
+ control: 'combine',
252
+ textPrefix: '珠边',
253
+ combineOptions: [
254
+ { label: '有', needValue: true, suffix: '', hideLabel: true },
255
+ { label: '无', needValue: false },
256
+ ],
257
+ },
258
+ { key: 'waistInnerNote', label: '备注', control: 'input' },
240
259
  ],
241
260
  },
242
261
  {
@@ -265,26 +284,7 @@ export const PANTS_STYLE_GROUPS = [
265
284
  control: 'radio',
266
285
  options: ['卷边连腰', '卷边不连腰', '拷边'],
267
286
  },
268
- {
269
- key: 'clothMaterial',
270
- label: '袋布材质',
271
- control: 'radio',
272
- options: ['80棉20涤', '65棉35涤', '100涤', '100棉', '面料', '其他'],
273
- inputWhen: ['其他'],
274
- inputPlaceholder: '材质',
275
- },
276
- {
277
- key: 'clothTexture',
278
- label: '袋布纹理',
279
- control: 'radio',
280
- options: ['斜纹', '平纹', '人字纹', '网眼', '无'],
281
- },
282
- {
283
- key: 'clothColor',
284
- label: '袋布颜色',
285
- control: 'radio',
286
- options: ['配色', '黑', '藏青', '本白', '漂白'],
287
- },
287
+ { key: 'pocketFacingNote', label: '备注', control: 'input' },
288
288
  ],
289
289
  },
290
290
  {
@@ -329,8 +329,11 @@ export const PANTS_STYLE_GROUPS = [
329
329
  key: 'bartackColor',
330
330
  label: '配色',
331
331
  control: 'radio',
332
- options: ['套结配面料色'],
332
+ options: ['套结配面料色', '其他'],
333
+ inputWhen: ['其他'],
334
+ inputPlaceholder: '配色',
333
335
  },
336
+ { key: 'bartackNote', label: '备注', control: 'input' },
334
337
  ],
335
338
  },
336
339
  { key: 'mark', label: '标识', items: MARK_ITEMS },
@@ -341,7 +344,7 @@ export const PANTS_STYLE_GROUPS = [
341
344
  items: [
342
345
  {
343
346
  key: 'button',
344
- label: '纽扣',
347
+ label: '裤子纽扣',
345
348
  control: 'combine',
346
349
  multiFill: true,
347
350
  combineOptions: [
@@ -400,6 +403,7 @@ export const PANTS_STYLE_GROUPS = [
400
403
  { label: '无', needValue: false },
401
404
  ],
402
405
  },
406
+ { key: 'accessoryNote', label: '备注', control: 'input' },
403
407
  ],
404
408
  },
405
409
  {
@@ -413,6 +417,7 @@ export const PANTS_STYLE_GROUPS = [
413
417
  textPrefix: '挺缝线',
414
418
  options: ['有', '无'],
415
419
  },
420
+ { key: 'creaseNote', label: '备注', control: 'input' },
416
421
  ],
417
422
  },
418
423
  {
@@ -466,6 +471,7 @@ export const PANTS_STYLE_GROUPS = [
466
471
  control: 'radio',
467
472
  options: ['挂装', '平箱有裤架', '平箱无裤架'],
468
473
  },
474
+ { key: 'packNote', label: '备注', control: 'input' },
469
475
  ],
470
476
  },
471
477
  ];
@@ -526,9 +532,9 @@ export const JACKET_STYLE_GROUPS = [
526
532
  options: [
527
533
  '贴袋',
528
534
  '平袋有袋盖',
529
- '平袋双嵌线袋',
535
+ '平袋双嵌线袋有袋盖',
530
536
  '斜袋有袋盖',
531
- '斜袋双嵌线袋',
537
+ '斜袋双嵌线袋有袋盖',
532
538
  ],
533
539
  },
534
540
  {
@@ -604,6 +610,7 @@ export const JACKET_STYLE_GROUPS = [
604
610
  inputWhen: ['备注'],
605
611
  inputPlaceholder: '备注',
606
612
  },
613
+ { key: 'fuseNote', label: '备注', control: 'input' },
607
614
  ],
608
615
  },
609
616
  {
@@ -660,6 +667,7 @@ export const JACKET_STYLE_GROUPS = [
660
667
  { label: '无', needValue: false },
661
668
  ],
662
669
  },
670
+ { key: 'edgeNote', label: '备注', control: 'input' },
663
671
  ],
664
672
  },
665
673
  {
@@ -676,9 +684,9 @@ export const JACKET_STYLE_GROUPS = [
676
684
  },
677
685
  {
678
686
  key: 'sleeveButton',
679
- label: '纽扣',
687
+ label: '袖扣',
680
688
  control: 'combine',
681
- textPrefix: '纽扣',
689
+ textPrefix: '袖扣',
682
690
  combineOptions: [
683
691
  { label: '叠扣', needValue: true, suffix: '' },
684
692
  { label: '平扣', needValue: true, suffix: '' },
@@ -686,12 +694,12 @@ export const JACKET_STYLE_GROUPS = [
686
694
  },
687
695
  {
688
696
  key: 'sleeveButtonCount',
689
- label: '每个袖口纽扣数',
697
+ label: '每个袖口袖扣数',
690
698
  control: 'input',
691
699
  },
692
700
  {
693
701
  key: 'buttons',
694
- label: '扣子',
702
+ label: '门襟大扣',
695
703
  control: 'combine',
696
704
  multiFill: true,
697
705
  combineOptions: [
@@ -710,9 +718,9 @@ export const JACKET_STYLE_GROUPS = [
710
718
  {
711
719
  key: 'spare',
712
720
  label: '备扣',
713
- control: 'fixed',
721
+ control: 'input',
714
722
  textPrefix: '备扣',
715
- fixedText: '一大一小',
723
+ defaultInput: '一大一小',
716
724
  },
717
725
  {
718
726
  key: 'thread',
@@ -726,12 +734,14 @@ export const JACKET_STYLE_GROUPS = [
726
734
  {
727
735
  key: 'sleeveLining',
728
736
  label: '袖里',
729
- control: 'radio',
737
+ control: 'combine',
730
738
  textPrefix: '袖里',
731
- options: ['普通白底黑条', 'PLN白底细黑条', '配面料色', '其他'],
732
- inputWhen: ['其他'],
733
- inputPlaceholder: '袖里',
739
+ combineOptions: [
740
+ { label: '有', needValue: true, suffix: '', hideLabel: true },
741
+ { label: '无', needValue: false },
742
+ ],
734
743
  },
744
+ { key: 'sleeveNote', label: '备注', control: 'input' },
735
745
  ],
736
746
  },
737
747
  {
@@ -747,6 +757,7 @@ export const JACKET_STYLE_GROUPS = [
747
757
  { label: '里布', needValue: true, suffix: '' },
748
758
  { label: '丝带', needValue: true, suffix: '' },
749
759
  { label: '领标', needValue: true, suffix: '' },
760
+ { label: '其他', needValue: true, suffix: '' },
750
761
  ],
751
762
  },
752
763
  {
@@ -759,6 +770,7 @@ export const JACKET_STYLE_GROUPS = [
759
770
  { label: '特殊材料', needValue: true, suffix: '' },
760
771
  ],
761
772
  },
773
+ { key: 'collarNote', label: '备注', control: 'input' },
762
774
  ],
763
775
  },
764
776
  {
@@ -804,13 +816,14 @@ export const JACKET_STYLE_GROUPS = [
804
816
  },
805
817
  {
806
818
  key: 'pocketCloth',
807
- label: '口袋布',
819
+ label: '上衣口袋布',
808
820
  control: 'combine',
809
821
  combineOptions: [
810
822
  { label: '有', needValue: true, suffix: '', hideLabel: true },
811
823
  { label: '无', needValue: false },
812
824
  ],
813
825
  },
826
+ { key: 'innerNote', label: '备注', control: 'input' },
814
827
  ],
815
828
  },
816
829
  {
@@ -823,9 +836,9 @@ export const JACKET_STYLE_GROUPS = [
823
836
  control: 'combine',
824
837
  textPrefix: '大袋',
825
838
  combineOptions: [
826
- { label: '直套结', needValue: false },
827
- { label: 'D套结', needValue: false },
828
- { label: '横套结0.6CM', needValue: false },
839
+ { label: '直套结', needValue: true, suffix: '' },
840
+ { label: 'D套结', needValue: true, suffix: '' },
841
+ { label: '横套结0.6CM', needValue: true, suffix: '' },
829
842
  { label: '无', needValue: false },
830
843
  ],
831
844
  },
@@ -835,11 +848,12 @@ export const JACKET_STYLE_GROUPS = [
835
848
  control: 'combine',
836
849
  textPrefix: '里袋',
837
850
  combineOptions: [
838
- { label: '直套结', needValue: false },
839
- { label: 'D套结', needValue: false },
851
+ { label: '直套结', needValue: true, suffix: '' },
852
+ { label: 'D套结', needValue: true, suffix: '' },
840
853
  { label: '无', needValue: false },
841
854
  ],
842
855
  },
856
+ { key: 'bartackNote', label: '备注', control: 'input' },
843
857
  ],
844
858
  },
845
859
  { key: 'mark', label: '标识', items: MARK_ITEMS },
@@ -847,7 +861,10 @@ export const JACKET_STYLE_GROUPS = [
847
861
  {
848
862
  key: 'handmade',
849
863
  label: '特殊手工',
850
- items: [{ key: 'handmade', label: '特殊手工', control: 'input' }],
864
+ items: [
865
+ { key: 'handmade', label: '特殊手工', control: 'input' },
866
+ { key: 'handmadeNote', label: '备注', control: 'input' },
867
+ ],
851
868
  },
852
869
  {
853
870
  key: 'pack',
@@ -899,6 +916,7 @@ export const JACKET_STYLE_GROUPS = [
899
916
  control: 'radio',
900
917
  options: ['挂装', '平箱有衣架', '平箱无衣架', '立体箱'],
901
918
  },
919
+ { key: 'packNote', label: '备注', control: 'input' },
902
920
  ],
903
921
  },
904
922
  ];
@@ -5,17 +5,81 @@ export const payloadField = z.string().describe('JSON 请求体,字段与后
5
5
  export const idsField = z.string().describe('主键 ID,多个用英文逗号分隔');
6
6
  /** 单个主键字段,字符串避免雪花 ID 精度丢失 */
7
7
  export const idField = z.string().describe('主键 ID');
8
+ /** 样衣类型白名单 */
9
+ export const SAMPLE_TYPES = new Set(['proto', 'pre_prod', 'bulk', 'mass']);
10
+ /** ≥16 位数字字面量改成字符串,避免雪花 ID 精度丢失 */
11
+ export function preserveSnowflakeIds(text) {
12
+ return text.replace(/([:\[,]\s*)(\d{16,})(\s*[,\]}])/g, '$1"$2"$3');
13
+ }
14
+ /** 解析保真后的 JSON */
15
+ export function parseJsonSafe(text) {
16
+ return JSON.parse(preserveSnowflakeIds(text));
17
+ }
18
+ /** 校验对象内 id 类字段不为 number */
19
+ function assertNoNumberIds(value, path = '') {
20
+ if (value == null || typeof value !== 'object') {
21
+ return;
22
+ }
23
+ if (Array.isArray(value)) {
24
+ value.forEach((item, i) => assertNoNumberIds(item, `${path}[${i}]`));
25
+ return;
26
+ }
27
+ for (const [key, child] of Object.entries(value)) {
28
+ const here = path ? `${path}.${key}` : key;
29
+ if (typeof child === 'number' && (key === 'id' || key === 'ids' || /Id$/.test(key))) {
30
+ throw new Error(`雪花 ID 必须字符串:${here}=${child}(勿用 JSON number)`);
31
+ }
32
+ assertNoNumberIds(child, here);
33
+ }
34
+ }
8
35
  /** 解析 JSON 请求体 */
9
36
  export function parsePayload(args) {
10
37
  const raw = args.payload;
11
38
  if (typeof raw === 'string') {
12
- return JSON.parse(raw);
39
+ const parsed = parseJsonSafe(raw);
40
+ assertNoNumberIds(parsed);
41
+ return parsed;
13
42
  }
14
43
  if (raw && typeof raw === 'object') {
44
+ assertNoNumberIds(raw);
15
45
  return raw;
16
46
  }
17
47
  throw new Error('payload 必须是 JSON 字符串');
18
48
  }
49
+ /** 规整 style_save:自动包 styles,禁止空列表 */
50
+ export function normalizeStyleSavePayload(raw) {
51
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
52
+ throw new Error('style_save payload 须为对象');
53
+ }
54
+ const input = raw;
55
+ let styles = input.styles;
56
+ if (!Array.isArray(styles)) {
57
+ if (input.styleNo != null || input.colors != null || input.styleName != null) {
58
+ const { orderId, ...rest } = input;
59
+ return { orderId, styles: [rest] };
60
+ }
61
+ throw new Error('style_save 须 payload.styles:[{...}],勿把款式字段放顶层');
62
+ }
63
+ if (styles.length === 0) {
64
+ throw new Error('style_save 的 styles 不能为空数组');
65
+ }
66
+ return { ...input, styles };
67
+ }
68
+ /** 规整样衣写入:类型白名单 + sampleNo→roundName */
69
+ export function normalizeSamplePayload(raw) {
70
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
71
+ throw new Error('样衣 payload 须为对象');
72
+ }
73
+ const payload = { ...raw };
74
+ if (payload.sampleType != null && !SAMPLE_TYPES.has(String(payload.sampleType))) {
75
+ throw new Error(`sampleType 仅允许 proto/pre_prod/bulk/mass,收到:${payload.sampleType}`);
76
+ }
77
+ if (payload.roundName == null && payload.sampleNo != null) {
78
+ payload.roundName = payload.sampleNo;
79
+ delete payload.sampleNo;
80
+ }
81
+ return payload;
82
+ }
19
83
  /** 规整工艺保存入参 */
20
84
  export function normalizeCraftSavePayload(raw) {
21
85
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {