devin-search-mcp 1.0.0
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/LICENSE +21 -0
- package/README.md +101 -0
- package/bin/devin-search-mcp.mjs +12 -0
- package/package.json +49 -0
- package/src/cache.mjs +49 -0
- package/src/detector.mjs +158 -0
- package/src/extract-key.mjs +175 -0
- package/src/fetch.mjs +75 -0
- package/src/index.mjs +226 -0
- package/src/response-repair.mjs +119 -0
- package/src/search.mjs +149 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Devin Search MCP
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/devin-search-mcp)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
基于 Devin 原生引擎的 AI 联网网页搜索与深度抓取 MCP (Model Context Protocol) 插件。
|
|
7
|
+
|
|
8
|
+
项目整体设计与工程规范深度对齐 `@sammysnake/fast-context-mcp`,支持跨平台本地凭证自动提取、健壮的模型输出修复、全网实时搜索与智能正文清洗。
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 特性亮点
|
|
13
|
+
|
|
14
|
+
* **全网实时搜索 (`devin_web_search`)**:直接使用 Devin 官方针对开发者深度优化的搜索网关,获取最新的开源项目、技术文档、版本发布与报错解决方案。
|
|
15
|
+
* **本地凭证自动提取 (`extract_devin_key`)**:深度模仿 `fast-context-mcp`,依赖 `sql.js`(纯 WASM/JS)自动扫描 `state.vscdb` 和 `credentials.toml`,一键导出有效凭据。
|
|
16
|
+
* **深度网页正文抓取 (`devin_web_fetch`)**:自动剔除网页广告、导航、页脚等噪声,返回整洁的 Markdown 格式。
|
|
17
|
+
* **响应自动修复与容错 (`response-repair`)**:内置括号深度平衡与畸形 JSON 自动修复算法,避免大模型输出解析崩溃。
|
|
18
|
+
* **智能缓存加速 (`cache`)**:内置基于 MD5 与 TTL 的轻量缓存,相同搜索毫秒级返回,节约配额并加速响应。
|
|
19
|
+
* **CI/CD 全自动发布**:配置了 GitHub Actions 自动化发布工作流,开箱即支持一键发包到 npm。
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 提供的 MCP 工具
|
|
24
|
+
|
|
25
|
+
| 工具名称 | 功能描述 | 关键参数 |
|
|
26
|
+
| :--- | :--- | :--- |
|
|
27
|
+
| `devin_web_search` | 执行全网实时搜索,返回结构化网页列表及摘要 | `query` (必需), `num_results`, `detailed` |
|
|
28
|
+
| `devin_web_fetch` | 抓取并阅读指定网址的深度清洁正文 | `url` (必需), `extract_mode` (`markdown`/`text`/`summary`) |
|
|
29
|
+
| `extract_devin_key` | 跨平台自动提取本地 Devin / Windsurf API Key | 无 |
|
|
30
|
+
| `devin_status` | 诊断检测本地 Devin 安装路径、登录身份与可用性状态 | 无 |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 快速配置使用
|
|
35
|
+
|
|
36
|
+
在任意 AI 客户端(如 CodeBuddy、Claude Desktop、Cursor 等)的 MCP 配置文件中添加:
|
|
37
|
+
|
|
38
|
+
### 本地路径运行方式
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"mcpServers": {
|
|
43
|
+
"devin-search": {
|
|
44
|
+
"command": "node",
|
|
45
|
+
"args": [
|
|
46
|
+
"E:\\project\\devin-search-mcp\\bin\\devin-search-mcp.mjs"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### npm 发布后的运行方式 (与 fast-context-mcp 一致)
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"devin-search": {
|
|
59
|
+
"command": "npx",
|
|
60
|
+
"args": [
|
|
61
|
+
"-y",
|
|
62
|
+
"devin-search-mcp"
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 验证与测试
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
cd E:\project\devin-search-mcp
|
|
75
|
+
|
|
76
|
+
# 运行底层业务功能测试 (搜索与抓取)
|
|
77
|
+
npm test
|
|
78
|
+
|
|
79
|
+
# 运行真实的 MCP 客户端 Stdio 协议模拟测试
|
|
80
|
+
npm run test:mcp
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 如何发布到 npm
|
|
86
|
+
|
|
87
|
+
1. **登录 npm**:
|
|
88
|
+
```bash
|
|
89
|
+
npm login
|
|
90
|
+
```
|
|
91
|
+
2. **发布公有包**:
|
|
92
|
+
```bash
|
|
93
|
+
npm publish --access public
|
|
94
|
+
```
|
|
95
|
+
3. **GitHub Actions 自动发布**:
|
|
96
|
+
在 GitHub 仓库中配置 Secret `NPM_TOKEN`,每次创建 Release Tag 时会自动触发打包发布。
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 许可证
|
|
101
|
+
MIT License
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "devin-search-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Devin AI 驱动的联网搜索与网页抓取 MCP 服务 (基于 Devin Desktop / CLI)",
|
|
5
|
+
"main": "src/index.mjs",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"devin-search-mcp": "./bin/devin-search-mcp.mjs"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node ./bin/devin-search-mcp.mjs",
|
|
12
|
+
"test": "node test.mjs",
|
|
13
|
+
"test:mcp": "node test-mcp-protocol.mjs"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin",
|
|
17
|
+
"src",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"devin",
|
|
24
|
+
"web-search",
|
|
25
|
+
"search",
|
|
26
|
+
"webfetch",
|
|
27
|
+
"ai",
|
|
28
|
+
"claude"
|
|
29
|
+
],
|
|
30
|
+
"author": "suvon",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public",
|
|
33
|
+
"registry": "https://registry.npmjs.org/"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/soulvon/devin-search-mcp.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/soulvon/devin-search-mcp/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/soulvon/devin-search-mcp#readme",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
46
|
+
"sql.js": "^1.14.2",
|
|
47
|
+
"zod": "^3.23.8"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/cache.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 轻量级搜索缓存模块
|
|
3
|
+
* 支持内存缓存与基于 TTL 的自动失效机制
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
class SimpleCache {
|
|
9
|
+
constructor(defaultTtlMs = 30 * 60 * 1000) {
|
|
10
|
+
this.cache = new Map();
|
|
11
|
+
this.defaultTtlMs = defaultTtlMs;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
generateKey(namespace, query, options = {}) {
|
|
15
|
+
const raw = `${namespace}:${query}:${JSON.stringify(options)}`;
|
|
16
|
+
return createHash("md5").update(raw).digest("hex");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get(key) {
|
|
20
|
+
const entry = this.cache.get(key);
|
|
21
|
+
if (!entry) return null;
|
|
22
|
+
|
|
23
|
+
if (Date.now() > entry.expireAt) {
|
|
24
|
+
this.cache.delete(key);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return entry.value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
set(key, value, ttlMs = this.defaultTtlMs) {
|
|
32
|
+
// 限制最大缓存数量为 100 条
|
|
33
|
+
if (this.cache.size >= 100) {
|
|
34
|
+
const firstKey = this.cache.keys().next().value;
|
|
35
|
+
if (firstKey) this.cache.delete(firstKey);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.cache.set(key, {
|
|
39
|
+
value,
|
|
40
|
+
expireAt: Date.now() + ttlMs,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
clear() {
|
|
45
|
+
this.cache.clear();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const searchCache = new SimpleCache();
|
package/src/detector.mjs
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devin 运行环境与安装路径探测模块
|
|
3
|
+
* 支持自动探测 Windows / macOS / Linux 下的 devin.exe 和登录凭据
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { homedir, platform } from "node:os";
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 获取系统平台候选的 devin.exe / devin 路径
|
|
13
|
+
*/
|
|
14
|
+
export function getDevinPathCandidates() {
|
|
15
|
+
const plat = platform();
|
|
16
|
+
const home = homedir();
|
|
17
|
+
const candidates = [];
|
|
18
|
+
|
|
19
|
+
// 1. 优先使用环境变量指定
|
|
20
|
+
if (process.env.DEVIN_PATH) {
|
|
21
|
+
candidates.push(process.env.DEVIN_PATH);
|
|
22
|
+
}
|
|
23
|
+
if (process.env.DEVIN_EXE_PATH) {
|
|
24
|
+
candidates.push(process.env.DEVIN_EXE_PATH);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (plat === "win32") {
|
|
28
|
+
const localAppData = process.env.LOCALAPPDATA || join(home, "AppData", "Local");
|
|
29
|
+
const programFiles = process.env.ProgramFiles || "C:\\Program Files";
|
|
30
|
+
|
|
31
|
+
candidates.push(
|
|
32
|
+
// 用户当前机器的主安装路径
|
|
33
|
+
"E:\\Program\\devin\\resources\\app\\extensions\\windsurf\\devin\\bin\\devin.exe",
|
|
34
|
+
"D:\\Program\\devin\\resources\\app\\extensions\\windsurf\\devin\\bin\\devin.exe",
|
|
35
|
+
"C:\\Program\\devin\\resources\\app\\extensions\\windsurf\\devin\\bin\\devin.exe",
|
|
36
|
+
// Windows 默认安装位置
|
|
37
|
+
join(localAppData, "Programs", "Devin", "resources", "app", "extensions", "windsurf", "devin", "bin", "devin.exe"),
|
|
38
|
+
join(programFiles, "Devin", "resources", "app", "extensions", "windsurf", "devin", "bin", "devin.exe"),
|
|
39
|
+
// 官方独立 CLI 安装路径
|
|
40
|
+
join(localAppData, "devin", "bin", "devin.exe"),
|
|
41
|
+
join(home, ".devin", "bin", "devin.exe")
|
|
42
|
+
);
|
|
43
|
+
} else if (plat === "darwin") {
|
|
44
|
+
candidates.push(
|
|
45
|
+
"/Applications/Devin.app/Contents/Resources/app/extensions/windsurf/devin/bin/devin",
|
|
46
|
+
join(home, "Applications", "Devin.app", "Contents", "Resources", "app", "extensions", "windsurf", "devin", "bin", "devin"),
|
|
47
|
+
join(home, ".local", "bin", "devin"),
|
|
48
|
+
"/usr/local/bin/devin"
|
|
49
|
+
);
|
|
50
|
+
} else {
|
|
51
|
+
// Linux
|
|
52
|
+
candidates.push(
|
|
53
|
+
join(home, ".local", "bin", "devin"),
|
|
54
|
+
join(home, ".devin", "bin", "devin"),
|
|
55
|
+
"/usr/local/bin/devin",
|
|
56
|
+
"/usr/bin/devin"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return candidates;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 自动查找有效的 devin 可执行文件路径
|
|
65
|
+
*/
|
|
66
|
+
export function findDevinExecutable() {
|
|
67
|
+
const candidates = getDevinPathCandidates();
|
|
68
|
+
for (const candidate of candidates) {
|
|
69
|
+
if (candidate && existsSync(candidate)) {
|
|
70
|
+
return candidate;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 尝试直接通过系统 PATH 执行 devin
|
|
75
|
+
try {
|
|
76
|
+
const cmd = platform() === "win32" ? "where.exe devin" : "which devin";
|
|
77
|
+
const stdout = execFileSync(platform() === "win32" ? "cmd.exe" : "sh", [
|
|
78
|
+
platform() === "win32" ? "/c" : "-c",
|
|
79
|
+
cmd,
|
|
80
|
+
], { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }).trim();
|
|
81
|
+
|
|
82
|
+
const firstMatch = stdout.split(/\r?\n/)[0]?.trim();
|
|
83
|
+
if (firstMatch && existsSync(firstMatch)) {
|
|
84
|
+
return firstMatch;
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// PATH 中未找到
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 获取凭证配置文件路径
|
|
95
|
+
*/
|
|
96
|
+
export function getCredentialsPath() {
|
|
97
|
+
const plat = platform();
|
|
98
|
+
const home = homedir();
|
|
99
|
+
|
|
100
|
+
let credPath = "";
|
|
101
|
+
if (plat === "win32") {
|
|
102
|
+
const appdata = process.env.APPDATA || join(home, "AppData", "Roaming");
|
|
103
|
+
credPath = join(appdata, "devin", "credentials.toml");
|
|
104
|
+
} else {
|
|
105
|
+
credPath = join(home, ".local", "share", "devin", "credentials.toml");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (existsSync(credPath)) {
|
|
109
|
+
return credPath;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 读取 Devin 登录状态与信息
|
|
116
|
+
*/
|
|
117
|
+
export function inspectDevinStatus() {
|
|
118
|
+
const executable = findDevinExecutable();
|
|
119
|
+
const credentialsPath = getCredentialsPath();
|
|
120
|
+
|
|
121
|
+
let hasToken = false;
|
|
122
|
+
let tokenSnippet = null;
|
|
123
|
+
|
|
124
|
+
if (credentialsPath && existsSync(credentialsPath)) {
|
|
125
|
+
try {
|
|
126
|
+
const content = readFileSync(credentialsPath, "utf-8");
|
|
127
|
+
const match = content.match(/windsurf_api_key\s*=\s*"([^"]+)"/);
|
|
128
|
+
if (match && match[1]) {
|
|
129
|
+
hasToken = true;
|
|
130
|
+
tokenSnippet = match[1].slice(0, 25) + "...";
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
// 忽略读取错误
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let authStatusText = "";
|
|
138
|
+
if (executable) {
|
|
139
|
+
try {
|
|
140
|
+
authStatusText = execFileSync(executable, ["auth", "status"], {
|
|
141
|
+
encoding: "utf-8",
|
|
142
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
143
|
+
timeout: 5000,
|
|
144
|
+
}).trim();
|
|
145
|
+
} catch (err) {
|
|
146
|
+
authStatusText = err.stdout || err.message;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
executable,
|
|
152
|
+
credentialsPath,
|
|
153
|
+
hasToken,
|
|
154
|
+
tokenSnippet,
|
|
155
|
+
isLoggedIn: authStatusText.includes("Logged in"),
|
|
156
|
+
statusSummary: authStatusText,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devin / Windsurf API Key 本地自动提取模块
|
|
3
|
+
* 模仿 fast-context-mcp 的跨平台本地凭证自动提取实现
|
|
4
|
+
* 依赖 sql.js (纯 WebAssembly/JS),无需本地 C++ 编译环境
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { homedir, platform } from "node:os";
|
|
10
|
+
import initSqlJs from "sql.js";
|
|
11
|
+
|
|
12
|
+
const TOML_API_KEY_FIELDS = [
|
|
13
|
+
"windsurf_api_key",
|
|
14
|
+
"devin_api_key",
|
|
15
|
+
"api_key",
|
|
16
|
+
"apiKey",
|
|
17
|
+
"access_token",
|
|
18
|
+
"token",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 获取平台相关的 state.vscdb 候选路径
|
|
23
|
+
*/
|
|
24
|
+
export function getDbPathCandidates() {
|
|
25
|
+
const plat = platform();
|
|
26
|
+
const home = homedir();
|
|
27
|
+
const env = process.env;
|
|
28
|
+
|
|
29
|
+
if (plat === "darwin") {
|
|
30
|
+
return ["Devin", "Windsurf"].map((appName) =>
|
|
31
|
+
join(home, "Library", "Application Support", appName, "User", "globalStorage", "state.vscdb")
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (plat === "win32") {
|
|
36
|
+
const appdata = env.APPDATA || join(home, "AppData", "Roaming");
|
|
37
|
+
return ["Devin", "Windsurf"].map((appName) =>
|
|
38
|
+
join(appdata, appName, "User", "globalStorage", "state.vscdb")
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const config = env.XDG_CONFIG_HOME || join(home, ".config");
|
|
43
|
+
return ["Devin", "Windsurf"].map((appName) =>
|
|
44
|
+
join(config, appName, "User", "globalStorage", "state.vscdb")
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 获取 credentials.toml 候选路径
|
|
50
|
+
*/
|
|
51
|
+
export function getTomlPathCandidates() {
|
|
52
|
+
const plat = platform();
|
|
53
|
+
const home = homedir();
|
|
54
|
+
const env = process.env;
|
|
55
|
+
|
|
56
|
+
if (plat === "win32") {
|
|
57
|
+
const appdata = env.APPDATA || join(home, "AppData", "Roaming");
|
|
58
|
+
return [
|
|
59
|
+
join(appdata, "devin", "credentials.toml"),
|
|
60
|
+
join(home, ".devin", "credentials.toml"),
|
|
61
|
+
join(home, ".codeium", "credentials.toml"),
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return [
|
|
66
|
+
join(home, ".local", "share", "devin", "credentials.toml"),
|
|
67
|
+
join(home, ".config", "devin", "credentials.toml"),
|
|
68
|
+
join(home, ".devin", "credentials.toml"),
|
|
69
|
+
join(home, ".codeium", "credentials.toml"),
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 从 TOML 文本中提取 key
|
|
75
|
+
*/
|
|
76
|
+
function extractFromTomlText(text) {
|
|
77
|
+
for (const field of TOML_API_KEY_FIELDS) {
|
|
78
|
+
const match = text.match(new RegExp(`^\\s*${field}\\s*=\\s*(?:"([^"]+)"|'([^']+)'|([^\\s#]+))`, "m"));
|
|
79
|
+
const val = (match?.[1] || match?.[2] || match?.[3] || "").trim();
|
|
80
|
+
if (val && val.length > 10) return val;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 从 SQLite state.vscdb 中提取 session token
|
|
87
|
+
*/
|
|
88
|
+
async function extractFromSqlite(dbPath) {
|
|
89
|
+
if (!existsSync(dbPath)) return null;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const fileBuffer = readFileSync(dbPath);
|
|
93
|
+
const SQL = await initSqlJs();
|
|
94
|
+
const db = new SQL.Database(fileBuffer);
|
|
95
|
+
|
|
96
|
+
// 查询所有可能包含 token 的键值
|
|
97
|
+
const res = db.exec("SELECT key, value FROM ItemTable WHERE key LIKE '%devin%' OR key LIKE '%windsurf%' OR key LIKE '%codeium%' OR key LIKE '%token%' OR key LIKE '%auth%'");
|
|
98
|
+
if (!res || !res.length || !res[0].values) return null;
|
|
99
|
+
|
|
100
|
+
for (const row of res[0].values) {
|
|
101
|
+
const val = String(row[1] || "");
|
|
102
|
+
// 匹配常见 Token 格式
|
|
103
|
+
const tokenMatch = val.match(/devin-session-token\$[a-zA-Z0-9._-]+|sk-ws-[a-zA-Z0-9._-]+|eyJ[a-zA-Z0-9._-]+/);
|
|
104
|
+
if (tokenMatch) {
|
|
105
|
+
return tokenMatch[0];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// 忽略解析错误
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 跨平台自动提取 Devin API Key
|
|
116
|
+
*/
|
|
117
|
+
export async function extractDevinKey() {
|
|
118
|
+
// 1. 优先读取环境变量
|
|
119
|
+
if (process.env.DEVIN_API_KEY) {
|
|
120
|
+
return {
|
|
121
|
+
api_key: process.env.DEVIN_API_KEY,
|
|
122
|
+
source: "env:DEVIN_API_KEY",
|
|
123
|
+
app: "Devin",
|
|
124
|
+
length: process.env.DEVIN_API_KEY.length,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (process.env.WINDSURF_API_KEY) {
|
|
128
|
+
return {
|
|
129
|
+
api_key: process.env.WINDSURF_API_KEY,
|
|
130
|
+
source: "env:WINDSURF_API_KEY",
|
|
131
|
+
app: "Windsurf",
|
|
132
|
+
length: process.env.WINDSURF_API_KEY.length,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 2. 查找 credentials.toml
|
|
137
|
+
for (const tomlPath of getTomlPathCandidates()) {
|
|
138
|
+
if (existsSync(tomlPath)) {
|
|
139
|
+
try {
|
|
140
|
+
const text = readFileSync(tomlPath, "utf-8");
|
|
141
|
+
const key = extractFromTomlText(text);
|
|
142
|
+
if (key) {
|
|
143
|
+
return {
|
|
144
|
+
api_key: key,
|
|
145
|
+
source: tomlPath,
|
|
146
|
+
app: tomlPath.includes("devin") ? "Devin" : "Windsurf",
|
|
147
|
+
length: key.length,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
} catch {}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 3. 查找 state.vscdb
|
|
155
|
+
for (const dbPath of getDbPathCandidates()) {
|
|
156
|
+
if (existsSync(dbPath)) {
|
|
157
|
+
const key = await extractFromSqlite(dbPath);
|
|
158
|
+
if (key) {
|
|
159
|
+
return {
|
|
160
|
+
api_key: key,
|
|
161
|
+
source: dbPath,
|
|
162
|
+
app: dbPath.includes("Devin") ? "Devin" : "Windsurf",
|
|
163
|
+
length: key.length,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
api_key: null,
|
|
171
|
+
source: null,
|
|
172
|
+
app: null,
|
|
173
|
+
length: 0,
|
|
174
|
+
};
|
|
175
|
+
}
|
package/src/fetch.mjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devin 网页抓取与阅读模块
|
|
3
|
+
* 调用 Devin CLI 的 webfetch 工具提取指定网页的清洁正文
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { findDevinExecutable } from "./detector.mjs";
|
|
8
|
+
import { searchCache } from "./cache.mjs";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 抓取指定 URL 的网页内容
|
|
12
|
+
* @param {Object} options
|
|
13
|
+
* @param {string} options.url - 目标网页 URL
|
|
14
|
+
* @param {"markdown"|"text"|"summary"} [options.extractMode="markdown"] - 提取格式
|
|
15
|
+
* @param {number} [options.timeoutMs=35000] - 超时毫秒数
|
|
16
|
+
*/
|
|
17
|
+
export async function executeWebFetch({ url, extractMode = "markdown", timeoutMs = 35000 }) {
|
|
18
|
+
if (!url || typeof url !== "string" || !/^https?:\/\//i.test(url)) {
|
|
19
|
+
throw new Error("请提供有效的 HTTP/HTTPS 网页 URL");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const cacheKey = searchCache.generateKey("web_fetch", url, { extractMode });
|
|
23
|
+
const cached = searchCache.get(cacheKey);
|
|
24
|
+
if (cached) {
|
|
25
|
+
return { ...cached, cached: true };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const devinExe = findDevinExecutable();
|
|
29
|
+
if (!devinExe) {
|
|
30
|
+
throw new Error("未找到 Devin 可执行文件,请确认已安装 Devin 或设置 DEVIN_PATH 环境变量。");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let prompt = "";
|
|
34
|
+
if (extractMode === "summary") {
|
|
35
|
+
prompt = `请使用 webfetch 工具读取 ${url} 的内容,并提供一份精准详实的核心内容提炼与关键要点总结。`;
|
|
36
|
+
} else if (extractMode === "text") {
|
|
37
|
+
prompt = `请使用 webfetch 工具读取 ${url} 的内容,去除网页导航、广告和页脚,直接输出主要正文纯文本。`;
|
|
38
|
+
} else {
|
|
39
|
+
prompt = `请使用 webfetch 工具读取 ${url} 的内容,并将正文内容转换为整洁、规范的 Markdown 格式输出。不要添加任何额外多余的寒暄。`;
|
|
40
|
+
}
|
|
41
|
+
|
|
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 || ""}`));
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
resolve(stdout.trim());
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const resultPayload = {
|
|
67
|
+
url,
|
|
68
|
+
extractMode,
|
|
69
|
+
content: output,
|
|
70
|
+
cached: false,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
searchCache.set(cacheKey, resultPayload);
|
|
74
|
+
return resultPayload;
|
|
75
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devin Search MCP Server
|
|
3
|
+
* 遵循 Model Context Protocol 标准实现 (对齐 fast-context-mcp 规范)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import {
|
|
9
|
+
CallToolRequestSchema,
|
|
10
|
+
ListToolsRequestSchema,
|
|
11
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
+
|
|
13
|
+
import { inspectDevinStatus } from "./detector.mjs";
|
|
14
|
+
import { extractDevinKey } from "./extract-key.mjs";
|
|
15
|
+
import { executeWebSearch } from "./search.mjs";
|
|
16
|
+
import { executeWebFetch } from "./fetch.mjs";
|
|
17
|
+
|
|
18
|
+
export function createDevinSearchServer() {
|
|
19
|
+
const server = new Server(
|
|
20
|
+
{
|
|
21
|
+
name: "devin-search-mcp",
|
|
22
|
+
version: "1.0.0",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
capabilities: {
|
|
26
|
+
tools: {},
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
32
|
+
return {
|
|
33
|
+
tools: [
|
|
34
|
+
{
|
|
35
|
+
name: "devin_web_search",
|
|
36
|
+
description:
|
|
37
|
+
"AI 驱动的实时网页搜索工具 (基于 Devin 联网引擎)。通过自然语言检索全网技术文档、最新版本特性、解决方案和开源生态,输出包含标题、URL及核心摘录的结构化结果。\n\n参数提示:\n- query: 搜索词或自然语言问题 (必填)\n- num_results: 返回条数 (默认 5)\n- detailed: 设置为 true 可额外附带深度技术见解与综合总结",
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
query: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "自然语言搜索查询 (例如: 'Next.js 15 features', 'Vue 3.5 reactive props')",
|
|
44
|
+
},
|
|
45
|
+
num_results: {
|
|
46
|
+
type: "integer",
|
|
47
|
+
minimum: 1,
|
|
48
|
+
maximum: 15,
|
|
49
|
+
default: 5,
|
|
50
|
+
description: "期望返回的结果数量 (默认 5,建议 3-10)",
|
|
51
|
+
},
|
|
52
|
+
max_results: {
|
|
53
|
+
type: "integer",
|
|
54
|
+
description: "num_results 的兼容别名",
|
|
55
|
+
},
|
|
56
|
+
detailed: {
|
|
57
|
+
type: "boolean",
|
|
58
|
+
default: false,
|
|
59
|
+
description: "是否返回深度 AI 综合总结与结论分析",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
required: ["query"],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "devin_web_fetch",
|
|
67
|
+
description:
|
|
68
|
+
"抓取并深度阅读指定网页的正文。自动过滤广告、导航、页脚与杂乱代码,支持输出纯正文或规范的 Markdown 格式。",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
url: {
|
|
73
|
+
type: "string",
|
|
74
|
+
description: "目标网页的完整 URL (必须以 http:// 或 https:// 开头)",
|
|
75
|
+
},
|
|
76
|
+
extract_mode: {
|
|
77
|
+
type: "string",
|
|
78
|
+
enum: ["markdown", "text", "summary"],
|
|
79
|
+
default: "markdown",
|
|
80
|
+
description: "正文提取格式: 'markdown' (结构化文档,默认), 'text' (纯文本), 'summary' (核心要点摘要)",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
required: ["url"],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "extract_devin_key",
|
|
88
|
+
description:
|
|
89
|
+
"从本地安装中自动提取 Devin / Windsurf API Key (对齐 fast-context-mcp 的 extract_windsurf_key)。自动检测操作系统,读取 state.vscdb 或 credentials.toml 并输出有效凭据。",
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "devin_status",
|
|
97
|
+
description: "诊断工具: 检测本地 Devin 安装路径、登录身份与可用性状态。",
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: "object",
|
|
100
|
+
properties: {},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
108
|
+
const { name, arguments: args } = request.params;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
if (name === "devin_web_search") {
|
|
112
|
+
const query = args?.query;
|
|
113
|
+
const numResults = args?.num_results ?? args?.max_results ?? 5;
|
|
114
|
+
const detailed = args?.detailed ?? false;
|
|
115
|
+
|
|
116
|
+
const result = await executeWebSearch({
|
|
117
|
+
query,
|
|
118
|
+
num_results: numResults,
|
|
119
|
+
detailed,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const lines = [
|
|
123
|
+
`[config] ${result.config} (耗时: ${result.latencyMs}ms, 缓存命中: ${result.cached})`,
|
|
124
|
+
"",
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
if (detailed && result.summary) {
|
|
128
|
+
lines.push(result.summary);
|
|
129
|
+
} else {
|
|
130
|
+
lines.push(JSON.stringify(result.results, null, 2));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
content: [
|
|
135
|
+
{
|
|
136
|
+
type: "text",
|
|
137
|
+
text: lines.join("\n"),
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (name === "devin_web_fetch") {
|
|
144
|
+
const url = args?.url;
|
|
145
|
+
const extractMode = args?.extract_mode ?? "markdown";
|
|
146
|
+
|
|
147
|
+
const result = await executeWebFetch({
|
|
148
|
+
url,
|
|
149
|
+
extractMode,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
content: [
|
|
154
|
+
{
|
|
155
|
+
type: "text",
|
|
156
|
+
text: result.content,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (name === "extract_devin_key") {
|
|
163
|
+
const res = await extractDevinKey();
|
|
164
|
+
if (!res.api_key) {
|
|
165
|
+
return {
|
|
166
|
+
content: [
|
|
167
|
+
{
|
|
168
|
+
type: "text",
|
|
169
|
+
text: "未在本地找到有效的 Devin / Windsurf API Key。请确认 Devin Desktop 处于登录状态。",
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const text = [
|
|
176
|
+
"Devin API Key extracted successfully",
|
|
177
|
+
"",
|
|
178
|
+
` Key: ${res.api_key.slice(0, 25)}...${res.api_key.slice(-15)}`,
|
|
179
|
+
` Length: ${res.length}`,
|
|
180
|
+
` Source: ${res.source}`,
|
|
181
|
+
` App: ${res.app}`,
|
|
182
|
+
"",
|
|
183
|
+
"Usage:",
|
|
184
|
+
` export DEVIN_API_KEY="${res.api_key}"`,
|
|
185
|
+
].join("\n");
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
content: [{ type: "text", text }],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (name === "devin_status") {
|
|
193
|
+
const status = inspectDevinStatus();
|
|
194
|
+
return {
|
|
195
|
+
content: [
|
|
196
|
+
{
|
|
197
|
+
type: "text",
|
|
198
|
+
text: JSON.stringify(status, null, 2),
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
throw new Error(`未知的工具名称: ${name}`);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
return {
|
|
207
|
+
isError: true,
|
|
208
|
+
content: [
|
|
209
|
+
{
|
|
210
|
+
type: "text",
|
|
211
|
+
text: `[devin-search-mcp 出错] ${error.message}`,
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return server;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function runServer() {
|
|
222
|
+
const server = createDevinSearchServer();
|
|
223
|
+
const transport = new StdioServerTransport();
|
|
224
|
+
await server.connect(transport);
|
|
225
|
+
console.error("Devin Search MCP 服务器已成功启动 (stdio)");
|
|
226
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 响应解析与容错修复模块
|
|
3
|
+
* 借鉴 fast-context-mcp 的模型输出深度解析与括号平衡修复能力
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 修复常见畸形或截断的 JSON 文本
|
|
8
|
+
*/
|
|
9
|
+
export function repairJson(raw) {
|
|
10
|
+
if (!raw || typeof raw !== "string") return null;
|
|
11
|
+
|
|
12
|
+
let text = raw.trim();
|
|
13
|
+
|
|
14
|
+
// 1. 去除代码块包裹
|
|
15
|
+
const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)(?:```|$)/i);
|
|
16
|
+
if (codeBlockMatch && codeBlockMatch[1]) {
|
|
17
|
+
text = codeBlockMatch[1].trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 2. 找到第一个有效起始符号 '[' 或 '{'
|
|
21
|
+
const firstBracket = text.indexOf("[");
|
|
22
|
+
const firstBrace = text.indexOf("{");
|
|
23
|
+
let startIdx = -1;
|
|
24
|
+
let expectType = "";
|
|
25
|
+
|
|
26
|
+
if (firstBracket !== -1 && (firstBrace === -1 || firstBracket < firstBrace)) {
|
|
27
|
+
startIdx = firstBracket;
|
|
28
|
+
expectType = "array";
|
|
29
|
+
} else if (firstBrace !== -1) {
|
|
30
|
+
startIdx = firstBrace;
|
|
31
|
+
expectType = "object";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (startIdx === -1) return null;
|
|
35
|
+
text = text.slice(startIdx);
|
|
36
|
+
|
|
37
|
+
// 3. 统计括号深度并补全缺失闭合
|
|
38
|
+
const stack = [];
|
|
39
|
+
let inString = false;
|
|
40
|
+
let escapeNext = false;
|
|
41
|
+
let validEnd = text.length;
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < text.length; i++) {
|
|
44
|
+
const char = text[i];
|
|
45
|
+
|
|
46
|
+
if (escapeNext) {
|
|
47
|
+
escapeNext = false;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (char === "\\") {
|
|
52
|
+
escapeNext = true;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (char === '"') {
|
|
57
|
+
inString = !inString;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (inString) continue;
|
|
62
|
+
|
|
63
|
+
if (char === "{" || char === "[") {
|
|
64
|
+
stack.push(char);
|
|
65
|
+
} else if (char === "}") {
|
|
66
|
+
if (stack[stack.length - 1] === "{") {
|
|
67
|
+
stack.pop();
|
|
68
|
+
if (stack.length === 0) {
|
|
69
|
+
validEnd = i + 1;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} else if (char === "]") {
|
|
74
|
+
if (stack[stack.length - 1] === "[") {
|
|
75
|
+
stack.pop();
|
|
76
|
+
if (stack.length === 0) {
|
|
77
|
+
validEnd = i + 1;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let slice = text.slice(0, validEnd);
|
|
85
|
+
|
|
86
|
+
// 补齐字符串引号
|
|
87
|
+
if (inString) {
|
|
88
|
+
slice += '"';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 补齐末尾闭合括号
|
|
92
|
+
while (stack.length > 0) {
|
|
93
|
+
const top = stack.pop();
|
|
94
|
+
slice += top === "{" ? "}" : "]";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 移除数组或对象末尾的多余逗号
|
|
98
|
+
slice = slice.replace(/,\s*([}\]])/g, "$1");
|
|
99
|
+
|
|
100
|
+
return slice;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 健壮的 JSON 解析器,先原生解析,失败则尝试修复
|
|
105
|
+
*/
|
|
106
|
+
export function parseJsonWithRepair(raw) {
|
|
107
|
+
if (!raw) return null;
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(raw);
|
|
110
|
+
} catch {
|
|
111
|
+
const repaired = repairJson(raw);
|
|
112
|
+
if (!repaired) return null;
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(repaired);
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
package/src/search.mjs
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devin 核心网页搜索模块
|
|
3
|
+
* 深度解析、结构化提炼与重试机制 (对齐 fast-context-mcp 规范)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { findDevinExecutable } from "./detector.mjs";
|
|
8
|
+
import { searchCache } from "./cache.mjs";
|
|
9
|
+
import { parseJsonWithRepair } from "./response-repair.mjs";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 文本正则提取兜底逻辑
|
|
13
|
+
*/
|
|
14
|
+
function fallbackParseResults(text) {
|
|
15
|
+
const results = [];
|
|
16
|
+
const lines = text.split(/\r?\n/);
|
|
17
|
+
let currentItem = null;
|
|
18
|
+
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
const urlMatch = line.match(/https?:\/\/[^\s)\]"'>]+/);
|
|
21
|
+
if (urlMatch) {
|
|
22
|
+
if (currentItem) results.push(currentItem);
|
|
23
|
+
currentItem = {
|
|
24
|
+
title: line.replace(urlMatch[0], "").replace(/^[-*#\d.\s]+/, "").trim() || "网页结果",
|
|
25
|
+
url: urlMatch[0],
|
|
26
|
+
snippet: "",
|
|
27
|
+
};
|
|
28
|
+
} else if (currentItem && line.trim()) {
|
|
29
|
+
currentItem.snippet += (currentItem.snippet ? " " : "") + line.trim();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (currentItem) results.push(currentItem);
|
|
33
|
+
return results;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 执行带重试的 Devin 网页搜索
|
|
38
|
+
*/
|
|
39
|
+
export async function executeWebSearch({
|
|
40
|
+
query,
|
|
41
|
+
num_results = 5,
|
|
42
|
+
detailed = false,
|
|
43
|
+
timeoutMs = 35000,
|
|
44
|
+
maxRetries = 1,
|
|
45
|
+
}) {
|
|
46
|
+
if (!query || typeof query !== "string") {
|
|
47
|
+
throw new Error("请提供有效的搜索关键词 (query)");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const startTime = Date.now();
|
|
51
|
+
const cacheKey = searchCache.generateKey("web_search", query, { num_results, detailed });
|
|
52
|
+
const cached = searchCache.get(cacheKey);
|
|
53
|
+
if (cached) {
|
|
54
|
+
return {
|
|
55
|
+
...cached,
|
|
56
|
+
latencyMs: 0,
|
|
57
|
+
cached: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const devinExe = findDevinExecutable();
|
|
62
|
+
if (!devinExe) {
|
|
63
|
+
throw new Error("未检测到本地 Devin 安装,请确认已安装 Devin 或配置 DEVIN_PATH 环境变量。");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let prompt = "";
|
|
67
|
+
if (!detailed) {
|
|
68
|
+
prompt = `请调用内置的 web_search 工具搜索 "${query}"(返回约 ${num_results} 条)。不要输出任何多余寒暄、说明或额外文本,只输出一个标准的 JSON 数组,每个元素包含 "title", "url", "snippet"。`;
|
|
69
|
+
} else {
|
|
70
|
+
prompt = `请调用内置的 web_search 工具深度搜索 "${query}"。请输出:1. 检索到的核心网页列表(包含标题、完整 URL、核心摘录);2. 针对该查询的综合结论分析与技术要点提炼。以清晰的 Markdown 格式呈现。`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const runAttempt = () =>
|
|
74
|
+
new Promise((resolve, reject) => {
|
|
75
|
+
execFile(
|
|
76
|
+
devinExe,
|
|
77
|
+
["--permission-mode", "dangerous", "--respect-workspace-trust", "false", "-p", prompt],
|
|
78
|
+
{
|
|
79
|
+
timeout: timeoutMs,
|
|
80
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
81
|
+
encoding: "utf-8",
|
|
82
|
+
env: { ...process.env, DEVIN_PERMISSION_MODE: "dangerous" },
|
|
83
|
+
},
|
|
84
|
+
(error, stdout, stderr) => {
|
|
85
|
+
if (error) {
|
|
86
|
+
if (error.killed) {
|
|
87
|
+
reject(new Error(`搜索超时 (超过 ${timeoutMs / 1000}s)`));
|
|
88
|
+
} else {
|
|
89
|
+
reject(new Error(`执行搜索失败: ${error.message}\n${stderr || ""}`));
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
resolve(stdout.trim());
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
let rawOutput = "";
|
|
99
|
+
let lastErr = null;
|
|
100
|
+
|
|
101
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
102
|
+
try {
|
|
103
|
+
rawOutput = await runAttempt();
|
|
104
|
+
break;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
lastErr = err;
|
|
107
|
+
if (attempt < maxRetries) {
|
|
108
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!rawOutput && lastErr) {
|
|
114
|
+
throw lastErr;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let structuredResults = [];
|
|
118
|
+
let summary = "";
|
|
119
|
+
|
|
120
|
+
if (!detailed) {
|
|
121
|
+
const parsed = parseJsonWithRepair(rawOutput);
|
|
122
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
123
|
+
structuredResults = parsed.slice(0, num_results).map((item) => ({
|
|
124
|
+
title: item.title || item.name || "网页结果",
|
|
125
|
+
url: item.url || item.link || "",
|
|
126
|
+
snippet: item.snippet || item.description || item.summary || "",
|
|
127
|
+
}));
|
|
128
|
+
} else {
|
|
129
|
+
structuredResults = fallbackParseResults(rawOutput).slice(0, num_results);
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
summary = rawOutput;
|
|
133
|
+
structuredResults = fallbackParseResults(rawOutput).slice(0, num_results);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const latencyMs = Date.now() - startTime;
|
|
137
|
+
const resultPayload = {
|
|
138
|
+
query,
|
|
139
|
+
count: structuredResults.length,
|
|
140
|
+
results: structuredResults,
|
|
141
|
+
summary: summary || undefined,
|
|
142
|
+
latencyMs,
|
|
143
|
+
config: `query="${query}" num_results=${num_results} detailed=${detailed}`,
|
|
144
|
+
cached: false,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
searchCache.set(cacheKey, resultPayload);
|
|
148
|
+
return resultPayload;
|
|
149
|
+
}
|