@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 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
@@ -1,5 +1,7 @@
1
1
  # cninfo-mcp
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@youhaozhao/cninfo-mcp)](https://www.npmjs.com/package/@youhaozhao/cninfo-mcp)
4
+
3
5
  通过 MCP 协议查询和下载巨潮资讯网上市公司年报的工具,适用于 Claude Desktop。
4
6
 
5
7
  ## 使用方法
package/bin/cninfo-mcp.js CHANGED
@@ -1,23 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * CNINFO MCP Server Launcher
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
- // Configuration
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
- // Continue to next command
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
- // Check if mcp package is installed
47
+ // 检查 mcp 包是否已安装
54
48
  const checkResult = await spawnAsync(pythonCmd, ['-c', 'import mcp']);
55
49
  } catch (error) {
56
- // Dependencies not installed, install them
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
- // Check if Python script exists
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
- // Launch MCP Server with stdio
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
- // Handle child process exit
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.2",
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/cninfo_mcp#readme",
14
+ "homepage": "https://github.com/youhaozhao/cninfo-mcp#readme",
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "git+https://github.com/youhaozhao/cninfo_mcp.git"
17
+ "url": "git+https://github.com/youhaozhao/cninfo-mcp.git"
18
18
  },
19
19
  "bugs": {
20
- "url": "https://github.com/youhaozhao/cninfo_mcp/issues"
20
+ "url": "https://github.com/youhaozhao/cninfo-mcp/issues"
21
21
  },
22
+ "license": "MIT",
22
23
  "bin": {
23
- "cninfo-mcp": "./bin/cninfo-mcp.js"
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
+ }
@@ -1,31 +1,34 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- CNINFO MCP Server
4
- Model Context Protocol server for querying and downloading annual reports from CNINFO
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
- # Add current directory to path for imports
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 query_annual_reports, download_annual_reports, saving_path
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
- # Create MCP server
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
- 'success': False,
50
- 'stock_code': stock_code,
51
- 'year': year,
52
- 'count': 0,
53
- 'reports': [],
54
- 'message': f'No annual reports found for stock {stock_code}' + (f' in year {year}' if year else '')
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
- # Extract relevant information
61
+ # 提取关键字段
62
+ base_url = "https://static.cninfo.com.cn/"
58
63
  report_details = []
59
64
  for report in reports:
