feat(lzwcai-agile-db): 更新AgileDB技能至v0.4.2版本并扩展工具集

- 将技能版本从0.2.0升级至0.4.2
- 工具数量从33个扩展至57个,新增数据源管理、AI训练、库表关联配置等功能
- 新增MQTT字段关联同步模块(8个工具)和库表关联配置(3个工具)
- 添加重要的契约提示和安全确认原则,包括target默认值、alter_table操作限制等
- 修正工具参数说明,如execute_sql的executableSql改为sql,参数结构优化
- 增强安全机制,明确危险操作的用户确认流程和目标资源选择规则
- 更新README.md中的工具数量统计和功能描述
This commit is contained in:
2026-06-17 14:40:43 +08:00
parent 557361632c
commit ba5cd4bbe1
115 changed files with 7587 additions and 575 deletions

View File

@@ -0,0 +1,36 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
venv/
env/
ENV/
.venv/
# IDE
.vscode/
.idea/
*.swp
*.swo
# Project outputs
_out/

View File

@@ -0,0 +1,159 @@
# lzwcai-mcpskills-generate-reports
用户提供 **docx 模板 + JSON 数据**,本包负责渲染成 docx并可选做样式迁移。
本包**不内置模板**,模板完全由调用方维护。
## 安装
```powershell
cd lzwcai_mcpskills_generate_reports
pip install -e .
```
## Python API
### 渲染文档
```python
from lzwcai_mcpskills_generate_reports import generate
out_path = generate(
data="data.json", # dict 或 JSON 文件路径
template="./模板.docx", # 用户自己的 docx 模板路径
out_path="_out/报价方案.docx",
style_ref="./用户样式.docx", # 可选:把用户模板的 theme/字体套过来
)
```
### 扫描模板占位符
```python
from lzwcai_mcpskills_generate_reports import scan_template
result = scan_template("./模板.docx")
print(result)
# {
# "placeholders": ["project_title", "contact_person", "equipments", ...],
# "blocks": [
# {"type": "for", "iterator": "eq", "variable": "equipments"},
# {"type": "if", "condition": "show_layout"},
# ...
# ]
# }
```
## 命令行
```powershell
# 渲染
generate-report generate --template ./模板.docx --data data.json --out _out/报价方案.docx
# 扫描占位符
generate-report scan --template ./模板.docx
# 样式迁移
generate-report generate --template ./模板.docx --data data.json --style-ref ./用户样式.docx --out _out/报价方案_定制.docx
```
## MCP Server
本包同时提供 MCP Serverstdio 模式),把渲染引擎暴露成 3 个 MCP 工具:
| 工具 | 说明 | 必填参数 |
|------|------|----------|
| `generate_report` | 模板 + 数据 → 渲染输出 docx返回输出文件绝对路径 | `template`, `data`, `out`(可选 `style_ref` |
| `scan_template` | 扫描模板占位符与 for/if 块结构 | `template` |
| `validate_report_data` | 校验数据契约(不渲染) | `data` |
其中 `data` 既可以传 JSON 对象,也可以传指向 JSON 文件的路径字符串。
### 启动
```powershell
# 安装后用 console script 启动
lzwcai-mcpskills-generate-reports
# 或直接运行入口模块
python main.py
```
### MCP 客户端配置示例
```json
{
"mcpServers": {
"generate-reports": {
"command": "lzwcai-mcpskills-generate-reports"
}
}
}
```
> stdio 模式下 stdout 被 MCP 协议占用,所有日志写入 `logs/` 目录与 stderr。
## 数据契约QuoteData
```json
{
"project_title": "大米罐装线",
"contact_person": "张经理",
"contact_phone": "138-0000-0000",
"requirements": ["要求1", "要求2"],
"layout_image": "",
"layout_title": "整线布局尺寸图",
"equipments": [
{
"index": "四",
"name": "自动理瓶机",
"images": [""],
"features": [{"title": "特点", "lines": ["说明"]}],
"params": [{"k": "材料", "v": "不锈钢"}]
}
],
"quote_items": [
{"name": "设备名", "qty": "一套", "image": "", "desc": "说明", "price": "面议"}
]
}
```
图片字段约定:路径/URL → 真实图;`""` → 默认占位图;`None` → 不显示。
## 目录结构
```
lzwcai_mcpskills_generate_reports/
├── pyproject.toml
├── README.md
├── templates/ # 用户模板(示例,不在包内)
│ └── standard/
│ ├── template.docx
│ └── meta.json
├── samples/ # 示例数据(不在包内)
│ └── sample_data.json
└── lzwcai_mcpskills_generate_reports/ # Python 包
├── __init__.py # 公共 API 入口
├── cli.py # 命令行
├── pipeline.py # 总入口
├── schema.py # 数据契约 + 校验器
├── render_quote.py # 渲染引擎
├── style_transfer.py # 样式迁移
└── template_scanner.py # 模板占位符扫描
```
## 模板约定
- 使用 [Jinja2](https://jinja.palletsprojects.com/) 语法写占位符,如 `{{ project_title }}``{% for eq in equipments %}`
- 模板文件旁的 `meta.json`(可选)声明图片字段宽度,例如:
```json
{
"image_fields": {
"layout_image": {"width_mm": 160},
"equipments[].images[]": {"width_mm": 120},
"quote_items[].image": {"width_mm": 30}
}
}
```
- BUSINESS 逻辑(内置模板维护、模板市场等)交给包外的上层系统处理。

View File

@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
"""
lzwcai-mcpskills-generate-reports
纯渲染引擎,不内置模板。
对外暴露的公共 API
- generate: 数据 + 模板路径 -> docx核心入口
- scan_template: 模板路径 -> 占位符 JSON
- validate: 校验数据契约
- normalize: 归一化数据
- transplant_style: 将用户模板样式迁移到结果文档
"""
__version__ = "0.1.0"
from .pipeline import generate
from .template_scanner import scan_template
from .schema import validate, normalize, DEFAULTS
from .style_transfer import transplant_style
__all__ = [
"generate",
"scan_template",
"validate",
"normalize",
"transplant_style",
"DEFAULTS",
"__version__",
]

View File

@@ -0,0 +1,122 @@
# -*- coding: utf-8 -*-
"""
cli.py公共入口。
程序调用(推荐):
from lzwcai_mcpskills_generate_reports.cli import generate_report, scan_report
generate_report(
template="./模板.docx",
data={"project_name": "x", ...},
out="_out/a.docx",
)
scan_report(template="./模板.docx")
命令行:
generate-report generate --template ./模板.docx --data data.json --out _out/a.docx
generate-report scan --template ./模板.docx
"""
import argparse
import json
import os
import sys
try:
sys.stdout.reconfigure(encoding="utf-8")
except (AttributeError, OSError):
pass
from lzwcai_mcpskills_generate_reports import generate, scan_template
def _load_data(data):
"""把 data 归一化为 dict支持 dict 或 JSON 文件路径字符串。"""
if isinstance(data, str):
if not os.path.isfile(data):
raise FileNotFoundError(f"数据文件不存在: {data}")
with open(data, "r", encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, dict):
raise TypeError(f"data 必须是 dict 或 JSON 文件路径字符串,实际类型: {type(data).__name__}")
return data
def generate_report(template, data, out, style_ref=None):
"""生成 docx 报告。
参数:
template: 模板 docx 文件路径
data: dict 数据 或 JSON 文件路径字符串
out: 输出 docx 文件路径
style_ref: 用户样式参考 docx 路径(可选)
返回:
dict: {"output": 输出文件绝对路径}
"""
data = _load_data(data)
out = generate(data=data, template=template, out_path=out, style_ref=style_ref)
return {"output": out}
def scan_report(template):
"""扫描模板占位符。
参数:
template: 模板 docx 文件路径
返回:
dict: 占位符扫描结果
"""
return scan_template(template)
def _build_arg_parser():
parser = argparse.ArgumentParser(
description="docx 模板渲染与占位符扫描",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
generate-report generate --template ./模板.docx --data data.json --out _out/a.docx
generate-report generate --template ./模板.docx --data data.json --style-ref 用户样式.docx --out _out/b.docx
generate-report scan --template ./模板.docx
""",
)
sub = parser.add_subparsers(dest="command", required=True)
gen_parser = sub.add_parser("generate", help="渲染生成 docx")
gen_parser.add_argument("--template", required=True, help="模板 docx 文件路径")
gen_parser.add_argument("--data", required=True, help="数据 JSON 文件路径")
gen_parser.add_argument("--out", required=True, help="输出 docx 文件路径")
gen_parser.add_argument("--style-ref", default=None, help="用户上传的样式参考 docx可选")
scan_parser = sub.add_parser("scan", help="扫描模板占位符")
scan_parser.add_argument("--template", required=True, help="模板 docx 文件路径")
return parser
def main():
"""命令行入口console_scripts 调用)。"""
parser = _build_arg_parser()
args = parser.parse_args()
try:
if args.command == "generate":
result = generate_report(
template=args.template,
data=args.data,
out=args.out,
style_ref=args.style_ref,
)
print(f"生成成功: {result['output']}")
elif args.command == "scan":
result = scan_report(template=args.template)
print(json.dumps(result, ensure_ascii=False, indent=2))
except Exception as e:
print(f"错误: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,37 @@
2026-06-16 16:27:35 - root - INFO - [logger_config.py:95] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\logs
2026-06-16 16:27:35 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports'
2026-06-16 16:27:35 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
2026-06-16 16:27:35 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
2026-06-16 16:27:35 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:129] - 收到 ListTools 请求,返回 3 个工具
2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=validate_report_data, arguments={"data": "samples\\sample_data.json"}
2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: validate_report_data
2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=scan_template, arguments={"template": "templates\\standard\\template.docx"}
2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: scan_template
2026-06-16 16:28:01 - root - INFO - [logger_config.py:95] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\logs
2026-06-16 16:28:01 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports'
2026-06-16 16:28:01 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
2026-06-16 16:28:01 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
2026-06-16 16:28:01 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:129] - 收到 ListTools 请求,返回 3 个工具
2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=validate_report_data, arguments={"data": "samples\\sample_data.json"}
2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: validate_report_data
2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=scan_template, arguments={"template": "templates\\standard\\template.docx"}
2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: scan_template
2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=generate_report, arguments={"template": "templates\\standard\\template.docx", "data": "samples\\sample_data.json", "out": "_out\\mcp_test.docx"}
2026-06-16 16:28:20 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: generate_report
2026-06-17 10:11:02 - root - INFO - [logger_config.py:95] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\logs
2026-06-17 10:11:02 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports'
2026-06-17 10:11:02 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
2026-06-17 10:11:02 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
2026-06-17 10:11:02 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:215] - ==================================================
2026-06-17 10:11:02 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:216] - lzwcai-mcpskills-generate-reports MCP Server 启动
2026-06-17 10:11:02 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:217] - ==================================================
2026-06-17 10:11:02 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:218] - 开始运行 MCP Server (stdio 模式)
2026-06-17 10:11:02 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
2026-06-17 10:11:02 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
2026-06-17 10:11:03 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001E55CC2C2C0>
2026-06-17 10:11:03 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
2026-06-17 10:11:03 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
2026-06-17 10:11:03 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:129] - 收到 ListTools 请求,返回 3 个工具
2026-06-17 10:11:03 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent

View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
"""
pipeline.py总入口 — 串起 数据校验 -> 渲染 -> (可选)样式迁移。
注意:本包不维护内置模板。调用方需要自己准备 .docx 模板文件,
并通过 template 参数传入路径。
"""
import os
import json
from .schema import validate, normalize
from .render_quote import render
from .style_transfer import transplant_style
def _load_data(data):
"""把 data 归一化为 dict支持 dict 或 JSON 文件路径。"""
if isinstance(data, str):
if not os.path.isfile(data):
raise FileNotFoundError(f"数据文件不存在: {data}")
with open(data, "r", encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, dict):
raise TypeError(f"data 必须是 dict 或 JSON 文件路径,实际类型: {type(data).__name__}")
return data
def _load_meta_for_template(template_path):
"""如果模板同目录下存在 meta.json则读取否则返回空 dict。"""
meta_file = os.path.join(os.path.dirname(template_path), "meta.json")
if os.path.isfile(meta_file):
with open(meta_file, "r", encoding="utf-8") as f:
return json.load(f)
return {}
def generate(data, template, out_path, style_ref=None):
"""生成报价文档。
参数:
data: QuoteData dict或 JSON 文件路径字符串)
template: 模板 docx 文件路径
out_path: 输出 docx 路径
style_ref: 用户上传的样式参考 docx 路径(可选)
返回:
生成的 docx 绝对路径
"""
data = _load_data(data)
if not isinstance(template, str):
raise TypeError(f"template 必须是文件路径字符串,实际类型: {type(template).__name__}")
if not os.path.isfile(template):
raise FileNotFoundError(f"模板文件不存在: {template}")
template_path = os.path.abspath(template)
# 归一化 + 校验
data = normalize(data)
errs = validate(data)
if errs:
raise ValueError("数据校验失败:\n" + "\n".join(errs))
# 确保输出目录存在
out_dir = os.path.dirname(os.path.abspath(out_path))
if out_dir:
os.makedirs(out_dir, exist_ok=True)
# 渲染(优先读取同目录 meta.json 作为图片配置)
meta = _load_meta_for_template(template_path)
render(data, out_path, template_path, meta=meta)
# 可选样式迁移
if style_ref:
if not os.path.isfile(style_ref):
raise FileNotFoundError(f"样式参考文件不存在: {style_ref}")
transplant_style(out_path, style_ref, out_path)
return os.path.abspath(out_path)

View File

@@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-
"""
render_quote.py渲染引擎 — data + 模板路径 -> docx。
从 reports4 移植,做了两处通用化改造:
1. 模板路径参数化: render(data, out_path, template_path)
2. 图片字段配置驱动: 读 meta.json 的 image_fields 声明,不再写死字段名
"""
import copy
import os
import ssl
import sys
import tempfile
import urllib.request
import json
try:
sys.stdout.reconfigure(encoding="utf-8")
except (AttributeError, OSError):
pass
from docxtpl import DocxTemplate, InlineImage
from docx.shared import Mm
def _resolve_image_path(src, tmp_files):
"""把图片字段值解析为本地文件路径。
下载成功的临时文件会记录到 tmp_files由调用方在渲染结束后统一清理。
"""
def _download(url):
try:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
data = urllib.request.urlopen(req, context=ctx, timeout=30).read()
ext = os.path.splitext(url.split("?")[0])[1] or ".png"
fd, path = tempfile.mkstemp(suffix=ext)
os.write(fd, data)
os.close(fd)
tmp_files.append(path)
return path
except Exception as e:
print(f"[warn] 下载图片失败({url}): {e}")
return None
if src is None:
return ""
if src == "":
return ""
if isinstance(src, str) and src.lower().startswith(("http://", "https://")):
return _download(src) or ""
if os.path.exists(src):
return src
print(f"[warn] 找不到图片({src}),跳过")
return ""
def _make_inline_image(tpl, src, width_mm, tmp_files):
"""创建 InlineImagesrc 为 "" 或 None 时不创建(返回 None"""
resolved = _resolve_image_path(src, tmp_files)
if resolved == "":
return None
return InlineImage(tpl, resolved, width=Mm(width_mm))
def _resolve_field_path(data, path):
"""根据字段路径提取所有目标值。
支持: "root_field" / "list[].field" / "list[].field[]"
"""
results = []
parts = path.split(".")
if len(parts) == 1:
key = parts[0]
if key in data:
results.append((data, key))
return results
# 嵌套列表: "list[].field[]" —— 必须先于单层判断,否则会被 parts[0] 分支误捕获
if len(parts) == 2 and "[]" in parts[1]:
list_name = parts[0].replace("[]", "")
field = parts[1].replace("[]", "")
for item in data.get(list_name, []):
lst = item.get(field, [])
if isinstance(lst, list):
for idx in range(len(lst)):
results.append((lst, idx))
return results
if len(parts) == 2 and "[]" in parts[0]:
list_name = parts[0].replace("[]", "")
field = parts[1]
for item in data.get(list_name, []):
if field in item:
results.append((item, field))
return results
if len(parts) == 2:
list_name = parts[0]
field = parts[1]
for item in data.get(list_name, []):
if field in item:
results.append((item, field))
return results
return results
def _fill_images_from_meta(tpl, data, meta, tmp_files):
"""根据 meta.json 中的 image_fields 声明填充所有图片字段。"""
image_fields = meta.get("image_fields", {})
for path, width_meta in image_fields.items():
width_mm = width_meta.get("width_mm", 50)
refs = _resolve_field_path(data, path)
for container, key in refs:
val = container[key]
if val is None:
container[key] = ""
elif isinstance(val, list):
container[key] = [
_make_inline_image(tpl, x, width_mm, tmp_files) if x is not None else None
for x in val
]
container[key] = [img for img in container[key] if img is not None]
else:
img = _make_inline_image(tpl, val, width_mm, tmp_files)
container[key] = img if img else ""
return data
def render(data, out_path, template_path, meta=None):
"""渲染报价文档。"""
if meta is None:
meta_dir = os.path.dirname(template_path)
meta_file = os.path.join(meta_dir, "meta.json")
if os.path.isfile(meta_file):
with open(meta_file, "r", encoding="utf-8") as f:
meta = json.load(f)
else:
meta = {}
# 深拷贝:避免修改调用方传入的数据,同时把字符串字段替换成 InlineImage
data = copy.deepcopy(dict(data))
tpl = DocxTemplate(template_path)
tmp_files = []
try:
data = _fill_images_from_meta(tpl, data, meta, tmp_files)
tpl.render(data)
tpl.save(out_path)
finally:
for p in tmp_files:
try:
os.remove(p)
except OSError:
pass
return out_path

View File

@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
"""
schema.pyQuoteData 数据契约、校验器、归一化。
所有模板共用同一契约,沿用 reports4 的 SAMPLE 字段结构。
"""
import copy
# ── 缺省值表 ──────────────────────────────────────────────
DEFAULTS = {
"layout_title": "整线布局尺寸图",
"show_layout": True,
}
# 中文数字扩展(用于缺 index 时自动补)
_CN_DIGITS = [
"", "", "", "", "", "", "", "", "", "",
"十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十",
]
# 设备之前的固定章节数:一 公司简介、二 客户要求及分析、三 布局图
_SECTION_OFFSET = 3
def validate(data):
"""校验 QuoteData 结构,返回错误信息列表(空=通过)。
检查:
- 必填顶层字段: project_title, contact_person, contact_phone, requirements
- equipments 元素: 必须有 name
- quote_items 元素: 必须有 name
- features 元素: 必须有 title 和 lines
- params 元素: 必须有 k 和 v
"""
errors = []
for field in ("project_title", "contact_person", "contact_phone", "requirements"):
val = data.get(field)
if val is None or (isinstance(val, str) and val.strip() == ""):
errors.append(f"必填字段缺失或为空: {field}")
# equipments
eqs = data.get("equipments", [])
if not isinstance(eqs, list):
errors.append("equipments 必须为列表")
else:
for i, eq in enumerate(eqs):
if not isinstance(eq, dict):
errors.append(f"equipments[{i}] 必须为对象")
continue
if not eq.get("name"):
errors.append(f"equipments[{i}] 缺少 name")
# features 子元素
for j, feat in enumerate(eq.get("features", [])):
if not isinstance(feat, dict):
errors.append(f"equipments[{i}].features[{j}] 必须为对象")
continue
if not feat.get("title"):
errors.append(f"equipments[{i}].features[{j}] 缺少 title")
if not feat.get("lines") or not isinstance(feat.get("lines"), list):
errors.append(f"equipments[{i}].features[{j}] 缺少 lines 或类型错误")
# params 子元素
for j, p in enumerate(eq.get("params", [])):
if not isinstance(p, dict):
errors.append(f"equipments[{i}].params[{j}] 必须为对象")
continue
if not p.get("k") or not p.get("v"):
errors.append(f"equipments[{i}].params[{j}] 缺少 k 或 v")
# quote_items
items = data.get("quote_items", [])
if not isinstance(items, list):
errors.append("quote_items 必须为列表")
else:
for i, it in enumerate(items):
if not isinstance(it, dict):
errors.append(f"quote_items[{i}] 必须为对象")
continue
if not it.get("name"):
errors.append(f"quote_items[{i}] 缺少 name")
return errors
def normalize(data):
"""归一化数据填缺省值、requirements list->str、index 缺失时自动补。
不修改入参,返回新副本。
"""
d = copy.deepcopy(data)
# requirements: list -> str
req = d.get("requirements")
if isinstance(req, (list, tuple)):
d["requirements"] = "\n".join(str(x) for x in req)
# 缺省值
for k, v in DEFAULTS.items():
if k not in d:
d[k] = v
# equipment index 自动补(从"四"开始,前面三节是公司简介/客户要求/布局图)
for i, eq in enumerate(d.get("equipments", [])):
if not eq.get("index"):
idx = i + 1 + _SECTION_OFFSET
eq["index"] = _CN_DIGITS[idx - 1] if idx <= len(_CN_DIGITS) else str(idx)
# quote_items 缺 image 给空串
for it in d.get("quote_items", []):
it.setdefault("image", "")
# equipments 缺 images 给 [""]
for eq in d.get("equipments", []):
eq.setdefault("images", [""])
# 计算后续章节的动态序号:报价表 & 售后服务
eq_count = len(d.get("equipments", []))
quote_idx = eq_count + _SECTION_OFFSET + 1
after_sales_idx = eq_count + _SECTION_OFFSET + 2
d["section_quote_table"] = _CN_DIGITS[quote_idx - 1] if quote_idx <= len(_CN_DIGITS) else str(quote_idx)
d["section_after_sales"] = _CN_DIGITS[after_sales_idx - 1] if after_sales_idx <= len(_CN_DIGITS) else str(after_sales_idx)
return d

View File

@@ -0,0 +1,223 @@
#!/usr/bin/env python3
"""
lzwcai-mcpskills-generate-reports MCP Server
把 docx 模板渲染引擎封装成 MCP 工具,提供三个工具:
- generate_report: 数据 + 模板路径 -> 渲染输出 docx
- scan_template: 扫描模板占位符 / for / if 块
- validate_report_data: 校验数据契约(不渲染)
stdio 模式运行;所有日志走 stderrstdout 留给 MCP 协议。
"""
import os
import json
import logging
import anyio
import mcp.types as types
from mcp.server import NotificationOptions, Server
from mcp.server.models import InitializationOptions
from mcp.server.stdio import stdio_server
try:
from .utils.logger_config import setup_system_logging, get_logger
from . import generate, scan_template, validate, normalize
except ImportError:
from lzwcai_mcpskills_generate_reports.utils.logger_config import (
setup_system_logging, get_logger,
)
from lzwcai_mcpskills_generate_reports import (
generate, scan_template, validate, normalize,
)
# 初始化日志系统
setup_system_logging(app_name="lzwcai_mcpskills_generate_reports", log_level=logging.DEBUG)
logger = get_logger(__name__)
# 初始化 MCP Server
server = Server("lzwcai_mcpskills_generate_reports")
def _load_data(data):
"""把 data 归一化为 dict支持 dict 或 JSON 文件路径字符串。"""
if isinstance(data, str):
if not os.path.isfile(data):
raise FileNotFoundError(f"数据文件不存在: {data}")
with open(data, "r", encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, dict):
raise TypeError(
f"data 必须是对象或 JSON 文件路径字符串,实际类型: {type(data).__name__}"
)
return data
# ── 工具定义 ──────────────────────────────────────────────
_DATA_DESC = "报价数据:可以是 JSON 对象,或指向 JSON 文件的路径字符串"
TOOL_DEFS = [
types.Tool(
name="generate_report",
description=(
"用 docx 模板 + 结构化数据渲染生成报价文档 docx返回输出文件绝对路径。"
"模板由调用方提供,本工具不内置模板。"
),
inputSchema={
"type": "object",
"properties": {
"template": {
"type": "string",
"description": "模板 docx 文件路径(必填)",
},
"data": {
"type": ["object", "string"],
"description": _DATA_DESC + "(必填)",
},
"out": {
"type": "string",
"description": "输出 docx 文件路径(必填)",
},
"style_ref": {
"type": "string",
"description": "用户上传的样式参考 docx 路径(可选),会把其 theme/字体套到结果文档",
},
},
"required": ["template", "data", "out"],
},
),
types.Tool(
name="scan_template",
description=(
"扫描 docx 模板中的 Jinja2 占位符,返回需要外部提供的顶层变量列表和 for/if 块结构。"
"用于在渲染前了解模板需要哪些数据字段。"
),
inputSchema={
"type": "object",
"properties": {
"template": {
"type": "string",
"description": "模板 docx 文件路径(必填)",
},
},
"required": ["template"],
},
),
types.Tool(
name="validate_report_data",
description=(
"校验报价数据是否符合契约必填字段、equipments/quote_items/features/params 结构),"
"不渲染文档。返回校验结果和错误列表。"
),
inputSchema={
"type": "object",
"properties": {
"data": {
"type": ["object", "string"],
"description": _DATA_DESC + "(必填)",
},
},
"required": ["data"],
},
),
]
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""列出所有可用工具"""
logger.info(f"收到 ListTools 请求,返回 {len(TOOL_DEFS)} 个工具")
return TOOL_DEFS
# ── 工具实现(同步函数,放线程池执行)──────────────────────
def _do_generate_report(arguments: dict) -> dict:
template = arguments["template"]
data = arguments["data"]
out = arguments["out"]
style_ref = arguments.get("style_ref")
data = _load_data(data)
out_path = generate(data=data, template=template, out_path=out, style_ref=style_ref)
return {"output": out_path}
def _do_scan_template(arguments: dict) -> dict:
return scan_template(arguments["template"])
def _do_validate(arguments: dict) -> dict:
data = _load_data(arguments["data"])
norm = normalize(data)
errors = validate(norm)
return {"valid": not errors, "errors": errors}
_HANDLERS = {
"generate_report": _do_generate_report,
"scan_template": _do_scan_template,
"validate_report_data": _do_validate,
}
@server.call_tool()
async def handle_call_tool(
name: str,
arguments: dict | None,
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""调用工具"""
logger.info(
f"收到 CallTool 请求: name={name}, "
f"arguments={json.dumps(arguments, ensure_ascii=False) if arguments else 'None'}"
)
handler = _HANDLERS.get(name)
if handler is None:
logger.error(f"未找到工具: {name}")
raise ValueError(f"未知工具: {name}")
args = arguments or {}
try:
# 渲染/扫描是同步阻塞的 CPU/IO 任务,放线程池避免霸占 event loop
result = await anyio.to_thread.run_sync(handler, args)
logger.info(f"工具执行成功: {name}")
except Exception as e:
logger.error(f"工具执行失败: {name}: {e}", exc_info=True)
result = {"error": str(e), "tool_name": name}
return [
types.TextContent(
type="text",
text=json.dumps(result, ensure_ascii=False, indent=2),
)
]
async def run_server():
"""运行 MCP Server (stdio 模式)"""
async with stdio_server() as streams:
await server.run(
streams[0],
streams[1],
InitializationOptions(
server_name="lzwcai_mcpskills_generate_reports",
server_version="0.1.0",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)
def main():
"""主入口"""
logger.info("=" * 50)
logger.info("lzwcai-mcpskills-generate-reports MCP Server 启动")
logger.info("=" * 50)
logger.info("开始运行 MCP Server (stdio 模式)")
anyio.run(run_server)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
"""
style_transfer.py从用户上传的 docx 抽取视觉样式theme1.xml + styles.xml 的默认字体引用),
套到内置骨架渲染结果上,产出"风格接近用户模板"的文档。
Level 1 (MVP):整体替换 theme + 默认字体引用,不整体覆盖 styles避免版式崩
"""
import os
import zipfile
import tempfile
import shutil
import xml.etree.ElementTree as ET
# Word OOXML 命名空间
_NS = {
"a": "http://schemas.openxmlformats.org/drawingml/2006/main",
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
}
# 主题字体引用中 major/minor 属性的前缀
_THEME_PREFIXES = ("majorHAnsi", "minorHAnsi", "majorEastAsia", "minorEastAsia",
"majorCs", "minorCs", "major", "minor")
def _unzip_docx(docx_path, target_dir):
"""解压 docxzip到目标目录。"""
with zipfile.ZipFile(docx_path, "r") as z:
z.extractall(target_dir)
def _zip_docx(source_dir, docx_path):
"""将目录内容打包为 docxzip"""
with zipfile.ZipFile(docx_path, "w", zipfile.ZIP_DEFLATED) as z:
for root, _dirs, files in os.walk(source_dir):
for fname in files:
full = os.path.join(root, fname)
arcname = os.path.relpath(full, source_dir)
z.write(full, arcname)
def _safe_xml_read(xml_path):
"""安全读取 XML 文件,返回 ElementTree 和根元素。失败返回 (None, None)。"""
try:
tree = ET.parse(xml_path)
return tree, tree.getroot()
except Exception:
return None, None
def _copy_theme(src_dir, dst_dir):
"""将 src 的 word/theme/theme1.xml 复制到 dst。"""
src_theme = os.path.join(src_dir, "word", "theme", "theme1.xml")
dst_theme = os.path.join(dst_dir, "word", "theme", "theme1.xml")
if os.path.isfile(src_theme):
os.makedirs(os.path.dirname(dst_theme), exist_ok=True)
shutil.copy2(src_theme, dst_theme)
return True
return False
def _copy_font_table(src_dir, dst_dir):
"""将 src 的 word/fontTable.xml 复制到 dst。"""
src_ft = os.path.join(src_dir, "word", "fontTable.xml")
dst_ft = os.path.join(dst_dir, "word", "fontTable.xml")
if os.path.isfile(src_ft):
shutil.copy2(src_ft, dst_ft)
return True
return False
def _merge_default_fonts(src_styles_path, dst_styles_path):
"""合并默认字体引用docDefaults 中的 rFonts从 src 到 dst。
不整体覆盖 styles.xml只替换 docDefaults 里的字体引用。
"""
if not os.path.isfile(src_styles_path) or not os.path.isfile(dst_styles_path):
return False
_, src_root = _safe_xml_read(src_styles_path)
dst_tree, dst_root = _safe_xml_read(dst_styles_path)
if src_root is None or dst_root is None:
return False
w_ns = _NS["w"]
# 找 src 的 docDefaults
src_doc_defaults = src_root.find(f".//{{{w_ns}}}docDefaults")
if src_doc_defaults is None:
return False
src_rpr = src_doc_defaults.find(f"{{{w_ns}}}rPrDefault/{{{w_ns}}}rFonts")
if src_rpr is None:
return False
# 找 dst 的 docDefaults
dst_doc_defaults = dst_root.find(f".//{{{w_ns}}}docDefaults")
if dst_doc_defaults is None:
return False
dst_rpr = dst_doc_defaults.find(f"{{{w_ns}}}rPrDefault/{{{w_ns}}}rFonts")
if dst_rpr is None:
# 如果 dst 没有 rFonts创建并追加
rpr_default = dst_doc_defaults.find(f"{{{w_ns}}}rPrDefault")
if rpr_default is None:
rpr_default = ET.SubElement(dst_doc_defaults, f"{{{w_ns}}}rPrDefault")
rpr_default.append(src_rpr)
else:
# 替换 rFonts 的属性(主题字体引用)
for attr_name, attr_val in src_rpr.attrib.items():
if any(attr_name.endswith(prefix) for prefix in _THEME_PREFIXES):
dst_rpr.set(attr_name, attr_val)
# 保存
dst_tree.write(dst_styles_path, xml_declaration=True, encoding="UTF-8", standalone="yes")
return True
def transplant_style(result_path, user_template_path, out_path):
"""将用户模板的视觉样式移植到结果文档上。
参数:
result_path: 内置骨架渲染出的 docx 路径
user_template_path: 用户上传的样式参考 docx
out_path: 输出路径(可与 result_path 相同,原地覆盖)
"""
# 校验用户模板可打开
try:
from docx import Document
Document(user_template_path)
except Exception as e:
print(f"[warn] 用户上传模板无法打开,跳过样式迁移: {e}")
if out_path != result_path:
shutil.copy2(result_path, out_path)
return out_path
# 在打包覆盖前先记录原文档段落数out_path 可能 == result_path原地覆盖后就读不到原值了
src_count = None
try:
from docx import Document
src_count = len(Document(result_path).paragraphs)
except Exception as e:
print(f"[warn] 读取原文档段落数失败,迁移后将跳过段落数校验: {e}")
# 创建临时工作目录
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(tmpdir, "src") # 用户上传模板
dst_dir = os.path.join(tmpdir, "dst") # 渲染结果
os.makedirs(src_dir)
os.makedirs(dst_dir)
# 解压
_unzip_docx(user_template_path, src_dir)
_unzip_docx(result_path, dst_dir)
# 1) 替换 theme1.xml
_copy_theme(src_dir, dst_dir)
# 2) 替换 fontTable.xml
_copy_font_table(src_dir, dst_dir)
# 3) 合并默认字体引用
src_styles = os.path.join(src_dir, "word", "styles.xml")
dst_styles = os.path.join(dst_dir, "word", "styles.xml")
_merge_default_fonts(src_styles, dst_styles)
# 重新打包
_zip_docx(dst_dir, out_path)
# 校验:能正常打开且段落数与迁移前一致
try:
from docx import Document
dst_count = len(Document(out_path).paragraphs)
if src_count is None:
print(f"[ok] 样式迁移完成(无原始段落数可比对),输出段落数: {dst_count}")
elif dst_count == 0 or dst_count != src_count:
print(f"[warn] 样式迁移后段落数不一致 (src={src_count}, dst={dst_count}),可能损坏")
else:
print(f"[ok] 样式迁移完成,段落数一致: {src_count}")
except Exception as e:
print(f"[error] 样式迁移后文档无法打开: {e}")
return out_path

View File

@@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
"""
template_scanner.py扫描 docx 模板中的 Jinja2 占位符。
只读模板,不渲染;返回模板里要求外部提供的数据字段清单。
"""
import re
import zipfile
import xml.etree.ElementTree as ET
from jinja2 import Environment, meta
from jinja2 import nodes as jinja_nodes
def _iter_docx_text(docx_path):
"""遍历 docx 中所有 XML 文本节点,产出原始字符串片段。"""
with zipfile.ZipFile(docx_path, "r") as z:
for name in z.namelist():
# 只关心 word 主文档、页眉、页脚
if not (name.startswith("word/document") or name.startswith("word/header") or name.startswith("word/footer")):
continue
data = z.read(name)
try:
root = ET.fromstring(data)
except ET.ParseError:
continue
# w:t 节点存放文本
for elem in root.iter():
if elem.tag.endswith("}t"):
if elem.text:
yield elem.text
def _extract_source(docx_path):
"""把 docx 所有文本片段拼成一段连续的源文本,便于 Jinja2 解析。"""
return "\n".join(_iter_docx_text(docx_path))
def _normalize_docxtpl_tags(source):
"""把 docxtpl 段落/行/单元格级标签 {%p ... %} {%tr ... %} {%tc ... %} 还原为 {% ... %}。
docxtpl 用 {%p、{%tr、{%tc 控制块作用于段落、表格行、表格单元格;
扫描占位符时不需要这些粒度信息,统一成标准 Jinja2 标签即可解析。
"""
return re.sub(r"{%\s*(?:p|tr|tc)\s+", "{% ", source)
def _walk_blocks(node, result=None):
"""遍历 Jinja2 AST收集 For / If 块信息。"""
if result is None:
result = []
if node is None:
return result
if isinstance(node, jinja_nodes.For):
iter_name = _expression_name(node.iter)
target_name = _expression_name(node.target)
result.append({
"type": "for",
"iterator": target_name,
"variable": iter_name,
})
for child in node.body:
_walk_blocks(child, result)
for child in node.else_ or []:
_walk_blocks(child, result)
return result
if isinstance(node, jinja_nodes.If):
test_name = _expression_name(node.test)
result.append({
"type": "if",
"condition": test_name,
})
for child in node.body:
_walk_blocks(child, result)
for child in node.else_ or []:
_walk_blocks(child, result)
return result
if hasattr(node, "body"):
for child in node.body:
_walk_blocks(child, result)
if hasattr(node, "else_") and node.else_:
for child in node.else_:
_walk_blocks(child, result)
return result
def _expression_name(expr):
"""把 Jinja2 表达式尽量还原为可读的字符串。"""
if expr is None:
return None
if isinstance(expr, jinja_nodes.Name):
return expr.name
if isinstance(expr, jinja_nodes.Const):
return str(expr.value)
if isinstance(expr, jinja_nodes.Getattr):
return f"{_expression_name(expr.node)}.{expr.attr}"
if isinstance(expr, jinja_nodes.Getitem):
return f"{_expression_name(expr.node)}[{_expression_name(expr.arg)}]"
return str(expr)
def scan_template(template_path):
"""扫描 docx 模板,返回占位符信息 JSON。
返回:
{
"placeholders": ["project_title", "equipments", ...], # 需要外部提供的最顶层变量
"blocks": [
{"type": "for", "iterator": "eq", "variable": "equipments"},
{"type": "if", "condition": "show_layout"},
...
]
}
"""
source = _normalize_docxtpl_tags(_extract_source(template_path))
if not source.strip():
return {"placeholders": [], "blocks": []}
env = Environment()
ast = env.parse(source)
variables = sorted(meta.find_undeclared_variables(ast))
blocks = _walk_blocks(ast)
return {
"placeholders": variables,
"blocks": blocks,
}

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
"""Utils package for lzwcai_mcpskills_generate_reports"""
from .logger_config import setup_system_logging, get_logger
__all__ = ["setup_system_logging", "get_logger"]

View File

@@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
"""
统一日志配置模块
提供系统级别的日志配置和管理
注意MCP 协议使用 stdio 通信时stdout 被协议占用,
所有日志必须输出到 stderr 或文件,绝不能写 stdout。
"""
import os
import sys
import logging
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
from pathlib import Path
class LoggerConfig:
"""日志配置管理类"""
def __init__(self, logs_dir: str = None):
if logs_dir:
self.logs_dir = Path(logs_dir)
else:
project_root = Path(__file__).parent.parent
self.logs_dir = project_root / "logs"
self.logs_dir.mkdir(exist_ok=True)
self.log_format = '%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
self.date_format = '%Y-%m-%d %H:%M:%S'
self.log_level = self._get_log_level_from_env()
self._initialized = False
def _get_log_level_from_env(self) -> int:
log_level_str = os.getenv('LOG_LEVEL', 'INFO').upper()
level_mapping = {
'DEBUG': logging.DEBUG,
'INFO': logging.INFO,
'WARNING': logging.WARNING,
'WARN': logging.WARNING,
'ERROR': logging.ERROR,
'CRITICAL': logging.CRITICAL,
'FATAL': logging.CRITICAL,
}
return level_mapping.get(log_level_str, logging.INFO)
def setup_logging(self,
app_name: str = "lzwcai_mcpskills_generate_reports",
log_level: int = logging.INFO,
max_file_size: int = 10 * 1024 * 1024,
backup_count: int = 5,
console_output: bool = True) -> logging.Logger:
if self._initialized:
return logging.getLogger()
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
formatter = logging.Formatter(self.log_format, self.date_format)
# 1. 主日志文件 - 按大小滚动
main_log_file = self.logs_dir / f"{app_name}.log"
file_handler = RotatingFileHandler(
main_log_file, maxBytes=max_file_size,
backupCount=backup_count, encoding='utf-8',
)
file_handler.setLevel(log_level)
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
# 2. 错误日志文件
error_log_file = self.logs_dir / f"{app_name}_error.log"
error_handler = RotatingFileHandler(
error_log_file, maxBytes=max_file_size,
backupCount=backup_count, encoding='utf-8',
)
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(formatter)
root_logger.addHandler(error_handler)
# 3. 控制台输出到 stderrstdio 模式下 stdout 被 MCP 协议占用)
if console_output:
console_handler = logging.StreamHandler(sys.stderr)
console_handler.setLevel(log_level)
console_handler.setFormatter(logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
self.date_format,
))
root_logger.addHandler(console_handler)
self._initialized = True
root_logger.info(f"日志系统初始化完成 - 日志目录: {self.logs_dir}")
return root_logger
def get_module_logger(self, module_name: str) -> logging.Logger:
return logging.getLogger(module_name)
# 全局日志配置实例
logger_config = LoggerConfig()
def setup_system_logging(app_name: str = "lzwcai_mcpskills_generate_reports",
log_level: int = logging.INFO) -> logging.Logger:
return logger_config.setup_logging(app_name, log_level)
def get_logger(name: str) -> logging.Logger:
return logger_config.get_module_logger(name)

View File

@@ -0,0 +1,10 @@
"""
Entry point for lzwcai-mcpskills-generate-reports
Runs the MCP server (stdio mode) for docx report generation.
"""
from lzwcai_mcpskills_generate_reports.server import main
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,40 @@
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "lzwcai-mcpskills-generate-reports"
version = "0.1.0"
description = "Render styled quotation documents from user-supplied docx templates and structured data"
readme = "README.md"
requires-python = ">=3.12"
keywords = ["docx", "quotation", "report", "template", "jinja2"]
authors = [
{ name = "LzwCai", email = "lzwcai@example.com" },
]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"docxtpl>=0.16.0",
"python-docx>=1.1.0",
"Jinja2>=3.1.0",
"mcp>=1.0.0",
"anyio>=4.0.0",
]
[project.scripts]
generate-report = "lzwcai_mcpskills_generate_reports.cli:main"
lzwcai-mcpskills-generate-reports = "lzwcai_mcpskills_generate_reports.server:main"
[project.urls]
Homepage = "https://github.com/lzwcai/lzwcai-mcpskills-generate-reports"
Repository = "https://github.com/lzwcai/lzwcai-mcpskills-generate-reports"
[tool.setuptools.packages.find]
where = ["."]
include = ["lzwcai_mcpskills_generate_reports*"]
exclude = ["tests*", "_out*", "templates*", "samples*"]

View File

@@ -0,0 +1,412 @@
{
"project_title": "大米圆形纸罐全自动灌装包装整线项目",
"contact_person": "张卫国经理",
"contact_phone": "138-1568-9632",
"contact_company": "XX粮油食品有限公司",
"requirements": [
"包装物料成品精制大米流动性颗粒物料单罐净重规格250g、500g、1000g三档快速切换。",
"罐体参数:圆形食品级纸罐,配套易拉预封口+外旋螺纹盖双重密封纸罐固定外径100mm10cm高度分3档60mm(6cm)/120mm(12cm)/220mm(22cm),同线兼容三规格自动换型无需更换夹具。",
"额定包装速度以500g标准罐计稳定产能≥20罐/分钟连续24h不间断运行无卡罐、漏装。",
"整线标配:大米定量称重灌装机、罐身输送线、自动上盖机、四轮旋盖机、成品出料输送机;",
"客户选配完整模组:在线金属检测机(带自动剔除机构)+重量复检机(不合格自动分流剔除)+后端自动开箱机+折盖封箱一体机+机器人码垛单元;",
"全线材质要求物料接触部位304不锈钢符合食品QS/SC生产卫生规范支持水洗清洁。"
],
"layout_image": "https://dscache.tencent-cloud.cn/upload/nodir/rice-can-line-layout-202605.png",
"layout_title": "大米纸罐灌装整线平面布局尺寸总图",
"equipments": [
{
"name": "Z型大米颗粒上料提升机",
"images": [
"https://dscache.tencent-cloud.cn/upload/nodir/z-conveyor-01.png",
"https://dscache.tencent-cloud.cn/upload/nodir/z-conveyor-detail.png"
],
"features": [
{
"title": "粮食专用密封提升",
"lines": [
"封闭式料斗输送,无大米撒料、扬尘,车间粉尘达标;适配大米、杂粮等颗粒原料连续上料。",
"料斗加厚304不锈钢耐磨抗冲击不易积粮霉变便于高压水枪冲洗。"
]
},
{
"title": "变频调速稳定可控",
"lines": [
"独立变频电机调速,上料流量与灌装主机信号联动匹配,不会断料或溢料。",
"低噪音链条传动,连续运行故障率低,维护简单,可长期满负荷生产。"
]
}
],
"params": [
{
"k": "有效提升高度",
"v": "3.6m(支持现场按需加长定制)"
},
{
"k": "输送速度可调范围",
"v": "0~16m/min"
},
{
"k": "最大输送产能",
"v": "7.2m³/h满足25罐/分钟灌装余量"
},
{
"k": "驱动总功率",
"v": "0.55kW"
},
{
"k": "供电制式",
"v": "380V三相 50Hz"
},
{
"k": "整机净重",
"v": "385kg"
},
{
"k": "物料接触材质",
"v": "304食品级不锈钢"
}
]
},
{
"name": "多头称重式大米灌装机",
"images": [
"https://dscache.tencent-cloud.cn/upload/nodir/rice-filling-machine.png"
],
"features": [
{
"title": "高精度称重灌装",
"lines": [
"二级快慢加料250g/500g/1000g重量参数触摸屏一键调用单罐称重误差≤±0.8g。",
"独立称重料斗,不受物料料位高低影响,长时间灌装重量一致性稳定。"
]
},
{
"title": "食品级卫生设计",
"lines": [
"料仓、下料口快拆结构,无需工具即可拆卸清洗,无死角存粮。",
"整机带防尘外封板,避免蚊虫、杂物混入大米成品。"
]
}
],
"params": [
{
"k": "灌装量程",
"v": "100~1200g可调"
},
{
"k": "额定产能",
"v": "22~28罐/分钟500g规格"
},
{
"k": "允许罐型外径",
"v": "60~120mm"
},
{
"k": "整机功率",
"v": "1.2kW"
},
{
"k": "外形长宽高",
"v": "1450×920×1850mm"
},
{
"k": "整机重量",
"v": "420kg"
},
{
"k": "操作界面",
"v": "7寸触摸屏+PLC全自动控制"
}
]
},
{
"name": "自动纸罐理罐机",
"images": [
"https://dscache.tencent-cloud.cn/upload/nodir/can-sorter.png"
],
"features": [
{
"title": "圆形纸罐定向排序",
"lines": [
"自动散乱上罐、扶正定位、有序输送杜绝倒罐、卡罐适配本次100mm外径纸罐三高度规格。",
"定位挡板手摇快速调节换型时间≤3分钟。"
]
},
{
"title": "整机耐用易维护",
"lines": [
"机架304不锈钢输送带独立无极变频调速可和灌装主机速度同步联动。",
"机械结构简洁,易损件少,车间操作工可独立日常检修。"
]
}
],
"params": [
{
"k": "主体材质",
"v": "304不锈钢机架+食品级PU输送带"
},
{
"k": "整机外形尺寸",
"v": "1020×810×1220mm"
},
{
"k": "最大处理产能",
"v": "30~50罐/分钟"
},
{
"k": "理罐转盘直径",
"v": "800mm"
},
{
"k": "适配罐体外径",
"v": "60~200mm"
},
{
"k": "工作电压",
"v": "单相220V50Hz"
},
{
"k": "整机重量",
"v": "145kg"
}
]
},
{
"name": "自动上盖机+单头四轮旋盖一体机",
"images": [
"https://dscache.tencent-cloud.cn/upload/nodir/capping-twist-machine.png"
],
"features": [
{
"title": "自动送盖+定位旋盖一体化",
"lines": [
"料仓自动整理螺纹外盖,分盖、落盖精准套入罐口,四轮橡胶轮柔性夹紧旋紧,不会压扁纸质罐口。",
"旋盖扭矩数字可调,杜绝滑盖、拧过紧纸罐变形,适配易拉内封+外旋盖双层封口工艺。"
]
},
{
"title": "速度同步联动",
"lines": [
"变频调速跟随灌装线主线速度,无空罐漏旋,缺盖自动停机报警提示补料。"
]
}
],
"params": [
{
"k": "供电规格",
"v": "220V 50Hz"
},
{
"k": "整机装机功率",
"v": "1.0kW"
},
{
"k": "适配罐口直径",
"v": "35~130mm"
},
{
"k": "稳定旋盖速度",
"v": "25~32罐/分钟"
},
{
"k": "设备外形尺寸",
"v": "2020×660×1510mm"
},
{
"k": "整机重量",
"v": "295kg"
},
{
"k": "扭矩调节方式",
"v": "数字扭矩电控调节"
}
]
},
{
"name": "在线金属检测机(带剔除)",
"images": [
"https://dscache.tencent-cloud.cn/upload/nodir/metal-detector-reject.png"
],
"features": [
{
"title": "高精度金属异物检测",
"lines": [
"可检出铁、不锈钢、铜、铝等混入大米内金属碎屑、螺钉刀片,不合格罐气动自动剔除分流,不混入合格品。",
"检测灵敏度数字可调,产品记忆存储,多规格一键切换。"
]
}
],
"params": [
{
"k": "检测通道尺寸",
"v": "宽140mm×高280mm"
},
{
"k": "检测灵敏度",
"v": "Feφ1.0mm、SUSφ2.2mm"
},
{
"k": "剔除方式",
"v": "气动推杆自动剔除"
},
{
"k": "适配线速",
"v": "0~30m/min"
},
{
"k": "功率",
"v": "0.37kW"
}
]
},
{
"name": "成品重量复检机",
"images": [
"https://dscache.tencent-cloud.cn/upload/nodir/checkweigher.png"
],
"features": [
{
"title": "缺料超重自动分选",
"lines": [
"在线动态称重,低于下限、高于上限罐体自动剔除,杜绝少装、多装次品流入装箱工序。"
]
}
],
"params": [
{
"k": "称重量程",
"v": "0~2000g"
},
{
"k": "称重精度",
"v": "±0.3g"
},
{
"k": "剔除方式",
"v": "气动拨杆剔除"
}
]
},
{
"name": "后端自动开箱+折盖封箱一体机",
"images": [
"https://dscache.tencent-cloud.cn/upload/nodir/case-sealer.png"
],
"features": [
{
"title": "纸箱成型封底+上盖折平封箱一次完成",
"lines": [
"整垛纸箱自动吸取撑开、底部胶带封牢,成品罐装箱后自动折左右上盖,上下工字封箱,适配整线连续自动化装箱。"
]
}
],
"params": [
{
"k": "适用纸箱尺寸范围",
"v": "长250~450×宽180~320×高150~400mm"
},
{
"k": "封箱速度",
"v": "6~12箱/分钟"
},
{
"k": "总功率",
"v": "1.8kW"
}
]
},
{
"name": "机械臂码垛机",
"images": [
"https://dscache.tencent-cloud.cn/upload/nodir/robot-palletizer.png"
],
"features": [
{
"title": "纸箱自动堆叠码垛",
"lines": [
"伺服抓手抓取成品整箱,按预设垛型整齐码放在托盘上,码垛高度、层数程序可调,替代人工堆垛。"
]
}
],
"params": [
{
"k": "最大负载",
"v": "25kg/箱"
},
{
"k": "码垛高度上限",
"v": "1800mm"
},
{
"k": "工作节拍",
"v": "8~12箱/分钟"
}
]
}
],
"quote_items": [
{
"name": "Z型大米上料提升机",
"qty": "1套",
"image": "https://dscache.tencent-cloud.cn/upload/nodir/z-conveyor-01.png",
"desc": "304不锈钢封闭式粮食提升变频调速配套灌装主机联动上料",
"price": "面议"
},
{
"name": "多头称重大米灌装机",
"qty": "1套",
"image": "https://dscache.tencent-cloud.cn/upload/nodir/rice-filling-machine.png",
"desc": "三规格重量一键切换,高精度称重下料,食品级快拆清洗结构",
"price": "面议"
},
{
"name": "自动纸罐理罐机",
"qty": "1套",
"image": "https://dscache.tencent-cloud.cn/upload/nodir/can-sorter.png",
"desc": "圆形纸罐自动排序扶正适配Φ100mm三高度纸罐快速换型",
"price": "面议"
},
{
"name": "自动上盖+四轮旋盖一体机",
"qty": "1套",
"image": "https://dscache.tencent-cloud.cn/upload/nodir/capping-twist-machine.png",
"desc": "自动分盖上盖,数字扭矩旋紧,适配纸罐易拉盖+外旋盖双层封口",
"price": "面议"
},
{
"name": "金属检测机(带剔除)",
"qty": "1套",
"image": "https://dscache.tencent-cloud.cn/upload/nodir/metal-detector-reject.png",
"desc": "在线金属异物检测,不合格罐体自动剔除分流,食品生产合规必备",
"price": "面议"
},
{
"name": "重量复检剔除机",
"qty": "1套",
"image": "https://dscache.tencent-cloud.cn/upload/nodir/checkweigher.png",
"desc": "动态在线称重,超重欠重次品自动剔除,保证净含量达标",
"price": "面议"
},
{
"name": "自动开箱折盖封箱一体机",
"qty": "1套",
"image": "https://dscache.tencent-cloud.cn/upload/nodir/case-sealer.png",
"desc": "纸箱自动成型封底、装箱后折盖封箱,后端自动化装箱配套",
"price": "面议"
},
{
"name": "机器人码垛单元",
"qty": "1套",
"image": "https://dscache.tencent-cloud.cn/upload/nodir/robot-palletizer.png",
"desc": "抓取成品纸箱自动码垛堆托,解放后端人工搬运堆叠",
"price": "面议"
},
{
"name": "全线不锈钢输送过渡机架+电控总控制柜",
"qty": "1套",
"image": "https://dscache.tencent-cloud.cn/upload/nodir/line-conveyor-total.png",
"desc": "各设备接驳输送线、整机联动PLC总控制系统、急停、报警、联动互锁整套电气配套",
"price": "面议"
}
]
}

View File

@@ -0,0 +1,11 @@
{
"name": "标准罐装线模板",
"description": "默认模板,适用于大多数罐装线报价方案",
"image_fields": {
"layout_image": {"width_mm": 160, "scope": "root"},
"equipments[].images[]": {"width_mm": 120, "scope": "list"},
"quote_items[].image": {"width_mm": 30, "scope": "list"}
},
"required": ["project_title", "contact_person", "contact_phone", "requirements"],
"optional_sections": ["show_layout"]
}