devin-search-mcp 1.0.5 → 1.0.6

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/fetch.mjs +23 -77
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devin-search-mcp",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Devin AI 驱动的联网搜索与网页抓取 MCP 服务 (基于 Devin Desktop / CLI)",
5
5
  "main": "src/index.mjs",
6
6
  "type": "module",
package/src/fetch.mjs CHANGED
@@ -1,78 +1,26 @@
1
1
  /**
2
2
  * Devin 网页抓取与阅读模块
3
- * 调用 Devin CLI 的 webfetch 工具提取指定网页的清洁正文
4
- * 支持 retryable 错误自动重试与原生网络抓取兜底
3
+ * 严格调用 Devin CLI 的 webfetch 官方能力提取正文
4
+ * 遵循 Debug-First 准则:仅处理 retryable 官方重试,绝不引入隐式回退或静默降级代码,彻底暴露底层真实异常
5
5
  */
6
6
 
7
7
  import { execFile } from "node:child_process";
8
8
  import { findDevinExecutable } from "./detector.mjs";
9
9
  import { searchCache } from "./cache.mjs";
10
10
 
11
- /**
12
- * 极简 HTML 清洗工具 (原生兜底用)
13
- */
14
- function cleanHtmlToMarkdown(html) {
15
- let text = html
16
- .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "")
17
- .replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "")
18
- .replace(/<nav\b[^<]*(?:(?!<\/nav>)<[^<]*)*<\/nav>/gi, "")
19
- .replace(/<footer\b[^<]*(?:(?!<\/footer>)<[^<]*)*<\/footer>/gi, "");
20
-
21
- // 简易转换为 Markdown
22
- text = text
23
- .replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, "\n# $1\n")
24
- .replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, "\n## $1\n")
25
- .replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, "\n### $1\n")
26
- .replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, "\n$1\n")
27
- .replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, "\n* $1")
28
- .replace(/<a\s+(?:[^>]*?\s+)?href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, "[$2]($1)")
29
- .replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, "\n```\n$1\n```\n")
30
- .replace(/<[^>]+>/g, " ")
31
- .replace(/&nbsp;/g, " ")
32
- .replace(/&amp;/g, "&")
33
- .replace(/&lt;/g, "<")
34
- .replace(/&gt;/g, ">")
35
- .replace(/&quot;/g, '"')
36
- .replace(/\n\s*\n\s*\n/g, "\n\n")
37
- .trim();
38
-
39
- return text;
40
- }
41
-
42
- /**
43
- * 原生 Fetch 极速抓取兜底
44
- */
45
- async function nativeFetchFallback(url) {
46
- const resp = await fetch(url, {
47
- headers: {
48
- "User-Agent":
49
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
50
- Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
51
- },
52
- signal: AbortSignal.timeout(20000),
53
- });
54
-
55
- if (!resp.ok) {
56
- throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
57
- }
58
-
59
- const html = await resp.text();
60
- return cleanHtmlToMarkdown(html);
61
- }
62
-
63
11
  /**
64
12
  * 抓取指定 URL 的网页内容
65
13
  * @param {Object} options
66
14
  * @param {string} options.url - 目标网页 URL
67
15
  * @param {"markdown"|"text"|"summary"} [options.extractMode="markdown"] - 提取格式
68
16
  * @param {number} [options.timeoutMs=60000] - 超时毫秒数
69
- * @param {number} [options.maxRetries=2] - 重试次数
17
+ * @param {number} [options.maxRetries=1] - retryable 瞬时网络错误重试次数
70
18
  */
71
19
  export async function executeWebFetch({
72
20
  url,
73
21
  extractMode = "markdown",
74
22
  timeoutMs = parseInt(process.env.DEVIN_TIMEOUT_MS || "60000", 10),
75
- maxRetries = 2,
23
+ maxRetries = 1,
76
24
  }) {
77
25
  if (!url || typeof url !== "string" || !/^https?:\/\//i.test(url)) {
78
26
  throw new Error("请提供有效的 HTTP/HTTPS 网页 URL");
@@ -85,6 +33,9 @@ export async function executeWebFetch({
85
33
  }
86
34
 
87
35
  const devinExe = findDevinExecutable();
36
+ if (!devinExe) {
37
+ throw new Error("未检测到本地 Devin 安装,请确认已安装 Devin 或配置 DEVIN_PATH 环境变量。");
38
+ }
88
39
 
89
40
  let prompt = "";
90
41
  if (extractMode === "summary") {
@@ -111,7 +62,7 @@ export async function executeWebFetch({
111
62
  if (error.killed) {
112
63
  reject(new Error(`网页读取超时(超过 ${timeoutMs / 1000} 秒): ${url}`));
113
64
  } else {
114
- reject(new Error(`${error.message}\n${stderr || ""}`));
65
+ reject(new Error(error.message + (stderr ? `\n${stderr}` : "")));
115
66
  }
116
67
  return;
117
68
  }
@@ -123,32 +74,27 @@ export async function executeWebFetch({
123
74
  let output = "";
124
75
  let lastErr = null;
125
76
 
126
- if (devinExe) {
127
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
128
- try {
129
- output = await runDevinAttempt();
130
- if (output) break;
131
- } catch (err) {
132
- lastErr = err;
133
- // 如果是可重试的频控错误,进行指数退避
134
- if (/resource_exhausted|retryable|ETIMEDOUT/i.test(err.message)) {
135
- if (attempt < maxRetries) {
136
- await new Promise((r) => setTimeout(r, 1500 * (attempt + 1)));
137
- continue;
138
- }
77
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
78
+ try {
79
+ output = await runDevinAttempt();
80
+ if (output) break;
81
+ } catch (err) {
82
+ lastErr = err;
83
+ // 仅当官方返回 retryable: true 或连接瞬时重置时做退避重试
84
+ if (/retryable|ETIMEDOUT|ECONNRESET/i.test(err.message)) {
85
+ if (attempt < maxRetries) {
86
+ await new Promise((r) => setTimeout(r, 2000));
87
+ continue;
139
88
  }
140
- break;
141
89
  }
90
+ // 非 retryable 错误不盲目重试,立即向上抛出
91
+ throw err;
142
92
  }
143
93
  }
144
94
 
145
- // 如果 Devin 暂时频控或遇到网络故障,自动无缝降级走本地原生 fetch 提取
95
+ // 彻底暴露问题:如果没有产出,坚决抛出真实异常,严禁任何伪造成功或隐式本地降级
146
96
  if (!output) {
147
- try {
148
- output = await nativeFetchFallback(url);
149
- } catch (fallbackErr) {
150
- throw new Error(`网页抓取失败: ${lastErr?.message || fallbackErr.message}`);
151
- }
97
+ throw lastErr || new Error(`Devin 抓取网页未返回任何内容: ${url}`);
152
98
  }
153
99
 
154
100
  const resultPayload = {