@youhaozhao/cninfo-mcp 1.0.6 → 1.1.0
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/README.md +2 -1
- package/bin/cninfo-mcp.js +91 -49
- package/package.json +2 -2
- package/python/bridge.py +161 -0
- package/python/spider.py +39 -12
- package/scripts/install-python-deps.js +29 -4
- package/python/__pycache__/mcp_server.cpython-314.pyc +0 -0
- package/python/__pycache__/spider.cpython-314.pyc +0 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@youhaozhao/cninfo-mcp)
|
|
4
4
|
|
|
5
|
-
通过 MCP
|
|
5
|
+
通过 MCP 协议查询和下载巨潮资讯网上市公司年报 PDF 的工具,适用于 Claude Desktop。
|
|
6
6
|
|
|
7
7
|
## 使用方法
|
|
8
8
|
|
|
@@ -50,3 +50,4 @@
|
|
|
50
50
|
|
|
51
51
|
爬虫逻辑基于 [gaodechen/cninfo_process](https://github.com/gaodechen/cninfo_process)。
|
|
52
52
|
|
|
53
|
+
|
package/bin/cninfo-mcp.js
CHANGED
|
@@ -5,22 +5,45 @@
|
|
|
5
5
|
* 自动检测 Python 并安装依赖,然后启动 Python MCP 服务器。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const { spawn } = require(
|
|
9
|
-
const path = require(
|
|
10
|
-
const fs = require(
|
|
8
|
+
const { spawn } = require("child_process");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const os = require("os");
|
|
11
12
|
|
|
12
13
|
// 配置路径
|
|
13
|
-
const PYTHON_SCRIPT = path.join(__dirname,
|
|
14
|
-
const PYTHON_REQUIREMENTS = path.join(
|
|
14
|
+
const PYTHON_SCRIPT = path.join(__dirname, "..", "python", "mcp_server.py");
|
|
15
|
+
const PYTHON_REQUIREMENTS = path.join(
|
|
16
|
+
__dirname,
|
|
17
|
+
"..",
|
|
18
|
+
"python",
|
|
19
|
+
"requirements.txt",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// 虚拟环境目录,放在用户目录下保证跨 npx 调用持久化
|
|
23
|
+
const VENV_DIR = path.join(os.homedir(), ".cninfo-mcp", "venv");
|
|
24
|
+
|
|
25
|
+
// 获取虚拟环境中的 Python 可执行文件路径
|
|
26
|
+
function getVenvPython() {
|
|
27
|
+
if (process.platform === "win32") {
|
|
28
|
+
return path.join(VENV_DIR, "Scripts", "python.exe");
|
|
29
|
+
}
|
|
30
|
+
return path.join(VENV_DIR, "bin", "python3");
|
|
31
|
+
}
|
|
15
32
|
|
|
16
|
-
//
|
|
33
|
+
// 查找可用的系统 Python 可执行文件(仅用于创建 venv)
|
|
17
34
|
async function findPython() {
|
|
18
|
-
const pythonCommands = [
|
|
35
|
+
const pythonCommands = [
|
|
36
|
+
"python3",
|
|
37
|
+
"python",
|
|
38
|
+
"python3.12",
|
|
39
|
+
"python3.11",
|
|
40
|
+
"python3.10",
|
|
41
|
+
];
|
|
19
42
|
|
|
20
43
|
for (const cmd of pythonCommands) {
|
|
21
44
|
try {
|
|
22
|
-
const result = await spawnAsync(cmd, [
|
|
23
|
-
if (result.stdout && result.stdout.includes(
|
|
45
|
+
const result = await spawnAsync(cmd, ["--version"]);
|
|
46
|
+
if (result.stdout && result.stdout.includes("Python")) {
|
|
24
47
|
return cmd;
|
|
25
48
|
}
|
|
26
49
|
} catch (error) {
|
|
@@ -29,38 +52,57 @@ async function findPython() {
|
|
|
29
52
|
}
|
|
30
53
|
|
|
31
54
|
throw new Error(
|
|
32
|
-
|
|
33
|
-
|
|
55
|
+
"Python not found. Please install Python 3.10+ from https://python.org\n" +
|
|
56
|
+
"After installation, restart your terminal and try again.",
|
|
34
57
|
);
|
|
35
58
|
}
|
|
36
59
|
|
|
37
|
-
//
|
|
38
|
-
async function
|
|
60
|
+
// 创建虚拟环境(如果不存在)
|
|
61
|
+
async function ensureVenv(systemPythonCmd) {
|
|
62
|
+
const venvPython = getVenvPython();
|
|
63
|
+
if (fs.existsSync(venvPython)) {
|
|
64
|
+
return venvPython;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.error("Creating Python virtual environment...");
|
|
68
|
+
fs.mkdirSync(path.dirname(VENV_DIR), { recursive: true });
|
|
69
|
+
await spawnAsync(systemPythonCmd, ["-m", "venv", VENV_DIR], {
|
|
70
|
+
stdio: "inherit",
|
|
71
|
+
});
|
|
72
|
+
console.error("Virtual environment created\n");
|
|
73
|
+
return venvPython;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 检查并安装 Python 依赖(使用 venv 中的 python)
|
|
77
|
+
async function ensureDependencies(venvPython) {
|
|
39
78
|
const requirementsPath = PYTHON_REQUIREMENTS;
|
|
40
79
|
|
|
41
80
|
if (!fs.existsSync(requirementsPath)) {
|
|
42
|
-
console.error(
|
|
81
|
+
console.error("Error: requirements.txt not found at", requirementsPath);
|
|
43
82
|
process.exit(1);
|
|
44
83
|
}
|
|
45
84
|
|
|
46
85
|
try {
|
|
47
86
|
// 检查 mcp 包是否已安装
|
|
48
|
-
|
|
87
|
+
await spawnAsync(venvPython, ["-c", "import mcp"]);
|
|
49
88
|
} catch (error) {
|
|
50
89
|
// 未安装,执行安装
|
|
51
|
-
console.error(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
90
|
+
console.error("Installing Python dependencies...");
|
|
91
|
+
try {
|
|
92
|
+
await spawnAsync(
|
|
93
|
+
venvPython,
|
|
94
|
+
["-m", "pip", "install", "-r", requirementsPath],
|
|
95
|
+
{
|
|
96
|
+
stdio: "inherit",
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
console.error("Python dependencies installed successfully\n");
|
|
100
|
+
} catch (installError) {
|
|
101
|
+
console.error("\n❌ Failed to install Python dependencies");
|
|
102
|
+
console.error("Please run manually:");
|
|
103
|
+
console.error(` ${venvPython} -m pip install -r ${requirementsPath}`);
|
|
60
104
|
process.exit(1);
|
|
61
105
|
}
|
|
62
|
-
|
|
63
|
-
console.error('✅ Python dependencies installed successfully\n');
|
|
64
106
|
}
|
|
65
107
|
}
|
|
66
108
|
|
|
@@ -68,28 +110,28 @@ async function ensureDependencies(pythonCmd) {
|
|
|
68
110
|
function spawnAsync(command, args, options = {}) {
|
|
69
111
|
return new Promise((resolve, reject) => {
|
|
70
112
|
const child = spawn(command, args, {
|
|
71
|
-
stdio: options.stdio ||
|
|
72
|
-
shell: process.platform ===
|
|
73
|
-
...options
|
|
113
|
+
stdio: options.stdio || "pipe",
|
|
114
|
+
shell: process.platform === "win32",
|
|
115
|
+
...options,
|
|
74
116
|
});
|
|
75
117
|
|
|
76
|
-
let stdout =
|
|
77
|
-
let stderr =
|
|
118
|
+
let stdout = "";
|
|
119
|
+
let stderr = "";
|
|
78
120
|
let code = null;
|
|
79
121
|
|
|
80
122
|
if (child.stdout) {
|
|
81
|
-
child.stdout.on(
|
|
123
|
+
child.stdout.on("data", (data) => {
|
|
82
124
|
stdout += data.toString();
|
|
83
125
|
});
|
|
84
126
|
}
|
|
85
127
|
|
|
86
128
|
if (child.stderr) {
|
|
87
|
-
child.stderr.on(
|
|
129
|
+
child.stderr.on("data", (data) => {
|
|
88
130
|
stderr += data.toString();
|
|
89
131
|
});
|
|
90
132
|
}
|
|
91
133
|
|
|
92
|
-
child.on(
|
|
134
|
+
child.on("close", (exitCode) => {
|
|
93
135
|
code = exitCode;
|
|
94
136
|
if (code === 0) {
|
|
95
137
|
resolve({ stdout, stderr, code });
|
|
@@ -102,7 +144,7 @@ function spawnAsync(command, args, options = {}) {
|
|
|
102
144
|
}
|
|
103
145
|
});
|
|
104
146
|
|
|
105
|
-
child.on(
|
|
147
|
+
child.on("error", (error) => {
|
|
106
148
|
reject(error);
|
|
107
149
|
});
|
|
108
150
|
});
|
|
@@ -112,36 +154,36 @@ async function main() {
|
|
|
112
154
|
try {
|
|
113
155
|
// 检查 Python 脚本是否存在
|
|
114
156
|
if (!fs.existsSync(PYTHON_SCRIPT)) {
|
|
115
|
-
console.error(
|
|
157
|
+
console.error("Error: mcp_server.py not found at", PYTHON_SCRIPT);
|
|
116
158
|
process.exit(1);
|
|
117
159
|
}
|
|
118
160
|
|
|
119
|
-
const
|
|
120
|
-
await
|
|
161
|
+
const systemPython = await findPython();
|
|
162
|
+
const venvPython = await ensureVenv(systemPython);
|
|
163
|
+
await ensureDependencies(venvPython);
|
|
121
164
|
|
|
122
165
|
// 启动 MCP 服务器
|
|
123
|
-
console.error(
|
|
124
|
-
const child = spawn(
|
|
125
|
-
stdio:
|
|
126
|
-
shell: process.platform ===
|
|
166
|
+
console.error("巨潮资讯 MCP 服务器已启动,等待连接...");
|
|
167
|
+
const child = spawn(venvPython, [PYTHON_SCRIPT], {
|
|
168
|
+
stdio: "inherit",
|
|
169
|
+
shell: process.platform === "win32",
|
|
127
170
|
env: {
|
|
128
171
|
...process.env,
|
|
129
|
-
PYTHONPATH: path.join(__dirname,
|
|
130
|
-
}
|
|
172
|
+
PYTHONPATH: path.join(__dirname, "..", "python"),
|
|
173
|
+
},
|
|
131
174
|
});
|
|
132
175
|
|
|
133
176
|
// 处理子进程退出
|
|
134
|
-
child.on(
|
|
135
|
-
console.error(
|
|
177
|
+
child.on("error", (error) => {
|
|
178
|
+
console.error("Failed to start MCP Server:", error.message);
|
|
136
179
|
process.exit(1);
|
|
137
180
|
});
|
|
138
181
|
|
|
139
|
-
child.on(
|
|
182
|
+
child.on("exit", (code) => {
|
|
140
183
|
process.exit(code || 0);
|
|
141
184
|
});
|
|
142
|
-
|
|
143
185
|
} catch (error) {
|
|
144
|
-
console.error(
|
|
186
|
+
console.error("Error:", error.message);
|
|
145
187
|
process.exit(1);
|
|
146
188
|
}
|
|
147
189
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@youhaozhao/cninfo-mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "MCP Server for querying and downloading Chinese listed companies' annual reports from CNINFO (巨潮资讯网)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -50,4 +50,4 @@
|
|
|
50
50
|
"README.md",
|
|
51
51
|
"LICENSE"
|
|
52
52
|
]
|
|
53
|
-
}
|
|
53
|
+
}
|
package/python/bridge.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""CLI bridge that exposes cninfo spider functions as JSON in/out for Node.js."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import traceback
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
11
|
+
|
|
12
|
+
from spider import ( # noqa: E402
|
|
13
|
+
download_annual_reports,
|
|
14
|
+
download_prospectus,
|
|
15
|
+
query_annual_reports,
|
|
16
|
+
query_prospectus,
|
|
17
|
+
saving_path,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
BASE_URL = "https://static.cninfo.com.cn/"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _format_reports(reports: list[dict]) -> list[dict]:
|
|
25
|
+
formatted = []
|
|
26
|
+
for report in reports:
|
|
27
|
+
adj = report.get("adjunctUrl", "")
|
|
28
|
+
formatted.append(
|
|
29
|
+
{
|
|
30
|
+
"announcementTitle": report.get("announcementTitle", ""),
|
|
31
|
+
"announcementTime": report.get("announcementTime", ""),
|
|
32
|
+
"secCode": report.get("secCode", ""),
|
|
33
|
+
"secName": report.get("secName", ""),
|
|
34
|
+
"adjunctUrl": BASE_URL + adj if adj else "",
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
return formatted
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _require_stock_code(payload: dict) -> str:
|
|
41
|
+
stock_code = (payload.get("stock_code") or "").strip()
|
|
42
|
+
if not stock_code:
|
|
43
|
+
raise ValueError("stock_code is required")
|
|
44
|
+
return stock_code
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _optional_year(payload: dict) -> Optional[int]:
|
|
48
|
+
year = payload.get("year")
|
|
49
|
+
if year is None:
|
|
50
|
+
return None
|
|
51
|
+
return int(year)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _resolve_save_path(payload: dict) -> str:
|
|
55
|
+
save_path = payload.get("save_path")
|
|
56
|
+
return save_path if save_path else saving_path
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def action_query_annual_reports(payload: dict) -> dict:
|
|
60
|
+
stock_code = _require_stock_code(payload)
|
|
61
|
+
year = _optional_year(payload)
|
|
62
|
+
reports = query_annual_reports(stock_code, year)
|
|
63
|
+
suffix = f" for year {year}" if year else ""
|
|
64
|
+
if not reports:
|
|
65
|
+
return {
|
|
66
|
+
"success": False,
|
|
67
|
+
"stock_code": stock_code,
|
|
68
|
+
"year": year,
|
|
69
|
+
"count": 0,
|
|
70
|
+
"reports": [],
|
|
71
|
+
"message": f"No annual reports found for stock {stock_code}{suffix}",
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
"success": True,
|
|
75
|
+
"stock_code": stock_code,
|
|
76
|
+
"year": year,
|
|
77
|
+
"count": len(reports),
|
|
78
|
+
"reports": _format_reports(reports),
|
|
79
|
+
"message": f"Found {len(reports)} annual report(s){suffix}",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def action_download_annual_reports(payload: dict) -> dict:
|
|
84
|
+
stock_code = _require_stock_code(payload)
|
|
85
|
+
year = _optional_year(payload)
|
|
86
|
+
output_dir = _resolve_save_path(payload)
|
|
87
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
88
|
+
result = download_annual_reports(stock_code, year, save_path=output_dir)
|
|
89
|
+
result["stock_code"] = stock_code
|
|
90
|
+
result["year"] = year
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def action_query_prospectus(payload: dict) -> dict:
|
|
95
|
+
stock_code = _require_stock_code(payload)
|
|
96
|
+
reports = query_prospectus(stock_code)
|
|
97
|
+
if not reports:
|
|
98
|
+
return {
|
|
99
|
+
"success": False,
|
|
100
|
+
"stock_code": stock_code,
|
|
101
|
+
"count": 0,
|
|
102
|
+
"reports": [],
|
|
103
|
+
"message": f"No prospectus found for stock {stock_code}",
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
"success": True,
|
|
107
|
+
"stock_code": stock_code,
|
|
108
|
+
"count": len(reports),
|
|
109
|
+
"reports": _format_reports(reports),
|
|
110
|
+
"message": f"Found {len(reports)} prospectus document(s)",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def action_download_prospectus(payload: dict) -> dict:
|
|
115
|
+
stock_code = _require_stock_code(payload)
|
|
116
|
+
output_dir = _resolve_save_path(payload)
|
|
117
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
118
|
+
result = download_prospectus(stock_code, save_path=output_dir)
|
|
119
|
+
result["stock_code"] = stock_code
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
ACTIONS = {
|
|
124
|
+
"query_annual_reports": action_query_annual_reports,
|
|
125
|
+
"download_annual_reports": action_download_annual_reports,
|
|
126
|
+
"query_prospectus": action_query_prospectus,
|
|
127
|
+
"download_prospectus": action_download_prospectus,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def main() -> int:
|
|
132
|
+
if len(sys.argv) < 3:
|
|
133
|
+
sys.stderr.write("usage: bridge.py <action> <json-payload>\n")
|
|
134
|
+
return 2
|
|
135
|
+
|
|
136
|
+
action = sys.argv[1]
|
|
137
|
+
raw_payload = sys.argv[2]
|
|
138
|
+
|
|
139
|
+
handler = ACTIONS.get(action)
|
|
140
|
+
if handler is None:
|
|
141
|
+
sys.stdout.write(json.dumps({"ok": False, "error": f"Unknown action: {action}"}))
|
|
142
|
+
return 1
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
payload = json.loads(raw_payload) if raw_payload else {}
|
|
146
|
+
except json.JSONDecodeError as exc:
|
|
147
|
+
sys.stdout.write(json.dumps({"ok": False, "error": f"Invalid JSON payload: {exc}"}))
|
|
148
|
+
return 1
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
result: Any = handler(payload)
|
|
152
|
+
sys.stdout.write(json.dumps({"ok": True, "payload": result}, ensure_ascii=False))
|
|
153
|
+
return 0
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
sys.stderr.write(traceback.format_exc())
|
|
156
|
+
sys.stdout.write(json.dumps({"ok": False, "error": str(exc)}))
|
|
157
|
+
return 1
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
raise SystemExit(main())
|
package/python/spider.py
CHANGED
|
@@ -18,6 +18,13 @@ _saving_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pdf")
|
|
|
18
18
|
saving_path = _saving_path + "/"
|
|
19
19
|
logger = logging.getLogger(__name__)
|
|
20
20
|
|
|
21
|
+
# 巨潮资讯历史公告的实际下限约为 2001 会计年度,再往前查询无数据返回。
|
|
22
|
+
EARLIEST_DATE = "2001-01-01"
|
|
23
|
+
# 接口单页最大返回条数
|
|
24
|
+
PAGE_SIZE = 30
|
|
25
|
+
# 翻页安全上限,防止异常情况下无限循环
|
|
26
|
+
MAX_PAGES = 100
|
|
27
|
+
|
|
21
28
|
User_Agent = [
|
|
22
29
|
"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)",
|
|
23
30
|
"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
|
|
@@ -55,6 +62,26 @@ def _date_range(start_date: str) -> str:
|
|
|
55
62
|
return f"{start_date}~{today}"
|
|
56
63
|
|
|
57
64
|
|
|
65
|
+
def _paginate(fetch_fn, stock):
|
|
66
|
+
"""
|
|
67
|
+
对单页查询函数翻页,汇总所有页的公告。
|
|
68
|
+
|
|
69
|
+
巨潮接口单页最多返回 PAGE_SIZE 条,放开时间区间后历史年报会跨越多页,
|
|
70
|
+
必须翻页才能取全。以“返回数量不足一页”作为终止条件,并设安全上限。
|
|
71
|
+
"""
|
|
72
|
+
all_items = []
|
|
73
|
+
for page in range(1, MAX_PAGES + 1):
|
|
74
|
+
items = fetch_fn(page, stock)
|
|
75
|
+
if not items:
|
|
76
|
+
break
|
|
77
|
+
all_items.extend(items)
|
|
78
|
+
if len(items) < PAGE_SIZE: # 不足一页说明已到最后一页
|
|
79
|
+
break
|
|
80
|
+
else:
|
|
81
|
+
logger.warning("翻页达到上限 %s,结果可能被截断(%s)", MAX_PAGES, stock)
|
|
82
|
+
return all_items
|
|
83
|
+
|
|
84
|
+
|
|
58
85
|
def _is_annual_report_title(
|
|
59
86
|
title: str, year_filter: Optional[Union[int, str]] = None
|
|
60
87
|
) -> bool:
|
|
@@ -101,7 +128,7 @@ def szseAnnual(page, stock):
|
|
|
101
128
|
query_path = "http://www.cninfo.com.cn/new/hisAnnouncement/query"
|
|
102
129
|
query = {
|
|
103
130
|
"pageNum": page, # 页码
|
|
104
|
-
"pageSize":
|
|
131
|
+
"pageSize": PAGE_SIZE,
|
|
105
132
|
"tabName": "fulltext",
|
|
106
133
|
"column": "szse", # 深交所
|
|
107
134
|
"stock": "",
|
|
@@ -110,7 +137,7 @@ def szseAnnual(page, stock):
|
|
|
110
137
|
"plate": "sz",
|
|
111
138
|
"category": "category_ndbg_szsh", # 年度报告
|
|
112
139
|
"trade": "",
|
|
113
|
-
"seDate": _date_range(
|
|
140
|
+
"seDate": _date_range(EARLIEST_DATE), # 时间区间
|
|
114
141
|
}
|
|
115
142
|
|
|
116
143
|
namelist = requests.post(
|
|
@@ -127,7 +154,7 @@ def sseAnnual(page, stock):
|
|
|
127
154
|
query_path = "http://www.cninfo.com.cn/new/hisAnnouncement/query"
|
|
128
155
|
query = {
|
|
129
156
|
"pageNum": page, # 页码
|
|
130
|
-
"pageSize":
|
|
157
|
+
"pageSize": PAGE_SIZE,
|
|
131
158
|
"tabName": "fulltext",
|
|
132
159
|
"column": "sse",
|
|
133
160
|
"stock": "",
|
|
@@ -136,7 +163,7 @@ def sseAnnual(page, stock):
|
|
|
136
163
|
"plate": "sh",
|
|
137
164
|
"category": "category_ndbg_szsh", # 年度报告
|
|
138
165
|
"trade": "",
|
|
139
|
-
"seDate": _date_range(
|
|
166
|
+
"seDate": _date_range(EARLIEST_DATE), # 时间区间
|
|
140
167
|
}
|
|
141
168
|
|
|
142
169
|
namelist = requests.post(
|
|
@@ -153,7 +180,7 @@ def szseStock(page, stock):
|
|
|
153
180
|
query_path = "http://www.cninfo.com.cn/new/hisAnnouncement/query"
|
|
154
181
|
query = {
|
|
155
182
|
"pageNum": page, # 页码
|
|
156
|
-
"pageSize":
|
|
183
|
+
"pageSize": PAGE_SIZE,
|
|
157
184
|
"tabName": "fulltext",
|
|
158
185
|
"column": "szse",
|
|
159
186
|
"stock": "",
|
|
@@ -162,7 +189,7 @@ def szseStock(page, stock):
|
|
|
162
189
|
"plate": "sz",
|
|
163
190
|
"category": "",
|
|
164
191
|
"trade": "",
|
|
165
|
-
"seDate": _date_range(
|
|
192
|
+
"seDate": _date_range(EARLIEST_DATE), # 时间区间
|
|
166
193
|
}
|
|
167
194
|
|
|
168
195
|
namelist = requests.post(
|
|
@@ -179,7 +206,7 @@ def sseStock(page, stock):
|
|
|
179
206
|
query_path = "http://www.cninfo.com.cn/new/hisAnnouncement/query"
|
|
180
207
|
query = {
|
|
181
208
|
"pageNum": page, # 页码
|
|
182
|
-
"pageSize":
|
|
209
|
+
"pageSize": PAGE_SIZE,
|
|
183
210
|
"tabName": "fulltext",
|
|
184
211
|
"column": "sse",
|
|
185
212
|
"stock": "",
|
|
@@ -188,7 +215,7 @@ def sseStock(page, stock):
|
|
|
188
215
|
"plate": "sh",
|
|
189
216
|
"category": "",
|
|
190
217
|
"trade": "",
|
|
191
|
-
"seDate": _date_range(
|
|
218
|
+
"seDate": _date_range(EARLIEST_DATE), # 时间区间
|
|
192
219
|
}
|
|
193
220
|
|
|
194
221
|
namelist = requests.post(
|
|
@@ -271,13 +298,13 @@ def query_prospectus(stock_code):
|
|
|
271
298
|
all_announcements = []
|
|
272
299
|
|
|
273
300
|
try:
|
|
274
|
-
announcements_sse = sseStock
|
|
301
|
+
announcements_sse = _paginate(sseStock, stock_code)
|
|
275
302
|
all_announcements.extend(announcements_sse)
|
|
276
303
|
except Exception as e:
|
|
277
304
|
logger.warning("沪市招股书查询失败: %s", e)
|
|
278
305
|
|
|
279
306
|
try:
|
|
280
|
-
announcements_szse = szseStock
|
|
307
|
+
announcements_szse = _paginate(szseStock, stock_code)
|
|
281
308
|
all_announcements.extend(announcements_szse)
|
|
282
309
|
except Exception as e:
|
|
283
310
|
logger.warning("深市招股书查询失败: %s", e)
|
|
@@ -323,14 +350,14 @@ def query_annual_reports(stock_code, year=None):
|
|
|
323
350
|
|
|
324
351
|
# 查询沪市
|
|
325
352
|
try:
|
|
326
|
-
announcements_sse = sseAnnual
|
|
353
|
+
announcements_sse = _paginate(sseAnnual, stock_code)
|
|
327
354
|
all_announcements.extend(announcements_sse)
|
|
328
355
|
except Exception as e:
|
|
329
356
|
logger.warning("沪市年报查询失败: %s", e)
|
|
330
357
|
|
|
331
358
|
# 查询深市
|
|
332
359
|
try:
|
|
333
|
-
announcements_szse = szseAnnual
|
|
360
|
+
announcements_szse = _paginate(szseAnnual, stock_code)
|
|
334
361
|
all_announcements.extend(announcements_szse)
|
|
335
362
|
except Exception as e:
|
|
336
363
|
logger.warning("深市年报查询失败: %s", e)
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
const { spawn } = require("child_process");
|
|
8
8
|
const fs = require("fs");
|
|
9
9
|
const path = require("path");
|
|
10
|
+
const os = require("os");
|
|
10
11
|
|
|
11
12
|
const REQUIREMENTS_FILE = path.join(
|
|
12
13
|
__dirname,
|
|
@@ -15,6 +16,15 @@ const REQUIREMENTS_FILE = path.join(
|
|
|
15
16
|
"requirements.txt",
|
|
16
17
|
);
|
|
17
18
|
|
|
19
|
+
const VENV_DIR = path.join(os.homedir(), ".cninfo-mcp", "venv");
|
|
20
|
+
|
|
21
|
+
function getVenvPython() {
|
|
22
|
+
if (process.platform === "win32") {
|
|
23
|
+
return path.join(VENV_DIR, "Scripts", "python.exe");
|
|
24
|
+
}
|
|
25
|
+
return path.join(VENV_DIR, "bin", "python3");
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
async function findPython() {
|
|
19
29
|
const pythonCommands = [
|
|
20
30
|
"python3",
|
|
@@ -75,16 +85,31 @@ async function main() {
|
|
|
75
85
|
return;
|
|
76
86
|
}
|
|
77
87
|
|
|
88
|
+
// 创建虚拟环境(如果不存在)
|
|
89
|
+
const venvPython = getVenvPython();
|
|
90
|
+
if (!fs.existsSync(venvPython)) {
|
|
91
|
+
console.log("Creating Python virtual environment...");
|
|
92
|
+
try {
|
|
93
|
+
fs.mkdirSync(path.dirname(VENV_DIR), { recursive: true });
|
|
94
|
+
await spawnCommand(pythonCmd, ["-m", "venv", VENV_DIR]);
|
|
95
|
+
console.log("Virtual environment created");
|
|
96
|
+
} catch (venvError) {
|
|
97
|
+
console.warn(" Failed to create virtual environment during npm install");
|
|
98
|
+
console.warn(" It will be created automatically on first run");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
78
103
|
try {
|
|
79
|
-
// 检查 mcp
|
|
80
|
-
await spawnCommand(
|
|
104
|
+
// 检查 mcp 是否已安装(用 venv 的 python)
|
|
105
|
+
await spawnCommand(venvPython, ["-c", "import mcp"]);
|
|
81
106
|
console.log("✅ Python dependencies already installed");
|
|
82
107
|
} catch (error) {
|
|
83
|
-
//
|
|
108
|
+
// 执行安装(用 venv 的 pip)
|
|
84
109
|
console.log("📦 Installing Python dependencies...");
|
|
85
110
|
try {
|
|
86
111
|
await spawnCommand(
|
|
87
|
-
|
|
112
|
+
venvPython,
|
|
88
113
|
["-m", "pip", "install", "-r", REQUIREMENTS_FILE],
|
|
89
114
|
{
|
|
90
115
|
stdio: "inherit",
|
|
Binary file
|
|
Binary file
|