@youhaozhao/cninfo-mcp 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2 -0
- package/bin/cninfo-mcp.js +12 -27
- package/package.json +9 -7
- package/python/__pycache__/spider.cpython-314.pyc +0 -0
- package/python/mcp_server.py +156 -64
- package/python/spider.py +203 -159
- package/scripts/install-python-deps.js +56 -35
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 youhaozhao
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
package/bin/cninfo-mcp.js
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* This script launches the Python MCP Server and handles stdio communication.
|
|
7
|
-
* It automatically detects Python and installs dependencies if needed.
|
|
4
|
+
* 巨潮资讯 MCP 服务器启动器
|
|
5
|
+
* 自动检测 Python 并安装依赖,然后启动 Python MCP 服务器。
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
8
|
const { spawn } = require('child_process');
|
|
11
9
|
const path = require('path');
|
|
12
10
|
const fs = require('fs');
|
|
13
11
|
|
|
14
|
-
//
|
|
12
|
+
// 配置路径
|
|
15
13
|
const PYTHON_SCRIPT = path.join(__dirname, '..', 'python', 'mcp_server.py');
|
|
16
14
|
const PYTHON_REQUIREMENTS = path.join(__dirname, '..', 'python', 'requirements.txt');
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
* Find Python executable
|
|
20
|
-
*/
|
|
16
|
+
// 查找可用的 Python 可执行文件
|
|
21
17
|
async function findPython() {
|
|
22
18
|
const pythonCommands = ['python3', 'python', 'python3.12', 'python3.11', 'python3.10'];
|
|
23
19
|
|
|
@@ -28,7 +24,7 @@ async function findPython() {
|
|
|
28
24
|
return cmd;
|
|
29
25
|
}
|
|
30
26
|
} catch (error) {
|
|
31
|
-
//
|
|
27
|
+
// 继续尝试下一个命令
|
|
32
28
|
}
|
|
33
29
|
}
|
|
34
30
|
|
|
@@ -38,9 +34,7 @@ async function findPython() {
|
|
|
38
34
|
);
|
|
39
35
|
}
|
|
40
36
|
|
|
41
|
-
|
|
42
|
-
* Check and install Python dependencies
|
|
43
|
-
*/
|
|
37
|
+
// 检查并安装 Python 依赖
|
|
44
38
|
async function ensureDependencies(pythonCmd) {
|
|
45
39
|
const requirementsPath = PYTHON_REQUIREMENTS;
|
|
46
40
|
|
|
@@ -50,10 +44,10 @@ async function ensureDependencies(pythonCmd) {
|
|
|
50
44
|
}
|
|
51
45
|
|
|
52
46
|
try {
|
|
53
|
-
//
|
|
47
|
+
// 检查 mcp 包是否已安装
|
|
54
48
|
const checkResult = await spawnAsync(pythonCmd, ['-c', 'import mcp']);
|
|
55
49
|
} catch (error) {
|
|
56
|
-
//
|
|
50
|
+
// 未安装,执行安装
|
|
57
51
|
console.error('Installing Python dependencies...');
|
|
58
52
|
const installResult = await spawnAsync(pythonCmd, ['-m', 'pip', 'install', '-r', requirementsPath], {
|
|
59
53
|
stdio: 'inherit'
|
|
@@ -70,9 +64,7 @@ async function ensureDependencies(pythonCmd) {
|
|
|
70
64
|
}
|
|
71
65
|
}
|
|
72
66
|
|
|
73
|
-
|
|
74
|
-
* Spawn a process and return result
|
|
75
|
-
*/
|
|
67
|
+
// 启动子进程并返回结果
|
|
76
68
|
function spawnAsync(command, args, options = {}) {
|
|
77
69
|
return new Promise((resolve, reject) => {
|
|
78
70
|
const child = spawn(command, args, {
|
|
@@ -116,24 +108,18 @@ function spawnAsync(command, args, options = {}) {
|
|
|
116
108
|
});
|
|
117
109
|
}
|
|
118
110
|
|
|
119
|
-
/**
|
|
120
|
-
* Main execution
|
|
121
|
-
*/
|
|
122
111
|
async function main() {
|
|
123
112
|
try {
|
|
124
|
-
//
|
|
113
|
+
// 检查 Python 脚本是否存在
|
|
125
114
|
if (!fs.existsSync(PYTHON_SCRIPT)) {
|
|
126
115
|
console.error('Error: mcp_server.py not found at', PYTHON_SCRIPT);
|
|
127
116
|
process.exit(1);
|
|
128
117
|
}
|
|
129
118
|
|
|
130
|
-
// Find Python
|
|
131
119
|
const pythonCmd = await findPython();
|
|
132
|
-
|
|
133
|
-
// Ensure dependencies are installed
|
|
134
120
|
await ensureDependencies(pythonCmd);
|
|
135
121
|
|
|
136
|
-
//
|
|
122
|
+
// 启动 MCP 服务器
|
|
137
123
|
console.error('巨潮资讯 MCP 服务器已启动,等待连接...');
|
|
138
124
|
const child = spawn(pythonCmd, [PYTHON_SCRIPT], {
|
|
139
125
|
stdio: 'inherit',
|
|
@@ -144,7 +130,7 @@ async function main() {
|
|
|
144
130
|
}
|
|
145
131
|
});
|
|
146
132
|
|
|
147
|
-
//
|
|
133
|
+
// 处理子进程退出
|
|
148
134
|
child.on('error', (error) => {
|
|
149
135
|
console.error('Failed to start MCP Server:', error.message);
|
|
150
136
|
process.exit(1);
|
|
@@ -160,5 +146,4 @@ async function main() {
|
|
|
160
146
|
}
|
|
161
147
|
}
|
|
162
148
|
|
|
163
|
-
// Run
|
|
164
149
|
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@youhaozhao/cninfo-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "MCP Server for querying and downloading Chinese listed companies' annual reports from CNINFO (巨潮资讯网)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -11,16 +11,17 @@
|
|
|
11
11
|
"finance",
|
|
12
12
|
"python"
|
|
13
13
|
],
|
|
14
|
-
"homepage": "https://github.com/youhaozhao/
|
|
14
|
+
"homepage": "https://github.com/youhaozhao/cninfo-mcp#readme",
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
17
|
-
"url": "git+https://github.com/youhaozhao/
|
|
17
|
+
"url": "git+https://github.com/youhaozhao/cninfo-mcp.git"
|
|
18
18
|
},
|
|
19
19
|
"bugs": {
|
|
20
|
-
"url": "https://github.com/youhaozhao/
|
|
20
|
+
"url": "https://github.com/youhaozhao/cninfo-mcp/issues"
|
|
21
21
|
},
|
|
22
|
+
"license": "MIT",
|
|
22
23
|
"bin": {
|
|
23
|
-
"cninfo-mcp": "
|
|
24
|
+
"cninfo-mcp": "bin/cninfo-mcp.js"
|
|
24
25
|
},
|
|
25
26
|
"scripts": {
|
|
26
27
|
"postinstall": "node scripts/install-python-deps.js",
|
|
@@ -46,6 +47,7 @@
|
|
|
46
47
|
"bin/",
|
|
47
48
|
"python/",
|
|
48
49
|
"scripts/",
|
|
49
|
-
"README.md"
|
|
50
|
+
"README.md",
|
|
51
|
+
"LICENSE"
|
|
50
52
|
]
|
|
51
|
-
}
|
|
53
|
+
}
|
|
Binary file
|
package/python/mcp_server.py
CHANGED
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
巨潮资讯 MCP 服务器
|
|
4
|
+
用于查询和下载 A 股年度报告的 MCP 工具服务
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
8
|
import sys
|
|
9
9
|
from typing import Optional
|
|
10
10
|
|
|
11
|
-
#
|
|
11
|
+
# 将当前目录加入模块搜索路径
|
|
12
12
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
13
13
|
|
|
14
14
|
from mcp.server import FastMCP
|
|
15
|
-
from spider import
|
|
15
|
+
from spider import (
|
|
16
|
+
query_annual_reports,
|
|
17
|
+
download_annual_reports,
|
|
18
|
+
query_prospectus,
|
|
19
|
+
download_prospectus,
|
|
20
|
+
saving_path,
|
|
21
|
+
)
|
|
16
22
|
|
|
17
|
-
#
|
|
23
|
+
# 创建 MCP 服务器实例
|
|
18
24
|
mcp = FastMCP(
|
|
19
25
|
name="cninfo-server",
|
|
20
|
-
instructions="CNINFO annual reports server - Query and download Chinese listed companies' annual reports from cninfo.com.cn"
|
|
26
|
+
instructions="CNINFO annual reports server - Query and download Chinese listed companies' annual reports from cninfo.com.cn",
|
|
21
27
|
)
|
|
22
28
|
|
|
23
29
|
|
|
24
30
|
@mcp.tool()
|
|
25
|
-
def query_annual_reports_tool(
|
|
26
|
-
stock_code: str,
|
|
27
|
-
year: Optional[int] = None
|
|
28
|
-
) -> dict:
|
|
31
|
+
def query_annual_reports_tool(stock_code: str, year: Optional[int] = None) -> dict:
|
|
29
32
|
"""
|
|
30
33
|
Query annual reports for a Chinese listed company
|
|
31
34
|
|
|
@@ -46,57 +49,61 @@ def query_annual_reports_tool(
|
|
|
46
49
|
|
|
47
50
|
if not reports:
|
|
48
51
|
return {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
"success": False,
|
|
53
|
+
"stock_code": stock_code,
|
|
54
|
+
"year": year,
|
|
55
|
+
"count": 0,
|
|
56
|
+
"reports": [],
|
|
57
|
+
"message": f"No annual reports found for stock {stock_code}"
|
|
58
|
+
+ (f" in year {year}" if year else ""),
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
#
|
|
61
|
+
# 提取关键字段
|
|
62
|
+
base_url = "https://static.cninfo.com.cn/"
|
|
58
63
|
report_details = []
|
|
59
64
|
for report in reports:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
adj = report.get("adjunctUrl", "")
|
|
66
|
+
report_details.append(
|
|
67
|
+
{
|
|
68
|
+
"announcementTitle": report.get("announcementTitle", ""),
|
|
69
|
+
"announcementTime": report.get("announcementTime", ""),
|
|
70
|
+
"secCode": report.get("secCode", ""),
|
|
71
|
+
"secName": report.get("secName", ""),
|
|
72
|
+
"adjunctUrl": base_url + adj if adj else "",
|
|
73
|
+
}
|
|
74
|
+
)
|
|
67
75
|
|
|
68
76
|
return {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
"success": True,
|
|
78
|
+
"stock_code": stock_code,
|
|
79
|
+
"year": year,
|
|
80
|
+
"count": len(reports),
|
|
81
|
+
"reports": report_details,
|
|
82
|
+
"message": f"Found {len(reports)} annual report(s)"
|
|
83
|
+
+ (f" for year {year}" if year else ""),
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
except Exception as e:
|
|
78
87
|
return {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
"success": False,
|
|
89
|
+
"stock_code": stock_code,
|
|
90
|
+
"year": year,
|
|
91
|
+
"count": 0,
|
|
92
|
+
"reports": [],
|
|
93
|
+
"error": str(e),
|
|
94
|
+
"message": f"Error querying annual reports: {str(e)}",
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
|
|
89
98
|
@mcp.tool()
|
|
90
|
-
def download_annual_reports_tool(
|
|
91
|
-
stock_code: str,
|
|
92
|
-
year: Optional[int] = None
|
|
93
|
-
) -> dict:
|
|
99
|
+
def download_annual_reports_tool(stock_code: str, year: Optional[int] = None, save_path: Optional[str] = None) -> dict:
|
|
94
100
|
"""
|
|
95
101
|
Download annual reports for a Chinese listed company
|
|
96
102
|
|
|
97
103
|
Args:
|
|
98
104
|
stock_code: Stock code (e.g., '000888' for 峨眉山, '688777' for 中科德芯)
|
|
99
105
|
year: Optional year to filter (e.g., 2024). If not provided, downloads all available years
|
|
106
|
+
save_path: Optional directory to save files (e.g., '/Users/me/reports'). Defaults to pdf/ in package directory
|
|
100
107
|
|
|
101
108
|
Returns:
|
|
102
109
|
Dictionary containing:
|
|
@@ -108,40 +115,125 @@ def download_annual_reports_tool(
|
|
|
108
115
|
- message: Status message
|
|
109
116
|
"""
|
|
110
117
|
try:
|
|
111
|
-
|
|
112
|
-
os.makedirs(
|
|
113
|
-
|
|
114
|
-
result = download_annual_reports(stock_code, year)
|
|
118
|
+
output_dir = save_path or saving_path
|
|
119
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
115
120
|
|
|
116
|
-
|
|
117
|
-
result[
|
|
118
|
-
result[
|
|
121
|
+
result = download_annual_reports(stock_code, year, save_path=output_dir)
|
|
122
|
+
result["stock_code"] = stock_code
|
|
123
|
+
result["year"] = year
|
|
119
124
|
|
|
120
125
|
return result
|
|
121
126
|
|
|
122
127
|
except Exception as e:
|
|
123
128
|
return {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
"success": False,
|
|
130
|
+
"stock_code": stock_code,
|
|
131
|
+
"year": year,
|
|
132
|
+
"downloaded": 0,
|
|
133
|
+
"path": save_path or saving_path,
|
|
134
|
+
"error": str(e),
|
|
135
|
+
"message": f"Error downloading annual reports: {str(e)}",
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
|
|
134
|
-
@mcp.
|
|
135
|
-
def
|
|
139
|
+
@mcp.tool()
|
|
140
|
+
def query_prospectus_tool(stock_code: str) -> dict:
|
|
136
141
|
"""
|
|
137
|
-
|
|
142
|
+
Query prospectus documents for a Chinese listed company
|
|
138
143
|
|
|
139
144
|
Args:
|
|
140
|
-
stock_code: Stock code (e.g., '000888')
|
|
145
|
+
stock_code: Stock code (e.g., '000888' for 峨眉山, '688777' for 中科德芯)
|
|
141
146
|
|
|
142
147
|
Returns:
|
|
143
|
-
|
|
148
|
+
Dictionary containing:
|
|
149
|
+
- success: Boolean indicating if the query was successful
|
|
150
|
+
- stock_code: The queried stock code
|
|
151
|
+
- count: Number of documents found
|
|
152
|
+
- reports: List of document details (announcementTitle, announcementTime, secCode, secName)
|
|
144
153
|
"""
|
|
154
|
+
try:
|
|
155
|
+
reports = query_prospectus(stock_code)
|
|
156
|
+
|
|
157
|
+
if not reports:
|
|
158
|
+
return {
|
|
159
|
+
"success": False,
|
|
160
|
+
"stock_code": stock_code,
|
|
161
|
+
"count": 0,
|
|
162
|
+
"reports": [],
|
|
163
|
+
"message": f"No prospectus found for stock {stock_code}",
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
base_url = "https://static.cninfo.com.cn/"
|
|
167
|
+
report_details = [
|
|
168
|
+
{
|
|
169
|
+
"announcementTitle": r.get("announcementTitle", ""),
|
|
170
|
+
"announcementTime": r.get("announcementTime", ""),
|
|
171
|
+
"secCode": r.get("secCode", ""),
|
|
172
|
+
"secName": r.get("secName", ""),
|
|
173
|
+
"adjunctUrl": base_url + r.get("adjunctUrl", "") if r.get("adjunctUrl") else "",
|
|
174
|
+
}
|
|
175
|
+
for r in reports
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
"success": True,
|
|
180
|
+
"stock_code": stock_code,
|
|
181
|
+
"count": len(reports),
|
|
182
|
+
"reports": report_details,
|
|
183
|
+
"message": f"Found {len(reports)} prospectus document(s)",
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
except Exception as e:
|
|
187
|
+
return {
|
|
188
|
+
"success": False,
|
|
189
|
+
"stock_code": stock_code,
|
|
190
|
+
"count": 0,
|
|
191
|
+
"reports": [],
|
|
192
|
+
"error": str(e),
|
|
193
|
+
"message": f"Error querying prospectus: {str(e)}",
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@mcp.tool()
|
|
198
|
+
def download_prospectus_tool(stock_code: str, save_path: Optional[str] = None) -> dict:
|
|
199
|
+
"""
|
|
200
|
+
Download prospectus documents for a Chinese listed company
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
stock_code: Stock code (e.g., '000888' for 峨眉山, '688777' for 中科德芯)
|
|
204
|
+
save_path: Optional directory to save files (e.g., '/Users/me/reports'). Defaults to pdf/ in package directory
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Dictionary containing:
|
|
208
|
+
- success: Boolean indicating if download was successful
|
|
209
|
+
- stock_code: The stock code
|
|
210
|
+
- downloaded: Number of files downloaded
|
|
211
|
+
- path: Directory where files were saved
|
|
212
|
+
- message: Status message
|
|
213
|
+
"""
|
|
214
|
+
try:
|
|
215
|
+
output_dir = save_path or saving_path
|
|
216
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
217
|
+
|
|
218
|
+
result = download_prospectus(stock_code, save_path=output_dir)
|
|
219
|
+
result["stock_code"] = stock_code
|
|
220
|
+
|
|
221
|
+
return result
|
|
222
|
+
|
|
223
|
+
except Exception as e:
|
|
224
|
+
return {
|
|
225
|
+
"success": False,
|
|
226
|
+
"stock_code": stock_code,
|
|
227
|
+
"downloaded": 0,
|
|
228
|
+
"path": save_path or saving_path,
|
|
229
|
+
"error": str(e),
|
|
230
|
+
"message": f"Error downloading prospectus: {str(e)}",
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@mcp.resource("annual-reports-list://{stock_code}")
|
|
235
|
+
def get_annual_reports_list(stock_code: str) -> str:
|
|
236
|
+
"""返回指定股票代码的年度报告格式化列表"""
|
|
145
237
|
try:
|
|
146
238
|
reports = query_annual_reports(stock_code)
|
|
147
239
|
|
|
@@ -151,9 +243,9 @@ def get_annual_reports_list(stock_code: str) -> str:
|
|
|
151
243
|
output = [f"Annual Reports for {stock_code}:", "=" * 60]
|
|
152
244
|
|
|
153
245
|
for report in reports:
|
|
154
|
-
title = report.get(
|
|
155
|
-
time = report.get(
|
|
156
|
-
name = report.get(
|
|
246
|
+
title = report.get("announcementTitle", "N/A")
|
|
247
|
+
time = report.get("announcementTime", "N/A")
|
|
248
|
+
name = report.get("secName", "N/A")
|
|
157
249
|
output.append(f"\n📄 {title}")
|
|
158
250
|
output.append(f" Company: {name}")
|
|
159
251
|
output.append(f" Date: {time}")
|
|
@@ -168,5 +260,5 @@ def get_annual_reports_list(stock_code: str) -> str:
|
|
|
168
260
|
|
|
169
261
|
|
|
170
262
|
if __name__ == "__main__":
|
|
171
|
-
#
|
|
263
|
+
# 以 stdio 方式运行服务器
|
|
172
264
|
mcp.run()
|
package/python/spider.py
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
3
|
-
公开招股书(招股说明书/招股意向书)
|
|
4
|
-
《年度报告》 16 17 18
|
|
2
|
+
从巨潮资讯下载年度报告和招股书
|
|
5
3
|
"""
|
|
4
|
+
|
|
6
5
|
import os
|
|
7
6
|
import random
|
|
8
7
|
import time
|
|
8
|
+
|
|
9
9
|
import requests
|
|
10
10
|
|
|
11
|
-
download_path =
|
|
11
|
+
download_path = "https://static.cninfo.com.cn/"
|
|
12
12
|
# 使用脚本所在目录的相对路径
|
|
13
|
-
_saving_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
14
|
-
saving_path = _saving_path +
|
|
13
|
+
_saving_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pdf")
|
|
14
|
+
saving_path = _saving_path + "/"
|
|
15
15
|
|
|
16
16
|
User_Agent = [
|
|
17
17
|
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
|
|
@@ -20,169 +20,169 @@ User_Agent = [
|
|
|
20
20
|
"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
|
|
21
21
|
"Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
|
|
22
22
|
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
|
|
23
|
-
"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0"
|
|
23
|
+
"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
headers = {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
headers = {
|
|
28
|
+
"Accept": "application/json, text/javascript, */*; q=0.01",
|
|
29
|
+
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
30
|
+
"Accept-Encoding": "gzip, deflate",
|
|
31
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-HK;q=0.6,zh-TW;q=0.5",
|
|
32
|
+
"Host": "www.cninfo.com.cn",
|
|
33
|
+
"Origin": "http://www.cninfo.com.cn",
|
|
34
|
+
"Referer": "http://www.cninfo.com.cn/new/commonUrl?url=disclosure/list/notice",
|
|
35
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
36
|
+
}
|
|
38
37
|
|
|
39
38
|
|
|
40
39
|
# 深市 年度报告
|
|
41
40
|
def szseAnnual(page, stock):
|
|
42
|
-
query_path =
|
|
43
|
-
headers[
|
|
44
|
-
query = {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
41
|
+
query_path = "http://www.cninfo.com.cn/new/hisAnnouncement/query"
|
|
42
|
+
headers["User-Agent"] = random.choice(User_Agent) # 定义User_Agent
|
|
43
|
+
query = {
|
|
44
|
+
"pageNum": page, # 页码
|
|
45
|
+
"pageSize": 30,
|
|
46
|
+
"tabName": "fulltext",
|
|
47
|
+
"column": "szse", # 深交所
|
|
48
|
+
"stock": "",
|
|
49
|
+
"searchkey": stock, # 使用searchkey查询股票代码或公司名
|
|
50
|
+
"secid": "",
|
|
51
|
+
"plate": "sz",
|
|
52
|
+
"category": "category_ndbg_szsh", # 年度报告
|
|
53
|
+
"trade": "",
|
|
54
|
+
"seDate": "2020-01-01~2026-02-15", # 时间区间
|
|
55
|
+
}
|
|
56
56
|
|
|
57
57
|
namelist = requests.post(query_path, headers=headers, data=query)
|
|
58
58
|
result = namelist.json()
|
|
59
|
-
if result and
|
|
60
|
-
return result[
|
|
59
|
+
if result and "announcements" in result and result["announcements"]:
|
|
60
|
+
return result["announcements"]
|
|
61
61
|
return []
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
# 沪市 年度报告
|
|
65
65
|
def sseAnnual(page, stock):
|
|
66
|
-
query_path =
|
|
67
|
-
headers[
|
|
68
|
-
query = {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
66
|
+
query_path = "http://www.cninfo.com.cn/new/hisAnnouncement/query"
|
|
67
|
+
headers["User-Agent"] = random.choice(User_Agent) # 定义User_Agent
|
|
68
|
+
query = {
|
|
69
|
+
"pageNum": page, # 页码
|
|
70
|
+
"pageSize": 30,
|
|
71
|
+
"tabName": "fulltext",
|
|
72
|
+
"column": "sse",
|
|
73
|
+
"stock": "",
|
|
74
|
+
"searchkey": stock, # 使用searchkey查询股票代码或公司名
|
|
75
|
+
"secid": "",
|
|
76
|
+
"plate": "sh",
|
|
77
|
+
"category": "category_ndbg_szsh", # 年度报告
|
|
78
|
+
"trade": "",
|
|
79
|
+
"seDate": "2020-01-01~2026-02-15", # 时间区间
|
|
80
|
+
}
|
|
80
81
|
|
|
81
82
|
namelist = requests.post(query_path, headers=headers, data=query)
|
|
82
83
|
result = namelist.json()
|
|
83
|
-
if result and
|
|
84
|
-
return result[
|
|
84
|
+
if result and "announcements" in result and result["announcements"]:
|
|
85
|
+
return result["announcements"]
|
|
85
86
|
return []
|
|
86
87
|
|
|
87
88
|
|
|
88
89
|
# 深市 招股
|
|
89
90
|
def szseStock(page, stock):
|
|
90
|
-
query_path =
|
|
91
|
-
headers[
|
|
92
|
-
query = {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
91
|
+
query_path = "http://www.cninfo.com.cn/new/hisAnnouncement/query"
|
|
92
|
+
headers["User-Agent"] = random.choice(User_Agent) # 定义User_Agent
|
|
93
|
+
query = {
|
|
94
|
+
"pageNum": page, # 页码
|
|
95
|
+
"pageSize": 30,
|
|
96
|
+
"tabName": "fulltext",
|
|
97
|
+
"column": "szse",
|
|
98
|
+
"stock": "",
|
|
99
|
+
"searchkey": stock + " 招股", # 组合搜索:股票代码 + 招股
|
|
100
|
+
"secid": "",
|
|
101
|
+
"plate": "sz",
|
|
102
|
+
"category": "",
|
|
103
|
+
"trade": "",
|
|
104
|
+
"seDate": "2015-01-01~2026-02-15", # 时间区间
|
|
105
|
+
}
|
|
104
106
|
|
|
105
107
|
namelist = requests.post(query_path, headers=headers, data=query)
|
|
106
108
|
result = namelist.json()
|
|
107
|
-
if result and
|
|
108
|
-
return result[
|
|
109
|
+
if result and "announcements" in result and result["announcements"]:
|
|
110
|
+
return result["announcements"]
|
|
109
111
|
return []
|
|
110
112
|
|
|
111
113
|
|
|
112
114
|
# 沪市 招股
|
|
113
115
|
def sseStock(page, stock):
|
|
114
|
-
query_path =
|
|
115
|
-
headers[
|
|
116
|
-
query = {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
116
|
+
query_path = "http://www.cninfo.com.cn/new/hisAnnouncement/query"
|
|
117
|
+
headers["User-Agent"] = random.choice(User_Agent) # 定义User_Agent
|
|
118
|
+
query = {
|
|
119
|
+
"pageNum": page, # 页码
|
|
120
|
+
"pageSize": 30,
|
|
121
|
+
"tabName": "fulltext",
|
|
122
|
+
"column": "sse",
|
|
123
|
+
"stock": "",
|
|
124
|
+
"searchkey": stock + " 招股", # 组合搜索:股票代码 + 招股
|
|
125
|
+
"secid": "",
|
|
126
|
+
"plate": "sh",
|
|
127
|
+
"category": "",
|
|
128
|
+
"trade": "",
|
|
129
|
+
"seDate": "2015-01-01~2026-02-15", # 时间区间
|
|
130
|
+
}
|
|
128
131
|
|
|
129
132
|
namelist = requests.post(query_path, headers=headers, data=query)
|
|
130
133
|
result = namelist.json()
|
|
131
|
-
if result and
|
|
132
|
-
return result[
|
|
134
|
+
if result and "announcements" in result and result["announcements"]:
|
|
135
|
+
return result["announcements"]
|
|
133
136
|
return []
|
|
134
137
|
|
|
135
138
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"""
|
|
139
|
-
Download PDF files from announcement list
|
|
140
|
-
|
|
141
|
-
Args:
|
|
142
|
-
single_page: List of announcement dictionaries
|
|
143
|
-
year_filter: Optional year to filter (e.g., 2024). If None, downloads all years
|
|
144
|
-
"""
|
|
139
|
+
def Download(single_page, year_filter=None, save_path=None):
|
|
140
|
+
"""下载公告列表中的 PDF 文件"""
|
|
145
141
|
if single_page is None:
|
|
146
142
|
return
|
|
147
143
|
|
|
148
|
-
headers = {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
144
|
+
headers = {
|
|
145
|
+
"Accept": "application/json, text/javascript, */*; q=0.01",
|
|
146
|
+
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
147
|
+
"Accept-Encoding": "gzip, deflate",
|
|
148
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-HK;q=0.6,zh-TW;q=0.5",
|
|
149
|
+
"Host": "www.cninfo.com.cn",
|
|
150
|
+
"Origin": "http://www.cninfo.com.cn",
|
|
151
|
+
}
|
|
155
152
|
|
|
156
|
-
#
|
|
153
|
+
# 按年份筛选允许下载的标题
|
|
157
154
|
allowed_list = []
|
|
158
155
|
if year_filter:
|
|
159
156
|
allowed_list = [
|
|
160
|
-
f
|
|
161
|
-
f
|
|
157
|
+
f"{year_filter}年年度报告(更新后)",
|
|
158
|
+
f"{year_filter}年年度报告",
|
|
162
159
|
]
|
|
163
160
|
else:
|
|
164
|
-
#
|
|
161
|
+
# 默认下载 2016-2025 年
|
|
165
162
|
for year in range(2016, 2026):
|
|
166
|
-
allowed_list.append(f
|
|
167
|
-
allowed_list.append(f
|
|
163
|
+
allowed_list.append(f"{year}年年度报告(更新后)")
|
|
164
|
+
allowed_list.append(f"{year}年年度报告")
|
|
168
165
|
|
|
169
166
|
allowed_list_2 = [
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
167
|
+
"招股书",
|
|
168
|
+
"招股说明书",
|
|
169
|
+
"招股意向书",
|
|
173
170
|
]
|
|
174
171
|
|
|
172
|
+
output_dir = (save_path or saving_path).rstrip("/") + "/"
|
|
173
|
+
downloaded_count = 0
|
|
174
|
+
|
|
175
175
|
for i in single_page:
|
|
176
|
-
title = i[
|
|
176
|
+
title = i["announcementTitle"]
|
|
177
177
|
|
|
178
|
-
#
|
|
179
|
-
if
|
|
178
|
+
# 跳过确认意见、取消公告、摘要等非正文文件
|
|
179
|
+
if "确认意见" in title or "取消" in title or "摘要" in title:
|
|
180
180
|
continue
|
|
181
181
|
|
|
182
|
-
#
|
|
182
|
+
# 检查标题是否精确匹配(避免"摘要"等变体被误下载)
|
|
183
183
|
allowed = False
|
|
184
184
|
for item in allowed_list:
|
|
185
|
-
if
|
|
185
|
+
if title == item:
|
|
186
186
|
allowed = True
|
|
187
187
|
break
|
|
188
188
|
|
|
@@ -194,102 +194,146 @@ def Download(single_page, year_filter=None):
|
|
|
194
194
|
|
|
195
195
|
if allowed:
|
|
196
196
|
download = download_path + i["adjunctUrl"]
|
|
197
|
-
name =
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
197
|
+
name = (
|
|
198
|
+
i["secCode"]
|
|
199
|
+
+ "_"
|
|
200
|
+
+ i["secName"]
|
|
201
|
+
+ "_"
|
|
202
|
+
+ i["announcementTitle"]
|
|
203
|
+
+ ".pdf"
|
|
204
|
+
)
|
|
205
|
+
if "*" in name:
|
|
206
|
+
name = name.replace("*", "")
|
|
207
|
+
file_path = output_dir + name
|
|
201
208
|
|
|
202
209
|
# 显示下载进度
|
|
203
210
|
print(f" ↓ {name}")
|
|
204
211
|
|
|
205
212
|
# 确保目录存在
|
|
206
|
-
os.makedirs(
|
|
213
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
207
214
|
|
|
208
215
|
time.sleep(random.random() * 2)
|
|
209
216
|
|
|
210
|
-
headers[
|
|
217
|
+
headers["User-Agent"] = random.choice(User_Agent)
|
|
211
218
|
r = requests.get(download)
|
|
212
219
|
|
|
213
220
|
f = open(file_path, "wb")
|
|
214
221
|
f.write(r.content)
|
|
215
222
|
f.close()
|
|
223
|
+
downloaded_count += 1
|
|
216
224
|
else:
|
|
217
225
|
continue
|
|
218
226
|
|
|
219
|
-
return
|
|
227
|
+
return downloaded_count
|
|
220
228
|
|
|
221
229
|
|
|
222
|
-
def
|
|
223
|
-
"""
|
|
224
|
-
|
|
230
|
+
def query_prospectus(stock_code):
|
|
231
|
+
"""查询指定股票代码的招股书公告列表"""
|
|
232
|
+
all_announcements = []
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
announcements_sse = sseStock(1, stock_code)
|
|
236
|
+
all_announcements.extend(announcements_sse)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
print(f"沪市招股书查询失败: {e}")
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
announcements_szse = szseStock(1, stock_code)
|
|
242
|
+
all_announcements.extend(announcements_szse)
|
|
243
|
+
except Exception as e:
|
|
244
|
+
print(f"深市招股书查询失败: {e}")
|
|
245
|
+
|
|
246
|
+
prospectus_keywords = ["招股书", "招股说明书", "招股意向书"]
|
|
247
|
+
filtered = [
|
|
248
|
+
a for a in all_announcements
|
|
249
|
+
if any(kw in a.get("announcementTitle", "") for kw in prospectus_keywords)
|
|
250
|
+
]
|
|
225
251
|
|
|
226
|
-
|
|
227
|
-
stock_code: Stock code (e.g., '000888', '688777')
|
|
228
|
-
year: Optional year filter (e.g., 2024). If None, returns all years
|
|
252
|
+
return filtered
|
|
229
253
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
"""
|
|
254
|
+
|
|
255
|
+
def download_prospectus(stock_code, save_path=None):
|
|
256
|
+
"""下载指定股票的招股书"""
|
|
257
|
+
announcements = query_prospectus(stock_code)
|
|
258
|
+
|
|
259
|
+
if not announcements:
|
|
260
|
+
return {
|
|
261
|
+
"success": False,
|
|
262
|
+
"message": f"未找到股票 {stock_code} 的招股书",
|
|
263
|
+
"downloaded": 0,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
output_dir = save_path or saving_path
|
|
267
|
+
count = Download(announcements, save_path=output_dir)
|
|
268
|
+
|
|
269
|
+
downloaded = count or 0
|
|
270
|
+
return {
|
|
271
|
+
"success": downloaded > 0,
|
|
272
|
+
"message": f"已下载 {stock_code} 招股书,共 {downloaded} 个文件"
|
|
273
|
+
if downloaded > 0
|
|
274
|
+
else f"未下载任何文件({stock_code} 招股书)",
|
|
275
|
+
"downloaded": downloaded,
|
|
276
|
+
"path": output_dir,
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def query_annual_reports(stock_code, year=None):
|
|
281
|
+
"""查询指定股票的年度报告列表"""
|
|
233
282
|
all_announcements = []
|
|
234
283
|
|
|
235
|
-
#
|
|
284
|
+
# 查询沪市
|
|
236
285
|
try:
|
|
237
286
|
announcements_sse = sseAnnual(1, stock_code)
|
|
238
287
|
all_announcements.extend(announcements_sse)
|
|
239
288
|
except Exception as e:
|
|
240
|
-
print(f"
|
|
289
|
+
print(f"沪市年报查询失败: {e}")
|
|
241
290
|
|
|
242
|
-
#
|
|
291
|
+
# 查询深市
|
|
243
292
|
try:
|
|
244
293
|
announcements_szse = szseAnnual(1, stock_code)
|
|
245
294
|
all_announcements.extend(announcements_szse)
|
|
246
295
|
except Exception as e:
|
|
247
|
-
print(f"
|
|
296
|
+
print(f"深市年报查询失败: {e}")
|
|
248
297
|
|
|
249
|
-
#
|
|
298
|
+
# 按年份过滤
|
|
250
299
|
if year:
|
|
251
300
|
year_str = str(year)
|
|
252
301
|
filtered = []
|
|
253
302
|
for announcement in all_announcements:
|
|
254
|
-
if year_str in announcement.get(
|
|
303
|
+
if year_str in announcement.get("announcementTitle", ""):
|
|
255
304
|
filtered.append(announcement)
|
|
256
305
|
all_announcements = filtered
|
|
257
306
|
|
|
258
307
|
return all_announcements
|
|
259
308
|
|
|
260
309
|
|
|
261
|
-
def download_annual_reports(stock_code, year=None):
|
|
262
|
-
"""
|
|
263
|
-
Download annual reports for a specific stock code
|
|
264
|
-
|
|
265
|
-
Args:
|
|
266
|
-
stock_code: Stock code (e.g., '000888', '688777')
|
|
267
|
-
year: Optional year filter (e.g., 2024). If None, downloads all years
|
|
268
|
-
|
|
269
|
-
Returns:
|
|
270
|
-
Dictionary with status and message
|
|
271
|
-
"""
|
|
310
|
+
def download_annual_reports(stock_code, year=None, save_path=None):
|
|
311
|
+
"""下载指定股票的年度报告"""
|
|
272
312
|
announcements = query_annual_reports(stock_code, year)
|
|
273
313
|
|
|
274
314
|
if not announcements:
|
|
275
315
|
return {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
316
|
+
"success": False,
|
|
317
|
+
"message": f"未找到股票 {stock_code} 的年度报告"
|
|
318
|
+
+ (f"({year} 年)" if year else ""),
|
|
319
|
+
"downloaded": 0,
|
|
279
320
|
}
|
|
280
321
|
|
|
281
|
-
|
|
282
|
-
|
|
322
|
+
output_dir = save_path or saving_path
|
|
323
|
+
count = Download(announcements, year_filter=year, save_path=output_dir)
|
|
283
324
|
|
|
325
|
+
downloaded = count or 0
|
|
326
|
+
year_suffix = f"({year} 年)" if year else ""
|
|
284
327
|
return {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
328
|
+
"success": downloaded > 0,
|
|
329
|
+
"message": f"已下载 {stock_code} 年度报告{year_suffix},共 {downloaded} 个文件"
|
|
330
|
+
if downloaded > 0
|
|
331
|
+
else f"未下载任何文件({stock_code} 年度报告{year_suffix})",
|
|
332
|
+
"downloaded": downloaded,
|
|
333
|
+
"path": output_dir,
|
|
289
334
|
}
|
|
290
335
|
|
|
291
336
|
|
|
292
|
-
# given page_number & stock number
|
|
293
337
|
def Run(page_number, stock):
|
|
294
338
|
try:
|
|
295
339
|
annual_report = szseAnnual(page_number, stock)
|
|
@@ -297,19 +341,19 @@ def Run(page_number, stock):
|
|
|
297
341
|
annual_report_ = sseAnnual(page_number, stock)
|
|
298
342
|
stock_report_ = sseStock(page_number, stock)
|
|
299
343
|
except Exception:
|
|
300
|
-
print(page_number,
|
|
344
|
+
print(page_number, "page error, retrying")
|
|
301
345
|
try:
|
|
302
346
|
annual_report = szseAnnual(page_number, stock)
|
|
303
347
|
except Exception:
|
|
304
|
-
print(page_number,
|
|
348
|
+
print(page_number, "page error")
|
|
305
349
|
Download(annual_report)
|
|
306
350
|
Download(stock_report)
|
|
307
351
|
Download(annual_report_)
|
|
308
352
|
Download(stock_report_)
|
|
309
353
|
|
|
310
354
|
|
|
311
|
-
if __name__ ==
|
|
312
|
-
with open(
|
|
355
|
+
if __name__ == "__main__":
|
|
356
|
+
with open("company_id.txt") as file:
|
|
313
357
|
lines = file.readlines()
|
|
314
358
|
for line in lines:
|
|
315
359
|
stock = line
|
|
@@ -1,28 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* This runs automatically after `npm install`
|
|
4
|
+
* npm install 后自动安装 Python 依赖
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
const { spawn } = require(
|
|
9
|
-
const fs = require(
|
|
10
|
-
const path = require(
|
|
7
|
+
const { spawn } = require("child_process");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
11
10
|
|
|
12
|
-
const REQUIREMENTS_FILE = path.join(
|
|
11
|
+
const REQUIREMENTS_FILE = path.join(
|
|
12
|
+
__dirname,
|
|
13
|
+
"..",
|
|
14
|
+
"python",
|
|
15
|
+
"requirements.txt",
|
|
16
|
+
);
|
|
13
17
|
|
|
14
18
|
async function findPython() {
|
|
15
|
-
const pythonCommands = [
|
|
19
|
+
const pythonCommands = [
|
|
20
|
+
"python3",
|
|
21
|
+
"python",
|
|
22
|
+
"python3.12",
|
|
23
|
+
"python3.11",
|
|
24
|
+
"python3.10",
|
|
25
|
+
];
|
|
16
26
|
|
|
17
27
|
for (const cmd of pythonCommands) {
|
|
18
28
|
try {
|
|
19
|
-
const result = await spawnCommand(cmd, [
|
|
20
|
-
if (result.stdout && result.stdout.includes(
|
|
29
|
+
const result = await spawnCommand(cmd, ["--version"]);
|
|
30
|
+
if (result.stdout && result.stdout.includes("Python")) {
|
|
21
31
|
return cmd;
|
|
22
32
|
}
|
|
23
|
-
} catch (error) {
|
|
24
|
-
// Continue
|
|
25
|
-
}
|
|
33
|
+
} catch (error) {}
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
return null;
|
|
@@ -30,51 +38,64 @@ async function findPython() {
|
|
|
30
38
|
|
|
31
39
|
function spawnCommand(cmd, args) {
|
|
32
40
|
return new Promise((resolve, reject) => {
|
|
33
|
-
const child = spawn(cmd, args, {
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
const child = spawn(cmd, args, {
|
|
42
|
+
stdio: "pipe",
|
|
43
|
+
shell: process.platform === "win32",
|
|
44
|
+
});
|
|
45
|
+
let stdout = "";
|
|
46
|
+
let stderr = "";
|
|
36
47
|
|
|
37
|
-
child.stdout?.on(
|
|
38
|
-
child.stderr?.on(
|
|
48
|
+
child.stdout?.on("data", (d) => (stdout += d));
|
|
49
|
+
child.stderr?.on("data", (d) => (stderr += d));
|
|
39
50
|
|
|
40
|
-
child.on(
|
|
51
|
+
child.on("close", (code) => {
|
|
41
52
|
if (code === 0) resolve({ stdout, stderr });
|
|
42
|
-
else reject(new Error(`Command failed: ${cmd} ${args.join(
|
|
53
|
+
else reject(new Error(`Command failed: ${cmd} ${args.join(" ")}`));
|
|
43
54
|
});
|
|
44
55
|
|
|
45
|
-
child.on(
|
|
56
|
+
child.on("error", reject);
|
|
46
57
|
});
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
async function main() {
|
|
50
|
-
//
|
|
61
|
+
// requirements.txt 不存在则跳过
|
|
51
62
|
if (!fs.existsSync(REQUIREMENTS_FILE)) {
|
|
52
|
-
console.log(
|
|
63
|
+
console.log(
|
|
64
|
+
"⚠️ requirements.txt not found, skipping Python dependencies installation",
|
|
65
|
+
);
|
|
53
66
|
return;
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
const pythonCmd = await findPython();
|
|
57
70
|
if (!pythonCmd) {
|
|
58
|
-
console.warn(
|
|
59
|
-
|
|
71
|
+
console.warn(
|
|
72
|
+
"⚠️ Python not found. Python dependencies will be installed on first run.",
|
|
73
|
+
);
|
|
74
|
+
console.warn(" Please install Python 3.10+ from https://python.org");
|
|
60
75
|
return;
|
|
61
76
|
}
|
|
62
77
|
|
|
63
78
|
try {
|
|
64
|
-
//
|
|
65
|
-
await spawnCommand(pythonCmd, [
|
|
66
|
-
console.log(
|
|
79
|
+
// 检查 mcp 是否已安装
|
|
80
|
+
await spawnCommand(pythonCmd, ["-c", "import mcp"]);
|
|
81
|
+
console.log("✅ Python dependencies already installed");
|
|
67
82
|
} catch (error) {
|
|
68
|
-
//
|
|
69
|
-
console.log(
|
|
83
|
+
// 执行安装
|
|
84
|
+
console.log("📦 Installing Python dependencies...");
|
|
70
85
|
try {
|
|
71
|
-
await spawnCommand(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
86
|
+
await spawnCommand(
|
|
87
|
+
pythonCmd,
|
|
88
|
+
["-m", "pip", "install", "-r", REQUIREMENTS_FILE],
|
|
89
|
+
{
|
|
90
|
+
stdio: "inherit",
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
console.log("✅ Python dependencies installed successfully");
|
|
75
94
|
} catch (installError) {
|
|
76
|
-
console.warn(
|
|
77
|
-
|
|
95
|
+
console.warn(
|
|
96
|
+
"⚠️ Failed to install Python dependencies during npm install",
|
|
97
|
+
);
|
|
98
|
+
console.warn(" They will be installed automatically on first run");
|
|
78
99
|
}
|
|
79
100
|
}
|
|
80
101
|
}
|