yindie-erp-mcp 2.0.6 → 2.0.7

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();
@@ -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
  /** 解析删除主键 */
package/dist/tool-meta.js CHANGED
@@ -96,25 +96,25 @@ export const ENTITY_META = {
96
96
  enums: 'brandCode 后端自动生成勿传;shortCode 复制订单号用缩写'
97
97
  },
98
98
  erp_pi: {
99
- module: 'PI管理',
100
- step: '业务流程第2步:形式发票',
99
+ module: '订单管理·关联PI',
100
+ step: '订单详情「关联PI」Tab(独立 PI 菜单已隐藏;与前端 PiFormPanel 同能力)',
101
101
  label: 'PI',
102
- prerequisite: '客户已创建',
103
- addRequired: 'customerId, sourceType, piNo',
104
- addOptional: 'orderId, piVersion, paymentTerms, totalAmount, currency, qtyTolerance, shippingMethod, packingType, shipmentPeriod, shippingAddress, piFileOssId, status, confirmedDate, remark, items',
102
+ prerequisite: '订单已创建;客户已建档',
103
+ addRequired: 'orderId, customerId, sourceType, piNo',
104
+ addOptional: 'piVersion, paymentTerms, totalAmount, currency, qtyTolerance, shippingMethod, packingType, shipmentPeriod, shippingAddress, piFileOssId, status, confirmedDate, remark, items',
105
105
  editRequired: 'id, customerId, sourceType, piNo',
106
- enums: 'sourceType:customer/internal; status:draft/confirmed/superseded(默认draft);piNo 手填可改,同号同版本重复报PI编号已存在;传 orderId 则关联订单(客户须一致);items 为明细行 [{rowKey, code, style, fabric, col, composition, subtotalQty, fobNingbo, subtotalAmount}],包装方式 packingType 保存时同步订单',
107
- next: 'erp_order_add → erp_order_link_pi(若 add 已带 orderId 可跳过 link)'
106
+ enums: 'sourceType:customer/internal; status:draft/confirmed/superseded(默认draft);piNo 手填;orderId 日常必填(对齐前端订单内建 PI);items:[{rowKey?, code, style, fabric, col, composition, subtotalQty, fobNingbo, subtotalAmount}];前端「同步款式」=先 style_list 再填 items,无单独 API;packingType 保存时同步订单;export 按 PI id 导出 Excel',
107
+ next: '已带 orderId 则无需 link_pi;继续 style_save / change_status'
108
108
  },
109
109
  erp_order: {
110
110
  module: '订单管理',
111
111
  step: '业务流程第3步:创建订单',
112
112
  label: '订单',
113
- prerequisite: '订单客户与季度已建立;先用 erp_order action=customer_list 找订单客户,再 action=season_list 取 seasonId',
114
- addRequired: 'orderNo, seasonId',
113
+ prerequisite: '订单客户已建立;先用 erp_order action=customer_list 取 orderCustomerId',
114
+ addRequired: 'orderNo, orderCustomerId',
115
115
  addOptional: 'orderStatus(默认inquiry), orderDate, deliveryDate, contractDate, depositReceived(Y/N), qtyTolerance, shippingMethod, packingType, priority, sourceChannel, parentOrderId, remark',
116
- editRequired: 'id, orderNo',
117
- enums: 'orderStatus见erp_business_flow资源;orderNo 手填全库唯一可改;重复报订单号已存在;客户/品牌/业务线由季度所属订单客户带出,勿传;跟单员只在订单客户维护,订单不再有跟单员',
116
+ editRequired: 'id',
117
+ enums: 'orderStatus见erp_business_flow资源;orderNo 手填全库唯一可改(edit 可省略则沿用库值);重复报订单号已存在;客户/品牌/业务线由订单客户带出,勿传;跟单员只在订单客户维护,订单不再有跟单员',
118
118
  removeConstraint: '删除会级联清该单 PI、款式树(含工艺/商标位置/色卡)和生产单,订单号改为 _原ID 释放号码',
119
119
  next: 'erp_order_link_pi → erp_order_style_save → erp_order_change_status'
120
120
  },
@@ -127,19 +127,8 @@ export const ENTITY_META = {
127
127
  addOptional: 'brandId, remark',
128
128
  editRequired: 'id, customerId, bizLine, assigneeId',
129
129
  enums: 'bizLine:direct/trader;assigneeId 多个用英文逗号分隔;同一客户+品牌+业务线不可重复建档',
130
- removeConstraint: '订单客户下存在季度时不可删除',
131
- next: 'erp_order action=season_add'
132
- },
133
- erp_order_season: {
134
- module: '订单管理',
135
- step: '业务流程第3步前置:在订单客户下建季度',
136
- label: '订单季度',
137
- prerequisite: '订单客户已建档;orderCustomerId 来自 erp_order action=customer_list',
138
- addRequired: 'orderCustomerId, seasonName',
139
- editRequired: 'id, orderCustomerId, seasonName',
140
- enums: 'seasonName 手填,如 2026SS / 历史订单',
141
- removeConstraint: '季度下存在订单时不可删除',
142
- next: 'erp_order action=add(带 seasonId)'
130
+ removeConstraint: '订单客户下存在订单时不可删除',
131
+ next: 'erp_order action=add(带 orderCustomerId)'
143
132
  },
