cscec-gwdoc-mcp 0.1.0__tar.gz
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.
- cscec_gwdoc_mcp-0.1.0/PKG-INFO +57 -0
- cscec_gwdoc_mcp-0.1.0/README.md +43 -0
- cscec_gwdoc_mcp-0.1.0/cscec_gwdoc_mcp/__init__.py +4 -0
- cscec_gwdoc_mcp-0.1.0/cscec_gwdoc_mcp/assets//345/205/254/346/226/207/346/240/274/345/274/217/350/214/203/346/226/207.docx +0 -0
- cscec_gwdoc_mcp-0.1.0/cscec_gwdoc_mcp/server.py +166 -0
- cscec_gwdoc_mcp-0.1.0/cscec_gwdoc_mcp/service.py +244 -0
- cscec_gwdoc_mcp-0.1.0/pyproject.toml +25 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cscec-gwdoc-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 中建企业公文 Word(.docx)生成 stdio MCP 服务,供百炼函数计算 uvx 托管接入。
|
|
5
|
+
Author: CSCEC
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Keywords: cscec,docx,mcp,word,公文
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: lxml>=4.9
|
|
10
|
+
Requires-Dist: mcp[cli]<2,>=1.0
|
|
11
|
+
Requires-Dist: pydantic>=2.0
|
|
12
|
+
Requires-Dist: python-docx>=1.1
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# cscec-gwdoc-mcp
|
|
16
|
+
|
|
17
|
+
中建企业公文 Word(.docx)生成 **stdio MCP** 服务。可被 `uvx` 安装运行,适配阿里云百炼函数计算(FC)代码包(uvx)托管模式,再由中建 AI 等外部 MCP 客户端经百炼接入。
|
|
18
|
+
|
|
19
|
+
## 工具
|
|
20
|
+
|
|
21
|
+
`generate_official_doc`:根据结构化公文内容生成 `.docx`。
|
|
22
|
+
|
|
23
|
+
| 参数 | 类型 | 说明 |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `title` | str | 公文标题(必填) |
|
|
26
|
+
| `sections` | list[dict] | 章节列表,每项 `{heading, paragraphs, subheadings?}` |
|
|
27
|
+
| `document_number` | str | 文号 |
|
|
28
|
+
| `recipient` | str | 主送单位 |
|
|
29
|
+
| `introduction` | str | 导语 |
|
|
30
|
+
| `attachments` | list[str] | 附件名称 |
|
|
31
|
+
| `issuer` | str | 发文单位 |
|
|
32
|
+
| `department` | str | 发文部门(代章) |
|
|
33
|
+
| `issue_date` | str | 发文日期 |
|
|
34
|
+
| `cc` | str | 抄送 |
|
|
35
|
+
| `document_type` | str | 公文类型,默认「通报」 |
|
|
36
|
+
|
|
37
|
+
## 运行
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uvx cscec-gwdoc-mcp
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
服务以 stdio 方式启动;生成的 `.docx` 作为 MCP 内嵌二进制资源(base64)随工具结果回传,不提供公网下载链接。
|
|
44
|
+
|
|
45
|
+
## 本地开发自测
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv venv
|
|
49
|
+
uv pip install -e .
|
|
50
|
+
python -m cscec_gwdoc_mcp.server
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 数据与排版说明
|
|
54
|
+
|
|
55
|
+
- 生成器优先套用包内范文的页面设置、Word 样式及页眉页脚,再重建正文。
|
|
56
|
+
- 未提供的文号、日期、单位、统计数字、整改时限等应标记为待补充,人工核对后再正式发文。
|
|
57
|
+
- 「方正小标宋简体」「仿宋_GB2312」「楷体_GB2312」等字体需目标环境安装,缺字体会被自动替换。
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# cscec-gwdoc-mcp
|
|
2
|
+
|
|
3
|
+
中建企业公文 Word(.docx)生成 **stdio MCP** 服务。可被 `uvx` 安装运行,适配阿里云百炼函数计算(FC)代码包(uvx)托管模式,再由中建 AI 等外部 MCP 客户端经百炼接入。
|
|
4
|
+
|
|
5
|
+
## 工具
|
|
6
|
+
|
|
7
|
+
`generate_official_doc`:根据结构化公文内容生成 `.docx`。
|
|
8
|
+
|
|
9
|
+
| 参数 | 类型 | 说明 |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| `title` | str | 公文标题(必填) |
|
|
12
|
+
| `sections` | list[dict] | 章节列表,每项 `{heading, paragraphs, subheadings?}` |
|
|
13
|
+
| `document_number` | str | 文号 |
|
|
14
|
+
| `recipient` | str | 主送单位 |
|
|
15
|
+
| `introduction` | str | 导语 |
|
|
16
|
+
| `attachments` | list[str] | 附件名称 |
|
|
17
|
+
| `issuer` | str | 发文单位 |
|
|
18
|
+
| `department` | str | 发文部门(代章) |
|
|
19
|
+
| `issue_date` | str | 发文日期 |
|
|
20
|
+
| `cc` | str | 抄送 |
|
|
21
|
+
| `document_type` | str | 公文类型,默认「通报」 |
|
|
22
|
+
|
|
23
|
+
## 运行
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uvx cscec-gwdoc-mcp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
服务以 stdio 方式启动;生成的 `.docx` 作为 MCP 内嵌二进制资源(base64)随工具结果回传,不提供公网下载链接。
|
|
30
|
+
|
|
31
|
+
## 本地开发自测
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv venv
|
|
35
|
+
uv pip install -e .
|
|
36
|
+
python -m cscec_gwdoc_mcp.server
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 数据与排版说明
|
|
40
|
+
|
|
41
|
+
- 生成器优先套用包内范文的页面设置、Word 样式及页眉页脚,再重建正文。
|
|
42
|
+
- 未提供的文号、日期、单位、统计数字、整改时限等应标记为待补充,人工核对后再正式发文。
|
|
43
|
+
- 「方正小标宋简体」「仿宋_GB2312」「楷体_GB2312」等字体需目标环境安装,缺字体会被自动替换。
|
|
Binary file
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""stdio MCP 服务:中建企业公文 Word 生成。
|
|
3
|
+
|
|
4
|
+
通过 FastMCP 以 stdio 方式运行(供百炼函数计算 uvx 托管)。
|
|
5
|
+
生成的 .docx 不再走公网下载链接,而是作为 MCP 内嵌二进制资源(base64)
|
|
6
|
+
随工具结果回传。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import base64
|
|
12
|
+
import re
|
|
13
|
+
import tempfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
from urllib.parse import quote
|
|
17
|
+
|
|
18
|
+
from importlib.resources import files
|
|
19
|
+
|
|
20
|
+
from mcp.server.fastmcp import FastMCP
|
|
21
|
+
from mcp.types import BlobResourceContents, EmbeddedResource, TextContent
|
|
22
|
+
|
|
23
|
+
from .service import OfficialDocument, SectionItem, build_document
|
|
24
|
+
|
|
25
|
+
DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
26
|
+
ASSET_NAME = "公文格式范文.docx"
|
|
27
|
+
|
|
28
|
+
mcp = FastMCP("cscec-gwdoc-mcp")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _template_bytes() -> bytes:
|
|
32
|
+
"""读取包内范文模板二进制。"""
|
|
33
|
+
return files("cscec_gwdoc_mcp.assets").joinpath(ASSET_NAME).read_bytes()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _safe_filename(title: str) -> str:
|
|
37
|
+
"""清理文件名中的非法字符。"""
|
|
38
|
+
name = re.sub(r'[\\/:*?"<>|\r\n]+', "_", title).strip()
|
|
39
|
+
return name or "公文"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _build_document_payload(
|
|
43
|
+
title: str,
|
|
44
|
+
sections: Optional[List[Dict[str, Any]]] = None,
|
|
45
|
+
document_number: str = "",
|
|
46
|
+
recipient: str = "",
|
|
47
|
+
introduction: str = "",
|
|
48
|
+
attachments: Optional[List[str]] = None,
|
|
49
|
+
issuer: str = "",
|
|
50
|
+
department: str = "",
|
|
51
|
+
issue_date: str = "",
|
|
52
|
+
cc: str = "",
|
|
53
|
+
document_type: str = "通报",
|
|
54
|
+
) -> OfficialDocument:
|
|
55
|
+
section_items: List[SectionItem] = []
|
|
56
|
+
for raw in sections or []:
|
|
57
|
+
if not isinstance(raw, dict):
|
|
58
|
+
continue
|
|
59
|
+
section_items.append(
|
|
60
|
+
SectionItem(
|
|
61
|
+
heading=str(raw.get("heading", "")),
|
|
62
|
+
paragraphs=[str(p) for p in raw.get("paragraphs", []) or []],
|
|
63
|
+
subheadings=[str(s) for s in raw.get("subheadings", []) or []],
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
return OfficialDocument(
|
|
67
|
+
title=title,
|
|
68
|
+
sections=section_items,
|
|
69
|
+
document_number=document_number,
|
|
70
|
+
recipient=recipient,
|
|
71
|
+
introduction=introduction,
|
|
72
|
+
attachments=list(attachments or []),
|
|
73
|
+
issuer=issuer,
|
|
74
|
+
department=department,
|
|
75
|
+
issue_date=issue_date,
|
|
76
|
+
cc=cc,
|
|
77
|
+
document_type=document_type,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def render_docx(payload: OfficialDocument) -> tuple[str, bytes]:
|
|
82
|
+
"""在临时目录生成 docx,返回 (文件名, 文件二进制)。"""
|
|
83
|
+
template_bytes = _template_bytes()
|
|
84
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
85
|
+
out_path = Path(tmp) / f"{_safe_filename(payload.title)}.docx"
|
|
86
|
+
build_document(payload, out_path, template_bytes)
|
|
87
|
+
return out_path.name, out_path.read_bytes()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@mcp.tool()
|
|
91
|
+
def generate_official_doc(
|
|
92
|
+
title: str,
|
|
93
|
+
sections: Optional[List[Dict[str, Any]]] = None,
|
|
94
|
+
document_number: str = "",
|
|
95
|
+
recipient: str = "",
|
|
96
|
+
introduction: str = "",
|
|
97
|
+
attachments: Optional[List[str]] = None,
|
|
98
|
+
issuer: str = "",
|
|
99
|
+
department: str = "",
|
|
100
|
+
issue_date: str = "",
|
|
101
|
+
cc: str = "",
|
|
102
|
+
document_type: str = "通报",
|
|
103
|
+
) -> List[Any]:
|
|
104
|
+
"""根据结构化公文内容生成 Word(.docx)公文。
|
|
105
|
+
|
|
106
|
+
所有数据必须来自用户资料;未提供的文号、日期、单位、统计数字、
|
|
107
|
+
整改时限等应在调用前标记为待补充,不要自行编造。
|
|
108
|
+
|
|
109
|
+
:param title: 公文标题(必填)
|
|
110
|
+
:param sections: 正文章节列表,每项含 heading(章节标题)、
|
|
111
|
+
paragraphs(正文段落列表)、subheadings(可选,括号序号子标题列表)
|
|
112
|
+
:param document_number: 文号,如「中建四华建安监函字〔2026〕60号」
|
|
113
|
+
:param recipient: 主送单位,如「司属各项目:」
|
|
114
|
+
:param introduction: 导语 / 开头说明
|
|
115
|
+
:param attachments: 附件名称列表
|
|
116
|
+
:param issuer: 发文单位
|
|
117
|
+
:param department: 发文部门(代章)
|
|
118
|
+
:param issue_date: 发文日期
|
|
119
|
+
:param cc: 抄送范围
|
|
120
|
+
:param document_type: 公文类型,默认「通报」
|
|
121
|
+
:return: 生成的 .docx 文件(内嵌二进制资源)及文字说明
|
|
122
|
+
"""
|
|
123
|
+
payload = _build_document_payload(
|
|
124
|
+
title=title,
|
|
125
|
+
sections=sections,
|
|
126
|
+
document_number=document_number,
|
|
127
|
+
recipient=recipient,
|
|
128
|
+
introduction=introduction,
|
|
129
|
+
attachments=attachments,
|
|
130
|
+
issuer=issuer,
|
|
131
|
+
department=department,
|
|
132
|
+
issue_date=issue_date,
|
|
133
|
+
cc=cc,
|
|
134
|
+
document_type=document_type,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
filename, data = render_docx(payload)
|
|
138
|
+
blob = base64.b64encode(data).decode()
|
|
139
|
+
|
|
140
|
+
text = (
|
|
141
|
+
f"公文已生成:{filename}\n"
|
|
142
|
+
f"类型:{payload.document_type};标题:{payload.title}\n"
|
|
143
|
+
f"文件大小:{len(data)} 字节;格式:.docx(Word)。\n"
|
|
144
|
+
f"正文共 {len(payload.sections)} 个章节;未提供的文号、日期、单位、"
|
|
145
|
+
f"数据与时限等需人工核对后再正式发文。"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return [
|
|
149
|
+
TextContent(type="text", text=text),
|
|
150
|
+
EmbeddedResource(
|
|
151
|
+
type="resource",
|
|
152
|
+
resource=BlobResourceContents(
|
|
153
|
+
uri=f"file:///{quote(filename)}",
|
|
154
|
+
mimeType=DOCX_MIME,
|
|
155
|
+
blob=blob,
|
|
156
|
+
),
|
|
157
|
+
),
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def main() -> None:
|
|
162
|
+
mcp.run(transport="stdio")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
if __name__ == "__main__":
|
|
166
|
+
main()
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""公文构建核心逻辑(纯函数,不含任何 HTTP / 服务层)。
|
|
3
|
+
|
|
4
|
+
由原 word_service.py 的 FastAPI 版本剥离而来:保留 OfficialDocument /
|
|
5
|
+
SectionItem 模型与 build_document 排版逻辑;模板范文从包内资源读取,
|
|
6
|
+
产物写入调用方给定的输出路径。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import io
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import List, Optional
|
|
15
|
+
|
|
16
|
+
from docx import Document
|
|
17
|
+
from docx.document import Document as DocumentType
|
|
18
|
+
from docx.oxml import OxmlElement
|
|
19
|
+
from docx.oxml.ns import qn
|
|
20
|
+
from docx.shared import Pt
|
|
21
|
+
from docx.text.paragraph import Paragraph
|
|
22
|
+
from lxml import etree
|
|
23
|
+
from pydantic import BaseModel, Field
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# 数据模型
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
class SectionItem(BaseModel):
|
|
31
|
+
"""正文章节。
|
|
32
|
+
|
|
33
|
+
heading:章节标题;paragraphs:正文段落列表;
|
|
34
|
+
subheadings:子标题(括号序号标题)列表,顺序排在正文段落之前。
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
heading: str
|
|
38
|
+
paragraphs: List[str] = Field(default_factory=list)
|
|
39
|
+
subheadings: List[str] = Field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class OfficialDocument(BaseModel):
|
|
43
|
+
"""结构化公文内容。所有未提供的字段应在提示词层标记为待补充。"""
|
|
44
|
+
|
|
45
|
+
title: str
|
|
46
|
+
sections: List[SectionItem] = Field(default_factory=list)
|
|
47
|
+
document_number: str = ""
|
|
48
|
+
recipient: str = ""
|
|
49
|
+
introduction: str = ""
|
|
50
|
+
attachments: List[str] = Field(default_factory=list)
|
|
51
|
+
issuer: str = ""
|
|
52
|
+
department: str = ""
|
|
53
|
+
issue_date: str = ""
|
|
54
|
+
cc: str = ""
|
|
55
|
+
document_type: str = "通报"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# 字号 / 字体工具
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
def _pt(size: Optional[str]) -> Optional[Pt]:
|
|
63
|
+
"""将配置里的字号字符串转为 Pt;已是 pt 数值时直接使用。"""
|
|
64
|
+
if not size:
|
|
65
|
+
return None
|
|
66
|
+
match = re.search(r"(\d+(?:\.\d+)?)", str(size))
|
|
67
|
+
if not match:
|
|
68
|
+
return None
|
|
69
|
+
return Pt(float(match.group(1)))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _set_run_font(run, font_name: str, size: Optional[Pt], bold: Optional[bool] = None) -> None:
|
|
73
|
+
run.font.name = font_name
|
|
74
|
+
if size is not None:
|
|
75
|
+
run.font.size = size
|
|
76
|
+
if bold is not None:
|
|
77
|
+
run.font.bold = bold
|
|
78
|
+
rpr = run._element.get_or_add_rPr()
|
|
79
|
+
rfonts = rpr.rFonts
|
|
80
|
+
if rfonts is None:
|
|
81
|
+
rfonts = OxmlElement("w:rFonts")
|
|
82
|
+
rpr.append(rfonts)
|
|
83
|
+
rfonts.set(qn("w:eastAsia"), font_name)
|
|
84
|
+
rfonts.set(qn("w:ascii"), font_name)
|
|
85
|
+
rfonts.set(qn("w:hAnsi"), font_name)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _set_paragraph_font(paragraph: Paragraph, font_name: str, size: Optional[Pt], bold: Optional[bool] = None) -> None:
|
|
89
|
+
"""段落已有文本时,统一设置整段字体。"""
|
|
90
|
+
for run in paragraph.runs:
|
|
91
|
+
_set_run_font(run, font_name, size, bold)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _set_doc_default_font(doc: DocumentType, font_name: str, size: Optional[Pt]) -> None:
|
|
95
|
+
styles = doc.styles
|
|
96
|
+
if "Normal" in styles:
|
|
97
|
+
normal = styles["Normal"]
|
|
98
|
+
normal.font.name = font_name
|
|
99
|
+
normal.font.size = size
|
|
100
|
+
rpr = normal.element.get_or_add_rPr()
|
|
101
|
+
rfonts = rpr.rFonts
|
|
102
|
+
if rfonts is None:
|
|
103
|
+
rfonts = OxmlElement("w:rFonts")
|
|
104
|
+
rpr.append(rfonts)
|
|
105
|
+
rfonts.set(qn("w:eastAsia"), font_name)
|
|
106
|
+
rfonts.set(qn("w:ascii"), font_name)
|
|
107
|
+
rfonts.set(qn("w:hAnsi"), font_name)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
# 文本处理
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
def _split_inline_bold(text: str) -> List[tuple]:
|
|
115
|
+
"""切分 ``**加粗**`` 片段,返回 [(文本, 是否加粗), ...]。"""
|
|
116
|
+
parts: List[tuple] = []
|
|
117
|
+
pattern = re.compile(r"\*\*(.+?)\*\*")
|
|
118
|
+
last = 0
|
|
119
|
+
for match in pattern.finditer(text):
|
|
120
|
+
if match.start() > last:
|
|
121
|
+
parts.append((text[last:match.start()], False))
|
|
122
|
+
parts.append((match.group(1), True))
|
|
123
|
+
last = match.end()
|
|
124
|
+
if last < len(text):
|
|
125
|
+
parts.append((text[last:], False))
|
|
126
|
+
if not parts:
|
|
127
|
+
parts.append((text, False))
|
|
128
|
+
return parts
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _add_body_paragraph(doc: DocumentType, text: str, font_name: str, size: Optional[Pt]) -> None:
|
|
132
|
+
paragraph = doc.add_paragraph()
|
|
133
|
+
paragraph.paragraph_format.first_line_indent = Pt(28)
|
|
134
|
+
paragraph.paragraph_format.line_spacing = 1.5
|
|
135
|
+
parts = _split_inline_bold(text)
|
|
136
|
+
for part_text, bold in parts:
|
|
137
|
+
run = paragraph.add_run(part_text)
|
|
138
|
+
_set_run_font(run, font_name, size, bold if bold else None)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# 主构建逻辑
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
def build_document(data: OfficialDocument, output_path: Path, template_bytes: bytes) -> Path:
|
|
146
|
+
"""根据结构化内容生成 Word 公文。
|
|
147
|
+
|
|
148
|
+
:param data: 结构化公文内容
|
|
149
|
+
:param output_path: 生成的 docx 写入路径
|
|
150
|
+
:param template_bytes: 包内范文的二进制内容(页面设置 / 样式 / 页眉页脚参考)
|
|
151
|
+
:return: 生成文件路径
|
|
152
|
+
"""
|
|
153
|
+
output_path = Path(output_path)
|
|
154
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
|
|
156
|
+
doc = Document(io.BytesIO(template_bytes))
|
|
157
|
+
|
|
158
|
+
# 清空模板正文(保留页面设置、样式与页眉页脚)
|
|
159
|
+
body = doc._element.body
|
|
160
|
+
for child in list(body):
|
|
161
|
+
if child.tag != qn("w:sectPr"):
|
|
162
|
+
body.remove(child)
|
|
163
|
+
|
|
164
|
+
_set_doc_default_font(doc, "仿宋_GB2312", _pt("16pt"))
|
|
165
|
+
|
|
166
|
+
# 文号
|
|
167
|
+
if data.document_number:
|
|
168
|
+
num_paragraph = doc.add_paragraph()
|
|
169
|
+
num_paragraph.alignment = 1 # 居中
|
|
170
|
+
num_paragraph.paragraph_format.space_before = Pt(6)
|
|
171
|
+
num_paragraph.paragraph_format.space_after = Pt(12)
|
|
172
|
+
num_run = num_paragraph.add_run(data.document_number)
|
|
173
|
+
_set_run_font(num_run, "仿宋_GB2312", _pt("16pt"))
|
|
174
|
+
|
|
175
|
+
# 标题
|
|
176
|
+
title_paragraph = doc.add_paragraph()
|
|
177
|
+
title_paragraph.alignment = 1 # 居中
|
|
178
|
+
title_paragraph.paragraph_format.space_before = Pt(12)
|
|
179
|
+
title_paragraph.paragraph_format.space_after = Pt(18)
|
|
180
|
+
title_run = title_paragraph.add_run(data.title)
|
|
181
|
+
_set_run_font(title_run, "方正小标宋简体", _pt("22pt"), True)
|
|
182
|
+
|
|
183
|
+
# 主送单位
|
|
184
|
+
if data.recipient:
|
|
185
|
+
recipient_paragraph = doc.add_paragraph()
|
|
186
|
+
recipient_paragraph.paragraph_format.line_spacing = 1.5
|
|
187
|
+
recipient_run = recipient_paragraph.add_run(data.recipient)
|
|
188
|
+
_set_run_font(recipient_run, "仿宋_GB2312", _pt("16pt"))
|
|
189
|
+
|
|
190
|
+
# 导语
|
|
191
|
+
if data.introduction:
|
|
192
|
+
_add_body_paragraph(doc, data.introduction, "仿宋_GB2312", _pt("16pt"))
|
|
193
|
+
|
|
194
|
+
# 章节
|
|
195
|
+
for section in data.sections:
|
|
196
|
+
if section.heading:
|
|
197
|
+
head_paragraph = doc.add_paragraph()
|
|
198
|
+
head_paragraph.paragraph_format.space_before = Pt(12)
|
|
199
|
+
head_paragraph.paragraph_format.space_after = Pt(6)
|
|
200
|
+
head_run = head_paragraph.add_run(section.heading)
|
|
201
|
+
_set_run_font(head_run, "黑体", _pt("16pt"), True)
|
|
202
|
+
|
|
203
|
+
for subheading in section.subheadings:
|
|
204
|
+
sub_paragraph = doc.add_paragraph()
|
|
205
|
+
sub_paragraph.paragraph_format.first_line_indent = Pt(28)
|
|
206
|
+
sub_paragraph.paragraph_format.line_spacing = 1.5
|
|
207
|
+
sub_run = sub_paragraph.add_run(subheading)
|
|
208
|
+
_set_run_font(sub_run, "楷体_GB2312", _pt("16pt"), True)
|
|
209
|
+
|
|
210
|
+
for paragraph_text in section.paragraphs:
|
|
211
|
+
_add_body_paragraph(doc, paragraph_text, "仿宋_GB2312", _pt("16pt"))
|
|
212
|
+
|
|
213
|
+
# 附件
|
|
214
|
+
if data.attachments:
|
|
215
|
+
attach_paragraph = doc.add_paragraph()
|
|
216
|
+
attach_paragraph.paragraph_format.space_before = Pt(12)
|
|
217
|
+
attach_text = "附件:" + "、".join(data.attachments)
|
|
218
|
+
attach_run = attach_paragraph.add_run(attach_text)
|
|
219
|
+
_set_run_font(attach_run, "仿宋_GB2312", _pt("16pt"))
|
|
220
|
+
|
|
221
|
+
# 落款
|
|
222
|
+
if data.issuer or data.department:
|
|
223
|
+
sign_paragraph = doc.add_paragraph()
|
|
224
|
+
sign_paragraph.alignment = 2 # 右对齐
|
|
225
|
+
sign_paragraph.paragraph_format.space_before = Pt(24)
|
|
226
|
+
sign_text = " ".join([part for part in [data.issuer, data.department] if part])
|
|
227
|
+
sign_run = sign_paragraph.add_run(sign_text)
|
|
228
|
+
_set_run_font(sign_run, "仿宋_GB2312", _pt("16pt"))
|
|
229
|
+
|
|
230
|
+
if data.issue_date:
|
|
231
|
+
date_paragraph = doc.add_paragraph()
|
|
232
|
+
date_paragraph.alignment = 2
|
|
233
|
+
date_run = date_paragraph.add_run(data.issue_date)
|
|
234
|
+
_set_run_font(date_run, "仿宋_GB2312", _pt("16pt"))
|
|
235
|
+
|
|
236
|
+
# 抄送
|
|
237
|
+
if data.cc:
|
|
238
|
+
cc_paragraph = doc.add_paragraph()
|
|
239
|
+
cc_paragraph.paragraph_format.space_before = Pt(18)
|
|
240
|
+
cc_run = cc_paragraph.add_run(f"抄送:{data.cc}")
|
|
241
|
+
_set_run_font(cc_run, "仿宋_GB2312", _pt("14pt"))
|
|
242
|
+
|
|
243
|
+
doc.save(str(output_path))
|
|
244
|
+
return output_path
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cscec-gwdoc-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "中建企业公文 Word(.docx)生成 stdio MCP 服务,供百炼函数计算 uvx 托管接入。"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "Proprietary" }
|
|
12
|
+
authors = [{ name = "CSCEC" }]
|
|
13
|
+
keywords = ["mcp", "word", "docx", "公文", "cscec"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"mcp[cli]>=1.0,<2",
|
|
16
|
+
"python-docx>=1.1",
|
|
17
|
+
"pydantic>=2.0",
|
|
18
|
+
"lxml>=4.9",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
cscec-gwdoc-mcp = "cscec_gwdoc_mcp.server:main"
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["cscec_gwdoc_mcp"]
|