yindie-erp-mcp 2.0.6 → 2.0.8

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.
@@ -77,7 +77,10 @@ function buildHowTo(item) {
77
77
  }
78
78
  text += `可多选部位:${(item.options || []).join('/')};勾选后每项再选尺寸 ${(item.sizeOptions || []).join('/')}`;
79
79
  if (item.allowCustomSize) {
80
- text += '或手填';
80
+ text += '或手填尺寸';
81
+ }
82
+ if (item.allowAdd) {
83
+ text += ';自定义部位写入 customOptions[] 并同步进 checkbox/combineMap';
81
84
  }
82
85
  if (item.syncSizeOptions?.length) {
83
86
  text += `;${item.syncSizeOptions.join('/')} 尺寸联动`;
@@ -193,13 +196,13 @@ export const CRAFT_SELECTION_GUIDE = {
193
196
  radio: '单选 → {"radio":"选项"};howToSelect 提示可填备注时再带 note',
194
197
  checkbox: '多选 → {"checkbox":["a","b"]}',
195
198
  'checkbox+perItemSize': '多选+尺寸 → {"checkbox":["领"],"combineMap":{"领":"0.15"}}',
196
- 'checkbox+gateNone': '先有无再多选 → 无:{"radio":"无","checkbox":["无"]};有:{"radio":"有","checkbox":["领"],"combineMap":{"领":"0.15"}}',
199
+ 'checkbox+gateNone': '先有无再多选 → 无:{"radio":"无","checkbox":["无"]};有:{"radio":"有","checkbox":["领"],"combineMap":{"领":"0.15"}};allowAdd:true 自定义部位须同时写 customOptions[]+checkbox(+combineMap 尺寸),缺 checkbox 拒写(导出也不显示)',
197
200
  combine: '结合单选 → {"combineKey":"净脚口三折光压线","combineValue":"3"};needValue=false 可不带 combineValue',
198
201
  'combine+multiFill': '多填 → {"combineMap":{"28L*":"2","24L*":"1"}}',
199
202
  input: '输入 → {"input":"文案"};各模块末尾 xxNote 备注、人工填写项同此'
200
203
  },
201
204
  colorCardFields: COLOR_CARD_FIELDS,
202
- colorCardNote: '色卡选料写在 style_save colors[].colorCardJson(JSON 字符串,键=上面 fieldKey,值={materialCode,materialName,titleCn};pantsPocketCloth 列仅裤子前后袋工艺有值时可填)',
205
+ colorCardNote: '色卡选料写在 style_save colors[].colorCardJson(JSON 字符串,键=上面 fieldKey,值={materialCode,materialName,titleCn};pantsPocketCloth 列仅裤子前后袋工艺有值时可填)。color_card_export 只读 colorCardJson,无选料拒写;工艺 onCard 只影响前端可选列,不是导出数据源;备注走 color_card_save',
203
206
  jacket: flattenGroups('jacket', JACKET_STYLE_GROUPS),
204
207
  pants: flattenGroups('pants', PANTS_STYLE_GROUPS)
205
208
  };
