init: 汽车零部件智能报价 MCP 服务(ERP + CRM)

This commit is contained in:
dongsk
2026-08-08 20:11:23 +08:00
commit ae992f45a1
16 changed files with 1502 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
# MCP for ERP — 汽车零部件智能报价 ERP 数据服务
## 概述
为汽车零部件(小型阀体外壳)智能报价智能体提供 ERP 数据访问能力,通过 MCP 协议暴露 5 个工具。
## 技术栈
- **框架**: MCPServer(mcp 2.0.0 内置高级框架)
- **语言**: Python 3.11+
- **数据库**: PostgreSQL 15+
- **传输协议**: Streamable HTTP(远程调用)
## 工具清单
| 工具名 | 功能 | 核价环节 |
|---|---|---|
| `query_material_master` | 按编码/参数查询物料主数据(含体积、重量、吨位) | 基础数据 |
| `match_material_grade` | 材质牌号对照(GB/ASTM/JIS/EN),返回等效牌号+密度 | 材质确认 |
| `get_die_casting_params` | 根据零件体积+吨位返回压铸参数(节拍、铝水重量) | **压铸成本** |
| `get_machining_estimate` | 根据孔数/孔深/螺纹规格返回机加工工时估算 | **机加工成本** |
| `calculate_part_cost` | 汇总核价:材料+压铸+机加工+后处理+管理费+利润 | **最终报价** |
## 核价逻辑
```
1. 铝水重量 = 体积(cm³) × 密度(g/cm³) / 1000 × 1.05(损耗)
2. 材料成本 = 铝水重量 × 铝锭单价
3. 压铸成本 = (节拍秒/3600) × 机台费率 / 模穴数
4. 机加工成本 = 总工时(小时) × 加工中心费率(60元/时)
5. 后处理成本 = 零件重量(kg) × 后处理单价
6. 总成本 = 材料 + 压铸 + 机加工 + 后处理
7. 报价 = 总成本 × (1+管理费率12%) × (1+利润率15%)
```
## 快速开始
```bash
pip install -r requirements.txt
cd src
python seed.py # 初始化数据库(可选,数据已在共享数据库中)
python server.py # 启动 MCP Server(端口 8001)
```
## 服务地址
- **HTTP 端点**: `http://0.0.0.0:8001/mcp`
- **传输协议**: Streamable HTTP
## 数据库配置
通过环境变量配置:
| 变量 | 默认值 | 说明 |
|---|---|---|
| `ERP_DB_HOST` | 192.168.1.206 | PostgreSQL 主机 |
| `ERP_DB_PORT` | 5432 | 端口 |
| `ERP_DB_USER` | postgres | 用户名 |
| `ERP_DB_PASSWORD` | digiwin | 密码 |
| `ERP_DB_NAME` | smart_quotation_auto | 数据库名 |
+3
View File
@@ -0,0 +1,3 @@
mcp>=1.0.0
pydantic>=2.0.0
asyncpg>=0.30.0
+234
View File
@@ -0,0 +1,234 @@
-- ============================================================
-- MCP for ERP — 汽车零部件智能报价 ERP 数据服务
-- 数据库:smart_quotation_auto
-- v3: 幂等可重跑(唯一约束+去重);扩充至 16 物料/13 牌号/9 吨位/26 节拍/10 后处理
-- 新增 O1 material_price_history、O2 machining_rate
-- ============================================================
-- 1. 物料主数据(含体积,用于算铝水重量)
CREATE TABLE IF NOT EXISTS material_master (
material_code VARCHAR(50) PRIMARY KEY,
part_name VARCHAR(200) NOT NULL,
part_category VARCHAR(50) NOT NULL,
material_grade VARCHAR(50) NOT NULL,
density DECIMAL(6,3) NOT NULL DEFAULT 2.700,
volume_cc DECIMAL(10,2) NOT NULL,
net_weight_kg DECIMAL(8,3) NOT NULL,
die_casting_ton INTEGER NOT NULL,
surface_treatment VARCHAR(50),
unit VARCHAR(10) DEFAULT '件',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 2. 材质牌号对照表
CREATE TABLE IF NOT EXISTS material_grade_mapping (
id SERIAL PRIMARY KEY,
grade_group VARCHAR(50) NOT NULL,
gb_code VARCHAR(80),
astm_code VARCHAR(80),
jis_code VARCHAR(80),
en_code VARCHAR(80),
iso_code VARCHAR(80),
internal_code VARCHAR(50) NOT NULL,
density DECIMAL(6,3) NOT NULL,
price_per_kg DECIMAL(10,2) NOT NULL
);
-- 3. 压铸参数表(吨位 → 固定节拍)
CREATE TABLE IF NOT EXISTS die_casting_params (
tonnage INTEGER PRIMARY KEY,
shot_weight_kg DECIMAL(8,3) NOT NULL,
cycle_time_sec DECIMAL(8,2) NOT NULL,
machine_rate DECIMAL(10,2) NOT NULL,
mold_cavities INTEGER DEFAULT 1
);
-- 4. 机加工节拍表(固化工装节拍)
CREATE TABLE IF NOT EXISTS machining_cycle (
id SERIAL PRIMARY KEY,
operation_type VARCHAR(30) NOT NULL,
spec VARCHAR(50) NOT NULL,
cycle_time_sec DECIMAL(8,2) NOT NULL,
tool_rate DECIMAL(8,4) DEFAULT 0,
notes VARCHAR(200)
);
-- 5. 后处理费率表(按重量计价)
CREATE TABLE IF NOT EXISTS post_process_rate (
id SERIAL PRIMARY KEY,
process_type VARCHAR(50) NOT NULL,
unit VARCHAR(10) DEFAULT 'kg',
rate DECIMAL(10,2) NOT NULL,
currency VARCHAR(10) DEFAULT 'CNY'
);
-- 6. O1 材料价格历史表(月度参考价,核价取最新月份)
CREATE TABLE IF NOT EXISTS material_price_history (
id SERIAL PRIMARY KEY,
internal_code VARCHAR(50) NOT NULL,
price_date DATE NOT NULL,
price_per_kg DECIMAL(10,2) NOT NULL,
source VARCHAR(100) DEFAULT '市场参考价',
UNIQUE (internal_code, price_date)
);
-- 7. O2 机加工费率表(按材质族,替换硬编码 60 元/h)
CREATE TABLE IF NOT EXISTS machining_rate (
grade_group VARCHAR(50) PRIMARY KEY,
rate_per_hour DECIMAL(10,2) NOT NULL,
notes VARCHAR(200)
);
-- ============================================================
-- 幂等性:去重 + 唯一约束(可重复执行)
-- ============================================================
DELETE FROM machining_cycle a USING machining_cycle b
WHERE a.id > b.id AND a.operation_type = b.operation_type AND a.spec = b.spec;
DELETE FROM material_grade_mapping a USING material_grade_mapping b
WHERE a.id > b.id AND a.internal_code = b.internal_code;
DELETE FROM post_process_rate a USING post_process_rate b
WHERE a.id > b.id AND a.process_type = b.process_type;
DO $$ BEGIN
ALTER TABLE machining_cycle ADD CONSTRAINT uq_machining_cycle UNIQUE (operation_type, spec);
EXCEPTION WHEN duplicate_table THEN NULL; END $$;
DO $$ BEGIN
ALTER TABLE material_grade_mapping ADD CONSTRAINT uq_grade_mapping UNIQUE (internal_code);
EXCEPTION WHEN duplicate_table THEN NULL; END $$;
DO $$ BEGIN
ALTER TABLE post_process_rate ADD CONSTRAINT uq_post_process UNIQUE (process_type);
EXCEPTION WHEN duplicate_table THEN NULL; END $$;
-- ============================================================
-- 种子数据
-- ============================================================
-- 物料主数据(16 种阀体外壳:铝12/灰铁3/球铁1,吨位 160~1000T 全覆盖)
INSERT INTO material_master (material_code, part_name, part_category, material_grade, density, volume_cc, net_weight_kg, die_casting_ton, surface_treatment) VALUES
('VHB-AT-001', '变速箱阀体外壳', '阀体外壳', 'ADC12', 2.700, 444.44, 1.200, 400, '阳极氧化'),
('VHB-BK-002', '制动阀体壳', '阀体外壳', 'HT250', 7.200, 388.89, 2.800, 630, '电泳涂装'),
('VHB-SP-003', '转向器阀体壳', '阀体外壳', 'A356-T6', 2.650, 296.30, 0.785, 280, '钝化'),
('VHB-EG-004', '发动机油路阀体壳', '阀体外壳', 'EN AC-4600', 2.700, 555.56, 1.500, 400, '阳极氧化'),
('VHB-CL-005', '离合器控制阀体壳', '阀体外壳', 'ADC10', 2.700, 480.00, 1.300, 400, '阳极氧化'),
('VHB-TR-006', '变速箱液压控制阀体', '阀体外壳', 'AlSi9Cu3', 2.700, 620.00, 1.670, 630, '电泳涂装'),
('VHB-EB-007', '电子制动阀体壳', '阀体外壳', 'AlSi10MnMg', 2.650, 350.00, 0.930, 400, '钝化'),
('VHB-QT-008', '悬架阻尼阀体壳', '阀体外壳', 'QT500-7', 7.100, 520.00, 3.690, 630, '喷涂'),
('VHB-MN-009', '微型泵阀体壳', '阀体外壳', 'ADC12', 2.700, 96.30, 0.260, 160, '钝化'),
('VHB-LG-010', '大型主阀体壳', '阀体外壳', 'ADC12', 2.700, 1037.00, 2.800, 800, '电泳涂装'),
('VHB-TC-011', '涡轮增压控制阀体壳', '阀体外壳', 'ADC12', 2.700, 320.00, 0.860, 350, '钝化'),
('VHB-EG-012', 'EGR阀体壳', '阀体外壳', 'ADC10', 2.700, 265.00, 0.720, 280, '钝化'),
('VHB-WP-013', '水泵阀体壳', '阀体外壳', 'AlSi12', 2.650, 380.00, 1.010, 400, '阳极氧化'),
('VHB-TM-014', '热管理阀体壳(新能源)', '阀体外壳', 'AlSi10MnMg', 2.650, 505.00, 1.340, 500, '钝化'),
('VHB-OP-015', '机油泵阀体壳', '阀体外壳', 'ZL101', 2.650, 210.00, 0.560, 200, '钝化'),
('VHB-HD-016', '重卡制动阀体壳', '阀体外壳', 'HT300', 7.250, 850.00, 6.160, 1000, '达克罗')
ON CONFLICT (material_code) DO NOTHING;
-- 材质牌号对照表(13 条:铝合金/铸铝/灰铸铁/球墨铸铁)
INSERT INTO material_grade_mapping (grade_group, gb_code, astm_code, jis_code, en_code, iso_code, internal_code, density, price_per_kg) VALUES
('铝合金', 'ZAlSi10Cu(ADC12)', 'A380', 'ADC12', 'EN AC-4600', 'AlSi9Cu3', 'AL-ADC12', 2.700, 18.50),
('铝合金', 'ZAlSi7Mg(A356)', 'A356', 'AC4C-T6', 'EN AC-4210', 'AlSi7Mg', 'AL-A356', 2.650, 20.00),
('铝合金', 'ZAlSi9Cu(AlSi9Cu)', 'B443', 'AC4B', 'EN AC-4600', 'AlSi9Cu', 'AL-4600', 2.700, 18.00),
('铸铝', 'ZL101', '356.0', 'AC4C', 'EN AC-4200', 'AlSi7', 'AL-ZL101', 2.650, 19.00),
('铸铁', 'HT250', 'G2500', 'FC250', 'EN-GJL-250', 'GJL-250', 'IR-HT250', 7.200, 6.50),
('球墨铸铁', 'QT500-7', 'D5007', 'FCD500', 'EN-GJS-500', 'GJS-500', 'IR-QT500', 7.100, 7.20),
('铝合金', 'ZAlSi8Cu(ADC10)', 'A384', 'ADC10', 'EN AC-4650', 'AlSi8Cu', 'AL-ADC10', 2.700, 18.20),
('铝合金', 'ZAlSi10MnMg', '—', '—', 'AlSi10MnMg', 'AlSi10MnMg', 'AL-S10MM', 2.650, 22.00),
('铝合金', 'ZAlSi12', '413.0', 'AC3A', 'EN AC-4420', 'AlSi12', 'AL-S12', 2.650, 17.80),
('铸铁', 'HT200', 'G3000', 'FC200', 'EN-GJL-200', 'GJL-200', 'IR-HT200', 7.200, 6.00),
('铸铁', 'HT300', 'G3500', 'FC300', 'EN-GJL-300', 'GJL-300', 'IR-HT300', 7.250, 6.80),
('球墨铸铁', 'QT450-10', 'D4510', 'FCD450', 'EN-GJS-450', 'GJS-450', 'IR-QT450', 7.100, 7.00),
('球墨铸铁', 'QT600-3', 'D6003', 'FCD600', 'EN-GJS-600', 'GJS-600', 'IR-QT600', 7.100, 7.50)
ON CONFLICT (internal_code) DO NOTHING;
-- 球铁牌号组别归位(历史数据曾归入"铸铁"组)
UPDATE material_grade_mapping SET grade_group = '球墨铸铁' WHERE internal_code LIKE 'IR-QT%';
-- 压铸参数表(9 档吨位:160~1000T;故意不含 1250T 用于回退测试)
INSERT INTO die_casting_params (tonnage, shot_weight_kg, cycle_time_sec, machine_rate, mold_cavities) VALUES
( 160, 0.50, 15.00, 120.00, 2),
( 200, 0.80, 18.00, 140.00, 2),
( 280, 1.20, 22.00, 180.00, 1),
( 350, 1.80, 26.00, 220.00, 1),
( 400, 2.50, 30.00, 260.00, 1),
( 500, 3.20, 36.00, 320.00, 1),
( 630, 4.00, 42.00, 380.00, 1),
( 800, 6.50, 55.00, 520.00, 1),
(1000, 8.50, 65.00, 650.00, 1)
ON CONFLICT (tonnage) DO NOTHING;
-- 机加工节拍表(26 条全规格谱系;故意不含 Φ25钻/M18攻 用于回退测试)
INSERT INTO machining_cycle (operation_type, spec, cycle_time_sec, tool_rate, notes) VALUES
('drill', 'Φ3', 2.00, 0.10, '通孔'),
('drill', 'Φ4', 2.20, 0.12, '通孔'),
('drill', 'Φ5', 2.50, 0.13, '通孔'),
('drill', 'Φ6', 3.00, 0.15, '通孔'),
('drill', 'Φ8', 4.00, 0.20, '通孔'),
('drill', 'Φ10', 5.00, 0.25, '通孔'),
('drill', 'Φ12', 6.00, 0.30, '通孔'),
('drill', 'Φ14', 7.00, 0.35, '通孔'),
('drill', 'Φ16', 8.00, 0.40, '通孔'),
('drill', 'Φ20', 10.00, 0.50, '通孔'),
('drill', 'Φ24', 12.00, 0.60, '通孔'),
('tap', 'M4×0.7', 3.00, 0.15, '盲孔'),
('tap', 'M5×0.8', 3.50, 0.18, '盲孔'),
('tap', 'M6×1.0', 4.00, 0.20, '盲孔'),
('tap', 'M8×1.25', 5.00, 0.25, '盲孔'),
('tap', 'M10×1.5', 6.00, 0.30, '盲孔'),
('tap', 'M12×1.75', 7.00, 0.35, '盲孔'),
('tap', 'M14×2.0', 8.00, 0.40, '盲孔'),
('tap', 'M16×2.0', 9.00, 0.45, '盲孔'),
('ream', 'Φ6H7', 5.00, 0.45, '铰孔'),
('ream', 'Φ8H7', 6.00, 0.50, '铰孔'),
('ream', 'Φ10H7', 7.00, 0.60, '铰孔'),
('ream', 'Φ12H7', 8.00, 0.70, '铰孔'),
('ream', 'Φ16H7', 10.00, 0.90, '铰孔'),
('ream', 'Φ20H7', 12.00, 1.10, '铰孔'),
('mill', 'face', 12.00, 0.80, '铣面(单面)')
ON CONFLICT (operation_type, spec) DO NOTHING;
-- 后处理费率表(10 种;故意不含"镀铬"用于回退测试)
INSERT INTO post_process_rate (process_type, unit, rate) VALUES
('喷砂', 'kg', 1.50),
('钝化', 'kg', 2.00),
('阳极氧化', 'kg', 5.00),
('电泳涂装', 'kg', 3.50),
('喷涂', 'kg', 2.50),
('粉末喷涂', 'kg', 3.00),
('磷化', 'kg', 1.80),
('达克罗', 'kg', 4.00),
('硬质阳极氧化', 'kg', 8.00),
('化学镀镍', 'kg', 9.00)
ON CONFLICT (process_type) DO NOTHING;
-- O2 机加工费率表(按材质族)
INSERT INTO machining_rate (grade_group, rate_per_hour, notes) VALUES
('铝合金', 60.00, '铝合金常规机加工费率'),
('铸铝', 60.00, '铸铝同铝合金费率'),
('铸铁', 75.00, '灰铸铁刀具损耗高,费率上浮'),
('球墨铸铁', 80.00, '球铁强度高,机加工费率最高')
ON CONFLICT (grade_group) DO NOTHING;
-- O1 材料价格历史表(2025-01 ~ 2026-08 共 20 个月 × 13 牌号,最新月系数=1.00 与对照表价格一致)
WITH factors(price_date, al_factor, ir_factor) AS (VALUES
('2025-01-01'::date, 0.92, 0.95), ('2025-02-01'::date, 0.93, 0.96),
('2025-03-01'::date, 0.95, 0.96), ('2025-04-01'::date, 0.94, 0.97),
('2025-05-01'::date, 0.96, 0.98), ('2025-06-01'::date, 0.98, 0.99),
('2025-07-01'::date, 0.97, 0.98), ('2025-08-01'::date, 0.99, 1.00),
('2025-09-01'::date, 1.01, 1.01), ('2025-10-01'::date, 1.00, 1.00),
('2025-11-01'::date, 0.98, 0.99), ('2025-12-01'::date, 0.97, 0.98),
('2026-01-01'::date, 0.99, 0.99), ('2026-02-01'::date, 1.02, 1.00),
('2026-03-01'::date, 1.01, 1.01), ('2026-04-01'::date, 1.00, 1.00),
('2026-05-01'::date, 0.99, 0.99), ('2026-06-01'::date, 1.01, 1.00),
('2026-07-01'::date, 1.00, 1.00), ('2026-08-01'::date, 1.00, 1.00)
)
INSERT INTO material_price_history (internal_code, price_date, price_per_kg, source)
SELECT m.internal_code, f.price_date,
ROUND(m.price_per_kg * CASE WHEN m.grade_group IN ('铸铁', '球墨铸铁') THEN f.ir_factor ELSE f.al_factor END, 2),
'市场参考价'
FROM material_grade_mapping m CROSS JOIN factors f
ON CONFLICT (internal_code, price_date) DO NOTHING;
+28
View File
@@ -0,0 +1,28 @@
"""PostgreSQL 连接池管理"""
import asyncpg
import os
DB_CONFIG = {
"host": os.getenv("ERP_DB_HOST", "192.168.1.206"),
"port": int(os.getenv("ERP_DB_PORT", "5432")),
"user": os.getenv("ERP_DB_USER", "postgres"),
"password": os.getenv("ERP_DB_PASSWORD", "digiwin"),
"database": os.getenv("ERP_DB_NAME", "smart_quotation_auto"),
}
_pool: asyncpg.Pool | None = None
async def get_pool() -> asyncpg.Pool:
global _pool
if _pool is None:
_pool = await asyncpg.create_pool(**DB_CONFIG, min_size=2, max_size=10)
return _pool
async def close_pool():
global _pool
if _pool:
await _pool.close()
_pool = None
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env python3
"""初始化 ERP 数据库表和种子数据"""
import asyncio
from pathlib import Path
from db import get_pool, close_pool
async def main():
pool = await get_pool()
sql_path = Path(__file__).parent.parent / "sql" / "init.sql"
sql = sql_path.read_text(encoding="utf-8")
await pool.execute(sql)
print("ERP 数据库初始化完成")
await close_pool()
if __name__ == "__main__":
asyncio.run(main())
+316
View File
@@ -0,0 +1,316 @@
"""
MCP for ERP — 汽车零部件智能报价 ERP 数据服务
使用 MCPServer 框架,5 个工具:物料查询、材质对照、压铸参数、机加工估算、成本核算
HTTP 模式,支持远程调用
"""
import asyncio
import json
from mcp.server import MCPServer
from db import get_pool
app = MCPServer(
name="mcp-for-erp-auto",
description="汽车零部件智能报价 ERP 数据服务",
version="1.0.0"
)
# ============================================================
# 工具 1: query_material_master — 查询物料主数据
# ============================================================
@app.tool()
async def query_material_master(
material_code: str = None,
part_category: str = None,
material_grade: str = None,
die_casting_ton: int = None
) -> str:
"""查询 ERP 物料主数据。支持按物料编码精确查询,或按零件类别、材质、吨位等参数组合查询。返回物料列表,包含体积、重量、吨位等关键参数。"""
pool = await get_pool()
query = "SELECT * FROM material_master WHERE 1=1"
params = []
if material_code:
query += f" AND material_code = ${len(params)+1}"
params.append(material_code)
if part_category:
query += f" AND part_category = ${len(params)+1}"
params.append(part_category)
if material_grade:
query += f" AND material_grade = ${len(params)+1}"
params.append(material_grade)
if die_casting_ton:
query += f" AND die_casting_ton = ${len(params)+1}"
params.append(die_casting_ton)
rows = await pool.fetch(query, *params)
materials = [dict(row) for row in rows]
return json.dumps({"total": len(materials), "materials": materials}, ensure_ascii=False, indent=2, default=str)
# ============================================================
# 工具 2: match_material_grade — 材质牌号对照
# ============================================================
@app.tool()
async def match_material_grade(customer_grade: str, standard: str = None) -> str:
"""将客户指定材质牌号自动对照为企业内部等效牌号。支持 GB/ASTM/JIS/EN/ISO 标准体系,返回等效牌号、密度、铝锭参考价。"""
pool = await get_pool()
query = """
SELECT * FROM material_grade_mapping
WHERE LOWER(gb_code) LIKE LOWER($1) OR LOWER(astm_code) LIKE LOWER($1)
OR LOWER(jis_code) LIKE LOWER($1) OR LOWER(en_code) LIKE LOWER($1)
OR LOWER(iso_code) LIKE LOWER($1) OR LOWER(internal_code) LIKE LOWER($1)
"""
rows = await pool.fetch(query, f"%{customer_grade}%")
# 牌号带热处理状态后缀(如 A356-T6 / ADC12-F)时,去后缀重试
if not rows:
stripped = customer_grade
for _ in range(2):
if "-" not in stripped:
break
stripped = stripped.rsplit("-", 1)[0]
rows = await pool.fetch(query, f"%{stripped}%")
if rows:
break
if not rows:
return json.dumps({"matched": False, "message": f"未找到材质 {customer_grade}"}, ensure_ascii=False, indent=2)
mappings = [dict(row) for row in rows]
best = mappings[0]
return json.dumps({
"matched": True, "customer_grade": customer_grade, "internal_code": best["internal_code"],
"density": float(best["density"]), "price_per_kg": float(best["price_per_kg"]), "all_mappings": mappings
}, ensure_ascii=False, indent=2, default=str)
# ============================================================
# 工具 3: get_die_casting_params — 获取压铸参数
# ============================================================
@app.tool()
async def get_die_casting_params(material_code: str, volume_cc: float = None) -> str:
"""根据物料编码获取压铸参数。返回压铸机吨位、模次节拍、铝水重量、压铸成本。铝水重量 = 体积 × 密度。"""
pool = await get_pool()
material = await pool.fetchrow("SELECT * FROM material_master WHERE material_code = $1", material_code)
if not material:
return json.dumps({"success": False, "message": f"未找到物料 {material_code}"}, ensure_ascii=False, indent=2)
vol = volume_cc or float(material["volume_cc"])
density = float(material["density"])
tonnage = material["die_casting_ton"]
aluminum_weight_kg = vol * density / 1000 * 1.05
casting = await pool.fetchrow("SELECT * FROM die_casting_params WHERE tonnage = $1", tonnage)
if not casting:
return json.dumps({"success": False, "message": f"未找到 {tonnage}T 压铸机参数"}, ensure_ascii=False, indent=2)
cycle_hours = float(casting["cycle_time_sec"]) / 3600
casting_cost = cycle_hours * float(casting["machine_rate"]) / casting["mold_cavities"]
return json.dumps({
"success": True, "material_code": material_code, "volume_cc": vol, "density": density,
"aluminum_weight_kg": round(aluminum_weight_kg, 3), "die_casting_ton": tonnage,
"cycle_time_sec": float(casting["cycle_time_sec"]), "machine_rate": float(casting["machine_rate"]),
"mold_cavities": casting["mold_cavities"], "casting_cost_per_part": round(casting_cost, 2)
}, ensure_ascii=False, indent=2)
# ============================================================
# 工具 4: get_machining_estimate — 机加工工时估算
# ============================================================
@app.tool()
async def get_machining_estimate(
material_code: str,
drill_holes: list[dict] = None,
tap_holes: list[dict] = None,
ream_holes: list[dict] = None,
mill_faces: int = 0
) -> str:
"""根据孔清单估算机加工工时。钻孔、攻丝、铰孔、铣面均有固化节拍,逐项累加得到总工时和成本。"""
pool = await get_pool()
drill_holes = drill_holes or []
tap_holes = tap_holes or []
ream_holes = ream_holes or []
total_time_sec = 0
total_tool_cost = 0
details = []
for hole in drill_holes:
spec = hole.get("spec", "Φ8")
count = hole.get("count", 1)
row = await pool.fetchrow("SELECT * FROM machining_cycle WHERE operation_type='drill' AND spec=$1", spec)
if row:
time_sec = float(row["cycle_time_sec"]) * count
tool_cost = float(row["tool_rate"]) * count
total_time_sec += time_sec
total_tool_cost += tool_cost
details.append({"operation": "drill", "spec": spec, "count": count, "total_time_sec": time_sec})
for hole in tap_holes:
spec = hole.get("spec", "M8×1.25")
count = hole.get("count", 1)
row = await pool.fetchrow("SELECT * FROM machining_cycle WHERE operation_type='tap' AND spec=$1", spec)
if row:
time_sec = float(row["cycle_time_sec"]) * count
tool_cost = float(row["tool_rate"]) * count
total_time_sec += time_sec
total_tool_cost += tool_cost
details.append({"operation": "tap", "spec": spec, "count": count, "total_time_sec": time_sec})
for hole in ream_holes:
spec = hole.get("spec", "Φ8H7")
count = hole.get("count", 1)
row = await pool.fetchrow("SELECT * FROM machining_cycle WHERE operation_type='ream' AND spec=$1", spec)
if row:
time_sec = float(row["cycle_time_sec"]) * count
tool_cost = float(row["tool_rate"]) * count
total_time_sec += time_sec
total_tool_cost += tool_cost
details.append({"operation": "ream", "spec": spec, "count": count, "total_time_sec": time_sec})
if mill_faces > 0:
row = await pool.fetchrow("SELECT * FROM machining_cycle WHERE operation_type='mill' AND spec='face'")
if row:
time_sec = float(row["cycle_time_sec"]) * mill_faces
tool_cost = float(row["tool_rate"]) * mill_faces
total_time_sec += time_sec
total_tool_cost += tool_cost
details.append({"operation": "mill", "spec": "face", "count": mill_faces, "total_time_sec": time_sec})
return json.dumps({
"success": True, "material_code": material_code, "total_time_sec": round(total_time_sec, 2),
"total_time_hours": round(total_time_sec / 3600, 4), "total_tool_cost": round(total_tool_cost, 2), "details": details
}, ensure_ascii=False, indent=2)
# ============================================================
# 工具 5: calculate_part_cost — 汇总核价
# ============================================================
@app.tool()
async def calculate_part_cost(
material_code: str,
annual_qty: int,
drill_holes: list[dict] = None,
tap_holes: list[dict] = None,
ream_holes: list[dict] = None,
mill_faces: int = 0,
overhead_rate: float = 0.12,
profit_rate: float = 0.15
) -> str:
"""汇总核价:材料成本(铝水重量×单价)+ 压铸成本 + 机加工成本 + 后处理成本 + 管理费 + 利润。返回完整成本明细和最终报价。"""
pool = await get_pool()
drill_holes = drill_holes or []
tap_holes = tap_holes or []
ream_holes = ream_holes or []
material = await pool.fetchrow("SELECT * FROM material_master WHERE material_code = $1", material_code)
if not material:
return json.dumps({"success": False, "message": f"未找到物料 {material_code}"}, ensure_ascii=False, indent=2)
vol = float(material["volume_cc"])
density = float(material["density"])
tonnage = material["die_casting_ton"]
surface_treatment = material["surface_treatment"]
grade_mapping = await pool.fetchrow(
"SELECT * FROM material_grade_mapping WHERE internal_code = $1 OR jis_code = $1 OR gb_code = $1 OR astm_code = $1 OR en_code = $1 OR iso_code = $1 LIMIT 1",
material["material_grade"]
)
aluminum_price = 18.50
internal_code = None
grade_group = None
price_basis_month = None
if grade_mapping:
internal_code = grade_mapping["internal_code"]
grade_group = grade_mapping["grade_group"]
aluminum_price = float(grade_mapping["price_per_kg"])
try:
price_row = await pool.fetchrow(
"SELECT price_per_kg, price_date FROM material_price_history WHERE internal_code = $1 ORDER BY price_date DESC LIMIT 1",
internal_code
)
if price_row:
aluminum_price = float(price_row["price_per_kg"])
price_basis_month = price_row["price_date"].strftime("%Y-%m")
except Exception:
pass
aluminum_weight_kg = vol * density / 1000 * 1.05
material_cost = aluminum_weight_kg * aluminum_price
casting = await pool.fetchrow("SELECT * FROM die_casting_params WHERE tonnage = $1", tonnage)
casting_cost = (float(casting["cycle_time_sec"]) / 3600) * float(casting["machine_rate"]) / casting["mold_cavities"]
machining_rate_per_hour = 60.0
if grade_group:
try:
rate_row = await pool.fetchrow("SELECT rate_per_hour FROM machining_rate WHERE grade_group = $1", grade_group)
if rate_row:
machining_rate_per_hour = float(rate_row["rate_per_hour"])
except Exception:
pass
total_machining_time_sec = 0
for hole in drill_holes:
row = await pool.fetchrow("SELECT cycle_time_sec FROM machining_cycle WHERE operation_type='drill' AND spec=$1", hole.get("spec", "Φ8"))
if row:
total_machining_time_sec += float(row["cycle_time_sec"]) * hole.get("count", 1)
for hole in tap_holes:
row = await pool.fetchrow("SELECT cycle_time_sec FROM machining_cycle WHERE operation_type='tap' AND spec=$1", hole.get("spec", "M8×1.25"))
if row:
total_machining_time_sec += float(row["cycle_time_sec"]) * hole.get("count", 1)
for hole in ream_holes:
row = await pool.fetchrow("SELECT cycle_time_sec FROM machining_cycle WHERE operation_type='ream' AND spec=$1", hole.get("spec", "Φ8H7"))
if row:
total_machining_time_sec += float(row["cycle_time_sec"]) * hole.get("count", 1)
if mill_faces > 0:
row = await pool.fetchrow("SELECT cycle_time_sec FROM machining_cycle WHERE operation_type='mill' AND spec='face'")
if row:
total_machining_time_sec += float(row["cycle_time_sec"]) * mill_faces
machining_cost = (total_machining_time_sec / 3600) * machining_rate_per_hour
post_process = await pool.fetchrow("SELECT rate FROM post_process_rate WHERE process_type = $1", surface_treatment)
post_process_cost = float(material["net_weight_kg"]) * float(post_process["rate"]) if post_process else 0
total_cost = material_cost + casting_cost + machining_cost + post_process_cost
overhead = total_cost * overhead_rate
unit_price = (total_cost + overhead) * (1 + profit_rate)
return json.dumps({
"success": True, "material_code": material_code, "part_name": material["part_name"],
"cost_breakdown": {
"material_cost": round(material_cost, 2), "aluminum_weight_kg": round(aluminum_weight_kg, 3),
"aluminum_price_per_kg": aluminum_price, "price_basis_month": price_basis_month,
"internal_code": internal_code, "grade_group": grade_group,
"casting_cost": round(casting_cost, 2),
"die_casting_ton": tonnage, "machining_cost": round(machining_cost, 2),
"machining_hours": round(total_machining_time_sec / 3600, 4),
"machining_rate_per_hour": machining_rate_per_hour,
"post_process_cost": round(post_process_cost, 2), "surface_treatment": surface_treatment
},
"total_cost": round(total_cost, 2), "overhead_rate": overhead_rate, "overhead": round(overhead, 2),
"profit_rate": profit_rate, "unit_price": round(unit_price, 2), "currency": "CNY"
}, ensure_ascii=False, indent=2)
# ============================================================
# 启动 HTTP Server
# ============================================================
if __name__ == "__main__":
asyncio.run(app.run_streamable_http_async(host="0.0.0.0", port=8001, streamable_http_path="/mcp"))