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.
- package/package.json +1 -1
- package/src/fetch.mjs +23 -77
package/package.json
CHANGED
package/src/fetch.mjs
CHANGED
|
@@ -1,78 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Devin 网页抓取与阅读模块
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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(/ /g, " ")
|
|
32
|
-
.replace(/&/g, "&")
|
|
33
|
-
.replace(/</g, "<")
|
|
34
|
-
.replace(/>/g, ">")
|
|
35
|
-
.replace(/"/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=
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
//
|
|
95
|
+
// 彻底暴露问题:如果没有产出,坚决抛出真实异常,严禁任何伪造成功或隐式本地降级
|
|
146
96
|
if (!output) {
|
|
147
|
-
|
|
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 = {
|