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.
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * devin-search-mcp 命令行入口
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devin-search-mcp",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Devin AI 驱动的联网搜索与网页抓取 MCP 服务 (基于 Devin Desktop / CLI)",
5
5
  "main": "src/index.mjs",
6
6
  "type": "module",
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: 5000,
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: authStatusText.includes("Logged in"),
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(/&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
+
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=35000] - 超时毫秒数
68
+ * @param {number} [options.timeoutMs=60000] - 超时毫秒数
69
+ * @param {number} [options.maxRetries=2] - 重试次数
16
70
  */
17
- export async function executeWebFetch({ url, extractMode = "markdown", timeoutMs = 35000 }) {
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 output = await new Promise((resolve, reject) => {
43
- execFile(
44
- devinExe,
45
- ["--permission-mode", "dangerous", "--respect-workspace-trust", "false", "-p", prompt],
46
- {
47
- timeout: timeoutMs,
48
- maxBuffer: 10 * 1024 * 1024,
49
- encoding: "utf-8",
50
- env: { ...process.env, DEVIN_PERMISSION_MODE: "dangerous" },
51
- },
52
- (error, stdout, stderr) => {
53
- if (error) {
54
- if (error.killed) {
55
- reject(new Error(`网页读取超时(超过 ${timeoutMs / 1000} 秒): ${url}`));
56
- } else {
57
- reject(new Error(`网页读取失败: ${error.message}\n${stderr || ""}`));
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
- return;
118
+ resolve(stdout.trim());
60
119
  }
61
- resolve(stdout.trim());
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
@@ -40,7 +40,7 @@ export async function executeWebSearch({
40
40
  query,
41
41
  num_results = 5,
42
42
  detailed = false,
43
- timeoutMs = 35000,
43
+ timeoutMs = parseInt(process.env.DEVIN_TIMEOUT_MS || "60000", 10),
44
44
  maxRetries = 1,
45
45
  }) {
46
46
  if (!query || typeof query !== "string") {