mcp-query-table 0.3.0__py3-none-any.whl → 0.3.3__py3-none-any.whl
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.
- mcp_query_table/__init__.py +2 -0
- mcp_query_table/__main__.py +7 -6
- mcp_query_table/_version.py +1 -1
- mcp_query_table/enums.py +2 -1
- mcp_query_table/providers/baidu.py +10 -4
- mcp_query_table/providers/n.py +36 -6
- mcp_query_table/providers/yuanbao.py +27 -4
- mcp_query_table/server.py +20 -14
- mcp_query_table/tool.py +102 -40
- {mcp_query_table-0.3.0.dist-info → mcp_query_table-0.3.3.dist-info}/METADATA +43 -28
- mcp_query_table-0.3.3.dist-info/RECORD +19 -0
- mcp_query_table-0.3.0.dist-info/RECORD +0 -19
- {mcp_query_table-0.3.0.dist-info → mcp_query_table-0.3.3.dist-info}/WHEEL +0 -0
- {mcp_query_table-0.3.0.dist-info → mcp_query_table-0.3.3.dist-info}/licenses/LICENSE +0 -0
- {mcp_query_table-0.3.0.dist-info → mcp_query_table-0.3.3.dist-info}/top_level.txt +0 -0
mcp_query_table/__init__.py
CHANGED
mcp_query_table/__main__.py
CHANGED
|
@@ -10,19 +10,20 @@ def main():
|
|
|
10
10
|
|
|
11
11
|
parser.add_argument("--format", type=str, help="输出格式",
|
|
12
12
|
default='markdown', choices=['markdown', 'csv', 'json'])
|
|
13
|
-
parser.add_argument("--
|
|
14
|
-
default=9222)
|
|
15
|
-
parser.add_argument("--
|
|
13
|
+
parser.add_argument("--cdp_endpoint", type=str, help="浏览器CDP调试地址",
|
|
14
|
+
default="http://127.0.0.1:9222")
|
|
15
|
+
parser.add_argument("--executable_path", type=str, help="浏览器类型",
|
|
16
16
|
default=r'C:\Program Files\Google\Chrome\Application\chrome.exe')
|
|
17
17
|
|
|
18
18
|
parser.add_argument("--transport", type=str, help="传输类型",
|
|
19
19
|
default='stdio', choices=['stdio', 'sse'])
|
|
20
|
-
parser.add_argument("--
|
|
20
|
+
parser.add_argument("--host", type=str, help="MCP服务端绑定地址",
|
|
21
21
|
default='0.0.0.0')
|
|
22
|
-
parser.add_argument("--
|
|
22
|
+
parser.add_argument("--port", type=int, help="MCP服务端绑定端口",
|
|
23
23
|
default='8000')
|
|
24
24
|
args = parser.parse_args()
|
|
25
|
-
serve(args.format, args.
|
|
25
|
+
serve(args.format, args.cdp_endpoint, args.executable_path,
|
|
26
|
+
args.transport, args.host, args.port)
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
if __name__ == "__main__":
|
mcp_query_table/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.3.
|
|
1
|
+
__version__ = "0.3.3"
|
mcp_query_table/enums.py
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
"""
|
|
6
6
|
import json
|
|
7
7
|
|
|
8
|
+
from loguru import logger
|
|
8
9
|
from playwright.async_api import Page
|
|
9
10
|
|
|
11
|
+
import mcp_query_table
|
|
10
12
|
from mcp_query_table.tool import GlobalVars
|
|
11
13
|
|
|
12
14
|
_PAGE0_ = "https://chat.baidu.com/search"
|
|
13
15
|
_PAGE1_ = "https://chat.baidu.com/aichat/api/conversation"
|
|
14
|
-
_TIMEOUT_ = 1000 * 120
|
|
15
16
|
|
|
16
17
|
G = GlobalVars()
|
|
17
18
|
|
|
@@ -55,9 +56,9 @@ async def on_response(response):
|
|
|
55
56
|
async def on_route(route):
|
|
56
57
|
# 避免出现 Protocol error (Network.getResponseBody): No data found for resource with given identifier
|
|
57
58
|
# print("on_route", route.request.url)
|
|
58
|
-
if route.request.url
|
|
59
|
+
if route.request.url == _PAGE1_:
|
|
59
60
|
# TODO 为何只要转发一下就没事了?
|
|
60
|
-
response = await route.fetch(timeout=
|
|
61
|
+
response = await route.fetch(timeout=mcp_query_table.TIMEOUT)
|
|
61
62
|
await route.fulfill(response=response)
|
|
62
63
|
else:
|
|
63
64
|
await route.continue_()
|
|
@@ -66,6 +67,7 @@ async def on_route(route):
|
|
|
66
67
|
async def chat(page: Page,
|
|
67
68
|
prompt: str,
|
|
68
69
|
create: bool,
|
|
70
|
+
files: list[str],
|
|
69
71
|
) -> str:
|
|
70
72
|
if not page.url.startswith(_PAGE0_):
|
|
71
73
|
create = True
|
|
@@ -73,8 +75,12 @@ async def chat(page: Page,
|
|
|
73
75
|
if create:
|
|
74
76
|
await page.goto(_PAGE0_)
|
|
75
77
|
|
|
78
|
+
if len(files) > 0:
|
|
79
|
+
# TODO
|
|
80
|
+
logger.warning("抱歉,百度AI搜索的上传文件功能未突破")
|
|
81
|
+
|
|
76
82
|
await page.route(_PAGE1_, on_route)
|
|
77
|
-
async with page.expect_response(_PAGE1_, timeout=
|
|
83
|
+
async with page.expect_response(_PAGE1_, timeout=mcp_query_table.TIMEOUT) as response_info:
|
|
78
84
|
await page.locator("#chat-input-box").fill(prompt)
|
|
79
85
|
await page.locator("#chat-input-box").press("Enter")
|
|
80
86
|
await on_response(await response_info.value)
|
mcp_query_table/providers/n.py
CHANGED
|
@@ -5,12 +5,13 @@ import json
|
|
|
5
5
|
|
|
6
6
|
from playwright.async_api import Page
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import mcp_query_table
|
|
9
|
+
from mcp_query_table.tool import GlobalVars, is_image
|
|
9
10
|
|
|
10
11
|
_PAGE0_ = "https://www.n.cn"
|
|
11
12
|
_PAGE1_ = "https://www.n.cn/search"
|
|
12
|
-
_PAGE2_ = "https://www.n.cn/api/common/chat/v2"
|
|
13
|
-
|
|
13
|
+
_PAGE2_ = "https://www.n.cn/api/common/chat/v2" # 对话
|
|
14
|
+
_PAGE3_ = "https://www.n.cn/api/image/upload" # 上传图片
|
|
14
15
|
|
|
15
16
|
G = GlobalVars()
|
|
16
17
|
|
|
@@ -48,7 +49,7 @@ def read_event_stream(text):
|
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
async def on_response(response):
|
|
51
|
-
if response == _PAGE2_:
|
|
52
|
+
if response.url == _PAGE2_:
|
|
52
53
|
# print("on_response", response.url)
|
|
53
54
|
text = await response.text()
|
|
54
55
|
G.set_text(read_event_stream(text))
|
|
@@ -57,18 +58,47 @@ async def on_response(response):
|
|
|
57
58
|
async def chat(page: Page,
|
|
58
59
|
prompt: str,
|
|
59
60
|
create: bool,
|
|
61
|
+
files: list[str],
|
|
60
62
|
) -> str:
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
page : playwright.async_api.Page
|
|
68
|
+
页面
|
|
69
|
+
prompt : str
|
|
70
|
+
问题
|
|
71
|
+
create : bool
|
|
72
|
+
是否创建新的对话
|
|
73
|
+
files : list[str] | None
|
|
74
|
+
上传的文件列表。目前仅支持上传图片
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
str
|
|
79
|
+
回答
|
|
80
|
+
"""
|
|
61
81
|
if not create:
|
|
62
82
|
if not page.url.startswith(_PAGE1_):
|
|
63
83
|
create = True
|
|
84
|
+
if len(files) > 0:
|
|
85
|
+
create = True
|
|
86
|
+
|
|
87
|
+
for file in files:
|
|
88
|
+
assert is_image(file), f"仅支持上传图片,{file}不是图片"
|
|
64
89
|
|
|
65
90
|
if create:
|
|
66
|
-
await page.goto(_PAGE0_)
|
|
67
91
|
name = "输入任何问题"
|
|
92
|
+
|
|
93
|
+
await page.goto(_PAGE0_)
|
|
94
|
+
if len(files) > 0:
|
|
95
|
+
# 只能在新会话中上传文件
|
|
96
|
+
async with page.expect_response(_PAGE3_, timeout=mcp_query_table.TIMEOUT) as response_info:
|
|
97
|
+
await page.locator("input[type=\"file\"]").set_input_files(files)
|
|
68
98
|
else:
|
|
69
99
|
name = "提出后续问题,Enter发送,Shift+Enter 换行"
|
|
70
100
|
|
|
71
|
-
async with page.expect_response(_PAGE2_, timeout=
|
|
101
|
+
async with page.expect_response(_PAGE2_, timeout=mcp_query_table.TIMEOUT) as response_info:
|
|
72
102
|
textbox = page.get_by_role("textbox", name=name)
|
|
73
103
|
await textbox.fill(prompt)
|
|
74
104
|
await textbox.press("Enter")
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
腾讯元宝
|
|
3
3
|
"""
|
|
4
4
|
import json
|
|
5
|
+
import re
|
|
5
6
|
|
|
6
7
|
from playwright.async_api import Page
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
import mcp_query_table
|
|
10
|
+
from mcp_query_table.tool import GlobalVars, is_image
|
|
9
11
|
|
|
10
12
|
_PAGE0_ = "https://yuanbao.tencent.com/"
|
|
11
13
|
_PAGE1_ = "https://yuanbao.tencent.com/api/chat"
|
|
12
|
-
|
|
14
|
+
_PAGE2_ = "https://yuanbao.tencent.com/api/resource/genUploadInfo"
|
|
13
15
|
|
|
14
16
|
G = GlobalVars()
|
|
15
17
|
|
|
@@ -52,7 +54,7 @@ async def on_route(route):
|
|
|
52
54
|
# print("on_route", route.request.url)
|
|
53
55
|
if route.request.url.startswith(_PAGE1_):
|
|
54
56
|
# TODO 这里会导致数据全部加载,逻辑变了,所以界面可能混乱
|
|
55
|
-
response = await route.fetch(timeout=
|
|
57
|
+
response = await route.fetch(timeout=mcp_query_table.TIMEOUT)
|
|
56
58
|
await route.fulfill(
|
|
57
59
|
# 强行加utf-8,否则编码搞不定
|
|
58
60
|
content_type="text/event-stream; charset=utf-8",
|
|
@@ -65,6 +67,7 @@ async def on_route(route):
|
|
|
65
67
|
async def chat(page: Page,
|
|
66
68
|
prompt: str,
|
|
67
69
|
create: bool,
|
|
70
|
+
files: list[str]
|
|
68
71
|
) -> str:
|
|
69
72
|
if not page.url.startswith(_PAGE0_):
|
|
70
73
|
create = True
|
|
@@ -72,8 +75,28 @@ async def chat(page: Page,
|
|
|
72
75
|
if create:
|
|
73
76
|
await page.goto(_PAGE0_)
|
|
74
77
|
|
|
78
|
+
if len(files) > 0:
|
|
79
|
+
is_img, is_doc = False, False
|
|
80
|
+
for f in files:
|
|
81
|
+
if is_image(f):
|
|
82
|
+
is_img = True
|
|
83
|
+
else:
|
|
84
|
+
is_doc = True
|
|
85
|
+
assert is_img ^ is_doc, "不能同时包含图片和文档"
|
|
86
|
+
|
|
87
|
+
# 点击上传文件按钮,才会出现上传文件的input
|
|
88
|
+
await page.get_by_role("button").filter(has_text=re.compile(r"^$")).last.click()
|
|
89
|
+
|
|
90
|
+
# 上传文件
|
|
91
|
+
async with page.expect_response(_PAGE2_, timeout=mcp_query_table.TIMEOUT) as response_info:
|
|
92
|
+
if is_img:
|
|
93
|
+
await page.locator("input[type=\"file\"]").nth(-2).set_input_files(files)
|
|
94
|
+
else:
|
|
95
|
+
await page.locator("input[type=\"file\"]").last.set_input_files(files)
|
|
96
|
+
|
|
97
|
+
# 提问
|
|
75
98
|
await page.route(f"{_PAGE1_}/*", on_route)
|
|
76
|
-
async with page.expect_response(f"{_PAGE1_}/*", timeout=
|
|
99
|
+
async with page.expect_response(f"{_PAGE1_}/*", timeout=mcp_query_table.TIMEOUT) as response_info:
|
|
77
100
|
textbox = page.locator(".ql-editor")
|
|
78
101
|
await textbox.fill(prompt)
|
|
79
102
|
await textbox.press("Enter")
|
mcp_query_table/server.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Annotated, Optional
|
|
1
|
+
from typing import Annotated, Optional, List
|
|
2
2
|
|
|
3
3
|
from loguru import logger
|
|
4
4
|
from mcp.server.fastmcp import FastMCP
|
|
@@ -10,9 +10,12 @@ from mcp_query_table.tool import BrowserManager
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class QueryServer:
|
|
13
|
-
def __init__(self,
|
|
13
|
+
def __init__(self,
|
|
14
|
+
format: str = 'markdown',
|
|
15
|
+
cdp_endpoint: Optional[str] = 'http://127.0.0.1:9222',
|
|
16
|
+
executable_path: Optional[str] = None) -> None:
|
|
14
17
|
self.format: str = format
|
|
15
|
-
self.browser = BrowserManager(
|
|
18
|
+
self.browser = BrowserManager(cdp_endpoint=cdp_endpoint, executable_path=executable_path, debug=False)
|
|
16
19
|
|
|
17
20
|
async def query(self, query_input: str, query_type: QueryType, max_page: int, site: Site):
|
|
18
21
|
page = await self.browser.get_page()
|
|
@@ -26,9 +29,9 @@ class QueryServer:
|
|
|
26
29
|
if self.format == 'json':
|
|
27
30
|
return df.to_json(force_ascii=False, indent=2)
|
|
28
31
|
|
|
29
|
-
async def chat(self, prompt: str, create: bool, provider: Provider):
|
|
32
|
+
async def chat(self, prompt: str, create: bool, files: List[str], provider: Provider):
|
|
30
33
|
page = await self.browser.get_page()
|
|
31
|
-
txt = await qt_chat(page, prompt, create, provider)
|
|
34
|
+
txt = await qt_chat(page, prompt, create, files, provider)
|
|
32
35
|
self.browser.release_page(page)
|
|
33
36
|
return txt
|
|
34
37
|
|
|
@@ -54,19 +57,22 @@ async def query(
|
|
|
54
57
|
async def chat(
|
|
55
58
|
prompt: Annotated[str, Field(description="提示词。如:`9.9大还是9.11大?`")],
|
|
56
59
|
create: Annotated[bool, Field(default=False, description="是否创建新对话")],
|
|
57
|
-
|
|
60
|
+
files: Annotated[List[str], Field(default=None, description="上传的文件列表。不同网站支持程度不同")],
|
|
61
|
+
provider: Annotated[
|
|
62
|
+
Provider, Field(default=Provider.Nami, description="提供商。支持`纳米搜索`、`腾讯元宝`、`百度AI搜索`")]
|
|
58
63
|
) -> str:
|
|
59
|
-
return await qsv.chat(prompt, create, provider)
|
|
64
|
+
return await qsv.chat(prompt, create, files, provider)
|
|
60
65
|
|
|
61
66
|
|
|
62
|
-
def serve(format,
|
|
67
|
+
def serve(format, cdp_endpoint, executable_path, transport, host, port):
|
|
63
68
|
qsv.format = format
|
|
64
|
-
qsv.
|
|
65
|
-
qsv.
|
|
66
|
-
logger.info("
|
|
69
|
+
qsv.cdp_endpoint = cdp_endpoint
|
|
70
|
+
qsv.executable_path = executable_path
|
|
71
|
+
logger.info(f"{format=},{transport=}")
|
|
72
|
+
logger.info(f"{cdp_endpoint=},{executable_path=}")
|
|
67
73
|
if transport == 'sse':
|
|
68
|
-
logger.info("
|
|
74
|
+
logger.info(f"{host=},{port=}", transport, host, port)
|
|
69
75
|
|
|
70
|
-
mcp.settings.host =
|
|
71
|
-
mcp.settings.port =
|
|
76
|
+
mcp.settings.host = host
|
|
77
|
+
mcp.settings.port = port
|
|
72
78
|
mcp.run(transport=transport)
|
mcp_query_table/tool.py
CHANGED
|
@@ -10,11 +10,6 @@ from playwright.async_api import async_playwright, Playwright, Page
|
|
|
10
10
|
|
|
11
11
|
from mcp_query_table.enums import QueryType, Site, Provider
|
|
12
12
|
|
|
13
|
-
browsers_path = {
|
|
14
|
-
"chrome.exe": r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
|
15
|
-
"msedge.exe": r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
|
|
16
|
-
}
|
|
17
|
-
|
|
18
13
|
|
|
19
14
|
def create_detached_process(command):
|
|
20
15
|
# 设置通用参数
|
|
@@ -33,6 +28,29 @@ def create_detached_process(command):
|
|
|
33
28
|
return subprocess.Popen(command, **kwargs)
|
|
34
29
|
|
|
35
30
|
|
|
31
|
+
def is_local_url(url: str) -> bool:
|
|
32
|
+
"""判断url是否是本地地址"""
|
|
33
|
+
for local in ('localhost', '127.0.0.1'):
|
|
34
|
+
if local in url.lower():
|
|
35
|
+
return True
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_executable_path(executable_path) -> Optional[str]:
|
|
40
|
+
"""获取浏览器可执行文件路径"""
|
|
41
|
+
browsers = {
|
|
42
|
+
"default": executable_path,
|
|
43
|
+
"chrome.exe": r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
|
44
|
+
"msedge.exe": r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
|
|
45
|
+
}
|
|
46
|
+
for k, v in browsers.items():
|
|
47
|
+
if v is None:
|
|
48
|
+
continue
|
|
49
|
+
if Path(v).exists():
|
|
50
|
+
return v
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
36
54
|
class BrowserManager:
|
|
37
55
|
async def __aenter__(self):
|
|
38
56
|
return self
|
|
@@ -40,29 +58,24 @@ class BrowserManager:
|
|
|
40
58
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
41
59
|
await self.cleanup()
|
|
42
60
|
|
|
43
|
-
def __init__(self,
|
|
61
|
+
def __init__(self,
|
|
62
|
+
cdp_endpoint: Optional[str] = None,
|
|
63
|
+
executable_path: Optional[str] = None,
|
|
64
|
+
debug: bool = False):
|
|
44
65
|
"""
|
|
45
66
|
|
|
46
67
|
Parameters
|
|
47
68
|
----------
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
cdp_endpoint:str
|
|
70
|
+
浏览器CDP地址
|
|
71
|
+
executable_path:str
|
|
72
|
+
浏览器可执行文件路径。推荐使用chrome,因为Microsoft Edge必须在任务管理器中完全退出才能启动调试端口
|
|
52
73
|
debug:bool
|
|
53
74
|
是否显示开发者工具
|
|
54
75
|
|
|
55
76
|
"""
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if Path(v).exists():
|
|
59
|
-
browser_path = v
|
|
60
|
-
break
|
|
61
|
-
if browser_path is None:
|
|
62
|
-
raise ValueError("未找到浏览器可执行文件")
|
|
63
|
-
|
|
64
|
-
self.port = port
|
|
65
|
-
self.browser_path = browser_path
|
|
77
|
+
self.cdp_endpoint = cdp_endpoint or 'http://127.0.0.1:9222'
|
|
78
|
+
self.executable_path = executable_path
|
|
66
79
|
self.debug = debug
|
|
67
80
|
|
|
68
81
|
self.playwright: Optional[Playwright] = None
|
|
@@ -77,25 +90,18 @@ class BrowserManager:
|
|
|
77
90
|
if self.playwright:
|
|
78
91
|
await self.playwright.stop()
|
|
79
92
|
|
|
80
|
-
async def
|
|
81
|
-
"""
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
https://blog.csdn.net/qq_30576521/article/details/142370538
|
|
86
|
-
|
|
87
|
-
"""
|
|
88
|
-
endpoint = f"http://127.0.0.1:{self.port}"
|
|
89
|
-
command = [self.browser_path, f'--remote-debugging-port={self.port}', '--start-maximized']
|
|
90
|
-
name = Path(self.browser_path).name
|
|
93
|
+
async def _connect_to_local(self) -> None:
|
|
94
|
+
"""连接本地浏览器"""
|
|
95
|
+
port = self.cdp_endpoint.split(':')[-1]
|
|
96
|
+
executable_path = get_executable_path(self.executable_path)
|
|
97
|
+
command = [executable_path, f'--remote-debugging-port={port}', '--start-maximized']
|
|
91
98
|
if self.debug:
|
|
92
99
|
command.append('--auto-open-devtools-for-tabs')
|
|
93
100
|
|
|
94
|
-
self.playwright = await async_playwright().start()
|
|
95
|
-
|
|
96
101
|
for i in range(2):
|
|
97
102
|
try:
|
|
98
|
-
self.browser = await self.playwright.chromium.connect_over_cdp(
|
|
103
|
+
self.browser = await self.playwright.chromium.connect_over_cdp(self.cdp_endpoint,
|
|
104
|
+
timeout=10000, slow_mo=1000)
|
|
99
105
|
break
|
|
100
106
|
except:
|
|
101
107
|
if i == 0:
|
|
@@ -105,7 +111,30 @@ class BrowserManager:
|
|
|
105
111
|
continue
|
|
106
112
|
if i == 1:
|
|
107
113
|
raise ConnectionError(
|
|
108
|
-
f"已提前打开了浏览器,但未开启远程调试端口?请关闭浏览器全部进程后重试 `taskkill /f /im {name}`")
|
|
114
|
+
f"已提前打开了浏览器,但未开启远程调试端口?请关闭浏览器全部进程后重试 `taskkill /f /im {Path(executable_path).name}`")
|
|
115
|
+
|
|
116
|
+
async def _connect_to_remote(self) -> None:
|
|
117
|
+
"""连接远程浏览器"""
|
|
118
|
+
try:
|
|
119
|
+
self.browser = await self.playwright.chromium.connect_over_cdp(self.cdp_endpoint,
|
|
120
|
+
timeout=10000, slow_mo=1000)
|
|
121
|
+
except:
|
|
122
|
+
raise ConnectionError(f"连接远程浏览器失败,请检查CDP地址和端口是否正确。{self.cdp_endpoint}")
|
|
123
|
+
|
|
124
|
+
async def _launch(self) -> None:
|
|
125
|
+
"""启动浏览器,并连接CDP协议
|
|
126
|
+
|
|
127
|
+
References
|
|
128
|
+
----------
|
|
129
|
+
https://blog.csdn.net/qq_30576521/article/details/142370538
|
|
130
|
+
|
|
131
|
+
"""
|
|
132
|
+
self.playwright = await async_playwright().start()
|
|
133
|
+
|
|
134
|
+
if is_local_url(self.cdp_endpoint):
|
|
135
|
+
await self._connect_to_local()
|
|
136
|
+
else:
|
|
137
|
+
await self._connect_to_remote()
|
|
109
138
|
|
|
110
139
|
self.context = self.browser.contexts[0]
|
|
111
140
|
# 复用打开的page
|
|
@@ -201,15 +230,48 @@ async def chat(
|
|
|
201
230
|
page: Page,
|
|
202
231
|
prompt: str = "9.9大还是9.11大?",
|
|
203
232
|
create: bool = False,
|
|
204
|
-
|
|
205
|
-
|
|
233
|
+
files: list[str] | None = None,
|
|
234
|
+
provider: Provider = Provider.Nami) -> str:
|
|
235
|
+
"""大语言对话
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
page : playwright.sync_api.Page
|
|
240
|
+
页面
|
|
241
|
+
prompt : str, optional
|
|
242
|
+
对话内容, by default "9.9大还是9.11大?"
|
|
243
|
+
create : bool, optional
|
|
244
|
+
是否创建新对话, by default False
|
|
245
|
+
files : list[str] | None, optional
|
|
246
|
+
上传的文件列表。不同网站支持程度不同
|
|
247
|
+
provider : Provider, optional
|
|
248
|
+
提供商, by default Provider.N
|
|
249
|
+
|
|
250
|
+
Returns
|
|
251
|
+
-------
|
|
252
|
+
str
|
|
253
|
+
对话结果
|
|
254
|
+
|
|
255
|
+
"""
|
|
256
|
+
# 空列表转None
|
|
257
|
+
if files is None:
|
|
258
|
+
files = []
|
|
259
|
+
|
|
260
|
+
if provider == Provider.Nami:
|
|
206
261
|
from mcp_query_table.providers.n import chat
|
|
207
|
-
return await chat(page, prompt, create)
|
|
262
|
+
return await chat(page, prompt, create, files)
|
|
208
263
|
if provider == Provider.YuanBao:
|
|
209
264
|
from mcp_query_table.providers.yuanbao import chat
|
|
210
|
-
return await chat(page, prompt, create)
|
|
265
|
+
return await chat(page, prompt, create, files)
|
|
211
266
|
if provider == Provider.BaiDu:
|
|
212
267
|
from mcp_query_table.providers.baidu import chat
|
|
213
|
-
return await chat(page, prompt, create)
|
|
268
|
+
return await chat(page, prompt, create, files)
|
|
214
269
|
|
|
215
270
|
raise ValueError(f"未支持的提供商:{provider}")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def is_image(path: str) -> bool:
|
|
274
|
+
"""判断是否是图片文件"""
|
|
275
|
+
img_ext = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
|
276
|
+
ext = Path(path).suffix.lower()
|
|
277
|
+
return ext in img_ext
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp_query_table
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: query table from website, support MCP
|
|
5
5
|
Author-email: wukan <wu-kan@163.com>
|
|
6
6
|
License: MIT License
|
|
@@ -70,31 +70,31 @@ from mcp_query_table import *
|
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
async def main() -> None:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
73
|
+
async with BrowserManager(cdp_endpoint="http://127.0.0.1:9222", executable_path=None, debug=True) as bm:
|
|
74
|
+
# 问财需要保证浏览器宽度>768,防止界面变成适应手机
|
|
75
|
+
page = await bm.get_page()
|
|
76
|
+
df = await query(page, '收益最好的200只ETF', query_type=QueryType.ETF, max_page=1, site=Site.THS)
|
|
77
|
+
print(df.to_markdown())
|
|
78
|
+
df = await query(page, '年初至今收益率前50', query_type=QueryType.Fund, max_page=1, site=Site.TDX)
|
|
79
|
+
print(df.to_csv())
|
|
80
|
+
df = await query(page, '流通市值前10的行业板块', query_type=QueryType.Index, max_page=1, site=Site.TDX)
|
|
81
|
+
print(df.to_csv())
|
|
82
|
+
# TODO 东财翻页要提前登录
|
|
83
|
+
df = await query(page, '今日涨幅前5的概念板块;', query_type=QueryType.Board, max_page=3, site=Site.EastMoney)
|
|
84
|
+
print(df)
|
|
85
|
+
|
|
86
|
+
output = await chat(page, "1+2等于多少?", provider=Provider.YuanBao)
|
|
87
|
+
print(output)
|
|
88
|
+
output = await chat(page, "3+4等于多少?", provider=Provider.YuanBao, create=True)
|
|
89
|
+
print(output)
|
|
90
|
+
|
|
91
|
+
print('done')
|
|
92
|
+
bm.release_page(page)
|
|
93
|
+
await page.wait_for_timeout(2000)
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
if __name__ == '__main__':
|
|
97
|
-
|
|
97
|
+
asyncio.run(main())
|
|
98
98
|
|
|
99
99
|
```
|
|
100
100
|
|
|
@@ -132,7 +132,8 @@ if __name__ == '__main__':
|
|
|
132
132
|
|
|
133
133
|
确保可以在控制台中执行`python -m mcp_query_table -h`。如果不能,可能要先`pip install mcp_query_table`
|
|
134
134
|
|
|
135
|
-
在`Cline`中可以配置如下。其中`command`是`python`的绝对路径,`
|
|
135
|
+
在`Cline`中可以配置如下。其中`command`是`python`的绝对路径,`executable_path`是`Chrome`的绝对路径,`timeout`是超时时间,单位为秒。
|
|
136
|
+
在各`AI`平台中由于返回时间常需1分钟以上,所以需要设置大的超时时间。
|
|
136
137
|
|
|
137
138
|
### STDIO方式
|
|
138
139
|
|
|
@@ -140,13 +141,16 @@ if __name__ == '__main__':
|
|
|
140
141
|
{
|
|
141
142
|
"mcpServers": {
|
|
142
143
|
"mcp_query_table": {
|
|
144
|
+
"timeout": 300,
|
|
143
145
|
"command": "D:\\Users\\Kan\\miniconda3\\envs\\py312\\python.exe",
|
|
144
146
|
"args": [
|
|
145
147
|
"-m",
|
|
146
148
|
"mcp_query_table",
|
|
147
149
|
"--format",
|
|
148
150
|
"markdown",
|
|
149
|
-
"--
|
|
151
|
+
"--cdp_endpoint",
|
|
152
|
+
"http://127.0.0.1:9222",
|
|
153
|
+
"--executable_path",
|
|
150
154
|
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
|
|
151
155
|
]
|
|
152
156
|
}
|
|
@@ -159,11 +163,21 @@ if __name__ == '__main__':
|
|
|
159
163
|
先在控制台中执行如下命令,启动`MCP`服务
|
|
160
164
|
|
|
161
165
|
```commandline
|
|
162
|
-
python -m mcp_query_table --format markdown --
|
|
166
|
+
python -m mcp_query_table --format markdown --transport sse --port 8000
|
|
163
167
|
```
|
|
164
168
|
|
|
165
169
|
然后就可以连接到`MCP`服务了
|
|
166
|
-
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"mcpServers": {
|
|
174
|
+
"mcp_query_table": {
|
|
175
|
+
"timeout": 300,
|
|
176
|
+
"url": "http://127.0.0.1:8000/sse"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
167
181
|
|
|
168
182
|
## 使用`MCP Inspector`进行调试
|
|
169
183
|
|
|
@@ -171,7 +185,8 @@ http://localhost:8000/sse
|
|
|
171
185
|
npx @modelcontextprotocol/inspector python -m mcp_query_table --format markdown
|
|
172
186
|
```
|
|
173
187
|
|
|
174
|
-
打开浏览器并翻页是一个比较耗时的操作,会导致`MCP Inspector`页面超时,可以`http://localhost:5173/?timeout=
|
|
188
|
+
打开浏览器并翻页是一个比较耗时的操作,会导致`MCP Inspector`页面超时,可以`http://localhost:5173/?timeout=300000`
|
|
189
|
+
表示超时时间为300秒
|
|
175
190
|
|
|
176
191
|
第一次尝试编写`MCP`项目,可能会有各种问题,欢迎大家交流。
|
|
177
192
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
mcp_query_table/__init__.py,sha256=gNXLL6MtH667HEdsUbJ4OAbGqwYO1dLdkqorV3-RtLM,238
|
|
2
|
+
mcp_query_table/__main__.py,sha256=W3tMnZZTvkhjlLHmSYHng3CTvYX6-LjyaBDhf-ypH7w,1186
|
|
3
|
+
mcp_query_table/_version.py,sha256=8KcCYTXH99C2-gCLuPILJvtT9YftRWJsartIx6TQ2ZY,22
|
|
4
|
+
mcp_query_table/enums.py,sha256=Hen0X1f2of69f08epun6HvYIgbpw_rf8BvoRQ184kS4,679
|
|
5
|
+
mcp_query_table/server.py,sha256=oV8CSi-EW5-xlPn38R1wLu7U-pCHgAJci1W8rJFsXSk,3430
|
|
6
|
+
mcp_query_table/tool.py,sha256=uKqqrUrnUnvwlOipX0YODUg9WMq_Us994X8sysxe1cM,8571
|
|
7
|
+
mcp_query_table/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
mcp_query_table/providers/baidu.py,sha256=VkjAN6mSD4xa9kfDvbCb_Xj9Su5F2dakzYNgjoLGsR4,2738
|
|
9
|
+
mcp_query_table/providers/n.py,sha256=GcqNMra1pc_DTw3QB3a2SUNzZCOrGjiuvuVDS-hTz5o,3006
|
|
10
|
+
mcp_query_table/providers/yuanbao.py,sha256=BfHg3A6RGOT-GxfxkrAoiOQFFo6qydtI5tZSoRGtPiY,3244
|
|
11
|
+
mcp_query_table/sites/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
mcp_query_table/sites/eastmoney.py,sha256=LImjpYVuM5YnXwnNzB2hkKfHofocZZScetGqMOCHZpk,4477
|
|
13
|
+
mcp_query_table/sites/iwencai.py,sha256=g56pj3pbxu4mXLNnaaS3Hdx-DvEy_9OBrQJe26z4z08,5059
|
|
14
|
+
mcp_query_table/sites/tdx.py,sha256=RIUQaB7Tn4AVyWaevk9SzTKIDwVO2f9erIlI-adXPLY,4126
|
|
15
|
+
mcp_query_table-0.3.3.dist-info/licenses/LICENSE,sha256=rbvv_CTd7biGwT21tvhgQ2zkbPFXOoON7WFQWEdElBA,1063
|
|
16
|
+
mcp_query_table-0.3.3.dist-info/METADATA,sha256=cRXUETyJhkJ12POY1fMsZv2YHvFdBor8c_BqNOZhy5g,8372
|
|
17
|
+
mcp_query_table-0.3.3.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
18
|
+
mcp_query_table-0.3.3.dist-info/top_level.txt,sha256=5M_8dkO1USOX7_EWbWS6O_TEsZ5yo-AodFNKeUEgvEQ,16
|
|
19
|
+
mcp_query_table-0.3.3.dist-info/RECORD,,
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
mcp_query_table/__init__.py,sha256=HqE0MJttWSwaeAhmm-tsPN3kZ4nrZUKwlfnyYJGHVRs,126
|
|
2
|
-
mcp_query_table/__main__.py,sha256=SmOaVfIDX4dcF-SV_xcn-Sfn3io-XunsdwKPnGfzwAU,1150
|
|
3
|
-
mcp_query_table/_version.py,sha256=VrXpHDu3erkzwl_WXrqINBm9xWkcyUy53IQOj042dOs,22
|
|
4
|
-
mcp_query_table/enums.py,sha256=378RbWeuCoRdJ2TxK-yMKg3rrJS0IpeI7UP5J1uveD8,625
|
|
5
|
-
mcp_query_table/server.py,sha256=QbkpK0s_5CDORrKesH845tT_GqosHUkRxKOWBrFsOsc,3101
|
|
6
|
-
mcp_query_table/tool.py,sha256=qfjWJN3KWncLoOtZz9N5s-hm2_cWjVLL2iHgoO2MCP8,6591
|
|
7
|
-
mcp_query_table/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
mcp_query_table/providers/baidu.py,sha256=CsmSjtnuzo-1aZxK5kGhIQjM9RpBmfpWVaEU5rofJyU,2541
|
|
9
|
-
mcp_query_table/providers/n.py,sha256=GYGoKN0BOOWkUKiW1DAn_9yyAvUBhZ6j3aNLy2cI99w,2138
|
|
10
|
-
mcp_query_table/providers/yuanbao.py,sha256=lkRbxe9EqciCCnJiucqRpMwD-sm4XkXRW_gVO7ZwzNU,2322
|
|
11
|
-
mcp_query_table/sites/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
mcp_query_table/sites/eastmoney.py,sha256=LImjpYVuM5YnXwnNzB2hkKfHofocZZScetGqMOCHZpk,4477
|
|
13
|
-
mcp_query_table/sites/iwencai.py,sha256=g56pj3pbxu4mXLNnaaS3Hdx-DvEy_9OBrQJe26z4z08,5059
|
|
14
|
-
mcp_query_table/sites/tdx.py,sha256=RIUQaB7Tn4AVyWaevk9SzTKIDwVO2f9erIlI-adXPLY,4126
|
|
15
|
-
mcp_query_table-0.3.0.dist-info/licenses/LICENSE,sha256=rbvv_CTd7biGwT21tvhgQ2zkbPFXOoON7WFQWEdElBA,1063
|
|
16
|
-
mcp_query_table-0.3.0.dist-info/METADATA,sha256=BNAaJWqlddE_wS2saWGSNK-QzgIAmlwAobBQrPQ24Eo,8044
|
|
17
|
-
mcp_query_table-0.3.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
18
|
-
mcp_query_table-0.3.0.dist-info/top_level.txt,sha256=5M_8dkO1USOX7_EWbWS6O_TEsZ5yo-AodFNKeUEgvEQ,16
|
|
19
|
-
mcp_query_table-0.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|