@@ -19,6 +19,101 @@ function main() {
19
19
  if (normalized.garmentPart !== undefined || normalized.craft !== undefined) {
20
20
  throw new Error('兼容键应删除');
21
21
  }
22
+ let rejected = false;
23
+ try {
24
+ normalizeCraftSavePayload({
25
+ styleId: '1',
26
+ crafts: [{ garmentPart: 'jacket', craftModule: 'style', fieldKey: 'description', fieldValue: '{"radio":"单挂9扣瞎填"}' }]
27
+ });
28
+ }
29
+ catch {
30
+ rejected = true;
31
+ }
32
+ if (!rejected) {
33
+ throw new Error('非法选项应拒');
34
+ }
35
+ normalizeCraftSavePayload({
36
+ styleId: '1',
37
+ crafts: [
38
+ {
39
+ garmentPart: 'jacket',
40
+ craftModule: 'edge',
41
+ fieldKey: 'outerNeedle',
42
+ fieldValue: '{"radio":"有","checkbox":["领"],"combineMap":{"领":"0.15"}}'
43
+ }
44
+ ]
45
+ });
46
+ let badPress = false;
47
+ try {
48
+ normalizeCraftSavePayload({
49
+ styleId: '1',
50
+ crafts: [
51
+ {
52
+ garmentPart: 'jacket',
53
+ craftModule: 'edge',
54
+ fieldKey: 'outerPress',
55
+ fieldValue: '{"radio":"瞎填","checkbox":["瞎填部位"]}'
56
+ }
57
+ ]
58
+ });
59
+ }
60
+ catch {
61
+ badPress = true;
62
+ }
63
+ if (!badPress) {
64
+ throw new Error('outerPress 非法 gate 应拒');
65
+ }
66
+ let badPressBox = false;
67
+ try {
68
+ normalizeCraftSavePayload({
69
+ styleId: '1',
70
+ crafts: [
71
+ {
72
+ garmentPart: 'jacket',
73
+ craftModule: 'edge',
74
+ fieldKey: 'outerPress',
75
+ fieldValue: '{"radio":"有","checkbox":["瞎填部位A"]}'
76
+ }
77
+ ]
78
+ });
79
+ }
80
+ catch {
81
+ badPressBox = true;
82
+ }
83
+ if (!badPressBox) {
84
+ throw new Error('outerPress 非法 checkbox 应拒');
85
+ }
86
+ normalizeCraftSavePayload({
87
+ styleId: '1',
88
+ crafts: [
89
+ {
90
+ garmentPart: 'jacket',
91
+ craftModule: 'edge',
92
+ fieldKey: 'outerPress',
93
+ fieldValue: '{"radio":"有","checkbox":["自定义部位"],"customOptions":["自定义部位"],"combineMap":{"自定义部位":"0.15"}}'
94
+ }
95
+ ]
96
+ });
97
+ let customOnly = false;
98
+ try {
99
+ normalizeCraftSavePayload({
100
+ styleId: '1',
101
+ crafts: [
102
+ {
103
+ garmentPart: 'jacket',
104
+ craftModule: 'edge',
105
+ fieldKey: 'outerPress',
106
+ fieldValue: '{"radio":"有","checkbox":["领"],"customOptions":["前门襟"],"combineMap":{"领":"0.15"}}'
107
+ }
108
+ ]
109
+ });
110
+ }
111
+ catch {
112
+ customOnly = true;
113
+ }
114
+ if (!customOnly) {
115
+ throw new Error('customOptions 未进 checkbox 应拒');
116
+ }
22
117
  console.log('normalizeCraftSavePayload self-check ok');
23
118
  }
24
119
  main();
@@ -531,9 +531,10 @@ export const JACKET_STYLE_GROUPS = [
531
531
  control: 'radio',
532
532
  options: [
533
533
  '贴袋',
534
- '平袋有袋盖',
534
+ '平袋单嵌线',
535
+ '平袋双嵌线',
535
536
  '平袋双嵌线袋有袋盖',
536
- '斜袋有袋盖',
537
+ '斜袋双嵌线',
537
538
  '斜袋双嵌线袋有袋盖',
538
539
  ],
539
540
  },
