internal-web-reader 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: internal-web-reader
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: MCP Server - let Claude Code read internal web pages via browser cookies
5
5
  Author: zhanghaoran18006
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "internal-web-reader"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "MCP Server - let Claude Code read internal web pages via browser cookies"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -2,9 +2,8 @@
2
2
 
3
3
  import logging
4
4
  import os
5
- import re
5
+ import sys
6
6
  from dataclasses import dataclass, field
7
- from pathlib import Path
8
7
  from urllib.parse import urljoin
9
8
 
10
9
  from bs4 import BeautifulSoup
@@ -20,7 +19,6 @@ _H2T.protect_links = True
20
19
  _H2T.wrap_links = False
21
20
  _H2T.unicode_snob = True
22
21
 
23
- # 持久化浏览器 profile 目录
24
22
  _USER_DATA_DIR = os.path.join(
25
23
  os.environ.get("APPDATA", os.path.expanduser("~")),
26
24
  "internal-web-reader",
@@ -36,15 +34,18 @@ class PageResult:
36
34
  links: list[dict] = field(default_factory=list)
37
35
 
38
36
 
39
- # 全局浏览器实例(进程内复用)
40
- _browser = None
41
37
  _context = None
42
38
  _pw = None
43
39
 
44
40
 
41
+ def _stderr(msg: str):
42
+ """写 stderr,保证在 MCP stdio 模式下用户能看到提示。"""
43
+ sys.stderr.write(f"\n{'='*60}\n{msg}\n{'='*60}\n")
44
+ sys.stderr.flush()
45
+
46
+
45
47
  def _ensure_browser():
46
- """启动 Playwright 浏览器(进程内单例)。"""
47
- global _browser, _context, _pw
48
+ global _context, _pw
48
49
  if _context is not None:
49
50
  return _context
50
51
 
@@ -61,58 +62,89 @@ def _ensure_browser():
61
62
  locale="zh-CN",
62
63
  args=["--disable-blink-features=AutomationControlled"],
63
64
  )
64
- log.info("浏览器已启动")
65
65
  return _context
66
66
 
67
67
 
68
68
  def _is_login_page(page) -> bool:
69
- """判断当前页面是否是登录页。"""
70
- url = page.url.lower()
71
- if any(k in url for k in ("/login", "/signin", "/sso", "/auth", "/cas/", "/oauth")):
69
+ url_lower = page.url.lower()
70
+ login_keywords = ["/login", "/signin", "/sso", "/cas", "/oauth", "/auth", "passport"]
71
+ if any(k in url_lower for k in login_keywords):
72
72
  return True
73
- # 检查页面是否有密码输入框
74
- pw_inputs = page.query_selector_all('input[type="password"]')
75
- if pw_inputs and len(pw_inputs) > 0:
73
+ if page.query_selector('input[type="password"]'):
76
74
  return True
77
75
  return False
78
76
 
79
77
 
