devin-search-mcp 1.0.3 → 1.0.5
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/bin/devin-search-mcp.mjs +1 -1
- package/package.json +1 -1
- package/src/detector.mjs +4 -2
- package/src/fetch.mjs +113 -25
- package/src/search.mjs +1 -1
package/bin/devin-search-mcp.mjs
CHANGED
package/package.json
CHANGED
package/src/detector.mjs
CHANGED
|
@@ -140,19 +140,21 @@ export function inspectDevinStatus() {
|
|
|
140
140
|
authStatusText = execFileSync(executable, ["auth", "status"], {
|
|
141
141
|
encoding: "utf-8",
|
|
142
142
|
stdio: ["pipe", "pipe", "pipe"],
|
|
143
|
-
timeout:
|
|
143
|
+
timeout: 15000,
|
|
144
144
|
}).trim();
|
|
145
145
|
} catch (err) {
|
|
146
146
|
authStatusText = err.stdout || err.message;
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
const isLoggedIn = authStatusText.includes("Logged in") || hasToken;
|
|
151
|
+
|
|
150
152
|
return {
|
|
151
153
|
executable,
|
|
152
154
|
credentialsPath,
|
|
153
155
|
hasToken,
|
|
154
156
|
tokenSnippet,
|
|
155
|
-
isLoggedIn
|
|
157
|
+
isLoggedIn,
|
|
156
158
|
statusSummary: authStatusText,
|
|
157
159
|
};
|
|
158
160
|
}
|
package/src/fetch.mjs
CHANGED
|
@@ -1,20 +1,79 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Devin 网页抓取与阅读模块
|
|
3
3
|
* 调用 Devin CLI 的 webfetch 工具提取指定网页的清洁正文
|
|
4
|
+
* 支持 retryable 错误自动重试与原生网络抓取兜底
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import { execFile } from "node:child_process";
|
|
7
8
|
import { findDevinExecutable } from "./detector.mjs";
|
|
8
9
|
import { searchCache } from "./cache.mjs";
|
|
9
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
|
+
|
|
10
63
|
/**
|
|
11
64
|
* 抓取指定 URL 的网页内容
|
|
12
65
|
* @param {Object} options
|
|
13
66
|
* @param {string} options.url - 目标网页 URL
|
|
14
67
|
* @param {"markdown"|"text"|"summary"} [options.extractMode="markdown"] - 提取格式
|
|
15
|
-
* @param {number} [options.timeoutMs=
|
|
68
|
+
* @param {number} [options.timeoutMs=60000] - 超时毫秒数
|
|
69
|
+
* @param {number} [options.maxRetries=2] - 重试次数
|
|
16
70
|
*/
|
|
17
|
-
export async function executeWebFetch({
|
|
71
|
+
export async function executeWebFetch({
|
|
72
|
+
url,
|
|
73
|
+
extractMode = "markdown",
|
|
74
|
+
timeoutMs = parseInt(process.env.DEVIN_TIMEOUT_MS || "60000", 10),
|
|
75
|
+
maxRetries = 2,
|
|
76
|
+
}) {
|
|
18
77
|
if (!url || typeof url !== "string" || !/^https?:\/\//i.test(url)) {
|
|
19
78
|
throw new Error("请提供有效的 HTTP/HTTPS 网页 URL");
|
|
20
79
|
}
|
|
@@ -26,9 +85,6 @@ export async function executeWebFetch({ url, extractMode = "markdown", timeoutMs
|
|
|
26
85
|
}
|
|
27
86
|
|
|
28
87
|
const devinExe = findDevinExecutable();
|
|
29
|
-
if (!devinExe) {
|
|
30
|
-
throw new Error("未找到 Devin 可执行文件,请确认已安装 Devin 或设置 DEVIN_PATH 环境变量。");
|
|
31
|
-
}
|
|
32
88
|
|
|
33
89
|
let prompt = "";
|
|
34
90
|
if (extractMode === "summary") {
|
|
@@ -39,29 +95,61 @@ export async function executeWebFetch({ url, extractMode = "markdown", timeoutMs
|
|
|
39
95
|
prompt = `请使用 webfetch 工具读取 ${url} 的内容,并将正文内容转换为整洁、规范的 Markdown 格式输出。不要添加任何额外多余的寒暄。`;
|
|
40
96
|
}
|
|
41
97
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (error
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
98
|
+
const runDevinAttempt = () =>
|
|
99
|
+
new Promise((resolve, reject) => {
|
|
100
|
+
execFile(
|
|
101
|
+
devinExe,
|
|
102
|
+
["--permission-mode", "dangerous", "--respect-workspace-trust", "false", "-p", prompt],
|
|
103
|
+
{
|
|
104
|
+
timeout: timeoutMs,
|
|
105
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
106
|
+
encoding: "utf-8",
|
|
107
|
+
env: { ...process.env, DEVIN_PERMISSION_MODE: "dangerous" },
|
|
108
|
+
},
|
|
109
|
+
(error, stdout, stderr) => {
|
|
110
|
+
if (error) {
|
|
111
|
+
if (error.killed) {
|
|
112
|
+
reject(new Error(`网页读取超时(超过 ${timeoutMs / 1000} 秒): ${url}`));
|
|
113
|
+
} else {
|
|
114
|
+
reject(new Error(`${error.message}\n${stderr || ""}`));
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
58
117
|
}
|
|
59
|
-
|
|
118
|
+
resolve(stdout.trim());
|
|
60
119
|
}
|
|
61
|
-
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
let output = "";
|
|
124
|
+
let lastErr = null;
|
|
125
|
+
|
|
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
|
+
}
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
62
141
|
}
|
|
63
|
-
|
|
64
|
-
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 如果 Devin 暂时频控或遇到网络故障,自动无缝降级走本地原生 fetch 提取
|
|
146
|
+
if (!output) {
|
|
147
|
+
try {
|
|
148
|
+
output = await nativeFetchFallback(url);
|
|
149
|
+
} catch (fallbackErr) {
|
|
150
|
+
throw new Error(`网页抓取失败: ${lastErr?.message || fallbackErr.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
65
153
|
|
|
66
154
|
const resultPayload = {
|
|
67
155
|
url,
|
package/src/search.mjs
CHANGED