@@ -0,0 +1,118 @@
1
+ import { JACKET_STYLE_GROUPS, PANTS_STYLE_GROUPS } from './template.js';
2
+ /** 字段索引 key=garmentPart.fieldKey */
3
+ const FIELD_INDEX = buildIndex();
4
+ /** 构建字段索引 */
5
+ function buildIndex() {
6
+ const map = new Map();
7
+ indexPart(map, 'jacket', JACKET_STYLE_GROUPS);
8
+ indexPart(map, 'pants', PANTS_STYLE_GROUPS);
9
+ return map;
10
+ }
11
+ /** 索引一个部位 */
12
+ function indexPart(map, part, groups) {
13
+ for (const group of groups) {
14
+ for (const item of group.items) {
15
+ map.set(`${part}.${item.key}`, item);
16
+ }
17
+ }
18
+ }
19
+ /** 校验 crafts 选项值(按 control / gateNone) */
20
+ export function assertCraftOptionValues(crafts) {
21
+ if (!Array.isArray(crafts)) {
22
+ return;
23
+ }
24
+ crafts.forEach((row, index) => {
25
+ if (!row || typeof row !== 'object' || Array.isArray(row)) {
26
+ return;
27
+ }
28
+ const craft = row;
29
+ const part = craft.garmentPart;
30
+ const fieldKey = craft.fieldKey;
31
+ const fieldValue = craft.fieldValue;
32
+ if (typeof part !== 'string' || typeof fieldKey !== 'string' || typeof fieldValue !== 'string') {
33
+ return;
34
+ }
35
+ const def = FIELD_INDEX.get(`${part}.${fieldKey}`);
36
+ if (!def || def.control === 'input' || !fieldValue.startsWith('{')) {
37
+ return;
38
+ }
39
+ const json = JSON.parse(fieldValue);
40
+ const prefix = `crafts[${index}] ${part}.${fieldKey}`;
41
+ const custom = new Set(json.customOptions || []);
42
+ const optionSet = new Set([...(def.options || []), ...custom]);
43
+ const combineSet = new Set((def.combineOptions || []).map((o) => o.label));
44
+ for (const label of custom) {
45
+ combineSet.add(label);
46
+ }
47
+ const noneLabel = def.noneLabel || '无';
48
+ if (def.control === 'radio') {
49
+ if (json.radio != null && json.radio !== '') {
50
+ assertIn(prefix, 'radio', json.radio, optionSet);
51
+ }
52
+ return;
53
+ }
54
+ if (def.control === 'combine') {
55
+ if (json.combineKey != null && json.combineKey !== '') {
56
+ assertIn(prefix, 'combineKey', json.combineKey, combineSet);
57
+ }
58
+ if (json.combineMap) {
59
+ for (const key of Object.keys(json.combineMap)) {
60
+ assertIn(prefix, 'combineMap', key, combineSet);
61
+ }
62
+ }
63
+ return;
64
+ }
65
+ if (def.control === 'checkbox') {
66
+ if (def.gateNone) {
67
+ const gateSet = new Set(['有', noneLabel]);
68
+ if (json.radio != null && json.radio !== '') {
69
+ assertIn(prefix, 'radio', json.radio, gateSet);
70
+ }
71
+ const bodyAllowed = new Set(optionSet);
72
+ bodyAllowed.add(noneLabel);
73
+ if (Array.isArray(json.checkbox)) {
74
+ for (const value of json.checkbox) {
75
+ assertIn(prefix, 'checkbox', value, bodyAllowed);
76
+ }
77
+ }
78
+ if (json.combineMap) {
79
+ for (const key of Object.keys(json.combineMap)) {
80
+ assertIn(prefix, 'combineMap', key, bodyAllowed);
81
+ }
82
+ }
83
+ assertCustomInCheckbox(prefix, json.customOptions, json.checkbox);
84
+ return;
85
+ }
86
+ if (Array.isArray(json.checkbox)) {
87
+ for (const value of json.checkbox) {
88
+ assertIn(prefix, 'checkbox', value, optionSet);
89
+ }
90
+ }
91
+ if (json.combineMap) {
92
+ for (const key of Object.keys(json.combineMap)) {
93
+ assertIn(prefix, 'combineMap', key, optionSet);
94
+ }
95
+ }
96
+ assertCustomInCheckbox(prefix, json.customOptions, json.checkbox);
97
+ }
98
+ });
99
+ }
100
+ /** customOptions 须同步进 checkbox,否则导出不显示 */
101
+ function assertCustomInCheckbox(prefix, customOptions, checkbox) {
102
+ if (!Array.isArray(customOptions) || customOptions.length === 0) {
103
+ return;
104
+ }
105
+ const boxes = new Set(Array.isArray(checkbox) ? checkbox : []);
106
+ for (const opt of customOptions) {
107
+ if (!boxes.has(opt)) {
108
+ throw new Error(`${prefix} customOptions 中的「${opt}」须同时写入 checkbox,否则导出不显示`);
109
+ }
110
+ }
111
+ }
112
+ /** 断言取值在白名单(allowAdd 自定义项须写入 customOptions 后才进白名单) */
113
+ function assertIn(prefix, kind, value, allowed) {
114
+ if (allowed.has(value)) {
115
+ return;
116
+ }
117
+ throw new Error(`${prefix} 取值非法:${value},可选:${[...allowed].join('/')}`);
118
+ }
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ async function main() {
21
21
  const moduleActions = legacy ? undefined : buildModuleActions(permissions);
22
22
  const server = new McpServer({
23
23
  name: 'yindie-erp',
24
- version: '2.0.1'
24
+ version: '2.0.17'
25
25
  }, {
26
26
  instructions: ERP_MCP_INSTRUCTIONS
27
27
  });
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { assertCraftOptionValues } from './craft/validate-options.js';
2
3
  /** JSON 请求体字段 */
3
4
  export const payloadField = z.string().describe('JSON 请求体,字段与后端 BO 一致');
4
5
  /** 主键字段 */
@@ -6,7 +7,7 @@ export const idsField = z.string().describe('主键 ID,多个用英文逗号
6
7
  /** 单个主键字段,字符串避免雪花 ID 精度丢失 */
7
8
  export const idField = z.string().describe('主键 ID');
8
9
  /** 样衣类型白名单 */
9
- export const SAMPLE_TYPES = new Set(['proto', 'pre_prod', 'bulk', 'mass']);
10
+ export const SAMPLE_TYPES = new Set(['proto', 'pre_prod', 'bulk', 'mass', 'sales', 'photo']);
10
11
  /** ≥16 位数字字面量改成字符串,避免雪花 ID 精度丢失 */
11
12
  export function preserveSnowflakeIds(text) {
12
13
  return text.replace(/([:\[,]\s*)(\d{16,})(\s*[,\]}])/g, '$1"$2"$3');
@@ -46,6 +47,63 @@ export function parsePayload(args) {
46
47
  }
47
48
  throw new Error('payload 必须是 JSON 字符串');
48
49
  }
50
+ /** 解析 get 主键:顶层 id|orderId → payload.id|orderId */
51
+ export function resolveGetId(args) {
52
+ const top = args.id ?? args.orderId;
53
+ if (top != null && top !== '') {
54
+ return String(top);
55
+ }
56
+ if (args.payload == null) {
57
+ return undefined;
58
+ }
59
+ const payload = parsePayload(args);
60
+ const id = payload.id ?? payload.orderId;
61
+ if (id == null || id === '') {
62
+ return undefined;
63
+ }
64
+ return String(id);
65
+ }
66
+ /** 解析逗号分隔或数组 ID 列表 */
67
+ export function asIdList(value) {
68
+ if (value == null || value === '') {
69
+ return undefined;
70
+ }
71
+ const raw = Array.isArray(value) ? value : String(value).split(',');
72
+ const ids = raw.map((item) => String(item).trim()).filter((item) => item !== '');
73
+ return ids.length > 0 ? ids : undefined;
74
+ }
75
+ /** 按逗号字段白名单裁剪对象/数组 */
76
+ export function pickFields(data, fields) {
77
+ if (fields == null || fields === '') {
78
+ return data;
79
+ }
80
+ const keys = fields.split(',').map((s) => s.trim()).filter((s) => s !== '');
81
+ if (keys.length === 0) {
82
+ return data;
83
+ }
84
+ if (Array.isArray(data)) {
85
+ return data.map((item) => pickFields(item, fields));
86
+ }
87
+ if (data == null || typeof data !== 'object') {
88
+ return data;
89
+ }
90
+ const src = data;
91
+ const out = {};
92
+ for (const key of keys) {
93
+ if (key in src) {
94
+ out[key] = src[key];
95
+ }
96
+ }
97
+ return out;
98
+ }
99
+ /** 解析 include 列表 */
100
+ export function parseInclude(value) {
101
+ if (value == null || value === '') {
102
+ return new Set();
103
+ }
104
+ const raw = Array.isArray(value) ? value : String(value).split(',');
105
+ return new Set(raw.map((item) => String(item).trim()).filter((item) => item !== ''));
106
+ }
49
107
  /** 规整 style_save:自动包 styles,禁止空列表 */
50
108
  export function normalizeStyleSavePayload(raw) {
51
109
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
@@ -72,7 +130,7 @@ export function normalizeSamplePayload(raw) {
72
130
  }
73
131
  const payload = { ...raw };
74
132
  if (payload.sampleType != null && !SAMPLE_TYPES.has(String(payload.sampleType))) {
75
- throw new Error(`sampleType 仅允许 proto/pre_prod/bulk/mass,收到:${payload.sampleType}`);
133
+ throw new Error(`sampleType 仅允许 proto/pre_prod/bulk/mass/sales/photo,收到:${payload.sampleType}`);
76
134
  }
77
135
  if (payload.roundName == null && payload.sampleNo != null) {
78
136
  payload.roundName = payload.sampleNo;
@@ -110,6 +168,7 @@ export function normalizeCraftSavePayload(raw) {
110
168
  if (topPart !== undefined) {
111
169
  delete payload.garmentPart;
112
170
  }
171
+ assertCraftOptionValues(payload.crafts);
113
172
  return payload;
114
173
  }
115
174
  /** 解析删除主键 */