144
133
  erp_task: {
145
134
  module: '任务协同',
@@ -219,14 +208,14 @@ export const ENTITY_META = {
219
208
  erp_production: {
220
209
  module: '生产管理',
221
210
  step: '手动新建生产单(通常用fromOrder)',
222
- label: '生产单',
223
- prerequisite: '订单已存在;【暂不用】前端制单中心菜单已隐藏,常规业务勿新建/推进生产单',
211
+ label: '制单中心生产单(PO)',
212
+ prerequisite: '【不用】制单中心整模块停用。勿与订单页款式行按钮「生产单」混淆:那个按钮=工艺编辑(craft_save/craft_export),仍要用;本域是 PO 主档 create_from_order/check/issue,不要主动调',
224
213
  addRequired: 'orderId, bizLine',
225
214
  addOptional: 'poVersion, poStatus(默认draft), deliveryDate, qtyTolerance, shippingMethod, shippingAddress, packingType, bulkSampleRequired(Y/N), bulkSampleDeadline, remark',
226
215
  editRequired: 'id, orderId, bizLine',
227
216
  enums: 'poStatus:draft/reviewing/issued/superseded;poNo 后端自动生成勿传',
228
217
  removeConstraint: '已下发(issued)且订单仍存在时不可删除;订单删除会连带清生产单;仅 draft 可修改',
229
- next: '【暂不用】勿引导 create_from_order / check / issue;订单侧款式工艺色卡尺寸表即可'
218
+ next: '【不用】勿引导 create_from_order / check / issue;订单侧用 craft(页面「生产单」按钮)'
230
219
  },
231
220
  erp_bid: {
232
221
  module: '招标管理',
@@ -248,8 +237,8 @@ export const WORKFLOW_META = {
248
237
  step: '推进订单生命周期',
249
238
  summary: '变更订单状态',
250
239
  prerequisite: '订单已创建',
251
- payload: '{id, orderStatus, remark?};优先 payload.id,顶层 id/orderId 仅兜底写入 payload.id',
252
- constraint: '流转: inquiry→sampling|doc_preparing|quoted(跳过打样); sampling→sample_shipped; sample_shipped→sample_confirmed; sample_confirmed→quoted; quoted→po_drafting; doc_preparing→pre_prod_sample; pre_prod_sample→pre_prod_confirmed; pre_prod_confirmed→po_drafting; po_drafting→po_ai_checking; po_ai_checking→color_card_confirmed|po_drafting; color_card_confirmed→po_reviewing; po_reviewing→po_issued',
240
+ payload: '{id|orderId, orderStatus, remark?};payload.id → payload.orderId → 顶层 id/orderId 依次兜底',
241
+ constraint: '流转: inquiry→sampling|doc_preparing|quoted(跳过打样); sampling→sample_shipped; sample_shipped→sample_confirmed; sample_confirmed→quoted; quoted→po_drafting; doc_preparing→pre_prod_sample; pre_prod_sample→pre_prod_confirmed; pre_prod_confirmed→po_drafting; po_drafting→po_ai_checking; po_ai_checking→color_card_confirmed|po_drafting; color_card_confirmed→po_reviewing; po_reviewing→po_issued;非法跳步报当前状态与允许后继',
253
242
  next: '状态推进到此结束即可;【暂不用】勿再调 erp_production_create_from_order(制单中心已隐藏)'
254
243
  },
255
244
  erp_order_link_pi: {
@@ -274,8 +263,9 @@ export const WORKFLOW_META = {
274
263
  step: '录入款式明细',
275
264
  summary: '保存订单主款式及颜色尺码/色卡物料,成功后回查 styles(含 id 作 styleId、roundStyles);页面配置确定即调本接口',
276
265
  prerequisite: '订单已创建;payload 内 orderId 用字符串',
277
- payload: '{orderId:string, styles:[{id?, styleNo?, styleName?, sampleNo?, shipSampleTip?, garmentType?, customerStyleRef?, materialUsage?, fabricMaterialId?, liningMaterialId?, styleImageOssId?, techFileOssId?, sizeChartJson?, remark?, colors:[{id?, fabricMaterialId, colorCode, colorName, customerStyleRef?, colorCardJson?, meterage?, materialUsage?, cuttableWidth?, totalQty?, remark?, sizes:[{sizeName, qty, cutQty?, actualQty?}]}]}]};必须 styles 非空数组;顶层误放单款字段时 MCP 会自动包一层;styleNo 可省略则默认订单号-序号;colors[].colorName 必填;qty/cutQty?/actualQty?',
278
- constraint: 'styles:[] 拒写;只传主款(勿塞 roundStyles);全量 styles;已有 id/同款号则更新并替换颜色尺码,工艺/商标保留;空 sizeChartJson/样板号/船样提示沿用库值;未出现的旧主款整树删除;轮次克隆由 sample_add 生成',
266
+ payload: '{orderId:string, confirmReduce?, styles:[{id?, styleNo?, styleName?, sampleNo?, shipSampleTip?, garmentType:jacket|pants|suit, customerStyleRef?, materialUsage?, fabricMaterialId?, liningMaterialId?, styleImageOssId?, techFileOssId?, sizeChartJson?, remark?, colors:[{id?, fabricMaterialId, colorCode, colorName, customerStyleRef?, colorCardJson?, meterage?, materialUsage?, cuttableWidth?, totalQty?, remark?, sizes:[{sizeName, qty, cutQty?, actualQty?}]}]}]};必须 styles 非空数组;顶层误放单款字段时 MCP 会自动包一层;styleNo 可省略则默认订单号-序号;colors[].colorName 必填;qty生产单/cutQty预裁单/actualQty实裁单',
267
+ enums: 'garmentType:{jacket:上衣, pants:裤子, suit:套装};与尺寸表 category top/bottom/set 不同',
268
+ constraint: '【警告】未出现在 styles 的旧主款会整树删除(含工艺/尺码/轮次克隆);数量减少时必须 confirmReduce:true,否则拒写;返回 deletedStyleIds/deletedRoundStyleIds。styles:[] 拒写;只传主款(勿塞 roundStyles);全量 styles;已有 id/同款号则更新并替换颜色尺码,工艺/商标保留;空 sizeChartJson/样板号/船样提示沿用库值;garmentType 仅 jacket/pants/suit;fabricMaterialId 等须真实存在;轮次克隆由 sample_add 生成',
279
269
  next: '从返回 styles[].id 取 styleId → craft_save;轮次尺寸表写 roundStyles[].styleNo;色卡备注 → color_card_save'
280
270
  },
281
271
  erp_order_style_color_card_save: {
@@ -284,8 +274,8 @@ export const WORKFLOW_META = {
284
274
  summary: '按 styleId 写 colorCardNote;与前端色卡弹窗一致',
285
275
  prerequisite: '款式已保存;选料已在 style_save.colorCardJson',
286
276
  payload: '{styleId:string, colorCardNote?, items?:[{garmentPart, fieldKey, colorCardCode?, materialCode?}]}',
287
- constraint: '前端常规只传 colorCardNote 且 items:[];选料不在本接口。items 写 craft 编号为遗留可选,一般勿填;色卡导出用 action=color_card_export',
288
- next: 'erp_order action=change_status(如 color_card_confirmed)'
277
+ constraint: '前端常规只传 colorCardNote 且 items:[];选料不在本接口。items 写 craft 编号为遗留可选,一般勿填;色卡导出用 action=color_card_export(只认 colorCardJson,无选料会拒)',
278
+ next: 'erp_order action=color_card_export 或 change_status(如 color_card_confirmed)'
289
279
  },
290
280
  erp_bid_close: {
291
281
  module: '招标管理',
@@ -319,17 +309,17 @@ export const WORKFLOW_META = {
319
309
  summary: '保存款式上衣/裤子工艺及商标位置(选项对齐打样单模板,非字典)',
320
310
  prerequisite: '款式已保存且有 styleId;先读 erp_order_craft_schema 取 fieldKey',
321
311
  payload: '{styleId, processNote?, crafts:[{garmentPart, craftModule, fieldKey, fieldValue}], labelPositions?:[{garmentPart, labelType, positionSide?, sizeCm?, positionDesc?, materialId?, diagramOssId?, remark?, sortOrder?}]}。键名必须是 crafts(复数)与 craftModule;每条带 garmentPart;fieldValue 为 JSON 字符串,旁框用 note 不是 input',
322
- enums: 'garmentPart:jacket/pants; craftModule/fieldKey 见 schema;labelType/positionSide 仍用字典 erp_label_*',
323
- constraint: '全量替换:按 styleId 先删该款全部工艺和商标位置再插入。jacket+pants 必须放进同一次 crafts;分部位连调会互相覆盖。已有商标位置时必须一并回传 labelPositions,漏传会清空。返回 data=写入条数。清空传 crafts:[];processNote 为工艺备注(订单页会传);色卡列 fieldValue 里的 colorCardCode/materialCode 由后端沿用旧值,勿手填;商标图片来自物料:materialId 选商标物料,diagramOssId 取该物料 imageOssId;色卡选料不在本接口,走 style_save colors[].colorCardJson',
324
- next: 'erp_order action=craft_get 校验 → erp_order_change_status 推进即可;【暂不用】勿再调生产单'
312
+ enums: 'garmentPart:jacket/pants; craftModule/fieldKey 见 schema;labelType/positionSide 见 business-flow.enums.labelType/labelPositionSide',
313
+ constraint: '全量替换:按 styleId 先删该款全部工艺和商标位置再插入。jacket+pants 必须放进同一次 crafts;分部位连调会互相覆盖。【警告】已有商标位置时必须一并回传 labelPositions,漏传会清空。按 control 校验:radio∈options;combine∈combineOptions;checkbox∈options;checkbox+gateNone 时 radio 仅有/无,checkbox/combineMap 再对 options(官方 example 含 radio:有 合法)。input/note 不校验。返回 data=写入条数。清空传 crafts:[];processNote 为工艺备注;色卡选料走 style_save.colorCardJson',
314
+ next: 'erp_order action=craft_get 校验 → 可选 craft_export → change_status;对应页面款式行「生产单」按钮(工艺),不是制单中心'
325
315
  },
326
316
  erp_order_style_size_chart_save: {
327
317
  module: '订单管理',
328
318
  step: '写入尺寸表入库视图',
329
319
  summary: '按 orderId+styleNo 写入款式 sizeChartJson(erpView);主款或轮次克隆款均可;成功写变更日志;UI 与 MCP 共用',
330
320
  prerequisite: '款式已存在;payload 为入库视图,不是整包;MCP 侧需用户确认,UI 点保存即可',
331
- payload: '{orderId:string, styleNo, sizeChartJson:{view_version,style_no,category:top|bottom|set,unit,source:{sha256},sizes:[{raw}],rows:[{std_key,std_zh,category:top|bottom,raw_name?,raw_name_zh?,delta?,is_extra?,values:[{size,type,raw,value_cm?}]}]}, confirmedBy:确认人ID, source?:mcp|ui}',
332
- constraint: 'sha256 为空拒写;style_no 与入参/视图不一致拒写;同键重试不写库;固定部位 std_key 对齐上衣/裤子模板,可加 is_extra 行;delta 空用 null;轮次表用克隆款 styleNo(sample_list.roundStyleId→style_list.roundStyles)',
321
+ payload: '{orderId:string, styleNo, sizeChartJson:{view_version,style_no,category:top|bottom|set,unit,source:{sha256},sizes:[{raw}],rows:[{std_key,std_zh,category:top|bottom,raw_name?,raw_name_zh?,delta?,is_extra?,values:[{size,type,raw,value_cm?}]}],issues?:[],pending?:{to_supplement:[],to_confirm:[]}}, confirmedBy:确认人ID, source?:mcp|ui}',
322
+ constraint: 'confirmedBy/sha256/缺字段一次报全;issues/pending 可省略(后端补空);style_no 与入参/视图不一致拒写;同键重试不写库;固定部位 std_key 对齐上衣/裤子模板,可加 is_extra 行;delta 空用 null;轮次表用克隆款 styleNo(sample_list.roundStyleId→style_list.roundStyles)',
333
323
  next: '可选 size_chart_export / oss.upload+attachment_add 留底;查历史 size_chart_logs'
334
324
  },
335
325
  erp_production_craft_save: {
@@ -345,9 +335,9 @@ export const WORKFLOW_META = {
345
335
  module: '对象存储',
346
336
  step: '上传本地文件拿 ossId',
347
337
  summary: 'multipart 上传本机文件到 /resource/oss/upload,返回 ossId/url/fileName',
348
- prerequisite: 'stdio MCP 与文件同机;账号有 system:oss:upload;OSS 配置可用',
338
+ prerequisite: 'stdio MCP 与文件同机;账号有 system:oss:upload;OSS 配置可用(缺配置会报 OSS上传失败:…)',
349
339
  payload: 'filePath: 本机绝对路径(不是 payload JSON)',
350
- constraint: '只读本地路径,不接收远程 URL;返回的 ossId 再交给 attachment_add',
340
+ constraint: '只读本地路径,不接收远程 URL;本地不可读/空文件先拒;后端失败透出 OSS上传失败:原因;返回的 ossId 再交给 attachment_add',
351
341
  returns: '{ossId, url, fileName}',
352
342
  next: 'erp_order_attachment_add(尺寸表 remark=原始|归一xlsx|归一json)'
353
343
  },
@@ -358,7 +348,7 @@ export const WORKFLOW_META = {
358
348
  prerequisite: '订单已创建;先 erp_oss_upload(或页面上传)拿到 ossId',
359
349
  payload: '{orderId, styleId?, attachType, fileName, ossId, fileSize?, source?, remark?}',
360
350
  enums: 'attachType:pi/tech_drawing/customer_file/other/size_chart/production_order/color_card/craft;尺寸表细分靠 remark=原始|归一xlsx|归一json',
361
- constraint: '时间线查询已过滤 eventType=attachment'
351
+ constraint: 'ossId 须存在于 OSS,否则拒写;时间线查询已过滤 eventType=attachment'
362
352
  },
363
353
  erp_order_attachment_remove: {
364
354
  module: '订单管理',
@@ -373,8 +363,8 @@ export const WORKFLOW_META = {
373
363
  summary: '新增订单轮次并克隆主款式(pending;页面 Tab「轮次」)',
374
364
  prerequisite: '订单已创建;styleId 必须是主款(parentStyleId 为空)',
375
365
  payload: '{orderId, sampleType, styleId, roundName?, priority?, colorName?, plannedFinishDate?, remark?};误传 sampleNo 会映射为 roundName',
376
- enums: 'sampleType 仅 proto/pre_prod/bulk/mass;priority:low/normal/high/urgent(默认normal);roundName 与 sampleType 独立',
377
- constraint: 'MCP 校验 sampleType 白名单;新建后 pending;后端克隆主款→roundStyles;list 回填 roundStyleId;尺寸表/工艺写克隆款'
366
+ enums: 'sampleType:{proto:头样,pre_prod:产前样,bulk:船样,mass:大货,sales:销售样,photo:照片样};priority:low/normal/high/urgent(默认normal);roundName 与 sampleType 独立',
367
+ constraint: 'MCP 校验 sampleType 白名单;新建后 pending;返回 data=新样衣ID;后端克隆主款→roundStyles(款号父款-R1);list 回填 roundStyleId;尺寸表/工艺写克隆款'
378
368
  },
379
369
  erp_order_sample_edit: {
380
370
  module: '订单管理',
@@ -382,6 +372,7 @@ export const WORKFLOW_META = {
382
372
  summary: '修改轮次基础信息(不含状态);换主款会重克隆',
383
373
  prerequisite: '轮次已存在;styleId 须为主款',
384
374
  payload: '{id, orderId, sampleType, styleId, roundName?, priority?, colorName?, plannedFinishDate?, shipDate?, trackingNo?, courierCompany?, remark?};sampleNo→roundName',
375
+ enums: 'sampleType 同 sample_add:{proto:头样,pre_prod:产前样,bulk:船样,mass:大货,sales:销售样,photo:照片样}',
385
376
  constraint: 'MCP 校验 sampleType;不可 edit 改状态;改 styleId/roundName 会重建克隆款'
386
377
  },
387
378
  erp_order_sample_change_status: {
@@ -519,22 +510,21 @@ export const READ_META = {
519
510
  returns: '{filePath, size},filePath 为本机绝对路径'
520
511
  },
521
512
  erp_order_list: {
522
- usage: '订单状态见 erp_business_flow;可按 orderStatus/customerId/brandId/bizLine/seasonId 筛选'
513
+ usage: '订单状态见 erp_business_flow;可按 orderStatus/orderCustomerId/customerId/brandId/bizLine 筛选'
523
514
  },
524
515
  erp_order_customer_list: {
525
516
  usage: '查订单客户(跟单员挂在此层);可按 customerId/bizLine/orderNo/orderStatus 筛选',
526
517
  returns: 'rows[].{id,customerId,brandId,bizLine,assigneeId,assigneeName}'
527
518
  },
528
- erp_order_season_list: {
529
- usage: '按 orderCustomerId 查季度,建订单前取 seasonId',
530
- returns: '[{id,orderCustomerId,seasonName}]'
519
+ erp_order_get: {
520
+ usage: '查订单主档;认顶层 id|orderId 或 payload.id|orderId;include=samples,shipments,timeline 追加;fields 逗号裁剪',
521
+ returns: '订单基本信息(可含 samples/shipments/timeline)'
531
522
  },
532
- erp_order_get: { usage: '查订单主档', returns: '订单基本信息' },
533
523
  erp_order_style_list: {
534
- usage: '查主款树:colors/sizes(含qty/cutQty/actualQty)/crafts/商标;轮次克隆在 roundStyles(parentStyleId/sampleId/roundName);入参 orderId 或 orderNo;写工艺/尺寸表前必查'
524
+ usage: '查主款树:colors/sizes(qty生产单/cutQty预裁单/actualQty实裁单)/crafts/商标;轮次克隆在 roundStyles;入参 orderId 或 orderNo;fields 逗号裁剪'
535
525
  },
536
526
  erp_order_craft_schema: {
537
- usage: '必读返回 selectionGuide:jacket/pants 每字段 options、howToSelect、fieldValueExample;再按示例拼 fieldValue JSON 字符串写入 craft_save;旁框键为 note'
527
+ usage: '返回 fields + selectionGuide;体积过大传 compact=true 省略 selectionGuide,完整选项读 yindie://erp/craft-guide;再按示例拼 fieldValue 写 craft_save'
538
528
  },
539
529
  erp_order_style_craft_list: {
540
530
  usage: '按 styleId 读已存工艺行,验证 craft_save;也可用 order.style_list 看 styles[].crafts'
@@ -544,21 +534,25 @@ export const READ_META = {
544
534
  returns: '[{garmentPart, fieldKey, titleCn, colorCardCode, materialCode, materialName, sort}]'
545
535
  },
546
536
  erp_order_style_craft_export: {
547
- usage: '单款导出工艺单 xlsx;qtyType=prod|cut|actual(默认 prod);默认 preview 不写附件,archive=true 才归档',
537
+ usage: '单款导出工艺 Excel(页面「生产单」导出);【已去掉】多款合并导出,勿传 styleIds,请逐款导出;qtyType=prod|cut|actual;默认文件名含 qtyType;preview 默认,archive=true 才归档(失败直接报错);fileName 只取 basename',
548
538
  returns: '{filePath, size},filePath 为本机绝对路径'
549
539
  },
550
540
  erp_order_style_size_chart_logs: {
551
- usage: 'styleId 必填;查尺寸表变更快照(sizeChartJson/source/sha256/operatorName)',
541
+ usage: '查尺寸表变更快照;styleId,或 orderId+styleNo 解析',
552
542
  returns: '[{id,orderId,styleId,styleNo,sizeChartJson,source,sha256,operatorId,operatorName,createTime}]'
553
543
  },
554
544
  erp_order_style_size_chart_export: {
555
- usage: 'styleId 必填;可传 sizeChartJson,或 orderId+styleId 从 style_list 回查;fileName 仅文件名(落盘临时目录/yindie-erp-exports);archive=true 写附件',
545
+ usage: 'styleId 必填;可传 sizeChartJson,或 orderId+styleId 从 style_list 回查;fileName 只取 basename;archive=true 写附件',
556
546
  returns: '{filePath, size},filePath 为本机绝对路径'
557
547
  },
558
548
  erp_order_style_color_card_export: {
559
- usage: '单款导出色卡 Excel 到本机;默认走 color-card/preview 不写附件,archive=true 才走 color-card/export 写订单附件',
549
+ usage: '单款导出色卡 Excel(与前端一致,勿传多款);硬前置 colorCardJson 有 materialCode,否则拒写并点名款号/色号;默认 preview,archive=true 才归档',
560
550
  returns: '{filePath, size},filePath 为本机绝对路径'
561
551
  },
552
+ erp_material_price_snapshot_export: {
553
+ usage: '导出比价快照 Excel(对齐前端材料比价列表导出);可筛 productName/materialCode/categoryName/domain',
554
+ returns: '{filePath, size}'
555
+ },
562
556
  erp_order_attachment_export: {
563
557
  usage: '按 ossId 下载订单附件到本机(含工艺单/色卡/尺寸表等全部附件);ossId 从 attachment_list 取',
564
558
  returns: '{filePath, size},filePath 为本机绝对路径'
@@ -575,9 +569,13 @@ export const READ_META = {
575
569
  usage: '查订单轮次(styleId=主款,roundStyleId=克隆款,roundName/priority);可不建轮次'
576
570
  },
577
571
  erp_order_shipment_list: { usage: '查订单出货记录,按 shipSeq 升序;一单可多批' },
578
- erp_production_list: { usage: '【暂不用】制单中心已隐藏;生产单状态 draft/reviewing/issued/superseded' },
579
- erp_production_get: { usage: '【暂不用】查生产单主档、styles(含 colors.colorCardJson)及缺项;辅料展示读款式配置已选面料与色卡' },
580
- erp_production_check_list: { usage: '【暂不用】查生产单历次校验记录及得分' },
572
+ erp_production_list: {
573
+ usage: '【不用】制单中心停用;勿与页面款式行「生产单」(工艺)混淆;状态 draft/reviewing/issued/superseded;筛 poNo/poStatus'
574
+ },
575
+ erp_production_get: {
576
+ usage: '【不用】制单中心 PO 主档;页面「生产单」按钮请走 order.craft_*'
577
+ },
578
+ erp_production_check_list: { usage: '【不用】制单中心校验记录' },
581
579
  erp_task_list: { usage: '任务状态 pending/in_progress/completed/cancelled' },
582
580
  erp_task_get: { usage: '查任务详情' },
583
581
  erp_material_category_list: { usage: '查物料分类树,新建物料前先查 categoryId' },
@@ -613,8 +611,8 @@ export const ERP_BUSINESS_FLOW = {
613
611
  mainFlow: [
614
612
  '1. erp_customer action=customer.add 建客户(勿传customerCode;可传 taxNo/shortCode)',
615
613
  '2. erp_customer action=contact.add / address.add / brand.add 补全资料(brand 可传 shortCode)',
616
- '3. erp_pi action=add 创建PI(需customerId, sourceType, piNo;可选 orderId 直接关联订单)',
617
- '4. erp_workbench action=merchandiser_options 取跟单员 → erp_order action=customer_add 建订单客户 → action=season_add 建季度 → action=add(需 orderNo,seasonId,默认inquiry;复制订单号规则26SB01)',
614
+ '3. 订单详情关联PI:erp_pi action=add(需 orderId, customerId, sourceType, piNo;独立 PI 菜单已隐藏)',
615
+ '4. erp_workbench action=merchandiser_options 取跟单员 → erp_order action=customer_add 建订单客户 → action=add(需 orderNo,orderCustomerId,默认inquiry;复制订单号规则26SB01)',
618
616
  '5. 若 PI 未带 orderId:erp_order action=link_pi 关联PI',
619
617
  '6. erp_material list/add 选或新增面料(fabric 必填色号 color)→ erp_order action=style_save(colors[].fabricMaterialId/colorCode/colorName/colorCardJson + 尺码)',
620
618
  '7. erp_order action=craft_schema → craft_save 写工艺选项(jacket+pants 一次全量)→ craft_get 校验',
@@ -708,7 +706,8 @@ export const ERP_BUSINESS_FLOW = {
708
706
  color_card: '色卡',
709
707
  craft: '工艺单'
710
708
  },
711
- sampleType: { proto: '打样', pre_prod: '产前样', bulk: '船样', mass: '大货' },
709
+ sampleType: { proto: '头样', pre_prod: '产前样', bulk: '船样', mass: '大货', sales: '销售样', photo: '照片样' },
710
+ garmentType: { jacket: '上衣', pants: '裤子', suit: '套装' },
712
711
  sampleStatus: { pending: '待制作', making: '制作中', shipped: '已寄出', confirmed: '已确认', rejected: '已驳回' },
713
712
  poItemCategory: {
714
713
  button: '裤子纽扣',
@@ -729,7 +728,7 @@ export const ERP_BUSINESS_FLOW = {
729
728
  note: 'radio/combine 旁侧编号或备注(不是 input)',
730
729
  colorCardCode: '色卡选料色号,后端按旧值补回,一般勿传',
731
730
  materialCode: '色卡选料物料编码,同上',
732
- customOptions: '+新增自定义选项 chip(不是编号)'
731
+ customOptions: 'allowAdd 字段的自定义 chip;须与 checkbox/combineMap 同写,勿只塞进 checkbox'
733
732
  },
734
733
  craftGarmentPart: { jacket: '上衣', pants: '裤子' },
735
734
  craftModuleJacket: {
@@ -757,28 +756,58 @@ export const ERP_BUSINESS_FLOW = {
757
756
  accessory: '辅料',
758
757
  crease: '挺缝线',
759
758
  pack: '包装'
760
- }
759
+ },
760
+ labelType: {
761
+ main_label: '上衣主标',
762
+ collar_label: '上衣领标',
763
+ small_woven: '小织标',
764
+ jacket_brand: '上衣商标',
765
+ pants_main: '裤子主标',
766
+ ear_loop: '裤子挂耳',
767
+ flag_label: '后裤片旗标',
768
+ fit_label: '版型标',
769
+ pants_brand: '裤子商标',
770
+ pants_flag: '裤子旗标'
771
+ },
772
+ labelPositionSide: { front: '前面', back: '后面' }
761
773
  },
762
774
  constraints: [
763
775
  '客户删除:PI 关联的订单仍在时不可删;关联订单已删的 PI 随客户一起清掉',
764
776
  '订单删除:级联清该单 PI、款式树和生产单,订单号改为 _原ID 释放号码可复用',
765
- '【暂不用】制单中心菜单已隐藏:勿主动 erp_production create_from_order / check / submit_review / issue,除非用户明确要求',
766
- '每订单仅一条生产单(erp_production_create_from_order);仅 orderStatus=quoted|pre_prod_confirmed|po_drafting 且尚无生产单时可调用;成功后订单强制 orderStatus=po_drafting',
767
- '生产单仅draft可修改/编辑明细;submit_review draft→reviewing;reject_review reviewing→draft;issue reviewing→issued 并同步订单→po_issued',
768
- '已下发(issued)生产单且订单仍在时不可删除;订单删除会连带清生产单',
777
+ '【命名】订单页款式行「生产单」=工艺(craft_save/craft_export),要用;「制单中心」erp_production=PO 主档,不用',
778
+ '【不用】制单中心已停用:勿主动 erp_production create_from_order / save_items / check / submit_review / issue',
779
+ '制单中心 API 仍保留:每单最多一条 PO;仅 quoted|pre_prod_confirmed|po_drafting 可 from_order;成功会把订单改成 po_drafting;仅用户点名时再动',
780
+ '制单中心 draft 可改;submit_review draft→reviewing;reject→draft;issue→issued 并订单 po_issued;issued 且订单在时不可删',
769
781
  '工艺选项与怎么选见 erp_order_craft_schema.selectionGuide 或资源 yindie://erp/craft-guide',
770
782
  '工艺选项固定模板(对齐打样单),不用 sys_dict;fieldValue 存 JSON(见 craftValueJson / fieldValueExample)',
771
783
  '工艺 fieldKey/craftModule 以 erp_order_craft_schema.fields 为准,勿再用旧字典码如 single_1',
772
784
  '款式面料色号:material.list/add → style_save colors[].fabricMaterialId,colorCode=物料 color,colorName 必填;勿传 fabricColorNo',
773
- '色卡选料:material.list(category_code=fieldKey)/add → style_save colors[].colorCardJson;色卡备注:color_card_save(常规 items:[])',
774
- 'Excel 导出/预览都是二进制:工艺单 craft_export(qtyType=prod|cut|actual)、色卡 color_card_export、尺寸表 size_chart_export、PI export,均落本机返回 filePath',
785
+ '【色卡生成】material.list/add → style_save colors[].colorCardJson(选料,键=白名单 fieldKey,值={materialCode,materialName,titleCn})→ 可选 craft_save(仅影响前端可选列 onCard)→ 可选 color_card_save(仅备注)→ color_card_export;导出只认 colorCardJson,无选料直接拒',
786
+ '色卡备注:color_card_save 常规 {styleId,colorCardNote,items:[]};选料不在 color_card_save',
787
+ 'Excel 导出/预览都是二进制:工艺单 craft_export(qtyType=prod生产单|cut预裁单|actual实裁单,标题同步)、色卡 color_card_export、尺寸表 size_chart_export、PI export,均落本机返回 filePath',
788
+ '【不用】erp_production 制单中心整模块停用;主链路止于 order.change_status;勿与页面「生产单」按钮(工艺)混淆',
775
789
  '轮次 sample_add 会克隆主款;style_list 主款挂 roundStyles;尺寸表/工艺按轮次用 roundStyleId;style_save 只传主款',
776
790
  '物料删除:有绑定订单/生产单引用/比价快照时不可删;删前用 material.bound_orders 查引用',
777
791
  '物料新增:编码命中已删除记录时直接恢复原记录并覆盖写入,不报重复',
778
792
  'craft_save 全量替换:同 styleId 的 jacket+pants 必须一次写入;分次调用后一次会删前一次',
779
793
  'craft_save 会连商标位置一起全量替换:已有标位时须回传 labelPositions,漏传即清空',
780
794
  'craft_save 键名 crafts/craftModule;兼容历史 craft/module;旁框用 note;保存后 craft_get 校验',
781
- '商标位置 labelType/positionSide 仍用系统字典',
795
+ '商标位置 labelType/positionSide 见 enums.labelType / labelPositionSide(字典 erp_label_type / erp_label_position_side),非法值后端拒写',
796
+ 'attachment_add 的 ossId 须已存在于 OSS,否则拒写',
797
+ '导出 archive=true 时 OSS/附件写入失败会直接报错(不再静默)',
798
+ 'craft_export 默认文件名 craft-<styleId>-<qtyType>.xlsx;后端归档名带生产单/预裁单/实裁单',
799
+ 'order.get / order.edit 认顶层 id|orderId 或 payload.id|orderId;雪花 ID 须字符串',
800
+ 'timeline_list 含 sample 轮次事件(attachment/size_chart 仍排除);轮次明细也可用 sample_list',
801
+ 'oss.upload 失败透出 OSS上传失败:原因;本地路径不可读/空文件先拒',
802
+ 'craft_export cut/actual 数量空时回退 qty;色卡导出缺料报错带款号/色号',
803
+ 'size_chart_save source.sha256 须 64 位;与视图字段同一遍校验汇总报错(有 type 等错误时不误报 sha 缺失)',
804
+ 'size_chart_logs 认 styleId 或 orderId+styleNo',
805
+ '【已去掉】工艺/色卡多款合并导出:与前端一致仅单款,勿传 styleIds;请逐款 craft_export / color_card_export',
806
+ '材料比价列表导出:erp_material action=price_snapshot.export(对齐前端)',
807
+ 'PI 日常入口=订单详情关联PI;add 带 orderId+customerId+piNo+sourceType;同步款式无独立 API,先 style_list 再填 items',
808
+ 'order.get include=samples,shipments,timeline;get/style_list 可用 fields 裁剪;craft_schema compact=true 省略 selectionGuide',
809
+ '订单无季度层:customer_add 后直接 add(orderNo,orderCustomerId);勿再调 season_*',
810
+ '订单客户下有订单不可删;list 可按 orderCustomerId 筛该客户全部订单',
782
811
  '轮次可选:询盘可 inquiry→quoted 跳过打样(返单/大货);记在 erp_order action=sample_*,不强制每单建轮次',
783
812
  '返单用 erp_order action=copy:复制主档+款式工艺,不复制PI/样衣/生产单,新单 parentOrderId 指向源单',
784
813
  '单据编号:customerCode/supplierCode/categoryCode/brandCode/supplierMaterialCode/poNo/bidNo 后端发号勿传;materialCode 手填必传;piNo/orderNo 手填必传(订单复制发26SB01);styleNo 可省略则默认订单号-序号',
@@ -786,9 +815,11 @@ export const ERP_BUSINESS_FLOW = {
786
815
  '招标 MCP 为内部接口,不含供应商门户 portal/login、portal/quote',
787
816
  '所有雪花 ID 入参必须字符串,禁止 number;MCP 会对 payload/响应中 ≥16 位数字字面量保真为字符串',
788
817
  'style_save 必须 styles 非空;顶层单款字段 MCP 会自动包 styles:[...];styles:[] 拒写',
789
- 'sampleType 仅 proto/pre_prod/bulk/mass;误传 sampleNo 映射为 roundName',
790
- 'change_status 优先 payload.id;顶层 id/orderId 会兜底写入 payload.id',
791
- '导出 fileName 仅取文件名,落盘系统临时目录/yindie-erp-exports'
818
+ '【警告】style_save 未出现的旧主款整树删除;减款须 confirmReduce:true;返回 deletedStyleIds/deletedRoundStyleIds',
819
+ 'garmentType 仅 jacket/pants/suit,与尺寸表 category top/bottom/set 不同',
820
+ 'sampleType 仅 {proto:头样,pre_prod:产前样,bulk:船样,mass:大货,sales:销售样,photo:照片样};误传 sampleNo 映射为 roundName',
821
+ 'change_status 认 payload.id / payload.orderId / 顶层 id|orderId',
822
+ '导出 fileName 传路径也只取 basename,落盘系统临时目录/yindie-erp-exports'
792
823
  ],
793
824
  entryPoint: 'erp_workbench action=summary',
794
825
  resourceHint: '操作前先读 yindie://erp/usage-guide、yindie://erp/business-flow、yindie://erp/modules'
@@ -801,15 +832,19 @@ export const ERP_MCP_INSTRUCTIONS = `银蝶 ERP MCP(域工具 + action 模式
801
832
 
802
833
  【调用格式】选域工具(erp_customer/erp_order/erp_material 等),必填 action。有子实体时 action 写 target.action,如 customer.add、material.add、price_snapshot.list、thread.link。写操作用 payload 传 JSON 字符串;删用 ids;单条查改 id。所有雪花 ID 必须字符串。
803
834
 
804
- 【主链路】customer.add →(订单客户)order.customer_add → order.season_add → order.add(seasonId) → pi.add → order.link_pi →(面料 material.list/add)order.style_save(colors…+sizes.qty/cutQty?/actualQty?) → order.craft_schema → order.craft_save →(可选)order.color_card_save →(可选)order.sample_add 克隆轮次 →(尺寸表)order.size_chart_save(主款或克隆款 styleNo)→ order.change_status。到订单推进为止。
835
+ 【主链路】customer.add →(订单客户)order.customer_add → order.add(orderCustomerId) → pi.add(orderId+…)(入口=订单关联PI)→(面料 material.list/add)order.style_save(colors…+sizes.qty/cutQty?/actualQty?+colorCardJson?) → order.craft_schema → order.craft_save →(可选)order.color_card_save →(可选)order.sample_add 克隆轮次 →(尺寸表)order.size_chart_save(主款或克隆款 styleNo)→ order.change_status。到订单推进为止;不要再进生产单。
836
+
837
+ 【命名别混】页面款式操作列「生产单」按钮 = 工艺编辑/导出(craft_save、craft_export,qtyType=prod 只是数量口径名),要用。侧栏「制单中心」/ erp_production = PO 主档,不用。
838
+
839
+ 【色卡】选料只写 style_save.colors[].colorCardJson;备注 color_card_save(items:[]);导出 color_card_export 只认 colorCardJson(无选料会拒)。craft_save 只影响前端哪些色卡列可填(onCard),不是导出数据源。
805
840
 
806
- 【工艺】先 craft_schema 拿 fieldKey 与 fieldValueExample;craft_save 全量替换含商标位置;色卡选料写 style_save.colorCardJson;备注走 color_card_save;导出 craft_export(qtyType=prod|cut|actual)/color_card_export/size_chart_export/attachment_export,默认不写附件,archive=true 才归档。
841
+ 【工艺】对应页面「生产单」按钮。先 craft_schema 拿 fieldKey 与 fieldValueExample(含 checkbox+gateNone 的 radio:有);craft_save 全量替换含商标位置;按 control 校验。导出 craft_export(qtyType=prod|cut|actual,仅单款;合并导出已去掉)/size_chart_export/attachment_export,默认不写附件,archive=true 才归档。
807
842
 
808
843
  【轮次/尺寸表】sample_add 的 styleId 只能是主款;返回后 sample_list.roundStyleId 为克隆款。style_list 主款含 roundStyles。尺寸表 size_chart_save 按 styleNo 写;历史 size_chart_logs;导出 size_chart_export。
809
844
 
810
845
  【物料】material.add(materialCode 手填;fabric 必填 color;lining 必填 composition);面料色号行填 fabricMaterialId;删前 material.bound_orders。
811
846
 
812
- 【制单·暂不用】前端「制单中心」菜单已隐藏;勿主动 production.create_from_order / check / submit_review / issue。API 仍保留,仅用户明确要求时再动。
847
+ 【制单中心·不用】erp_production 整模块停用;主链路不含它。勿主动 create_from_order / save_items / check / submit_review / issue。API 仅保留,用户点名再说。
813
848
 
814
849
  【权限】无权限 action 会报错;以 yindie://erp/modules 返回的 actions 为准。`;
815
850
  /** 合并工具用法资源 */
@@ -843,10 +878,6 @@ export const ERP_USAGE_GUIDE = {
843
878
  'customer_add',
844
879
  'customer_edit',
845
880
  'customer_remove',
846
- 'season_list',
847
- 'season_add',
848
- 'season_edit',
849
- 'season_remove',
850
881
  'list',
851
882
  'get',
852
883
  'add',
@@ -882,7 +913,7 @@ export const ERP_USAGE_GUIDE = {
882
913
  'attachment_export',
883
914
  'attachment_remove'
884
915
  ],
885
- note: '建订单前 customer_list→season_list 取 seasonId;style_save 只传主款;sample_add 克隆轮次;尺寸表按主款/克隆款 styleNo;craft_export 可带 qtyType;导出类 archive=true 才写附件'
916
+ note: '建订单前 customer_list 取 orderCustomerId;style_save 只传主款;sample_add 克隆轮次;尺寸表按主款/克隆款 styleNo;craft_export 可带 qtyType;导出类 archive=true 才写附件'
886
917
  },
887
918
  erp_production: {
888
919
  actions: [
@@ -907,7 +938,7 @@ export const ERP_USAGE_GUIDE = {
907
938
  erp_task: { actions: ['list', 'get', 'add', 'edit', 'remove'] },
908
939
  erp_material: {
909
940
  actions: ['material.*', 'category.*', 'supplier.*', 'price_snapshot.*', 'price_quote.*', 'price_point.*'],
910
- note: 'action 示例 material.add、material.bound_orders、price_quote.get、price_point.series;payload 见 payloadByEntity'
941
+ note: 'action 示例 material.add、price_snapshot.export、price_point.series;payload 见 payloadByEntity'
911
942
  },
912
943
  erp_supplier: { actions: ['list', 'get', 'add', 'edit', 'remove'] },
913
944
  erp_bid: {
@@ -936,6 +967,14 @@ export const ERP_USAGE_GUIDE = {
936
967
  payload: { customerName: 'Y', customerType: 'trader' }
937
968
  }
938
969
  },
970
+ {
971
+ intent: '创建订单(无季度,直挂订单客户)',
972
+ call: {
973
+ tool: 'erp_order',
974
+ action: 'add',
975
+ payload: { orderNo: '26SB03', orderCustomerId: '字符串', priority: 'normal' }
976
+ }
977
+ },
939
978
  {
940
979
  intent: '录订单款式',
941
980
  call: {
@@ -963,7 +1002,7 @@ export const ERP_USAGE_GUIDE = {
963
1002
  }
964
1003
  },
965
1004
  {
966
- intent: '导出工艺单(裁剪单数量)',
1005
+ intent: '导出工艺单(预裁单数量)',
967
1006
  call: { tool: 'erp_order', action: 'craft_export', styleId: '字符串', qtyType: 'cut' }
968
1007
  },
969
1008
  {
@@ -1027,6 +1066,7 @@ export const ERP_USAGE_GUIDE = {
1027
1066
  'order.color_card_list': READ_META.erp_order_style_color_card_list,
1028
1067
  'order.color_card_save': WORKFLOW_META.erp_order_style_color_card_save,
1029
1068
  'order.color_card_export': READ_META.erp_order_style_color_card_export,
1069
+ 'material.price_snapshot.export': READ_META.erp_material_price_snapshot_export,
1030
1070
  'oss.upload': WORKFLOW_META.erp_oss_upload,
1031
1071
  'order.sample_add': WORKFLOW_META.erp_order_sample_add,
1032
1072
  'order.sample_edit': WORKFLOW_META.erp_order_sample_edit,
@@ -86,10 +86,6 @@ const DOMAIN_TEMPLATES = [
86
86
  customer_add: { oldName: 'erp_order_customer_add' },
87
87
  customer_edit: { oldName: 'erp_order_customer_edit' },
88
88
  customer_remove: { oldName: 'erp_order_customer_remove' },
89
- season_list: { oldName: 'erp_order_season_list' },
90
- season_add: { oldName: 'erp_order_season_add' },
91
- season_edit: { oldName: 'erp_order_season_edit' },
92
- season_remove: { oldName: 'erp_order_season_remove' },
93
89
  list: { oldName: 'erp_order_list' },
94
90
  get: { oldName: 'erp_order_get' },
95
91
  add: { oldName: 'erp_order_add' },
@@ -188,6 +184,7 @@ const DOMAIN_TEMPLATES = [
188
184
  price_snapshot: {
189
185
  list: { oldName: 'erp_material_price_snapshot_list' },
190
186
  get: { oldName: 'erp_material_price_snapshot_get' },
187
+ export: { oldName: 'erp_material_price_snapshot_export' },
191
188
  add: { oldName: 'erp_material_price_snapshot_add' },
192
189
  edit: { oldName: 'erp_material_price_snapshot_edit' },
193
190
  remove: { oldName: 'erp_material_price_snapshot_remove' }
@@ -350,17 +347,24 @@ function inputSchemaFor(tpl, allowed) {
350
347
  ...base,
351
348
  orderNo: z.string().optional().describe('订单号'),
352
349
  orderId: optionalId.describe('订单 ID'),
353
- styleId: optionalId.describe('款式 ID;craft_get/size_chart_logs/size_chart_export 必填'),
350
+ styleId: optionalId.describe('款式 ID;craft_get/size_chart_export 等;logs 可改用 orderId+styleNo'),
351
+ styleNo: z.string().optional().describe('款号;size_chart_logs 与 orderId 联用'),
354
352
  customerId: optionalId.describe('客户 ID'),
355
353
  brandId: optionalId.describe('品牌 ID'),
356
354
  bizLine: z.string().optional().describe('业务线 direct/trader'),
357
355
  orderStatus: z.string().optional().describe('订单状态'),
358
- orderCustomerId: optionalId.describe('订单客户 ID(season_list 必填)'),
359
- qtyType: z.string().optional().describe('craft_export:prod/cut/actual,默认 prod'),
356
+ orderCustomerId: optionalId.describe('订单客户 ID(list/add 筛建订单用)'),
357
+ qtyType: z
358
+ .string()
359
+ .optional()
360
+ .describe('craft_export:prod生产单/cut预裁单/actual实裁单(默认 prod;Excel 标题同此)'),
360
361
  sizeChartJson: z.string().optional().describe('size_chart_export:入库视图 JSON;可省略则靠 orderId+styleId 回查'),
361
- fileName: z.string().optional().describe('导出类:仅文件名,落盘临时目录/yindie-erp-exports'),
362
+ fileName: z.string().optional().describe('导出类:仅文件名(传路径也取 basename)'),
362
363
  archive: z.boolean().optional().describe('导出类 action:true 归档到订单附件'),
363
- ossId: optionalId.describe('OSS 文件 ID(attachment_export 必填)')
364
+ ossId: optionalId.describe('OSS 文件 ID(attachment_export 必填)'),
365
+ include: z.string().optional().describe('get:samples,shipments,timeline 逗号分隔'),
366
+ fields: z.string().optional().describe('get/style_list:返回字段白名单,逗号分隔'),
367
+ compact: z.boolean().optional().describe('craft_schema:true 省略 selectionGuide')
364
368
  };
365
369
  }
366
370
  if (tpl.name === 'erp_production') {
@@ -369,7 +373,8 @@ function inputSchemaFor(tpl, allowed) {
369
373
  productionNo: z.string().optional().describe('生产单号'),
370
374
  orderId: optionalId.describe('订单 ID'),
371
375
  styleId: optionalId.describe('款式 ID(craft_get 必填)'),
372
- productionOrderId: optionalId.describe('生产单 ID')
376
+ productionOrderId: optionalId.describe('生产单 ID'),
377
+ compact: z.boolean().optional().describe('craft_schema:true 省略 selectionGuide')
373
378
  };
374
379
  }
375
380
  if (tpl.name === 'erp_task') {
@@ -398,7 +403,8 @@ function inputSchemaFor(tpl, allowed) {
398
403
  matchLevel: z.string().optional().describe('匹配度 A/B/C/D'),
399
404
  platform: z.string().optional().describe('渠道/平台'),
400
405
  riskLevel: z.string().optional().describe('风险级别 high/mid/low/info'),
401
- status: z.string().optional().describe('状态 active/inactive')
406
+ status: z.string().optional().describe('状态 active/inactive'),
407
+ fileName: z.string().optional().describe('price_snapshot.export:落盘文件名')
402
408
  };
403
409
  }
404
410
  if (tpl.name === 'erp_supplier') {
@@ -1,11 +1,21 @@
1
- import { readFile } from 'node:fs/promises';
1
+ import { access, readFile } from 'node:fs/promises';
2
2
  import { basename } from 'node:path';
3
+ import { constants as fsConstants } from 'node:fs';
3
4
  import { z } from 'zod';
4
5
  import { idField, idsField, normalizeCraftSavePayload, normalizeSamplePayload, normalizeStyleSavePayload, parseIds, parsePayload } from './tool-helpers.js';
5
6
  import { buildCrudDescription, buildWorkflowDescription, ENTITY_META, payloadFor, WORKFLOW_META } from './tool-meta.js';
6
7
  /** 本地文件组装 multipart */
7
8
  async function buildOssUploadForm(filePath) {
9
+ try {
10
+ await access(filePath, fsConstants.R_OK);
11
+ }
12
+ catch {
13
+ throw new Error(`本地文件不可读:${filePath}`);
14
+ }
8
15
  const bytes = await readFile(filePath);
16
+ if (bytes.length === 0) {
17
+ throw new Error(`本地文件为空:${filePath}`);
18
+ }
9
19
  const form = new FormData();
10
20
  form.append('file', new Blob([bytes]), basename(filePath));
11
21
  return form;
@@ -26,7 +36,16 @@ function entityCrud(name, basePath, perm) {
26
36
  description: buildCrudDescription('edit', meta),
27
37
  permission: `${perm}:edit`,
28
38
  inputSchema: { payload: payloadFor(meta, 'edit') },
29
- handler: (client, args) => client.put(basePath, parsePayload(args))
39
+ handler: (client, args) => {
40
+ const payload = parsePayload(args);
41
+ if (payload.id == null) {
42
+ const topId = args.id ?? args.orderId;
43
+ if (topId != null && topId !== '') {
44
+ payload.id = String(topId);
45
+ }
46
+ }
47
+ return client.put(basePath, payload);
48
+ }
30
49
  },
31
50
  {
32
51
  name: `${name}_remove`,
@@ -56,7 +75,6 @@ export const WRITE_TOOLS = [
56
75
  ...entityCrud('erp_pi', '/erp/pi', 'erp:pi'),
57
76
  ...entityCrud('erp_order', '/erp/order', 'erp:order'),
58
77
  ...entityCrud('erp_order_customer', '/erp/order/customer', 'erp:order'),
59
- ...entityCrud('erp_order_season', '/erp/order/season', 'erp:order'),
60
78
  ...entityCrud('erp_task', '/erp/task', 'erp:task'),
61
79
  ...entityCrud('erp_material', '/erp/material', 'erp:material'),
62
80
  ...entityCrud('erp_material_category', '/erp/material/category', 'erp:material:category'),
@@ -72,6 +90,9 @@ export const WRITE_TOOLS = [
72
90
  workflowTool('erp_bid_cancel', 'erp:bid:edit', { id: idField.describe('招标 ID,不可取消 awarded') }, (client, args) => client.put(`/erp/bid/cancel/${args.id}`)),
73
91
  workflowTool('erp_order_change_status', 'erp:order:edit', { payload: z.string().describe(WORKFLOW_META.erp_order_change_status.payload) }, (client, args) => {
74
92
  const payload = parsePayload(args);
93
+ if (payload.id == null && payload.orderId != null) {
94
+ payload.id = payload.orderId;
95
+ }
75
96
  if (payload.id == null) {
76
97
  const topId = args.id ?? args.orderId;
77
98
  if (topId != null && topId !== '') {
package/dist/tools.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { idField } from './tool-helpers.js';
2
+ import { idField, parseInclude, pickFields, resolveGetId } from './tool-helpers.js';
3
3
  import { CRAFT_SELECTION_GUIDE } from './craft/guide.js';
4
4
  import { buildReadDescription, ERP_BUSINESS_FLOW, READ_META } from './tool-meta.js';
5
5
  import { WRITE_TOOLS } from './tools-write.js';
@@ -34,7 +34,16 @@ export function hasPermission(permissions, required) {
34
34
  return permissions.includes(required);
35
35
  }
36
36
  /** 包装工艺 schema */
37
- function wrapCraftSchema(fields) {
37
+ function wrapCraftSchema(fields, compact) {
38
+ if (compact) {
39
+ return {
40
+ fields,
41
+ craftValueJson: ERP_BUSINESS_FLOW.enums.craftValueJson,
42
+ craftModuleJacket: ERP_BUSINESS_FLOW.enums.craftModuleJacket,
43
+ craftModulePants: ERP_BUSINESS_FLOW.enums.craftModulePants,
44
+ note: 'compact=true 省略 selectionGuide;选项读 yindie://erp/craft-guide 或 compact=false'
45
+ };
46
+ }
38
47
  return {
39
48
  fields,
40
49
  craftValueJson: ERP_BUSINESS_FLOW.enums.craftValueJson,
@@ -215,7 +224,7 @@ const READ_TOOLS = [
215
224
  inputSchema: {
216
225
  ...pageShape,
217
226
  orderNo: z.string().optional().describe('订单号'),
218
- seasonId: optionalId.describe('季度 ID,字符串'),
227
+ orderCustomerId: optionalId.describe('订单客户 ID,字符串'),
219
228
  customerId: optionalId.describe('客户 ID,字符串'),
220
229
  brandId: optionalId.describe('品牌 ID,字符串'),
221
230
  bizLine: z.string().optional().describe('业务线 direct/trader'),
@@ -224,7 +233,7 @@ const READ_TOOLS = [
224
233
  handler: (client, args) => client.get('/erp/order/list', {
225
234
  ...pageParams(args),
226
235
  orderNo: args.orderNo,
227
- seasonId: asId(args.seasonId),
236
+ orderCustomerId: asId(args.orderCustomerId),
228
237
  customerId: asId(args.customerId),
229
238
  brandId: asId(args.brandId),
230
239
  bizLine: args.bizLine,
@@ -250,31 +259,52 @@ const READ_TOOLS = [
250
259
  orderStatus: args.orderStatus
251
260
  })
252
261
  },
253
- {
254
- name: 'erp_order_season_list',
255
- description: '按订单客户查季度,创建订单前取 seasonId',
256
- permission: 'erp:order:list',
257
- inputSchema: {
258
- orderCustomerId: idField.describe('订单客户 ID,字符串')
259
- },
260
- handler: (client, args) => client.get('/erp/order/season/list', { orderCustomerId: asId(args.orderCustomerId) })
261
- },
262
262
  {
263
263
  name: 'erp_order_get',
264
- description: '按 ID 查询订单详情',
264
+ description: '按 ID 查询订单详情;认顶层 id|orderId 或 payload.id|orderId;include=samples,shipments,timeline 追加聚合;fields 逗号裁剪返回字段',
265
265
  permission: 'erp:order:query',
266
266
  inputSchema: {
267
- id: idField.describe('订单 ID,字符串')
267
+ id: optionalId.describe('订单 ID,字符串'),
268
+ include: z.string().optional().describe('追加聚合:samples,shipments,timeline 逗号分隔'),
269
+ fields: z.string().optional().describe('只返回这些字段,逗号分隔')
268
270
  },
269
- handler: (client, args) => client.get(`/erp/order/${asId(args.id)}`)
271
+ handler: async (client, args) => {
272
+ const id = resolveGetId(args);
273
+ if (!id) {
274
+ throw new Error('需要 id(顶层 id|orderId 或 payload.id|orderId)');
275
+ }
276
+ const order = (await client.get(`/erp/order/${id}`));
277
+ const include = parseInclude(args.include);
278
+ const jobs = [];
279
+ if (include.has('samples')) {
280
+ jobs.push(client.get('/erp/order/sample/list', { orderId: id }).then((rows) => {
281
+ order.samples = rows;
282
+ }));
283
+ }
284
+ if (include.has('shipments')) {
285
+ jobs.push(client.get('/erp/order/shipment/list', { orderId: id }).then((rows) => {
286
+ order.shipments = rows;
287
+ }));
288
+ }
289
+ if (include.has('timeline')) {
290
+ jobs.push(client.get('/erp/order/timeline/list', { orderId: id }).then((rows) => {
291
+ order.timeline = rows;
292
+ }));
293
+ }
294
+ if (jobs.length > 0) {
295
+ await Promise.all(jobs);
296
+ }
297
+ return pickFields(order, args.fields);
298
+ }
270
299
  },
271
300
  {
272
301
  name: 'erp_order_style_list',
273
- description: '查询订单主款式树(含 colors/sizes/crafts/商标;轮次克隆挂在 roundStyles);orderId 或 orderNo 二选一,ID 必须字符串',
302
+ description: '查询订单主款式树(含 colors/sizes/crafts/商标;轮次克隆挂在 roundStyles);orderId 或 orderNo 二选一;fields 逗号裁剪',
274
303
  permission: 'erp:order:query',
275
304
  inputSchema: {
276
305
  orderId: optionalId.describe('订单 ID,字符串雪花 ID'),
277
- orderNo: z.string().optional().describe('订单号,无 orderId 时用此解析')
306
+ orderNo: z.string().optional().describe('订单号,无 orderId 时用此解析'),
307
+ fields: z.string().optional().describe('只返回这些字段,逗号分隔(如 id,styleNo,colors)')
278
308
  },
279
309
  handler: async (client, args) => {
280
310
  let orderId = asId(args.orderId);
@@ -289,15 +319,18 @@ const READ_TOOLS = [
289
319
  if (!orderId) {
290
320
  throw new Error('需要 orderId(字符串)或 orderNo');
291
321
  }
292
- return client.get('/erp/order/style/list', { orderId });
322
+ const styles = await client.get('/erp/order/style/list', { orderId });
323
+ return pickFields(styles, args.fields);
293
324
  }
294
325
  },
295
326
  {
296
327
  name: 'erp_order_craft_schema',
297
- description: '查询工艺字段定义 + 完整选择指南(selectionGuide:选项/怎么选/fieldValueExample)。写工艺前必调',
328
+ description: '查询工艺字段定义 + 选择指南。compact=true 省略 selectionGuide(体积大时用);完整选项也可读 yindie://erp/craft-guide',
298
329
  permission: 'erp:order:query',
299
- inputSchema: {},
300
- handler: async (client) => wrapCraftSchema(await client.get('/erp/order/style/craft/schema'))
330
+ inputSchema: {
331
+ compact: z.boolean().optional().describe('true 时省略 selectionGuide,显著减小返回体')
332
+ },
333
+ handler: async (client, args) => wrapCraftSchema(await client.get('/erp/order/style/craft/schema'), args.compact === true)
301
334
  },
302
335
  {
303
336
  name: 'erp_order_style_craft_list',
@@ -310,31 +343,59 @@ const READ_TOOLS = [
310
343
  },
311
344
  {
312
345
  name: 'erp_order_style_craft_export',
313
- description: '按 styleId 导出工艺单 Excel 到本机并返回落盘路径;qtyType=prod|cut|actual 取尺码数量;默认 preview 不写附件,archive=true 才归档',
346
+ description: '单款导出工艺单 Excel(合并导出已去掉,勿传 styleIds);qtyType=prod|cut|actual;默认 preview,archive=true 才归档;fileName 只取 basename',
314
347
  permission: 'erp:order:query',
315
348
  inputSchema: {
316
- styleId: idField.describe('款式 ID,字符串'),
317
- qtyType: z.string().optional().describe('数量类型 prod生产单/cut裁剪单/actual实际单,默认 prod'),
349
+ styleId: idField.describe('款式 ID,字符串;仅单款'),
350
+ qtyType: z
351
+ .string()
352
+ .optional()
353
+ .describe('数量类型 prod生产单/cut预裁单/actual实裁单(默认 prod;Excel 标题同此文案)'),
318
354
  fileName: z
319
355
  .string()
320
356
  .optional()
321
- .describe('仅文件名(忽略目录),默认 craft-<styleId>.xlsx,落盘临时目录/yindie-erp-exports'),
322
- archive: z.boolean().optional().describe('true 时写入订单附件,需 erp:order:export 权限')
357
+ .describe('仅文件名(传路径也只取 basename),默认 craft-<styleId>-<qtyType>.xlsx'),
358
+ archive: z.boolean().optional().describe('true 时写入订单附件,需 erp:order:export 权限;失败直接报错')
323
359
  },
324
360
  handler: (client, args) => {
361
+ if (args.styleIds != null && args.styleIds !== '') {
362
+ throw new Error('工艺合并导出已去掉,请逐款传 styleId 调用 craft_export');
363
+ }
325
364
  const styleId = asId(args.styleId);
365
+ if (!styleId) {
366
+ throw new Error('需要 styleId');
367
+ }
326
368
  const qtyType = args.qtyType || 'prod';
327
- return client.download('POST', args.archive ? '/erp/order/style/craft/export-excel' : '/erp/order/style/craft/preview-excel', args.fileName ?? `craft-${styleId}.xlsx`, { params: { styleId, qtyType } });
369
+ return client.download('POST', args.archive ? '/erp/order/style/craft/export-excel' : '/erp/order/style/craft/preview-excel', args.fileName ?? `craft-${styleId}-${qtyType}.xlsx`, { params: { styleId, qtyType } });
328
370
  }
329
371
  },
330
372
  {
331
373
  name: 'erp_order_style_size_chart_logs',
332
- description: '按 styleId 查尺寸表变更记录(快照列表,含 sizeChartJson);styleId 必填',
374
+ description: '查尺寸表变更记录;styleId,或 orderId+styleNo 解析款式',
333
375
  permission: 'erp:order:query',
334
376
  inputSchema: {
335
- styleId: idField.describe('款式 ID,必填字符串(主款或轮次克隆)')
377
+ styleId: optionalId.describe('款式 ID(主款或轮次克隆)'),
378
+ orderId: optionalId.describe('订单 ID;与 styleNo 联用解析 styleId'),
379
+ styleNo: z.string().optional().describe('款号;与 orderId 联用')
336
380
  },
337
- handler: (client, args) => client.get('/erp/order/style/size-chart/logs', { styleId: asId(args.styleId) })
381
+ handler: async (client, args) => {
382
+ let styleId = asId(args.styleId);
383
+ if (!styleId) {
384
+ const orderId = asId(args.orderId);
385
+ const styleNo = typeof args.styleNo === 'string' ? args.styleNo : undefined;
386
+ if (!orderId || !styleNo) {
387
+ throw new Error('需要 styleId,或 orderId+styleNo');
388
+ }
389
+ const styles = (await client.get('/erp/order/style/list', { orderId }));
390
+ const hit = styles.find((s) => s.styleNo === styleNo) ||
391
+ styles.flatMap((s) => s.roundStyles || []).find((s) => s.styleNo === styleNo);
392
+ styleId = asId(hit?.id);
393
+ if (!styleId) {
394
+ throw new Error(`未找到款式:orderId=${orderId} styleNo=${styleNo}`);
395
+ }
396
+ }
397
+ return client.get('/erp/order/style/size-chart/logs', { styleId });
398
+ }
338
399
  },
339
400
  {
340
401
  name: 'erp_order_style_size_chart_export',
@@ -391,21 +452,32 @@ const READ_TOOLS = [
391
452
  },
392
453
  {
393
454
  name: 'erp_order_style_color_card_export',
394
- description: '按 styleId 导出色卡 Excel 到本机并返回落盘路径;默认 preview 不写附件,archive=true 才归档到订单附件',
455
+ description: '单款导出色卡 Excel(与前端一致,勿传多款 styleIds);默认 preview,archive=true 才归档;fileName 只取 basename',
395
456
  permission: 'erp:order:query',
396
457
  inputSchema: {
397
- styleId: idField.describe('款式 ID,字符串'),
398
- fileName: z.string().optional().describe('落盘文件名,默认 color-card-<styleId>.xlsx'),
458
+ styleId: idField.describe('款式 ID,字符串;仅单款'),
459
+ fileName: z.string().optional().describe('落盘文件名(传路径也只取 basename)'),
399
460
  archive: z.boolean().optional().describe('true 时写入订单附件,需 erp:order:export 权限')
400
461
  },
401
- handler: (client, args) => client.download('POST', args.archive ? '/erp/order/style/color-card/export' : '/erp/order/style/color-card/preview', args.fileName ?? `color-card-${asId(args.styleId)}.xlsx`, { body: { styleIds: [asId(args.styleId)] } })
462
+ handler: (client, args) => {
463
+ if (args.styleIds != null && args.styleIds !== '') {
464
+ throw new Error('色卡导出与前端一致仅支持单款,请传 styleId');
465
+ }
466
+ const styleId = asId(args.styleId);
467
+ if (!styleId) {
468
+ throw new Error('需要 styleId');
469
+ }
470
+ return client.download('POST', args.archive ? '/erp/order/style/color-card/export' : '/erp/order/style/color-card/preview', args.fileName ?? `color-card-${styleId}.xlsx`, { body: { styleIds: [styleId] } });
471
+ }
402
472
  },
403
473
  {
404
474
  name: 'erp_production_craft_schema',
405
- description: '生产单工艺 schema + selectionGuide(同订单)',
475
+ description: '生产单工艺 schema + selectionGuide(同订单);compact=true 省略 selectionGuide',
406
476
  permission: 'erp:production:query',
407
- inputSchema: {},
408
- handler: async (client) => wrapCraftSchema(await client.get('/erp/production/craft/schema'))
477
+ inputSchema: {
478
+ compact: z.boolean().optional().describe('true 时省略 selectionGuide')
479
+ },
480
+ handler: async (client, args) => wrapCraftSchema(await client.get('/erp/production/craft/schema'), args.compact === true)
409
481
  },
410
482
  {
411
483
  name: 'erp_production_craft_list',
@@ -562,12 +634,19 @@ const READ_TOOLS = [
562
634
  },
563
635
  {
564
636
  name: 'erp_material_bound_orders',
565
- description: '查询引用该物料的订单(含款号/绑定来源),删除或停用物料前先查',
637
+ description: '查询引用该物料的订单(含款号/绑定来源),删除或停用物料前先查;认 materialId 或 id',
566
638
  permission: 'erp:material:query',
567
639
  inputSchema: {
568
- id: idField.describe('物料 ID,字符串')
640
+ materialId: optionalId.describe('物料 ID,字符串(推荐)'),
641
+ id: optionalId.describe('同 materialId,兼容旧参')
569
642
  },
570
- handler: (client, args) => client.get(`/erp/material/${asId(args.id)}/boundOrders`)
643
+ handler: (client, args) => {
644
+ const materialId = asId(args.materialId) ?? asId(args.id);
645
+ if (!materialId) {
646
+ throw new Error('bound_orders 请传 materialId(物料 ID)');
647
+ }
648
+ return client.get(`/erp/material/${materialId}/boundOrders`);
649
+ }
571
650
  },
572
651
  {
573
652
  name: 'erp_material_supplier_list',
@@ -612,6 +691,26 @@ const READ_TOOLS = [
612
691
  },
613
692
  handler: (client, args) => client.get(`/erp/material/price/snapshot/${asId(args.id)}`)
614
693
  },
694
+ {
695
+ name: 'erp_material_price_snapshot_export',
696
+ description: '导出材料比价快照 Excel(与前端材料比价列表导出一致);可按品名/编码/品类/口径筛选',
697
+ permission: 'erp:material:price:export',
698
+ inputSchema: {
699
+ productName: z.string().optional().describe('品名'),
700
+ materialCode: z.string().optional().describe('物料编码'),
701
+ categoryName: z.string().optional().describe('品类'),
702
+ domain: z.string().optional().describe('比价口径 domestic/export'),
703
+ fileName: z.string().optional().describe('落盘文件名,默认 price-snapshot.xlsx')
704
+ },
705
+ handler: (client, args) => client.download('POST', '/erp/material/price/snapshot/export', args.fileName ?? 'price-snapshot.xlsx', {
706
+ params: {
707
+ productName: args.productName,
708
+ materialCode: args.materialCode,
709
+ categoryName: args.categoryName,
710
+ domain: args.domain
711
+ }
712
+ })
713
+ },
615
714
  {
616
715
  name: 'erp_material_price_quote_list',
617
716
  description: '分页查询比价明细列表',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yindie-erp-mcp",
3
- "version": "2.0.6",
3
+ "version": "2.0.7",
4
4
  "description": "银蝶 ERP 独立 MCP 服务,通过 RuoYi API 提供查询与写操作能力",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",