78
+ def _wait_for_login(page, timeout_ms: int = 300_000):
79
+ """
80
+ 检测到登录页后,在页面上注入提示,并等待用户完成登录。
81
+ """
82
+ _stderr(
83
+ " 需要登录!请切换到弹出的浏览器窗口完成登录。\n"
84
+ " 登录成功后会自动继续,不用回来操作终端。"
85
+ )
86
+
87
+ # 在页面上注入一个醒目的提示
88
+ page.evaluate("""() => {
89
+ const banner = document.createElement('div');
90
+ banner.id = '__iwr_login_hint__';
91
+ banner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:999999;'
92
+ + 'background:#ff6b35;color:white;padding:16px;text-align:center;'
93
+ + 'font-size:18px;font-weight:bold;font-family:sans-serif;'
94
+ + 'box-shadow:0 4px 12px rgba(0,0,0,0.3);';
95
+ banner.textContent = 'Internal Web Reader: Please log in here, then wait a few seconds...';
96
+ document.body.prepend(banner);
97
+ }""")
98
+
99
+ # 等待:密码框消失 + URL 不再包含 login 关键词
100
+ import time
101
+ start = time.time()
102
+ while (time.time() - start) * 1000 < timeout_ms:
103
+ time.sleep(2)
104
+ current_url = page.url.lower()
105
+ has_pw = page.query_selector('input[type="password"]')
106
+
107
+ # 如果 URL 不再含 login 关键词 且 没有密码框 → 登录成功
108
+ login_in_url = any(k in current_url for k in ["/login", "/signin", "/sso", "/cas", "/oauth", "passport"])
109
+ if not login_in_url and not has_pw:
110
+ # 移除提示
111
+ page.evaluate("""() => {
112
+ const el = document.getElementById('__iwr_login_hint__');
113
+ if (el) el.remove();
114
+ }""")
115
+ _stderr(" 登录成功!正在读取页面...")
116
+ page.wait_for_load_state("domcontentloaded", timeout=30_000)
117
+ return True
118
+
119
+ _stderr(" 登录等待超时(5分钟),尝试继续读取当前页面...")
120
+ return False
121
+
122
+
80
123
  def read_page(url: str, timeout: float = 60) -> PageResult:
81
- """用浏览器打开 URL,返回清洗后的 Markdown。"""
82
124
  ctx = _ensure_browser()
83
-
84
- # 用现有 page 或新建
85
125
  page = ctx.pages[0] if ctx.pages else ctx.new_page()
86
126
 
127
+ # 让浏览器窗口获得焦点
128
+ try:
129
+ page.bring_to_front()
130
+ except Exception:
131
+ pass
132
+
87
133
  page.goto(url, wait_until="domcontentloaded", timeout=timeout * 1000)
88
134
 
89
- # 如果是登录页,等用户手动登录
135
+ # 登录检测
90
136
  if _is_login_page(page):
91
- log.info("检测到登录页面,请在浏览器窗口中完成登录...")
92
- # 等 URL 变化(登录成功后会跳转)或者最多等 120 秒
93
- try:
94
- page.wait_for_url(
95
- lambda u: u != page.url and "login" not in u.lower(),
96
- timeout=120_000,
97
- )
98
- # 等页面加载完
99
- page.wait_for_load_state("domcontentloaded", timeout=30_000)
100
- log.info(f"登录成功,当前页面: {page.url}")
101
- except Exception:
102
- log.warning("等待登录超时,尝试继续...")
137
+ _wait_for_login(page)
103
138
 
104
- # 获取页面内容
105
139
  final_url = page.url
106
140
  html = page.content()
107
141
  title = page.title() or url
108
142
 
109
143
  soup = BeautifulSoup(html, "html.parser")
110
144
 
111
- # 去噪
112
145
  for tag in soup.find_all(["script", "style", "noscript", "iframe", "svg", "nav", "footer", "aside"]):
113
146
  tag.decompose()
114
147
 
115
- # 找主体
116
148
  main = None
117
149
  for sel in ("article", "main", '[role="main"]', ".content", ".article-content", ".markdown-body", "#content", "#main"):
118
150
  el = soup.select_one(sel)
@@ -125,14 +157,13 @@ def read_page(url: str, timeout: float = 60) -> PageResult:
125
157
  markdown = _H2T.handle(str(target)).strip()
126
158
 
127
159
  if len(markdown) > 200_000:
128
- markdown = markdown[:200_000] + "\n\n... [已截断]"
160
+ markdown = markdown[:200_000] + "\n\n... [truncated]"
129
161
 
130
162
  return PageResult(url=final_url, title=title, content=markdown, links=links)
131
163
 
132
164
 
133
165
  def close_browser():
134
- """关闭浏览器(进程退出时调用)。"""
135
- global _browser, _context, _pw
166
+ global _context, _pw
136
167
  if _context:
137
168
  try:
138
169
  _context.close()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: internal-web-reader
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: MCP Server - let Claude Code read internal web pages via browser cookies
5
5
  Author: zhanghaoran18006
6
6
  License-Expression: MIT