yindie-erp-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/auth.js ADDED
@@ -0,0 +1,80 @@
1
+ import { createCipheriv, createDecipheriv, createPrivateKey, createPublicKey, privateDecrypt, publicEncrypt, randomBytes } from 'node:crypto';
2
+ /** 默认 RSA 公钥(与 yindie-web 开发环境一致) */
3
+ const DEFAULT_RSA_PUBLIC_KEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvEDuRIOM3oZPWj9Ukoc5pQklR4PFH6/clnjeFqjDLIgDyQvjxhgqAZQA+E9eD6qu6FsXPmK8djcL+nh3cFHz4pX473jDvO3Sve+8yL3VRQ0n2pRgQ2a01MJsy+WwTZCBYWf0VnLRIvANUoWQgy9vz94q7Va44dg7A1/3ICf+xAwIDAQAB';
4
+ /** 默认 RSA 私钥(解密登录响应) */
5
+ const DEFAULT_RSA_PRIVATE_KEY = 'MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAObq7yrxfvyieZtTjAYyrdvi59tYTXxjO5ajmPCRSXBY9M9wQ1tli297JN6mnY53UJMNyOFNSZVi8WSFoIXjpR87FmvChJlzeN/dZdd3SEs48Ee66XKeSePYqxa8oO5GKDsnajgpsOHKXSeeVSIysiIPS2/WsEqk0In9P4w3RsRFAgMBAAECgYBiMEWwce24SPICnRzuScBpvmsudrbEDIH7BOd0a6LZlcnLJwZNJ7mJlshPsHNQb+WgEf135+BBGEhioPtn0yuTdEuKP4kB9UdYUKiayWCoWhJpesv7sAD4RDClV7dhuV+gcd1AXD+YzyRIPbGm0VC2U+4q8/+UPRpVjqskbLVTgQJBAPRpou7g3S8n4XB527kq0D8I3+ZYwMxZhszwhrCDpJU319+ucmpLVwYIzDmZVeID2QQdUaDfIEViFHu95xDrGiUCQQDx3YOKn3yaEctk/ERVn7hDAyAXUbd8/pv2b24/M/l1ZevlsFem8U4Jk5Mu64t3z3YGJoymEjQmbucwT01iKhehAkEAxlnccsRmfFh/KkqauKE4M4++NTAd9zlInpUsmZ+cN8UEGnF2RTEzRKBrLOt1uWCqBB7PGiE6DVTVjr7FAQPrSQJAT5yeY87DcONSk9cFlzmPqV8p/QME5rvYEnHzVBKDlkUKNPyqnWToTvaoh9U4fyNmsfeWbEOprszqhFhWHG3GgQJAK8+ynmyFhaw63+Hx2KU5zR4hVuQso2IzrEurGxCxybV6mR7VBerb4502+EPx3PmOgxQL+niUFhcMWxcvBFP9+A==';
6
+ /** 生成 32 字节 AES 密钥 */
7
+ function generateAesKey() {
8
+ return Buffer.from(randomBytes(16).toString('hex').slice(0, 32), 'utf8');
9
+ }
10
+ /** PEM 包装 */
11
+ function toPublicPem(key) {
12
+ return `-----BEGIN PUBLIC KEY-----\n${key}\n-----END PUBLIC KEY-----`;
13
+ }
14
+ /** PEM 包装 */
15
+ function toPrivatePem(key) {
16
+ return `-----BEGIN PRIVATE KEY-----\n${key}\n-----END PRIVATE KEY-----`;
17
+ }
18
+ /** AES/ECB 加密 */
19
+ function encryptWithAes(message, aesKey) {
20
+ const cipher = createCipheriv('aes-256-ecb', aesKey, null);
21
+ return Buffer.concat([cipher.update(message, 'utf8'), cipher.final()]).toString('base64');
22
+ }
23
+ /** AES/ECB 解密 */
24
+ function decryptWithAes(message, aesKey) {
25
+ const decipher = createDecipheriv('aes-256-ecb', aesKey, null);
26
+ return Buffer.concat([decipher.update(message, 'base64'), decipher.final()]).toString('utf8');
27
+ }
28
+ /** RSA 公钥加密 */
29
+ function rsaEncrypt(text, publicKey) {
30
+ const key = createPublicKey(toPublicPem(publicKey));
31
+ return publicEncrypt({ key, padding: 1 }, Buffer.from(text, 'utf8')).toString('base64');
32
+ }
33
+ /** RSA 私钥解密 */
34
+ function rsaDecrypt(cipherText, privateKey) {
35
+ const key = createPrivateKey(toPrivatePem(privateKey));
36
+ return privateDecrypt({ key, padding: 1 }, Buffer.from(cipherText, 'base64')).toString('utf8');
37
+ }
38
+ /** 解析可能被加密的 JSON 响应 */
39
+ function parseResponse(raw, headers, rsaPrivateKey) {
40
+ const encryptKey = headers.get('encrypt-key');
41
+ if (encryptKey) {
42
+ const aesKey = Buffer.from(rsaDecrypt(encryptKey, rsaPrivateKey), 'base64');
43
+ return JSON.parse(decryptWithAes(raw, aesKey));
44
+ }
45
+ return JSON.parse(raw);
46
+ }
47
+ /** 账号密码登录并返回 access_token */
48
+ export async function login(params) {
49
+ const aesKey = generateAesKey();
50
+ const body = JSON.stringify({
51
+ clientId: params.clientId,
52
+ grantType: 'password',
53
+ username: params.username,
54
+ password: params.password
55
+ });
56
+ const response = await fetch(new URL('/auth/login', params.baseUrl), {
57
+ method: 'POST',
58
+ headers: {
59
+ 'Content-Type': 'application/json',
60
+ 'encrypt-key': rsaEncrypt(Buffer.from(aesKey).toString('base64'), params.rsaPublicKey)
61
+ },
62
+ body: encryptWithAes(body, aesKey)
63
+ });
64
+ const result = parseResponse(await response.text(), response.headers, params.rsaPrivateKey);
65
+ if (result.code !== 200) {
66
+ throw new Error(result.msg || '登录失败');
67
+ }
68
+ if (!result.data?.access_token) {
69
+ throw new Error('登录失败:未返回 access_token');
70
+ }
71
+ return result.data.access_token;
72
+ }
73
+ /** 默认 RSA 公钥 */
74
+ export function defaultRsaPublicKey() {
75
+ return DEFAULT_RSA_PUBLIC_KEY;
76
+ }
77
+ /** 默认 RSA 私钥 */
78
+ export function defaultRsaPrivateKey() {
79
+ return DEFAULT_RSA_PRIVATE_KEY;
80
+ }
package/dist/client.js ADDED
@@ -0,0 +1,75 @@
1
+ /** ERP API 客户端 */
2
+ export class YindieClient {
3
+ /** 后端根地址 */
4
+ baseUrl;
5
+ /** 访问令牌 */
6
+ token;
7
+ /** 客户端 ID */
8
+ clientId;
9
+ /** token 过期时重新登录 */
10
+ relogin;
11
+ constructor(config) {
12
+ this.baseUrl = config.baseUrl;
13
+ this.token = config.token;
14
+ this.clientId = config.clientId;
15
+ this.relogin = config.relogin;
16
+ }
17
+ /** 拉取当前用户菜单权限 */
18
+ async loadPermissions() {
19
+ const info = await this.get('/system/user/getInfo');
20
+ return info.permissions ?? [];
21
+ }
22
+ /** GET 请求 */
23
+ async get(path, params) {
24
+ return (await this.request('GET', path, undefined, params));
25
+ }
26
+ /** POST 请求 */
27
+ async post(path, body) {
28
+ return this.request('POST', path, body);
29
+ }
30
+ /** PUT 请求 */
31
+ async put(path, body) {
32
+ return this.request('PUT', path, body);
33
+ }
34
+ /** DELETE 请求 */
35
+ async delete(path) {
36
+ return this.request('DELETE', path);
37
+ }
38
+ /** 统一 HTTP 请求 */
39
+ async request(method, path, body, params, retried = false) {
40
+ const url = new URL(path, this.baseUrl);
41
+ if (params) {
42
+ for (const [key, value] of Object.entries(params)) {
43
+ if (value !== undefined && value !== '') {
44
+ url.searchParams.set(key, String(value));
45
+ }
46
+ }
47
+ }
48
+ const headers = {
49
+ Authorization: `Bearer ${this.token}`,
50
+ clientid: this.clientId
51
+ };
52
+ const init = { method, headers };
53
+ if (body !== undefined && method !== 'GET') {
54
+ headers['Content-Type'] = 'application/json';
55
+ init.body = JSON.stringify(body);
56
+ }
57
+ const response = await fetch(url, init);
58
+ const result = (await response.json());
59
+ if (result.code === 401 && !retried) {
60
+ this.token = await this.relogin();
61
+ return this.request(method, path, body, params, true);
62
+ }
63
+ if (result.code !== 200) {
64
+ throw new Error(result.msg || `请求失败: ${path}`);
65
+ }
66
+ if (method === 'GET') {
67
+ return result.data;
68
+ }
69
+ return {
70
+ success: true,
71
+ msg: result.msg || '操作成功',
72
+ data: result.data
73
+ };
74
+ }
75
+ }
package/dist/config.js ADDED
@@ -0,0 +1,30 @@
1
+ import { defaultRsaPrivateKey, defaultRsaPublicKey, login } from './auth.js';
2
+ /** 从环境变量读取配置并自动登录 */
3
+ export async function loadConfig() {
4
+ const baseUrl = process.env.YINDIE_BASE_URL;
5
+ const username = process.env.YINDIE_USERNAME;
6
+ const password = process.env.YINDIE_PASSWORD;
7
+ const clientId = 'e5cd7e4891bf95d1d19206ce24a7b32e';
8
+ if (!baseUrl) {
9
+ throw new Error('缺少环境变量 YINDIE_BASE_URL');
10
+ }
11
+ if (!username || !password) {
12
+ throw new Error('缺少环境变量 YINDIE_USERNAME 或 YINDIE_PASSWORD');
13
+ }
14
+ const normalizedBaseUrl = baseUrl.replace(/\/$/, '');
15
+ const loginParams = {
16
+ baseUrl: normalizedBaseUrl,
17
+ username,
18
+ password,
19
+ clientId,
20
+ rsaPublicKey: process.env.YINDIE_RSA_PUBLIC_KEY ?? defaultRsaPublicKey(),
21
+ rsaPrivateKey: process.env.YINDIE_RSA_PRIVATE_KEY ?? defaultRsaPrivateKey()
22
+ };
23
+ const accessToken = await login(loginParams);
24
+ return {
25
+ baseUrl: normalizedBaseUrl,
26
+ token: accessToken,
27
+ clientId,
28
+ relogin: () => login(loginParams)
29
+ };
30
+ }
package/dist/index.js ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { YindieClient } from './client.js';
5
+ import { loadConfig } from './config.js';
6
+ import { ERP_BUSINESS_FLOW } from './tool-meta.js';
7
+ import { filterTools } from './tools.js';
8
+ /** 启动 ERP MCP 服务 */
9
+ async function main() {
10
+ const config = await loadConfig();
11
+ const client = new YindieClient(config);
12
+ const permissions = await client.loadPermissions();
13
+ const tools = filterTools(permissions);
14
+ const server = new McpServer({
15
+ name: 'yindie-erp',
16
+ version: '1.0.0'
17
+ });
18
+ for (const tool of tools) {
19
+ server.registerTool(tool.name, {
20
+ description: tool.description,
21
+ inputSchema: tool.inputSchema
22
+ }, async (args) => {
23
+ const data = await tool.handler(client, args ?? {});
24
+ return {
25
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
26
+ };
27
+ });
28
+ }
29
+ server.registerResource('erp_business_flow', 'yindie://erp/business-flow', {
30
+ description: 'ERP 主业务流程、订单/生产单状态机、枚举值与业务约束,操作前建议先读',
31
+ mimeType: 'application/json'
32
+ }, async () => ({
33
+ contents: [
34
+ {
35
+ uri: 'yindie://erp/business-flow',
36
+ mimeType: 'application/json',
37
+ text: JSON.stringify(ERP_BUSINESS_FLOW, null, 2)
38
+ }
39
+ ]
40
+ }));
41
+ server.registerResource('erp_modules', 'yindie://erp/modules', {
42
+ description: '当前账号可见的 ERP MCP 工具列表及权限',
43
+ mimeType: 'application/json'
44
+ }, async () => ({
45
+ contents: [
46
+ {
47
+ uri: 'yindie://erp/modules',
48
+ mimeType: 'application/json',
49
+ text: JSON.stringify({
50
+ toolCount: tools.length,
51
+ tools: tools.map((item) => ({
52
+ name: item.name,
53
+ description: item.description,
54
+ permission: item.permission
55
+ }))
56
+ }, null, 2)
57
+ }
58
+ ]
59
+ }));
60
+ const transport = new StdioServerTransport();
61
+ await server.connect(transport);
62
+ }
63
+ main().catch((error) => {
64
+ console.error('[yindie-erp-mcp]', error instanceof Error ? error.message : error);
65
+ process.exit(1);
66
+ });
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ /** JSON 请求体字段 */
3
+ export const payloadField = z.string().describe('JSON 请求体,字段与后端 BO 一致');
4
+ /** 主键字段 */
5
+ export const idsField = z.string().describe('主键 ID,多个用英文逗号分隔');
6
+ /** 单个主键字段,字符串避免雪花 ID 精度丢失 */
7
+ export const idField = z.string().describe('主键 ID');
8
+ /** 解析 JSON 请求体 */
9
+ export function parsePayload(args) {
10
+ const raw = args.payload;
11
+ if (typeof raw === 'string') {
12
+ return JSON.parse(raw);
13
+ }
14
+ if (raw && typeof raw === 'object') {
15
+ return raw;
16
+ }
17
+ throw new Error('payload 必须是 JSON 字符串');
18
+ }
19
+ /** 解析删除主键 */
20
+ export function parseIds(args) {
21
+ if (typeof args.ids === 'string') {
22
+ return args.ids;
23
+ }
24
+ if (typeof args.id === 'number' || typeof args.id === 'string') {
25
+ return String(args.id);
26
+ }
27
+ throw new Error('缺少 ids 或 id');
28
+ }
@@ -0,0 +1,428 @@
1
+ import { z } from 'zod';
2
+ /** 拼接工具描述 */
3
+ function joinParts(parts) {
4
+ return parts.filter(Boolean).join(' ');
5
+ }
6
+ /** 生成 CRUD 工具描述 */
7
+ export function buildCrudDescription(action, meta) {
8
+ const verb = action === 'add' ? '新增' : action === 'edit' ? '修改' : '删除';
9
+ if (action === 'remove') {
10
+ return joinParts([
11
+ `【${meta.module}】${verb}${meta.label}。`,
12
+ meta.prerequisite ? `【前置】${meta.prerequisite}` : undefined,
13
+ meta.removeConstraint ? `【约束】${meta.removeConstraint}` : undefined
14
+ ]);
15
+ }
16
+ const fields = action === 'add' ? meta.addRequired : meta.editRequired ?? meta.addRequired;
17
+ const optional = action === 'add' ? meta.addOptional : undefined;
18
+ return joinParts([
19
+ `【${meta.module}】${verb}${meta.label},${meta.step}。`,
20
+ meta.prerequisite ? `【前置】${meta.prerequisite}` : undefined,
21
+ `【必填】${fields}。`,
22
+ optional ? `【可选】${optional}。` : undefined,
23
+ meta.enums ? `【枚举】${meta.enums}` : undefined,
24
+ meta.next ? `【后续】${meta.next}` : undefined
25
+ ]);
26
+ }
27
+ /** 生成 payload 字段描述 */
28
+ export function payloadFor(meta, action) {
29
+ const fields = action === 'add' ? meta.addRequired : meta.editRequired ?? meta.addRequired;
30
+ const optional = action === 'add' ? meta.addOptional : undefined;
31
+ const enumPart = meta.enums ? ` ${meta.enums}` : '';
32
+ const optionalPart = optional ? ` 可选:${optional}` : '';
33
+ return z
34
+ .string()
35
+ .describe(`JSON 对象。必填:${fields}${optionalPart}.${enumPart}`);
36
+ }
37
+ /** 生成流程工具描述 */
38
+ export function buildWorkflowDescription(meta) {
39
+ return joinParts([
40
+ `【${meta.module}】${meta.summary},${meta.step}。`,
41
+ `【前置】${meta.prerequisite}`,
42
+ `【入参】${meta.payload}`,
43
+ meta.enums ? `【枚举】${meta.enums}` : undefined,
44
+ meta.constraint ? `【约束】${meta.constraint}` : undefined,
45
+ meta.returns ? `【返回】${meta.returns}` : undefined,
46
+ meta.next ? `【后续】${meta.next}` : undefined
47
+ ]);
48
+ }
49
+ /** 生成查询工具描述 */
50
+ export function buildReadDescription(base, meta) {
51
+ if (!meta) {
52
+ return base;
53
+ }
54
+ return joinParts([base, meta.usage, meta.returns ? `返回:${meta.returns}` : undefined]);
55
+ }
56
+ /** 实体元数据表 */
57
+ export const ENTITY_META = {
58
+ erp_customer: {
59
+ module: '客户管理',
60
+ step: '业务流程第1步:建档',
61
+ label: '客户',
62
+ addRequired: 'customerCode, customerName, customerType',
63
+ addOptional: 'country, defaultCurrency, communicationChannel, email, phone, assigneeId, status, remark',
64
+ editRequired: 'id, customerCode, customerName, customerType',
65
+ enums: 'customerType:direct/trader/foreign_trade; status:active/inactive(默认active)',
66
+ removeConstraint: '客户存在关联PI时不可删除',
67
+ next: 'erp_customer_contact_add → erp_customer_address_add → erp_pi_add'
68
+ },
69
+ erp_customer_contact: {
70
+ module: '客户管理',
71
+ step: '客户建档后补充联系人',
72
+ label: '客户联系人',
73
+ prerequisite: '客户已创建',
74
+ addRequired: 'customerId, contactName',
75
+ addOptional: 'position, email, phone, whatsapp, isPrimary(Y/N), remark',
76
+ editRequired: 'id, customerId, contactName'
77
+ },
78
+ erp_customer_address: {
79
+ module: '客户管理',
80
+ step: '客户建档后补充地址',
81
+ label: '客户地址',
82
+ prerequisite: '客户已创建',
83
+ addRequired: 'customerId, addressType, detailAddress',
84
+ addOptional: 'recipient, company, country, province, city, postalCode, phone, isDefault(Y/N), remark',
85
+ editRequired: 'id, customerId, addressType, detailAddress',
86
+ enums: 'addressType:shipping/billing/sample'
87
+ },
88
+ erp_customer_brand: {
89
+ module: '客户管理',
90
+ step: '客户建档后维护品牌',
91
+ label: '客户品牌',
92
+ prerequisite: '客户已创建',
93
+ addRequired: 'customerId, brandName',
94
+ addOptional: 'brandCode, status, remark',
95
+ editRequired: 'id, customerId, brandName'
96
+ },
97
+ erp_pi: {
98
+ module: 'PI管理',
99
+ step: '业务流程第2步:形式发票',
100
+ label: 'PI',
101
+ prerequisite: '客户已创建',
102
+ addRequired: 'customerId, piNo, sourceType',
103
+ addOptional: 'orderId, piVersion, paymentTerms, totalAmount, currency, qtyTolerance, shippingMethod, shippingAddress, piFileOssId, status, confirmedDate, remark',
104
+ editRequired: 'id, customerId, piNo, sourceType',
105
+ enums: 'sourceType:customer/internal; status:draft/confirmed/superseded(默认draft)',
106
+ next: 'erp_order_add → erp_order_link_pi'
107
+ },
108
+ erp_order: {
109
+ module: '订单管理',
110
+ step: '业务流程第3步:创建订单',
111
+ label: '订单',
112
+ prerequisite: '客户已创建',
113
+ addRequired: 'orderNo, customerId, bizLine, assigneeId',
114
+ addOptional: 'brandId, orderStatus(默认inquiry), orderDate, deliveryDate, contractDate, depositReceived(Y/N), qtyTolerance, shippingMethod, packingType, priority, sourceChannel, parentOrderId, remark',
115
+ editRequired: 'id, orderNo, customerId, bizLine, assigneeId',
116
+ enums: 'bizLine:direct/trader; orderStatus见erp_business_flow资源',
117
+ next: 'erp_order_link_pi → erp_order_style_save → erp_order_change_status'
118
+ },
119
+ erp_task: {
120
+ module: '任务协同',
121
+ step: '订单跟进过程中创建待办',
122
+ label: '跟进任务',
123
+ addRequired: 'taskType, taskTitle, assigneeId',
124
+ addOptional: 'orderId, taskContent, priority, dueDate, remindDate, taskStatus, source, relatedId, relatedType, remark',
125
+ editRequired: 'id, taskType, taskTitle, assigneeId',
126
+ enums: 'taskType:accessory_follow/bulk_sample/pre_prod_feedback/fabric_arrival/customer_follow/address_verify/custom; taskStatus:pending/in_progress/completed/cancelled; priority:low/normal/high/urgent'
127
+ },
128
+ erp_material: {
129
+ module: '主数据',
130
+ step: '维护面料/辅料等物料档案',
131
+ label: '物料',
132
+ addRequired: 'categoryId, materialCode, materialName',
133
+ addOptional: 'spec, color, unit, defaultSupplierId, supplierMaterialCode, composition, width, weight, status, remark',
134
+ editRequired: 'id, categoryId, materialCode, materialName'
135
+ },
136
+ erp_supplier: {
137
+ module: '主数据',
138
+ step: '维护供应商档案',
139
+ label: '供应商',
140
+ addRequired: 'supplierCode, supplierName',
141
+ addOptional: 'supplierType, contactName, phone, wechat, address, leadTimeDays, status, remark',
142
+ editRequired: 'id, supplierCode, supplierName',
143
+ enums: 'supplierType:fabric/accessory/both'
144
+ },
145
+ erp_email_account: {
146
+ module: '邮件',
147
+ step: '配置收发邮件账号',
148
+ label: '邮箱账号',
149
+ addRequired: 'accountName, emailAddress, imapHost, imapPort, authType, credential',
150
+ addOptional: 'smtpHost, smtpPort, ownerId, syncEnabled(Y/N), status',
151
+ editRequired: 'id, accountName, emailAddress, imapHost, imapPort, authType, credential'
152
+ },
153
+ erp_production: {
154
+ module: '生产管理',
155
+ step: '手动新建生产单(通常用fromOrder)',
156
+ label: '生产单',
157
+ prerequisite: '订单已存在',
158
+ addRequired: 'orderId, poNo, bizLine',
159
+ addOptional: 'poVersion, poStatus(默认draft), deliveryDate, qtyTolerance, shippingMethod, shippingAddress, packingType, bulkSampleRequired(Y/N), remark',
160
+ editRequired: 'id, orderId, poNo, bizLine',
161
+ enums: 'poStatus:draft/reviewing/issued/superseded',
162
+ removeConstraint: '已下发(issued)不可删除;仅draft可修改',
163
+ next: 'erp_production_save_items → erp_production_check → erp_production_submit_review → erp_production_issue'
164
+ }
165
+ };
166
+ /** 流程工具元数据表 */
167
+ export const WORKFLOW_META = {
168
+ erp_order_change_status: {
169
+ module: '订单管理',
170
+ step: '推进订单生命周期',
171
+ summary: '变更订单状态',
172
+ prerequisite: '订单已创建',
173
+ payload: '{id, orderStatus, remark?}',
174
+ constraint: '流转: inquiry→sampling|doc_preparing; sampling→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→po_reviewing|po_drafting; po_reviewing→po_issued',
175
+ next: 'po_drafting后 erp_production_create_from_order'
176
+ },
177
+ erp_order_assign: {
178
+ module: '订单管理',
179
+ step: '订单分派跟单员',
180
+ summary: '分派或变更跟单员',
181
+ prerequisite: '订单已创建',
182
+ payload: '{id, assigneeId}',
183
+ next: 'erp_order_style_save'
184
+ },
185
+ erp_order_link_pi: {
186
+ module: '订单管理',
187
+ step: '订单关联PI',
188
+ summary: '将PI绑定到订单',
189
+ prerequisite: '订单和PI均已创建',
190
+ payload: '{orderId, piIds: number[]}',
191
+ next: 'erp_order_style_save'
192
+ },
193
+ erp_order_style_save: {
194
+ module: '订单管理',
195
+ step: '录入款式明细',
196
+ summary: '保存订单款式及颜色尺码',
197
+ prerequisite: '订单已创建',
198
+ payload: '{orderId, styles:[{styleNo, styleName?, colors:[{colorName, sizes:[{sizeName, qty}]}]}]}',
199
+ next: 'erp_order_style_craft_save'
200
+ },
201
+ erp_order_style_craft_save: {
202
+ module: '订单管理',
203
+ step: '录入结构化工艺',
204
+ summary: '保存款式上衣/裤子工艺及商标位置',
205
+ prerequisite: '款式已保存且有 styleId',
206
+ payload: '{styleId, crafts:[{garmentPart, craftModule, fieldKey, fieldValue}], labelPositions:[{garmentPart, labelType, positionSide?, sizeCm?, positionDesc?}]}',
207
+ enums: 'garmentPart:jacket/pants; fieldKey见 erp_order_craft_schema',
208
+ next: 'erp_order_change_status 或 erp_production_create_from_order'
209
+ },
210
+ erp_production_craft_save: {
211
+ module: '生产管理',
212
+ step: '填写结构化工艺',
213
+ summary: '生产单草稿阶段保存款式工艺',
214
+ prerequisite: '生产单 poStatus=draft,款式已存在',
215
+ payload: '同 erp_order_style_craft_save',
216
+ next: 'erp_production_check'
217
+ },
218
+ erp_order_attachment_add: {
219
+ module: '订单管理',
220
+ step: '上传订单附件',
221
+ summary: '新增订单附件',
222
+ prerequisite: '订单已创建,文件已上传OSS',
223
+ payload: '{orderId, attachType, fileName, ossId, fileSize?, source?, remark?}',
224
+ enums: 'attachType:pi/tech_drawing/customer_file/other'
225
+ },
226
+ erp_order_attachment_remove: {
227
+ module: '订单管理',
228
+ step: '删除订单附件',
229
+ summary: '删除订单附件',
230
+ prerequisite: '附件已存在',
231
+ payload: 'ids: 附件ID,多个逗号分隔'
232
+ },
233
+ erp_production_create_from_order: {
234
+ module: '生产管理',
235
+ step: '从订单生成生产单草稿',
236
+ summary: '一键创建生产单',
237
+ prerequisite: '订单已存在且尚无生产单',
238
+ payload: 'orderId: 订单ID',
239
+ constraint: '自动将订单状态改为po_drafting;生产单号=订单号-PO;每单仅一条',
240
+ next: 'erp_production_save_items → erp_production_check'
241
+ },
242
+ erp_production_save_items: {
243
+ module: '生产管理',
244
+ step: '填写生产单明细',
245
+ summary: '保存生产单物料/工艺明细',
246
+ prerequisite: '生产单处于draft',
247
+ payload: '{productionOrderId, items:[{itemCategory, itemName, materialId?, spec?, color?, qty?, supplierId?, isRequired?}]}',
248
+ enums: 'itemCategory:button/label/hangtag/zipper/thread/process/other',
249
+ next: 'erp_production_check'
250
+ },
251
+ erp_production_check: {
252
+ module: '生产管理',
253
+ step: '人工校验缺项',
254
+ summary: '执行生产单完整性校验',
255
+ prerequisite: '生产单已存在',
256
+ payload: 'id: 生产单ID',
257
+ returns: '校验分数、缺项列表(checkResult: pass/warning/fail)',
258
+ next: 'erp_production_submit_review'
259
+ },
260
+ erp_production_submit_review: {
261
+ module: '生产管理',
262
+ step: '提交审核',
263
+ summary: '生产单提交审核',
264
+ prerequisite: '生产单poStatus=draft',
265
+ payload: 'id: 生产单ID',
266
+ constraint: '提交后poStatus→reviewing',
267
+ next: 'erp_production_issue 或 erp_production_reject_review'
268
+ },
269
+ erp_production_reject_review: {
270
+ module: '生产管理',
271
+ step: '审核退回',
272
+ summary: '生产单审核退回草稿',
273
+ prerequisite: '生产单poStatus=reviewing,需erp:production:review权限',
274
+ payload: 'id: 生产单ID',
275
+ constraint: '退回后poStatus→draft',
276
+ next: 'erp_production_save_items'
277
+ },
278
+ erp_production_issue: {
279
+ module: '生产管理',
280
+ step: '下发工厂',
281
+ summary: '生产单正式下发',
282
+ prerequisite: '生产单poStatus=reviewing,需erp:production:issue权限',
283
+ payload: 'id: 生产单ID',
284
+ constraint: '下发后poStatus→issued,关联订单状态→po_issued',
285
+ next: '流程结束'
286
+ },
287
+ erp_email_thread_link: {
288
+ module: '邮件',
289
+ step: '邮件关联业务单据',
290
+ summary: '邮件线程关联订单或客户',
291
+ prerequisite: '邮件线程已同步',
292
+ payload: '{id, orderId?, customerId?}',
293
+ next: 'erp_order_timeline_list 查看跟进'
294
+ }
295
+ };
296
+ /** 查询工具补充说明 */
297
+ export const READ_META = {
298
+ erp_workbench_summary: {
299
+ usage: '【入口】建议首先调用,获取待办任务、超期任务、缺项生产单、近期订单概览'
300
+ },
301
+ erp_customer_list: {
302
+ usage: '筛选客户类型 direct/trader/foreign_trade,状态 active/inactive'
303
+ },
304
+ erp_customer_get: {
305
+ usage: '查客户主档详情',
306
+ returns: '客户基本信息'
307
+ },
308
+ erp_customer_contact_list: { usage: '查某客户下全部联系人' },
309
+ erp_customer_address_list: { usage: '查某客户地址,可按 shipping/billing/sample 筛选' },
310
+ erp_customer_brand_list: { usage: '查某客户品牌列表,建订单时 brandId 来源' },
311
+ erp_pi_list: { usage: 'PI状态 draft/confirmed/superseded,可按 customerId 筛选' },
312
+ erp_pi_get: { usage: '查PI详情含金额、运输、发货地址' },
313
+ erp_order_list: {
314
+ usage: '订单状态见 erp_business_flow;可按 orderStatus/assigneeId/customerId 筛选'
315
+ },
316
+ erp_order_get: { usage: '查订单主档', returns: '订单基本信息' },
317
+ erp_order_style_list: { usage: '查订单款式→颜色→尺码→工艺→商标位置,制单前必查' },
318
+ erp_order_craft_schema: { usage: '查工艺字段定义及受控字典类型,填工艺前建议先读' },
319
+ erp_production_craft_schema: { usage: '查生产单工艺字段定义,同订单 schema' },
320
+ erp_order_timeline_list: { usage: '查订单状态变更、生产事件等跟进时间线' },
321
+ erp_order_attachment_list: { usage: '查订单附件列表' },
322
+ erp_production_list: { usage: '生产单状态 draft/reviewing/issued/superseded' },
323
+ erp_production_get: { usage: '查生产单主档及缺项信息' },
324
+ erp_production_check_list: { usage: '查生产单历次校验记录及得分' },
325
+ erp_task_list: { usage: '任务状态 pending/in_progress/completed/cancelled' },
326
+ erp_task_get: { usage: '查任务详情' },
327
+ erp_material_category_list: { usage: '查物料分类树,新建物料前先查 categoryId' },
328
+ erp_material_list: { usage: '分页查物料,可按分类/编码/名称筛选' },
329
+ erp_material_get: { usage: '查物料详情' },
330
+ erp_supplier_list: { usage: '供应商类型 fabric/accessory/both' },
331
+ erp_supplier_get: { usage: '查供应商详情' },
332
+ erp_email_account_list: { usage: '查已配置邮箱账号' },
333
+ erp_email_account_get: { usage: '查邮箱账号详情' },
334
+ erp_email_thread_list: { usage: '查邮件线程,可按 orderId 筛选已关联邮件' },
335
+ erp_email_thread_get: { usage: '查邮件线程详情含正文', returns: '邮件正文及关联信息' }
336
+ };
337
+ /** ERP 业务流程文档 */
338
+ export const ERP_BUSINESS_FLOW = {
339
+ overview: '外贸服装ERP:客户建档 → PI形式发票 → 订单(款式/附件/跟单) → 生产单(校验/审核/下发)。物料/供应商为主数据,邮件/任务为协同手段。',
340
+ mainFlow: [
341
+ '1. erp_customer_add 建客户',
342
+ '2. erp_customer_contact_add / erp_customer_address_add / erp_customer_brand_add 补全资料',
343
+ '3. erp_pi_add 创建PI(需customerId, piNo, sourceType)',
344
+ '4. erp_order_add 创建订单(需orderNo, customerId, bizLine, assigneeId,默认inquiry)',
345
+ '5. erp_order_link_pi 关联PI',
346
+ '6. erp_order_style_save 录入款式颜色尺码',
347
+ '7. erp_order_change_status 按规则推进状态',
348
+ '8. erp_order_style_craft_save 录入上衣/裤子结构化工艺',
349
+ '9. erp_production_create_from_order 生成生产单草稿',
350
+ '9. erp_production_save_items 填写明细',
351
+ '10. erp_production_check 人工校验',
352
+ '11. erp_production_submit_review 提交审核',
353
+ '12. erp_production_issue 下发(订单→po_issued)'
354
+ ],
355
+ orderStatus: {
356
+ default: 'inquiry',
357
+ transitions: {
358
+ inquiry: ['sampling', 'doc_preparing'],
359
+ sampling: ['sample_confirmed'],
360
+ sample_confirmed: ['quoted'],
361
+ quoted: ['po_drafting'],
362
+ doc_preparing: ['pre_prod_sample'],
363
+ pre_prod_sample: ['pre_prod_confirmed'],
364
+ pre_prod_confirmed: ['po_drafting'],
365
+ po_drafting: ['po_ai_checking'],
366
+ po_ai_checking: ['po_reviewing', 'po_drafting'],
367
+ po_reviewing: ['po_issued'],
368
+ po_issued: []
369
+ },
370
+ labels: {
371
+ inquiry: '询盘沟通',
372
+ sampling: '打样中',
373
+ sample_confirmed: '样衣确认',
374
+ quoted: '报价签约',
375
+ doc_preparing: '资料整理',
376
+ pre_prod_sample: '产前样',
377
+ pre_prod_confirmed: '产前样确认',
378
+ po_drafting: '制单中',
379
+ po_ai_checking: 'AI校验',
380
+ po_reviewing: '人工审核',
381
+ po_issued: '已下发'
382
+ }
383
+ },
384
+ productionStatus: {
385
+ default: 'draft',
386
+ transitions: {
387
+ draft: ['reviewing'],
388
+ reviewing: ['issued', 'draft'],
389
+ issued: [],
390
+ superseded: []
391
+ },
392
+ labels: {
393
+ draft: '草稿',
394
+ reviewing: '待审核',
395
+ issued: '已下发',
396
+ superseded: '已作废'
397
+ }
398
+ },
399
+ enums: {
400
+ customerType: { direct: '自营', trader: '贸易商', foreign_trade: '外贸公司' },
401
+ customerStatus: { active: '正常', inactive: '停用' },
402
+ piSourceType: { customer: '客户提供', internal: '我方生成' },
403
+ piStatus: { draft: '草稿', confirmed: '已确认', superseded: '已作废' },
404
+ bizLine: { direct: '自营', trader: '贸易商' },
405
+ addressType: { shipping: '收货地址', billing: '账单地址', sample: '样衣寄送' },
406
+ attachType: { pi: 'PI文件', tech_drawing: '工艺图', customer_file: '客户文件', other: '其他' },
407
+ poItemCategory: {
408
+ button: '纽扣',
409
+ label: '商标',
410
+ hangtag: '吊牌',
411
+ zipper: '拉链',
412
+ thread: '线',
413
+ process: '工艺',
414
+ other: '其他'
415
+ }
416
+ },
417
+ constraints: [
418
+ '客户存在关联PI时不可删除',
419
+ '每订单仅一条生产单(erp_production_create_from_order)',
420
+ '生产单仅draft可修改/编辑明细',
421
+ '生产单submit_review: draft→reviewing',
422
+ '生产单reject_review: reviewing→draft(需review权限)',
423
+ '生产单issue: reviewing→issued(需issue权限),同步订单→po_issued',
424
+ '已下发(issued)生产单不可删除'
425
+ ],
426
+ entryPoint: 'erp_workbench_summary',
427
+ resourceHint: '操作前先读 yindie://erp/business-flow 和 yindie://erp/modules'
428
+ };
@@ -0,0 +1,69 @@
1
+ import { z } from 'zod';
2
+ import { idField, idsField, parseIds, parsePayload } from './tool-helpers.js';
3
+ import { buildCrudDescription, buildWorkflowDescription, ENTITY_META, payloadFor, WORKFLOW_META } from './tool-meta.js';
4
+ /** 生成标准 CRUD 写工具 */
5
+ function entityCrud(name, basePath, perm) {
6
+ const meta = ENTITY_META[name];
7
+ return [
8
+ {
9
+ name: `${name}_add`,
10
+ description: buildCrudDescription('add', meta),
11
+ permission: `${perm}:add`,
12
+ inputSchema: { payload: payloadFor(meta, 'add') },
13
+ handler: (client, args) => client.post(basePath, parsePayload(args))
14
+ },
15
+ {
16
+ name: `${name}_edit`,
17
+ description: buildCrudDescription('edit', meta),
18
+ permission: `${perm}:edit`,
19
+ inputSchema: { payload: payloadFor(meta, 'edit') },
20
+ handler: (client, args) => client.put(basePath, parsePayload(args))
21
+ },
22
+ {
23
+ name: `${name}_remove`,
24
+ description: buildCrudDescription('remove', meta),
25
+ permission: `${perm}:remove`,
26
+ inputSchema: { ids: idsField },
27
+ handler: (client, args) => client.delete(`${basePath}/${parseIds(args)}`)
28
+ }
29
+ ];
30
+ }
31
+ /** 生成流程写工具 */
32
+ function workflowTool(name, permission, inputSchema, handler) {
33
+ return {
34
+ name,
35
+ description: buildWorkflowDescription(WORKFLOW_META[name]),
36
+ permission,
37
+ inputSchema,
38
+ handler
39
+ };
40
+ }
41
+ /** ERP 写操作工具 */
42
+ export const WRITE_TOOLS = [
43
+ ...entityCrud('erp_customer', '/erp/customer', 'erp:customer'),
44
+ ...entityCrud('erp_customer_contact', '/erp/customer/contact', 'erp:customer'),
45
+ ...entityCrud('erp_customer_address', '/erp/customer/address', 'erp:customer'),
46
+ ...entityCrud('erp_customer_brand', '/erp/customer/brand', 'erp:customer'),
47
+ ...entityCrud('erp_pi', '/erp/pi', 'erp:pi'),
48
+ ...entityCrud('erp_order', '/erp/order', 'erp:order'),
49
+ ...entityCrud('erp_task', '/erp/task', 'erp:task'),
50
+ ...entityCrud('erp_material', '/erp/material', 'erp:material'),
51
+ ...entityCrud('erp_supplier', '/erp/supplier', 'erp:supplier'),
52
+ ...entityCrud('erp_email_account', '/erp/email/account', 'erp:email:account'),
53
+ ...entityCrud('erp_production', '/erp/production', 'erp:production'),
54
+ workflowTool('erp_order_change_status', 'erp:order:edit', { payload: z.string().describe(WORKFLOW_META.erp_order_change_status.payload) }, (client, args) => client.put('/erp/order/status', parsePayload(args))),
55
+ workflowTool('erp_order_assign', 'erp:order:assign', { payload: z.string().describe(WORKFLOW_META.erp_order_assign.payload) }, (client, args) => client.put('/erp/order/assign', parsePayload(args))),
56
+ workflowTool('erp_order_link_pi', 'erp:order:edit', { payload: z.string().describe(WORKFLOW_META.erp_order_link_pi.payload) }, (client, args) => client.put('/erp/order/pi/link', parsePayload(args))),
57
+ workflowTool('erp_order_style_save', 'erp:order:edit', { payload: z.string().describe(WORKFLOW_META.erp_order_style_save.payload) }, (client, args) => client.post('/erp/order/style/save', parsePayload(args))),
58
+ workflowTool('erp_order_style_craft_save', 'erp:order:edit', { payload: z.string().describe(WORKFLOW_META.erp_order_style_craft_save.payload) }, (client, args) => client.post('/erp/order/style/craft/save', parsePayload(args))),
59
+ workflowTool('erp_production_craft_save', 'erp:production:edit', { payload: z.string().describe(WORKFLOW_META.erp_production_craft_save.payload) }, (client, args) => client.post('/erp/production/craft/save', parsePayload(args))),
60
+ workflowTool('erp_order_attachment_add', 'erp:order:edit', { payload: z.string().describe(WORKFLOW_META.erp_order_attachment_add.payload) }, (client, args) => client.post('/erp/order/attachment', parsePayload(args))),
61
+ workflowTool('erp_order_attachment_remove', 'erp:order:remove', { ids: idsField }, (client, args) => client.delete(`/erp/order/attachment/${parseIds(args)}`)),
62
+ workflowTool('erp_production_create_from_order', 'erp:production:add', { orderId: idField.describe('订单 ID,每单仅可生成一条生产单') }, (client, args) => client.post(`/erp/production/fromOrder/${args.orderId}`)),
63
+ workflowTool('erp_production_save_items', 'erp:production:edit', { payload: z.string().describe(WORKFLOW_META.erp_production_save_items.payload) }, (client, args) => client.post('/erp/production/item/save', parsePayload(args))),
64
+ workflowTool('erp_production_check', 'erp:production:edit', { id: idField.describe('生产单 ID') }, (client, args) => client.post(`/erp/production/check/${args.id}`)),
65
+ workflowTool('erp_production_submit_review', 'erp:production:edit', { id: idField.describe('生产单 ID,需 poStatus=draft') }, (client, args) => client.put(`/erp/production/review/submit/${args.id}`)),
66
+ workflowTool('erp_production_reject_review', 'erp:production:review', { id: idField.describe('生产单 ID,需 poStatus=reviewing') }, (client, args) => client.put(`/erp/production/review/reject/${args.id}`)),
67
+ workflowTool('erp_production_issue', 'erp:production:issue', { id: idField.describe('生产单 ID,需 poStatus=reviewing') }, (client, args) => client.put(`/erp/production/issue/${args.id}`)),
68
+ workflowTool('erp_email_thread_link', 'erp:email:thread:link', { payload: z.string().describe(WORKFLOW_META.erp_email_thread_link.payload) }, (client, args) => client.put('/erp/email/thread/link', parsePayload(args)))
69
+ ];
package/dist/tools.js ADDED
@@ -0,0 +1,381 @@
1
+ import { z } from 'zod';
2
+ import { buildReadDescription, READ_META } from './tool-meta.js';
3
+ import { WRITE_TOOLS } from './tools-write.js';
4
+ /** 分页入参 */
5
+ const pageShape = {
6
+ pageNum: z.number().optional().describe('页码,默认 1'),
7
+ pageSize: z.number().optional().describe('每页条数,默认 20,最大 100')
8
+ };
9
+ /** 解析分页参数 */
10
+ function pageParams(args) {
11
+ const pageNum = args.pageNum == null ? 1 : Number(args.pageNum);
12
+ const pageSize = args.pageSize == null ? 20 : Math.min(Number(args.pageSize), 100);
13
+ return { pageNum, pageSize };
14
+ }
15
+ /** 判断用户是否拥有权限 */
16
+ export function hasPermission(permissions, required) {
17
+ if (required == null) {
18
+ return true;
19
+ }
20
+ if (permissions.includes('*:*:*')) {
21
+ return true;
22
+ }
23
+ return permissions.includes(required);
24
+ }
25
+ /** ERP 查询工具 */
26
+ const READ_TOOLS = [
27
+ {
28
+ name: 'erp_workbench_summary',
29
+ description: '查询 ERP 工作台摘要(待办、超期任务、缺项生产单、近期订单)',
30
+ permission: null,
31
+ inputSchema: {},
32
+ handler: (client) => client.get('/erp/workbench/summary')
33
+ },
34
+ {
35
+ name: 'erp_customer_list',
36
+ description: '分页查询客户列表,可按客户编码、名称、类型、状态筛选',
37
+ permission: 'erp:customer:list',
38
+ inputSchema: {
39
+ ...pageShape,
40
+ customerCode: z.string().optional().describe('客户编码'),
41
+ customerName: z.string().optional().describe('客户名称'),
42
+ customerType: z.string().optional().describe('客户类型'),
43
+ status: z.string().optional().describe('状态')
44
+ },
45
+ handler: (client, args) => client.get('/erp/customer/list', {
46
+ ...pageParams(args),
47
+ customerCode: args.customerCode,
48
+ customerName: args.customerName,
49
+ customerType: args.customerType,
50
+ status: args.status
51
+ })
52
+ },
53
+ {
54
+ name: 'erp_customer_get',
55
+ description: '按 ID 查询客户详情',
56
+ permission: 'erp:customer:query',
57
+ inputSchema: {
58
+ id: z.number().describe('客户 ID')
59
+ },
60
+ handler: (client, args) => client.get(`/erp/customer/${args.id}`)
61
+ },
62
+ {
63
+ name: 'erp_customer_contact_list',
64
+ description: '查询客户联系人列表',
65
+ permission: 'erp:customer:list',
66
+ inputSchema: {
67
+ customerId: z.number().describe('客户 ID'),
68
+ contactName: z.string().optional().describe('联系人姓名')
69
+ },
70
+ handler: (client, args) => client.get('/erp/customer/contact/list', {
71
+ customerId: args.customerId,
72
+ contactName: args.contactName
73
+ })
74
+ },
75
+ {
76
+ name: 'erp_customer_address_list',
77
+ description: '查询客户地址列表',
78
+ permission: 'erp:customer:list',
79
+ inputSchema: {
80
+ customerId: z.number().describe('客户 ID'),
81
+ addressType: z.string().optional().describe('地址类型 shipping/billing/sample')
82
+ },
83
+ handler: (client, args) => client.get('/erp/customer/address/list', {
84
+ customerId: args.customerId,
85
+ addressType: args.addressType
86
+ })
87
+ },
88
+ {
89
+ name: 'erp_customer_brand_list',
90
+ description: '查询客户品牌列表',
91
+ permission: 'erp:customer:list',
92
+ inputSchema: {
93
+ customerId: z.number().describe('客户 ID'),
94
+ brandName: z.string().optional().describe('品牌名称'),
95
+ status: z.string().optional().describe('状态')
96
+ },
97
+ handler: (client, args) => client.get('/erp/customer/brand/list', {
98
+ customerId: args.customerId,
99
+ brandName: args.brandName,
100
+ status: args.status
101
+ })
102
+ },
103
+ {
104
+ name: 'erp_pi_list',
105
+ description: '分页查询 PI(形式发票)列表',
106
+ permission: 'erp:pi:list',
107
+ inputSchema: {
108
+ ...pageShape,
109
+ piNo: z.string().optional().describe('PI 编号'),
110
+ customerId: z.number().optional().describe('客户 ID'),
111
+ status: z.string().optional().describe('状态')
112
+ },
113
+ handler: (client, args) => client.get('/erp/pi/list', {
114
+ ...pageParams(args),
115
+ piNo: args.piNo,
116
+ customerId: args.customerId,
117
+ status: args.status
118
+ })
119
+ },
120
+ {
121
+ name: 'erp_pi_get',
122
+ description: '按 ID 查询 PI 详情',
123
+ permission: 'erp:pi:query',
124
+ inputSchema: {
125
+ id: z.number().describe('PI ID')
126
+ },
127
+ handler: (client, args) => client.get(`/erp/pi/${args.id}`)
128
+ },
129
+ {
130
+ name: 'erp_order_list',
131
+ description: '分页查询订单列表',
132
+ permission: 'erp:order:list',
133
+ inputSchema: {
134
+ ...pageShape,
135
+ orderNo: z.string().optional().describe('订单号'),
136
+ customerId: z.number().optional().describe('客户 ID'),
137
+ orderStatus: z.string().optional().describe('订单状态'),
138
+ assigneeId: z.number().optional().describe('跟单员 ID')
139
+ },
140
+ handler: (client, args) => client.get('/erp/order/list', {
141
+ ...pageParams(args),
142
+ orderNo: args.orderNo,
143
+ customerId: args.customerId,
144
+ orderStatus: args.orderStatus,
145
+ assigneeId: args.assigneeId
146
+ })
147
+ },
148
+ {
149
+ name: 'erp_order_get',
150
+ description: '按 ID 查询订单详情',
151
+ permission: 'erp:order:query',
152
+ inputSchema: {
153
+ id: z.number().describe('订单 ID')
154
+ },
155
+ handler: (client, args) => client.get(`/erp/order/${args.id}`)
156
+ },
157
+ {
158
+ name: 'erp_order_style_list',
159
+ description: '查询订单款式及颜色尺码、结构化工艺、商标位置明细',
160
+ permission: 'erp:order:query',
161
+ inputSchema: {
162
+ orderId: z.number().describe('订单 ID')
163
+ },
164
+ handler: (client, args) => client.get('/erp/order/style/list', { orderId: args.orderId })
165
+ },
166
+ {
167
+ name: 'erp_order_craft_schema',
168
+ description: '查询工艺字段定义(上衣/裤子受控选项 schema)',
169
+ permission: 'erp:order:query',
170
+ inputSchema: {},
171
+ handler: (client) => client.get('/erp/order/style/craft/schema')
172
+ },
173
+ {
174
+ name: 'erp_production_craft_schema',
175
+ description: '查询生产单工艺字段定义',
176
+ permission: 'erp:production:query',
177
+ inputSchema: {},
178
+ handler: (client) => client.get('/erp/production/craft/schema')
179
+ },
180
+ {
181
+ name: 'erp_order_timeline_list',
182
+ description: '查询订单跟进时间线',
183
+ permission: 'erp:order:query',
184
+ inputSchema: {
185
+ orderId: z.number().describe('订单 ID')
186
+ },
187
+ handler: (client, args) => client.get('/erp/order/timeline/list', { orderId: args.orderId })
188
+ },
189
+ {
190
+ name: 'erp_order_attachment_list',
191
+ description: '查询订单附件列表',
192
+ permission: 'erp:order:query',
193
+ inputSchema: {
194
+ orderId: z.number().describe('订单 ID')
195
+ },
196
+ handler: (client, args) => client.get('/erp/order/attachment/list', { orderId: args.orderId })
197
+ },
198
+ {
199
+ name: 'erp_production_list',
200
+ description: '分页查询生产单列表',
201
+ permission: 'erp:production:list',
202
+ inputSchema: {
203
+ ...pageShape,
204
+ productionNo: z.string().optional().describe('生产单号'),
205
+ orderId: z.number().optional().describe('订单 ID'),
206
+ status: z.string().optional().describe('状态')
207
+ },
208
+ handler: (client, args) => client.get('/erp/production/list', {
209
+ ...pageParams(args),
210
+ productionNo: args.productionNo,
211
+ orderId: args.orderId,
212
+ status: args.status
213
+ })
214
+ },
215
+ {
216
+ name: 'erp_production_get',
217
+ description: '按 ID 查询生产单详情',
218
+ permission: 'erp:production:query',
219
+ inputSchema: {
220
+ id: z.number().describe('生产单 ID')
221
+ },
222
+ handler: (client, args) => client.get(`/erp/production/${args.id}`)
223
+ },
224
+ {
225
+ name: 'erp_production_check_list',
226
+ description: '查询生产单校验记录',
227
+ permission: 'erp:production:query',
228
+ inputSchema: {
229
+ productionOrderId: z.number().describe('生产单 ID')
230
+ },
231
+ handler: (client, args) => client.get('/erp/production/check/list', {
232
+ productionOrderId: args.productionOrderId
233
+ })
234
+ },
235
+ {
236
+ name: 'erp_task_list',
237
+ description: '分页查询跟进任务列表',
238
+ permission: 'erp:task:list',
239
+ inputSchema: {
240
+ ...pageShape,
241
+ taskTitle: z.string().optional().describe('任务标题'),
242
+ taskStatus: z.string().optional().describe('任务状态'),
243
+ assigneeId: z.number().optional().describe('负责人 ID')
244
+ },
245
+ handler: (client, args) => client.get('/erp/task/list', {
246
+ ...pageParams(args),
247
+ taskTitle: args.taskTitle,
248
+ taskStatus: args.taskStatus,
249
+ assigneeId: args.assigneeId
250
+ })
251
+ },
252
+ {
253
+ name: 'erp_task_get',
254
+ description: '按 ID 查询跟进任务详情',
255
+ permission: 'erp:task:query',
256
+ inputSchema: {
257
+ id: z.number().describe('任务 ID')
258
+ },
259
+ handler: (client, args) => client.get(`/erp/task/${args.id}`)
260
+ },
261
+ {
262
+ name: 'erp_material_category_list',
263
+ description: '查询物料分类树',
264
+ permission: 'erp:material:list',
265
+ inputSchema: {},
266
+ handler: (client) => client.get('/erp/material/category/list')
267
+ },
268
+ {
269
+ name: 'erp_material_list',
270
+ description: '分页查询物料列表',
271
+ permission: 'erp:material:list',
272
+ inputSchema: {
273
+ ...pageShape,
274
+ materialCode: z.string().optional().describe('物料编码'),
275
+ materialName: z.string().optional().describe('物料名称'),
276
+ categoryId: z.number().optional().describe('分类 ID')
277
+ },
278
+ handler: (client, args) => client.get('/erp/material/list', {
279
+ ...pageParams(args),
280
+ materialCode: args.materialCode,
281
+ materialName: args.materialName,
282
+ categoryId: args.categoryId
283
+ })
284
+ },
285
+ {
286
+ name: 'erp_material_get',
287
+ description: '按 ID 查询物料详情',
288
+ permission: 'erp:material:query',
289
+ inputSchema: {
290
+ id: z.number().describe('物料 ID')
291
+ },
292
+ handler: (client, args) => client.get(`/erp/material/${args.id}`)
293
+ },
294
+ {
295
+ name: 'erp_supplier_list',
296
+ description: '分页查询供应商列表',
297
+ permission: 'erp:supplier:list',
298
+ inputSchema: {
299
+ ...pageShape,
300
+ supplierCode: z.string().optional().describe('供应商编码'),
301
+ supplierName: z.string().optional().describe('供应商名称'),
302
+ status: z.string().optional().describe('状态')
303
+ },
304
+ handler: (client, args) => client.get('/erp/supplier/list', {
305
+ ...pageParams(args),
306
+ supplierCode: args.supplierCode,
307
+ supplierName: args.supplierName,
308
+ status: args.status
309
+ })
310
+ },
311
+ {
312
+ name: 'erp_supplier_get',
313
+ description: '按 ID 查询供应商详情',
314
+ permission: 'erp:supplier:query',
315
+ inputSchema: {
316
+ id: z.number().describe('供应商 ID')
317
+ },
318
+ handler: (client, args) => client.get(`/erp/supplier/${args.id}`)
319
+ },
320
+ {
321
+ name: 'erp_email_account_list',
322
+ description: '分页查询邮箱账号列表',
323
+ permission: 'erp:email:account:list',
324
+ inputSchema: {
325
+ ...pageShape,
326
+ accountName: z.string().optional().describe('账号名称'),
327
+ emailAddress: z.string().optional().describe('邮箱地址')
328
+ },
329
+ handler: (client, args) => client.get('/erp/email/account/list', {
330
+ ...pageParams(args),
331
+ accountName: args.accountName,
332
+ emailAddress: args.emailAddress
333
+ })
334
+ },
335
+ {
336
+ name: 'erp_email_account_get',
337
+ description: '按 ID 查询邮箱账号详情',
338
+ permission: 'erp:email:account:query',
339
+ inputSchema: {
340
+ id: z.number().describe('邮箱账号 ID')
341
+ },
342
+ handler: (client, args) => client.get(`/erp/email/account/${args.id}`)
343
+ },
344
+ {
345
+ name: 'erp_email_thread_list',
346
+ description: '分页查询邮件线程列表',
347
+ permission: 'erp:email:thread:list',
348
+ inputSchema: {
349
+ ...pageShape,
350
+ subject: z.string().optional().describe('邮件主题'),
351
+ orderId: z.number().optional().describe('关联订单 ID')
352
+ },
353
+ handler: (client, args) => client.get('/erp/email/thread/list', {
354
+ ...pageParams(args),
355
+ subject: args.subject,
356
+ orderId: args.orderId
357
+ })
358
+ },
359
+ {
360
+ name: 'erp_email_thread_get',
361
+ description: '按 ID 查询邮件线程详情(含邮件正文)',
362
+ permission: 'erp:email:thread:query',
363
+ inputSchema: {
364
+ id: z.number().describe('邮件线程 ID')
365
+ },
366
+ handler: (client, args) => client.get(`/erp/email/thread/${args.id}`)
367
+ }
368
+ ];
369
+ /** 补充查询工具描述 */
370
+ function enrichReadTools(tools) {
371
+ return tools.map((tool) => ({
372
+ ...tool,
373
+ description: buildReadDescription(tool.description, READ_META[tool.name])
374
+ }));
375
+ }
376
+ /** ERP 全部工具 */
377
+ export const ERP_TOOLS = [...enrichReadTools(READ_TOOLS), ...WRITE_TOOLS];
378
+ /** 按权限过滤可用工具 */
379
+ export function filterTools(permissions) {
380
+ return ERP_TOOLS.filter((tool) => hasPermission(permissions, tool.permission));
381
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "yindie-erp-mcp",
3
+ "version": "1.0.0",
4
+ "description": "印蝶 ERP 独立 MCP 服务,通过 RuoYi API 提供只读查询能力",
5
+ "type": "module",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "bin": {
10
+ "yindie-erp-mcp": "./dist/index.js"
11
+ },
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "start": "node dist/index.js",
15
+ "dev": "tsc && node dist/index.js",
16
+ "prepublishOnly": "npm run build",
17
+ "release": "npm publish --registry=https://registry.npmjs.org --access public",
18
+ "login": "npm login --registry=https://registry.npmjs.org/"
19
+ },
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "dependencies": {
24
+ "@modelcontextprotocol/sdk": "^1.12.1",
25
+ "zod": "^3.25.76"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^22.15.21",
29
+ "typescript": "^5.8.3"
30
+ }
31
+ }