60
- report_details.append({
61
- 'announcementTitle': report.get('announcementTitle', ''),
62
- 'announcementTime': report.get('announcementTime', ''),
63
- 'secCode': report.get('secCode', ''),
64
- 'secName': report.get('secName', ''),
65
- 'adjunctUrl': report.get('adjunctUrl', '')
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
- 'success': True,
70
- 'stock_code': stock_code,
71
- 'year': year,
72
- 'count': len(reports),
73
- 'reports': report_details,
74
- 'message': f'Found {len(reports)} annual report(s)' + (f' for year {year}' if year else '')
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
- 'success': False,
80
- 'stock_code': stock_code,
81
- 'year': year,
82
- 'count': 0,
83
- 'reports': [],
84
- 'error': str(e),
85
- 'message': f'Error querying annual reports: {str(e)}'
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
- # Ensure download directory exists
112
- os.makedirs(saving_path, exist_ok=True)
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
- # Add path information
117
- result['stock_code'] = stock_code
118
- result['year'] = year
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
- 'success': False,
125
- 'stock_code': stock_code,
126
- 'year': year,
127
- 'downloaded': 0,
128
- 'path': saving_path,
129
- 'error': str(e),
130
- 'message': f'Error downloading annual reports: {str(e)}'
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.resource("annual-reports-list://{stock_code}")
135
- def get_annual_reports_list(stock_code: str) -> str:
139
+ @mcp.tool()
140
+ def query_prospectus_tool(stock_code: str) -> dict:
136
141
  """
137
- Get a formatted list of annual reports for a stock code
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
- Formatted string with annual reports information
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('announcementTitle', 'N/A')
155
- time = report.get('announcementTime', 'N/A')
156
- name = report.get('secName', 'N/A')
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
- # Run the server with stdio transport
263
+ # stdio 方式运行服务器
172
264
  mcp.run()
package/python/spider.py CHANGED
@@ -1,17 +1,17 @@
1
1
  """
2
- downloads:
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 = 'https://static.cninfo.com.cn/'
11
+ download_path = "https://static.cninfo.com.cn/"
12
12
  # 使用脚本所在目录的相对路径
13
- _saving_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pdf')
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 = {'Accept': 'application/json, text/javascript, */*; q=0.01',
28
- "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
29
- "Accept-Encoding": "gzip, deflate",
30
- "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",
31
- 'Host': 'www.cninfo.com.cn',
32
- 'Origin': 'http://www.cninfo.com.cn',
33
- 'Referer': 'http://www.cninfo.com.cn/new/commonUrl?url=disclosure/list/notice',
34
- 'X-Requested-With': 'XMLHttpRequest'
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 = 'http://www.cninfo.com.cn/new/hisAnnouncement/query'
43
- headers['User-Agent'] = random.choice(User_Agent) # 定义User_Agent
44
- query = {'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
- }
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 'announcements' in result and result['announcements']:
60
- return result['announcements']
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 = 'http://www.cninfo.com.cn/new/hisAnnouncement/query'
67
- headers['User-Agent'] = random.choice(User_Agent) # 定义User_Agent
68
- query = {'pageNum': page, # 页码
69
- 'pageSize': 30,
70
- 'tabName': 'fulltext',
71
- 'column': 'sse',
72
- 'stock': '',
73
- 'searchkey': stock, # 使用searchkey查询股票代码或公司名
74
- 'secid': '',
75
- 'plate': 'sh',
76
- 'category': 'category_ndbg_szsh', # 年度报告
77
- 'trade': '',
78
- 'seDate': '2020-01-01~2026-02-15' # 时间区间
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 'announcements' in result and result['announcements']:
84
- return result['announcements']
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 = 'http://www.cninfo.com.cn/new/hisAnnouncement/query'
91
- headers['User-Agent'] = random.choice(User_Agent) # 定义User_Agent
92
- query = {'pageNum': page, # 页码
93
- 'pageSize': 30,
94
- 'tabName': 'fulltext',
95
- 'column': 'szse',
96
- 'stock': '',
97
- 'searchkey': stock + ' 招股', # 组合搜索:股票代码 + 招股
98
- 'secid': '',
99
- 'plate': 'sz',
100
- 'category': '',
101
- 'trade': '',
102
- 'seDate': '2015-01-01~2026-02-15' # 时间区间
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 'announcements' in result and result['announcements']:
108
- return result['announcements']
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 = 'http://www.cninfo.com.cn/new/hisAnnouncement/query'
115
- headers['User-Agent'] = random.choice(User_Agent) # 定义User_Agent
116
- query = {'pageNum': page, # 页码
117
- 'pageSize': 30,
118
- 'tabName': 'fulltext',
119
- 'column': 'sse',
120
- 'stock': '',
121
- 'searchkey': stock + ' 招股', # 组合搜索:股票代码 + 招股
122
- 'secid': '',
123
- 'plate': 'sh',
124
- 'category': '',
125
- 'trade': '',
126
- 'seDate': '2015-01-01~2026-02-15' # 时间区间
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 'announcements' in result and result['announcements']:
132
- return result['announcements']
134
+ if result and "announcements" in result and result["announcements"]:
135
+ return result["announcements"]
133
136
  return []
134
137
 
135
138
 
136
- # download PDF
137
- def Download(single_page, year_filter=None):
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 = {'Accept': 'application/json, text/javascript, */*; q=0.01',
149
- "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
150
- "Accept-Encoding": "gzip, deflate",
151
- "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",
152
- 'Host': 'www.cninfo.com.cn',
153
- 'Origin': 'http://www.cninfo.com.cn'
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
- # Build allowed list dynamically based on year_filter
153
+ # 按年份筛选允许下载的标题
157
154
  allowed_list = []
158
155
  if year_filter:
159
156
  allowed_list = [
160
- f'{year_filter}年年度报告(更新后)',
161
- f'{year_filter}年年度报告',
157
+ f"{year_filter}年年度报告(更新后)",
158
+ f"{year_filter}年年度报告",
162
159
  ]
163
160
  else:
164
- # Default: all years from 2016-2025
161
+ # 默认下载 2016-2025
165
162
  for year in range(2016, 2026):
166
- allowed_list.append(f'{year}年年度报告(更新后)')
167
- allowed_list.append(f'{year}年年度报告')
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['announcementTitle']
176
+ title = i["announcementTitle"]
177
177
 
178
- # 跳过确认意见等非正式报告
179
- if '确认意见' in title or '取消' in title:
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 item in title:
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 = i["secCode"] + '_' + i['secName'] + '_' + i['announcementTitle'] + '.pdf'
198
- if '*' in name:
199
- name = name.replace('*', '')
200
- file_path = saving_path + name
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(os.path.dirname(file_path), exist_ok=True)
213
+ os.makedirs(output_dir, exist_ok=True)
207
214
 
208
215
  time.sleep(random.random() * 2)
209
216
 
210
- headers['User-Agent'] = random.choice(User_Agent)
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 True
227
+ return downloaded_count
220
228
 
221
229
 
222
- def query_annual_reports(stock_code, year=None):
223
- """
224
- Query annual reports for a specific stock code
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
- Args:
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
- Returns:
231
- List of announcement dictionaries
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
- # Try SSE (Shanghai)
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"Error querying SSE: {e}")
289
+ print(f"沪市年报查询失败: {e}")
241
290
 
242
- # Try SZSE (Shenzhen)
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"Error querying SZSE: {e}")
296
+ print(f"深市年报查询失败: {e}")
248
297
 
249
- # Filter by year if specified
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('announcementTitle', ''):
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
- 'success': False,
277
- 'message': f'No annual reports found for stock {stock_code}' + (f' in year {year}' if year else ''),
278
- 'downloaded': 0
316
+ "success": False,
317
+ "message": f"未找到股票 {stock_code} 的年度报告"
318
+ + (f"({year} 年)" if year else ""),
319
+ "downloaded": 0,
279
320
  }
280
321
 
281
- # Download PDFs
282
- result = Download(announcements, year_filter=year)
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
- 'success': result,
286
- 'message': f'Downloaded reports for {stock_code}' + (f' year {year}' if year else ''),
287
- 'downloaded': len(announcements),
288
- 'path': saving_path
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, 'page error, retrying')
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, 'page error')
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__ == '__main__':
312
- with open('company_id.txt') as file:
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
- * Post-install script to install Python dependencies
5
- * This runs automatically after `npm install`
4
+ * npm install 后自动安装 Python 依赖
6
5
  */
7
6
 
8
- const { spawn } = require('child_process');
9
- const fs = require('fs');
10
- const path = require('path');
7
+ const { spawn } = require("child_process");
8
+ const fs = require("fs");
9
+ const path = require("path");
11
10
 
12
- const REQUIREMENTS_FILE = path.join(__dirname, '..', 'python', 'requirements.txt');
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 = ['python3', 'python', 'python3.12', 'python3.11', 'python3.10'];
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, ['--version']);
20
- if (result.stdout && result.stdout.includes('Python')) {
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, { stdio: 'pipe', shell: process.platform === 'win32' });
34
- let stdout = '';
35
- let stderr = '';
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('data', (d) => stdout += d);
38
- child.stderr?.on('data', (d) => stderr += d);
48
+ child.stdout?.on("data", (d) => (stdout += d));
49
+ child.stderr?.on("data", (d) => (stderr += d));
39
50
 
40
- child.on('close', (code) => {
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('error', reject);
56
+ child.on("error", reject);
46
57
  });
47
58
  }
48
59
 
49
60
  async function main() {
50
- // Skip if requirements.txt doesn't exist
61
+ // requirements.txt 不存在则跳过
51
62
  if (!fs.existsSync(REQUIREMENTS_FILE)) {
52
- console.log('⚠️ requirements.txt not found, skipping Python dependencies installation');
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('⚠️ Python not found. Python dependencies will be installed on first run.');
59
- console.warn(' Please install Python 3.10+ from https://python.org');
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
- // Check if mcp is already installed
65
- await spawnCommand(pythonCmd, ['-c', 'import mcp']);
66
- console.log('✅ Python dependencies already installed');
79
+ // 检查 mcp 是否已安装
80
+ await spawnCommand(pythonCmd, ["-c", "import mcp"]);
81
+ console.log("✅ Python dependencies already installed");
67
82
  } catch (error) {
68
- // Install dependencies
69
- console.log('📦 Installing Python dependencies...');
83
+ // 执行安装
84
+ console.log("📦 Installing Python dependencies...");
70
85
  try {
71
- await spawnCommand(pythonCmd, ['-m', 'pip', 'install', '-r', REQUIREMENTS_FILE], {
72
- stdio: 'inherit'
73
- });
74
- console.log('✅ Python dependencies installed successfully');
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('⚠️ Failed to install Python dependencies during npm install');
77
- console.warn(' They will be installed automatically on first run');
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
  }