scm-crawler-mcp-server 1.0.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.
@@ -0,0 +1,5 @@
1
+ OSS_ENDPOINT=https://oss-cn-shanghai.aliyuncs.com
2
+ OSS_ACCESS_KEY_ID=LTAI5tAe9gApiEsUh1HnpFzH
3
+ OSS_ACCESS_KEY_SECRET=dwNTjVYxUhNtFGTlI5A52SdzCFteC
4
+ OSS_BUCKET_NAME=gi-gicnisys-dev
5
+ OSS_PREFIX=scm-reports/
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.5
2
+ Name: scm-crawler-mcp-server
3
+ Version: 1.0.0
4
+ Summary: 中化采购平台公告抓取 MCP 服务
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: fastmcp>=0.2.0
7
+ Requires-Dist: openpyxl>=3.1.0
8
+ Requires-Dist: oss2>=2.18.0
9
+ Description-Content-Type: text/markdown
10
+
11
+ # 中化采购平台公告抓取 MCP 服务
12
+
13
+ 一个基于 [FastMCP](https://github.com/jlowin/fastmcp) 的 MCP 服务,用于抓取中化采购供应链平台(scm.esinochem.com)的公告信息,按关键字筛选近 7 天数据,生成 Excel 并上传至阿里云 OSS,返回下载链接。
14
+
15
+ ## 功能
16
+
17
+ - 抓取三个类别:招标公告、资格预审公告、采购公告
18
+ - 按标题搜索关键字:外包、派遣、招聘、猎头、培训
19
+ - 自动去重,保留命中关键字列表
20
+ - 过滤近 7 天发布且需求类型为"服务"的公告
21
+ - 生成格式化的多 Sheet Excel 文件
22
+ - 上传至阿里云 OSS,返回 7 天有效的签名下载链接
23
+ - 作为 MCP 工具 `fetch_notices` 供智能体调用
24
+
25
+ ## 安装
26
+
27
+ 本项目通过 PyPI 发布,可直接使用 `uvx` 运行:
28
+
29
+ ```bash
30
+ uvx scm-crawler-mcp-server
31
+ ```
32
+
33
+ 或在百炼 MCP 配置中引用:
34
+
35
+ ```json
36
+ {
37
+ "command": "uvx",
38
+ "args": ["scm-crawler-mcp-server"]
39
+ }
40
+ ```
41
+
42
+ ## 环境变量
43
+
44
+ 运行前需配置以下 OSS 环境变量(百炼部署时通过 MCP 配置的 `env` 注入):
45
+
46
+ | 变量名 | 说明 | 示例 |
47
+ | -------- | ------ | ------ |
48
+ | `OSS_ENDPOINT` | OSS 端点(不带协议前缀) | `oss-cn-shanghai.aliyuncs.com` |
49
+ | `OSS_ACCESS_KEY_ID` | 阿里云 AccessKey ID | `LTAI5t...` |
50
+ | `OSS_ACCESS_KEY_SECRET` | 阿里云 AccessKey Secret | `dwNTj...` |
51
+ | `OSS_BUCKET_NAME` | Bucket 名称 | `gi-gicnisys-dev` |
52
+ | `OSS_PREFIX` | 上传目录前缀(可选) | `scm-reports/` |
53
+
54
+ ## 使用
55
+
56
+ 在支持 MCP 的客户端(如 Cline、百炼智能体)中调用工具 `fetch_notices`,无参数,返回 JSON:
57
+
58
+ ```json
59
+ {
60
+ "status": "success",
61
+ "file_name": "中化采购平台_公告关键字搜索结果_近7天_2026-09-14.xlsx",
62
+ "oss_url": "https://...",
63
+ "date": "2026-09-14",
64
+ "categories": {
65
+ "招标公告": {"count": 3, "keyword_hits": {"外包": 2, "培训": 1}},
66
+ "采购公告": {"count": 5, "keyword_hits": {"招聘": 4, "培训": 1}}
67
+ }
68
+ }
69
+ ```
70
+
71
+ ## 本地开发
72
+
73
+ ```bash
74
+ # 创建虚拟环境
75
+ uv venv
76
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
77
+
78
+ # 安装依赖
79
+ uv add fastmcp openpyxl oss2
80
+
81
+ # 运行服务
82
+ uv run python -m scm_crawler_mcp_server.server
83
+ ```
84
+
85
+ ## 依赖
86
+
87
+ - Python >= 3.10
88
+ - fastmcp >= 0.2.0
89
+ - openpyxl >= 3.1.0
90
+ - oss2 >= 2.18.0
91
+
92
+ ## 许可证
93
+
94
+ MIT
@@ -0,0 +1,84 @@
1
+ # 中化采购平台公告抓取 MCP 服务
2
+
3
+ 一个基于 [FastMCP](https://github.com/jlowin/fastmcp) 的 MCP 服务,用于抓取中化采购供应链平台(scm.esinochem.com)的公告信息,按关键字筛选近 7 天数据,生成 Excel 并上传至阿里云 OSS,返回下载链接。
4
+
5
+ ## 功能
6
+
7
+ - 抓取三个类别:招标公告、资格预审公告、采购公告
8
+ - 按标题搜索关键字:外包、派遣、招聘、猎头、培训
9
+ - 自动去重,保留命中关键字列表
10
+ - 过滤近 7 天发布且需求类型为"服务"的公告
11
+ - 生成格式化的多 Sheet Excel 文件
12
+ - 上传至阿里云 OSS,返回 7 天有效的签名下载链接
13
+ - 作为 MCP 工具 `fetch_notices` 供智能体调用
14
+
15
+ ## 安装
16
+
17
+ 本项目通过 PyPI 发布,可直接使用 `uvx` 运行:
18
+
19
+ ```bash
20
+ uvx scm-crawler-mcp-server
21
+ ```
22
+
23
+ 或在百炼 MCP 配置中引用:
24
+
25
+ ```json
26
+ {
27
+ "command": "uvx",
28
+ "args": ["scm-crawler-mcp-server"]
29
+ }
30
+ ```
31
+
32
+ ## 环境变量
33
+
34
+ 运行前需配置以下 OSS 环境变量(百炼部署时通过 MCP 配置的 `env` 注入):
35
+
36
+ | 变量名 | 说明 | 示例 |
37
+ | -------- | ------ | ------ |
38
+ | `OSS_ENDPOINT` | OSS 端点(不带协议前缀) | `oss-cn-shanghai.aliyuncs.com` |
39
+ | `OSS_ACCESS_KEY_ID` | 阿里云 AccessKey ID | `LTAI5t...` |
40
+ | `OSS_ACCESS_KEY_SECRET` | 阿里云 AccessKey Secret | `dwNTj...` |
41
+ | `OSS_BUCKET_NAME` | Bucket 名称 | `gi-gicnisys-dev` |
42
+ | `OSS_PREFIX` | 上传目录前缀(可选) | `scm-reports/` |
43
+
44
+ ## 使用
45
+
46
+ 在支持 MCP 的客户端(如 Cline、百炼智能体)中调用工具 `fetch_notices`,无参数,返回 JSON:
47
+
48
+ ```json
49
+ {
50
+ "status": "success",
51
+ "file_name": "中化采购平台_公告关键字搜索结果_近7天_2026-09-14.xlsx",
52
+ "oss_url": "https://...",
53
+ "date": "2026-09-14",
54
+ "categories": {
55
+ "招标公告": {"count": 3, "keyword_hits": {"外包": 2, "培训": 1}},
56
+ "采购公告": {"count": 5, "keyword_hits": {"招聘": 4, "培训": 1}}
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## 本地开发
62
+
63
+ ```bash
64
+ # 创建虚拟环境
65
+ uv venv
66
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
67
+
68
+ # 安装依赖
69
+ uv add fastmcp openpyxl oss2
70
+
71
+ # 运行服务
72
+ uv run python -m scm_crawler_mcp_server.server
73
+ ```
74
+
75
+ ## 依赖
76
+
77
+ - Python >= 3.10
78
+ - fastmcp >= 0.2.0
79
+ - openpyxl >= 3.1.0
80
+ - oss2 >= 2.18.0
81
+
82
+ ## 许可证
83
+
84
+ MIT
@@ -0,0 +1,19 @@
1
+ [project]
2
+ name = "scm-crawler-mcp-server" # 包名(PyPI 上必须唯一)
3
+ version = "1.0.0" # 版本号(每次更新必须递增)
4
+ description = "中化采购平台公告抓取 MCP 服务" # 包描述
5
+ readme = "README.md" # README 文件
6
+ requires-python = ">=3.10" # Python 版本要求(百炼推荐 3.10)
7
+
8
+ dependencies = [ # 运行时依赖
9
+ "fastmcp>=0.2.0", # MCP 框架
10
+ "openpyxl>=3.1.0", # Excel 生成
11
+ "oss2>=2.18.0", # 阿里云 OSS SDK
12
+ ]
13
+
14
+ [project.scripts] # 声明命令行入口(百炼 uvx 调用此入口)
15
+ scm-crawler-mcp-server = "scm_crawler_mcp_server:main" # 入口函数
16
+
17
+ [build-system] # 构建系统配置
18
+ requires = ["hatchling"] # 使用 hatchling 构建
19
+ build-backend = "hatchling.build" # 构建后端
@@ -0,0 +1,7 @@
1
+ # src/scm_crawler_mcp_server/__init__.py
2
+
3
+ from .server import run # 从 server.py 导入 run 函数
4
+
5
+ def main() -> None:
6
+ """百炼 uvx 启动入口"""
7
+ run() # 调用 run() 启动 MCP 服务
@@ -0,0 +1,330 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 中化采购平台公告抓取 MCP Server
5
+ 功能:抓取平台公告 → 生成 Excel → 上传 OSS → 返回下载链接
6
+ """
7
+
8
+ import json # JSON 序列化
9
+ import os # 环境变量读取
10
+ import sys # 标准错误输出(日志)
11
+ import urllib.request # HTTP 请求
12
+ import urllib.error # HTTP 异常处理
13
+ from datetime import datetime, timedelta # 日期计算
14
+ import uuid # 生成唯一文件名
15
+ import oss2 # 阿里云 OSS SDK
16
+ from fastmcp import FastMCP # MCP 框架
17
+
18
+
19
+ # ==================== 全局配置 ====================
20
+
21
+ KEYWORDS = ["外包", "派遣", "招聘", "猎头", "培训"] # 搜索关键字
22
+
23
+ CATEGORIES = [ # 公告类别
24
+ ("招标公告", "01", "Bid"), # 招标公告
25
+ ("资格预审公告", "02", "Bid"), # 资格预审公告
26
+ ("采购公告", "01", "UnBid"), # 采购公告
27
+ ]
28
+
29
+ API_URL = "https://scm.esinochem.com/gateway/obs/business/notice/outer/page/queryPageList"
30
+
31
+ PAGE_SIZE = 50 # 每页记录数
32
+ REQUEST_TIMEOUT = 40 # 请求超时(秒)
33
+ MAX_RETRIES = 3 # 最大重试次数
34
+ DAYS = 7 # 抓取近 N 天
35
+
36
+
37
+ # ==================== 爬虫函数 ====================
38
+
39
+ def post_json(payload, retries=MAX_RETRIES):
40
+ """向平台接口发送 POST 请求,返回 JSON 字典。"""
41
+ data = json.dumps(payload, ensure_ascii=False).encode("utf-8") # 转为 UTF-8 字节流
42
+ req = urllib.request.Request(
43
+ API_URL,
44
+ data=data,
45
+ headers={
46
+ "Content-Type": "application/json;charset=UTF-8", # JSON 格式
47
+ "User-Agent": "Mozilla/5.0 ... Chrome/120.0", # 伪装浏览器
48
+ },
49
+ method="POST",
50
+ )
51
+ last_err = None
52
+ for i in range(retries): # 重试循环
53
+ try:
54
+ with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as resp:
55
+ return json.loads(resp.read().decode("utf-8")) # 解析响应
56
+ except Exception as e:
57
+ last_err = e
58
+ print(f" [WARN] 请求失败(第{i+1}次): {e}", file=sys.stderr)
59
+ raise RuntimeError(f"接口请求连续失败 {retries} 次: {last_err}")
60
+
61
+
62
+ def crawl_keyword(category_name, notice_type, plate_type, keyword):
63
+ """按类别+关键字抓取全部页,返回记录列表。"""
64
+ records = []
65
+ page = 0
66
+ while True:
67
+ start = page * PAGE_SIZE # 分页偏移
68
+ payload = {
69
+ "start": start,
70
+ "limit": PAGE_SIZE,
71
+ "currentPage": page + 1,
72
+ "model": {
73
+ "noticeType": notice_type, # 公告类型
74
+ "plateType": plate_type, # 板块类型
75
+ "title": keyword, # 标题关键字
76
+ },
77
+ }
78
+ j = post_json(payload)
79
+ if not j.get("status") or not j.get("data"):
80
+ raise RuntimeError(f"接口返回异常: {json.dumps(j, ensure_ascii=False)[:200]}")
81
+ data = j["data"]
82
+ total = int(data.get("totalCount", 0)) # 总条数
83
+ root = data.get("root") or [] # 当前页记录
84
+ records.extend(root)
85
+ print(f" [类别:{category_name} 关键字:{keyword}] 第{page+1}页, 累计{len(records)}/{total}", file=sys.stderr)
86
+ if start + PAGE_SIZE >= total or not root: # 已抓完
87
+ break
88
+ page += 1
89
+ return records
90
+
91
+
92
+ def collect():
93
+ """抓取全部类别×关键字,按 noticeId 去重。"""
94
+ result = {}
95
+ for cat_name, notice_type, plate_type in CATEGORIES:
96
+ rows = {}
97
+ for kw in KEYWORDS:
98
+ for r in crawl_keyword(cat_name, notice_type, plate_type, kw):
99
+ nid = r.get("noticeId") # 唯一标识
100
+ if not nid:
101
+ continue
102
+ if nid in rows: # 去重:合并关键字
103
+ if kw not in rows[nid]["keywords"]:
104
+ rows[nid]["keywords"].append(kw)
105
+ else:
106
+ rows[nid] = { # 首次记录
107
+ "title": r.get("title") or "",
108
+ "keywords": [kw],
109
+ "noticeTypeName": r.get("noticeTypeName") or "",
110
+ "purchaseMethodDesc": r.get("purchaseMethodDesc") or "",
111
+ "requireTypeDesc": r.get("requireTypeDesc") or "",
112
+ "purchaseCompanyName": r.get("purchaseCompanyName") or "",
113
+ "buName": r.get("buName") or "",
114
+ "createTimeStr": r.get("createTimeStr") or "",
115
+ "endTimeStr": r.get("endTimeStr") or "",
116
+ "noticeStatusDesc": r.get("noticeStatusDesc") or "",
117
+ "noticeId": nid,
118
+ }
119
+ result[cat_name] = rows
120
+ print(f"类别[{cat_name}] 去重后 {len(rows)} 条", file=sys.stderr)
121
+ return result
122
+
123
+
124
+ # ==================== Excel 生成 ====================
125
+
126
+ def build_excel_bytes(cat_rows):
127
+ """生成 Excel 字节流,返回 (字节流, 统计信息, 日期字符串)。"""
128
+ from openpyxl import Workbook # 工作簿
129
+ from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
130
+ from openpyxl.utils import get_column_letter # 列号转字母
131
+ from io import BytesIO # 内存字节流
132
+
133
+ TODAY = datetime.now()
134
+ THRESHOLD = (TODAY - timedelta(days=DAYS - 1)).strftime("%Y-%m-%d") # 近 N 天起始
135
+ TODAY_STR = TODAY.strftime("%Y-%m-%d")
136
+
137
+ wb = Workbook() # 新建工作簿
138
+
139
+ # -------- 样式 --------
140
+ hdr_font = Font(name="微软雅黑", size=10, bold=True, color="FFFFFF") # 表头白字
141
+ hdr_fill = PatternFill("solid", fgColor="42506B") # 表头深蓝底
142
+ hdr_align = Alignment(horizontal="center", vertical="center")
143
+ body_font = Font(name="微软雅黑", size=10)
144
+ stripe_fill = PatternFill("solid", fgColor="F2F6FC") # 斑马纹
145
+ center = Alignment(horizontal="center", vertical="center")
146
+ left = Alignment(horizontal="left", vertical="center")
147
+ thin = Side(style="thin", color="C0C4CC")
148
+ border = Border(left=thin, right=thin, top=thin, bottom=thin)
149
+
150
+ headers = ["序号", "公告标题", "命中关键词", "公告类型", "采购方式", "需求类型",
151
+ "采购单位", "需求单位", "发布日期", "截止日期", "公告状态", "公告链接"]
152
+ widths = [5, 55, 12, 12, 8, 8, 16, 22, 11, 11, 10, 50]
153
+
154
+ stats = {} # 各类别统计
155
+
156
+ # -------- 每个类别生成一个 Sheet --------
157
+ for idx, (cat_name, _n, _p) in enumerate(CATEGORIES):
158
+ # 过滤:近 N 天 + 需求类型为 "服务"
159
+ rows = [r for r in cat_rows[cat_name].values()
160
+ if r["createTimeStr"] and r["createTimeStr"] >= THRESHOLD
161
+ and r["requireTypeDesc"] == "服务"]
162
+ rows.sort(key=lambda r: r["createTimeStr"], reverse=True) # 按日期倒序
163
+
164
+ kw_count = {kw: 0 for kw in KEYWORDS}
165
+ for r in rows:
166
+ for kw in r["keywords"]:
167
+ if kw in kw_count:
168
+ kw_count[kw] += 1
169
+ stats[cat_name] = (len(rows), kw_count)
170
+
171
+ ws = wb.active if idx == 0 else wb.create_sheet() # 首个用默认 Sheet
172
+ ws.title = cat_name
173
+ ws.append(headers) # 写表头
174
+
175
+ for c in range(1, len(headers) + 1): # 表头样式
176
+ cell = ws.cell(row=1, column=c)
177
+ cell.font = hdr_font
178
+ cell.fill = hdr_fill
179
+ cell.alignment = hdr_align
180
+
181
+ for i, r in enumerate(rows, start=1): # 写数据行
182
+ link = "https://scm.esinochem.com/hpc/index.html#/content?noticeId=" + r["noticeId"]
183
+ ws.append([i, r["title"], ",".join(r["keywords"]), r["noticeTypeName"],
184
+ r["purchaseMethodDesc"], r["requireTypeDesc"], r["purchaseCompanyName"],
185
+ r["buName"], r["createTimeStr"], r["endTimeStr"],
186
+ r["noticeStatusDesc"], link])
187
+ excel_row = i + 1
188
+ for c in range(1, len(headers) + 1): # 正文样式
189
+ cell = ws.cell(row=excel_row, column=c)
190
+ cell.font = body_font
191
+ cell.border = border
192
+ cell.alignment = center if c not in (2, 12) else left
193
+ if excel_row % 2 == 0:
194
+ cell.fill = stripe_fill
195
+ ws.cell(row=excel_row, column=12).hyperlink = link # 可点击链接
196
+ ws.cell(row=excel_row, column=12).style = "Hyperlink"
197
+
198
+ for c, w in enumerate(widths, start=1): # 列宽
199
+ ws.column_dimensions[get_column_letter(c)].width = w
200
+ if rows:
201
+ ws.auto_filter.ref = f"A1:{get_column_letter(len(headers))}{len(rows)+1}"
202
+ ws.freeze_panes = "A2" # 冻结首行
203
+
204
+ # -------- 搜索说明 Sheet --------
205
+ ws = wb.create_sheet("搜索说明")
206
+ notes = [
207
+ (f"中化采购供应链平台 - 公告关键字搜索结果(近{DAYS}天)", True),
208
+ ("", False),
209
+ ("数据来源: https://scm.esinochem.com/hpc/index.html#/notice", False),
210
+ (f"生成日期: {TODAY_STR}", False),
211
+ (f"搜索方式: 标题包含关键字(共{len(KEYWORDS)}个)", False),
212
+ (f"数据范围: 仅保留近{DAYS}天发布且需求类型为「服务」的公告 ({THRESHOLD} 至 {TODAY_STR})", False),
213
+ ("", False),
214
+ ]
215
+ for i, (txt, bold) in enumerate(notes, start=1):
216
+ cell = ws.cell(row=i, column=1, value=txt)
217
+ cell.font = Font(name="微软雅黑", size=13 if bold else 10, bold=bold)
218
+
219
+ head_row = len(notes) + 1 # 统计表头行
220
+ matrix = ["类别"] + KEYWORDS + ["命中人次(含交叉)", "去重后条数"]
221
+ for c, h in enumerate(matrix, start=1):
222
+ cell = ws.cell(row=head_row, column=c, value=h)
223
+ cell.font = hdr_font
224
+ cell.fill = hdr_fill
225
+ cell.alignment = hdr_align
226
+
227
+ r = head_row + 1
228
+ for cat_name, _n, _p in CATEGORIES:
229
+ n, kw_count = stats[cat_name]
230
+ ws.cell(row=r, column=1, value=cat_name).font = body_font
231
+ for c, kw in enumerate(KEYWORDS, start=2):
232
+ ws.cell(row=r, column=c, value=kw_count[kw]).font = body_font
233
+ ws.cell(row=r, column=len(KEYWORDS) + 2, value=sum(kw_count.values())).font = body_font
234
+ ws.cell(row=r, column=len(KEYWORDS) + 3, value=n).font = body_font
235
+ r += 1
236
+
237
+ ws.cell(row=r, column=1, value="总计").font = Font(name="微软雅黑", size=10, bold=True)
238
+ for c in range(2, len(KEYWORDS) + 4): # 合计行公式
239
+ col = get_column_letter(c)
240
+ cell = ws.cell(row=r, column=c)
241
+ cell.value = f"=SUM({col}{head_row+1}:{col}{r-1})"
242
+ cell.font = Font(name="微软雅黑", size=10, bold=True)
243
+
244
+ ws.column_dimensions["A"].width = 22
245
+ for c in range(2, len(KEYWORDS) + 4):
246
+ ws.column_dimensions[get_column_letter(c)].width = 11
247
+
248
+ bio = BytesIO() # 写入内存
249
+ wb.save(bio)
250
+ bio.seek(0)
251
+ return bio.getvalue(), stats, TODAY_STR
252
+
253
+
254
+ # ==================== OSS 上传 ====================
255
+
256
+ def upload_to_oss(file_bytes, filename):
257
+ """上传字节流到 OSS,返回签名下载链接。"""
258
+ # 从环境变量读取(百炼部署时通过 MCP 配置的 env 注入)
259
+ endpoint = os.environ.get("OSS_ENDPOINT", "")
260
+ key_id = os.environ.get("OSS_ACCESS_KEY_ID", "")
261
+ key_secret = os.environ.get("OSS_ACCESS_KEY_SECRET", "")
262
+ bucket_name = os.environ.get("OSS_BUCKET_NAME", "")
263
+ prefix = os.environ.get("OSS_PREFIX", "")
264
+
265
+ if not all([endpoint, key_id, key_secret, bucket_name]): # 校验必填项
266
+ raise RuntimeError("OSS 环境变量未完全配置")
267
+
268
+ # 去除协议前缀(oss2 只接受主机名)
269
+ if endpoint.startswith("http://"):
270
+ endpoint = endpoint[7:]
271
+ elif endpoint.startswith("https://"):
272
+ endpoint = endpoint[8:]
273
+
274
+ auth = oss2.Auth(key_id, key_secret)
275
+ bucket = oss2.Bucket(auth, endpoint, bucket_name)
276
+
277
+ if prefix and not prefix.endswith("/"): # 确保前缀以 / 结尾
278
+ prefix += "/"
279
+ object_key = f"{prefix}{datetime.now().strftime('%Y%m%d')}/{uuid.uuid4().hex}_{filename}"
280
+
281
+ bucket.put_object(object_key, file_bytes) # 上传
282
+ url = bucket.sign_url('GET', object_key, 7 * 24 * 3600) # 7 天有效签名 URL
283
+ return url
284
+
285
+
286
+ # ==================== MCP 服务 ====================
287
+
288
+ mcp = FastMCP("scm-crawler-mcp-server") # 创建 MCP 实例
289
+
290
+
291
+ @mcp.tool() # 注册工具
292
+ def fetch_notices() -> str:
293
+ """
294
+ 抓取中化采购平台公告,生成 Excel 并上传 OSS,返回下载链接。
295
+ """
296
+ try:
297
+ print("开始抓取...", file=sys.stderr)
298
+ cat_rows = collect() # 1. 抓取
299
+ excel_bytes, stats, today_str = build_excel_bytes(cat_rows) # 2. 生成 Excel
300
+
301
+ filename = f"中化采购平台_公告关键字搜索结果_近{DAYS}天_{today_str}.xlsx"
302
+ oss_url = upload_to_oss(excel_bytes, filename) # 3. 上传 OSS
303
+ print(f"上传成功: {oss_url}", file=sys.stderr)
304
+
305
+ summary = {
306
+ "status": "success",
307
+ "file_name": filename,
308
+ "oss_url": oss_url,
309
+ "date": today_str,
310
+ "categories": {}
311
+ }
312
+ for cat_name, (count, kw_count) in stats.items():
313
+ summary["categories"][cat_name] = {
314
+ "count": count,
315
+ "keyword_hits": kw_count
316
+ }
317
+ return json.dumps(summary, ensure_ascii=False, indent=2)
318
+ except Exception as e:
319
+ import traceback
320
+ print(traceback.format_exc(), file=sys.stderr)
321
+ return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False)
322
+
323
+
324
+ def run():
325
+ """MCP 服务启动入口(百炼 uvx 调用此函数)。"""
326
+ mcp.run(transport='stdio') # 必须用 stdio 模式
327
+
328
+
329
+ if __name__ == "__main__":
330
+ run() # 本地调试入口