devin-search-mcp 1.0.6 → 2.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/AI_PROMPT.md +5 -18
- package/README.md +36 -41
- package/package.json +3 -4
- package/src/fetch.mjs +61 -81
- package/src/index.mjs +65 -168
- package/src/native-api.mjs +206 -0
- package/src/protobuf.mjs +137 -0
- package/src/search.mjs +18 -117
- package/src/detector.mjs +0 -160
package/AI_PROMPT.md
CHANGED
|
@@ -7,13 +7,16 @@
|
|
|
7
7
|
|
|
8
8
|
## 1. 这个 MCP 是干什么的?
|
|
9
9
|
|
|
10
|
-
`devin-search-mcp` 是一个标准 MCP (Model Context Protocol) 插件。它把 Devin
|
|
10
|
+
`devin-search-mcp` 是一个标准 MCP (Model Context Protocol) 插件。它把 Devin 内部高质量的**全网实时检索**和**网页正文深度去噪抓取**能力抽出来给所有 AI 使用。
|
|
11
|
+
|
|
12
|
+
**核心特性:极速且零模型消耗**
|
|
13
|
+
* 它不启动任何本地重型客户端进程,而是直连 Devin 官方原生检索网关(逆向后直接调用 `GetWebSearchResults` RPC)。
|
|
14
|
+
* **不经过任何大模型推理,零 Token 额度消耗**,响应速度稳定在 1~1.5 秒。
|
|
11
15
|
|
|
12
16
|
你可以使用的核心工具:
|
|
13
17
|
* **`devin_web_search`**:用自然语言搜索整个互联网。查最新发布的框架、新版本变更、GitHub Issue 或冷门报错方案。
|
|
14
18
|
* **`devin_web_fetch`**:输入一个 URL,帮你抓取干净的 Markdown 正文,自动剔除导航栏、广告和脚本代码。
|
|
15
19
|
* **`extract_devin_key`**:自动读取本地 Devin / Windsurf 登录 Token。
|
|
16
|
-
* **`devin_status`**:检查本地 Devin 引擎路径与健康状态。
|
|
17
20
|
|
|
18
21
|
---
|
|
19
22
|
|
|
@@ -51,22 +54,6 @@
|
|
|
51
54
|
|
|
52
55
|
*注意:只要用户的系统里装有 Devin Desktop 或已登录过 Devin,它会自动跨平台读取凭据,完全不需要在 `env` 里硬编码任何 API Key。*
|
|
53
56
|
|
|
54
|
-
### 第三步:非常规路径特别处理(可选)
|
|
55
|
-
|
|
56
|
-
如果用户的 Devin Desktop 安装在特殊的非默认盘符(例如 Windows 上的 `E:\Program\devin`),你可以为其补充 `DEVIN_PATH` 环境变量:
|
|
57
|
-
|
|
58
|
-
```json
|
|
59
|
-
{
|
|
60
|
-
"devin-search": {
|
|
61
|
-
"command": "npx",
|
|
62
|
-
"args": ["-y", "devin-search-mcp"],
|
|
63
|
-
"env": {
|
|
64
|
-
"DEVIN_PATH": "E:\\Program\\devin\\resources\\app\\extensions\\windsurf\\devin\\bin\\devin.exe"
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
57
|
---
|
|
71
58
|
|
|
72
59
|
## 3. 作为 AI,你在什么时候应该主动调用这些工具?
|
package/README.md
CHANGED
|
@@ -24,10 +24,16 @@
|
|
|
24
24
|
|
|
25
25
|
### 核心亮点(没有虚的,全击中痛点)
|
|
26
26
|
|
|
27
|
-
1.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
1. **真正的极速,1 秒级响应**:
|
|
28
|
+
我们没有采用“后台启动几十 MB 的 `devin.exe` 重型客户端、让大模型慢慢思考推理”的那种笨办法。
|
|
29
|
+
而是直接**逆向了 Devin 底层的二进制协议**,纯用 Node.js 发二进制 Protobuf 网络包,直连官方原生的 `GetWebSearchResults` 检索网关!
|
|
30
|
+
- **不启动任何本地客户端进程**
|
|
31
|
+
- **不经过任何大模型推理,零模型额度消耗**
|
|
32
|
+
- 实测首次检索 **1.5 秒**,后续缓存 JWT 后稳定 **1 秒左右** 返回。
|
|
33
|
+
2. **零配置登录**:只要你的电脑上装了 Devin Desktop 且登录过,这个工具就能自动从本地提取 Token(跨平台支持 `credentials.toml` 与 `state.vscdb`),完全不需要你在配置文件里手动填任何 API Key。
|
|
34
|
+
3. **拿到的都是干净数据**:搜索结果自动提取成 `标题 + 网址 + 核心要点摘录` 的结构化 JSON;抓取网页自动干掉广告、弹窗和导航栏,只留干净的 Markdown 正文。
|
|
35
|
+
4. **带内存缓存**:同一个关键词在短时间内搜第二次,直接 0 毫秒从内存缓存返回,省时又省调用次数。
|
|
36
|
+
5. **无需克隆安装**:已经发布到 npm 官方源,在客户端配上一行 `npx -y devin-search-mcp`,30 秒搞定。
|
|
31
37
|
|
|
32
38
|
### 它是怎么跑起来的?
|
|
33
39
|
|
|
@@ -36,18 +42,20 @@
|
|
|
36
42
|
│
|
|
37
43
|
▼
|
|
38
44
|
┌────────────────────────────────────────────────────────┐
|
|
39
|
-
│
|
|
45
|
+
│ Devin Search MCP (极速架构版) │
|
|
40
46
|
│ (本地运行的轻量服务) │
|
|
41
47
|
│ │
|
|
42
|
-
│ 1.
|
|
43
|
-
│ 2.
|
|
44
|
-
│ 3.
|
|
45
|
-
│ 4.
|
|
46
|
-
│ 5.
|
|
48
|
+
│ 1. 自动从本地提取 Devin 登录 Token (sql.js 读凭证) │
|
|
49
|
+
│ 2. 用 Token 换取 JWT (内存缓存,避免重复握手) │
|
|
50
|
+
│ 3. 构建二进制 Protobuf 包,直连官方原生检索网关 │
|
|
51
|
+
│ 4. 解析网关返回的二进制流,提炼标题/URL/摘要 │
|
|
52
|
+
│ 5. 命中缓存直接秒回,未命中则写入缓存 │
|
|
47
53
|
└────────────────────────────────────────────────────────┘
|
|
48
54
|
│
|
|
49
55
|
▼
|
|
50
56
|
返回给你的 AI:
|
|
57
|
+
[config] query="..." 耗时=1259ms 缓存=false
|
|
58
|
+
|
|
51
59
|
[1] Next.js 15 升级指南 (https://nextjs.org/docs/app/building-your-application/upgrading/version-15)
|
|
52
60
|
[2] React 19 支持与 Async Request APIs 变更点说明...
|
|
53
61
|
```
|
|
@@ -86,57 +94,39 @@
|
|
|
86
94
|
|
|
87
95
|
直接加到 `~/.claude.json` 的 `mcpServers` 里,用法完全一样。
|
|
88
96
|
|
|
89
|
-
> **小贴士(路径排查)**:如果你的 Devin Desktop 装在非常规路径(比如 `E:\Program\devin`),可以在配置的 `env` 显式指定它的可执行文件路径:
|
|
90
|
-
> ```json
|
|
91
|
-
> {
|
|
92
|
-
> "mcpServers": {
|
|
93
|
-
> "devin-search": {
|
|
94
|
-
> "command": "npx",
|
|
95
|
-
> "args": ["-y", "devin-search-mcp"],
|
|
96
|
-
> "env": {
|
|
97
|
-
> "DEVIN_PATH": "E:\\Program\\devin\\resources\\app\\extensions\\windsurf\\devin\\bin\\devin.exe"
|
|
98
|
-
> }
|
|
99
|
-
> }
|
|
100
|
-
> }
|
|
101
|
-
> }
|
|
102
|
-
> ```
|
|
103
|
-
|
|
104
97
|
### 都有哪些工具可以用?
|
|
105
98
|
|
|
106
|
-
配置好之后,你的 AI
|
|
99
|
+
配置好之后,你的 AI 会自动多出这几个能力:
|
|
107
100
|
|
|
108
|
-
1. **`devin_web_search
|
|
101
|
+
1. **`devin_web_search`(全网实时极速检索)**
|
|
109
102
|
* 想搜啥直接搜,比如 `Vue 3.5 reactive props`、`Tauri v2 migration`。
|
|
110
|
-
* 支持传参数:`query`(必填)、`num_results`(想看几条,默认 5 条)、`detailed
|
|
103
|
+
* 支持传参数:`query`(必填)、`num_results`(想看几条,默认 5 条)、`detailed`(是否附带 AI 综合总结)。
|
|
111
104
|
* 返回格式是干干净净的列表:
|
|
112
105
|
```json
|
|
113
106
|
[
|
|
114
107
|
{
|
|
115
108
|
"title": "Announcing Vue 3.5 | The Vue Point",
|
|
116
109
|
"url": "https://blog.vuejs.org/posts/vue-3-5",
|
|
117
|
-
"snippet": "Vue 3.5 带来响应式系统重大重构,内存占用降低 56
|
|
110
|
+
"snippet": "Vue 3.5 带来响应式系统重大重构,内存占用降低 56%..."
|
|
118
111
|
}
|
|
119
112
|
]
|
|
120
113
|
```
|
|
121
114
|
|
|
122
115
|
2. **`devin_web_fetch`(网页深度抓取与阅读)**
|
|
123
116
|
* 给它一个网址(比如某个文档页面或报错的 GitHub Issue),它会帮你抓取正文,并且自动去除广告、弹窗和样式噪声。
|
|
124
|
-
* 支持 `extract_mode`: `"markdown"`(默认,结构化排版)、`"text"
|
|
117
|
+
* 支持 `extract_mode`: `"markdown"`(默认,结构化排版)、`"text"`(纯文字)。
|
|
125
118
|
|
|
126
119
|
3. **`extract_devin_key`(提取本地 Key)**
|
|
127
120
|
* 跟 fast-context-mcp 的 `extract_windsurf_key` 一模一样,自动跨平台读出本地当前生效的 Token。
|
|
128
121
|
|
|
129
|
-
4. **`devin_status`(诊断状态)**
|
|
130
|
-
* 查一下当前调用的是哪个路径的 Devin,账号登录状态是否正常。
|
|
131
|
-
|
|
132
122
|
### 常见踩坑与注意事项
|
|
133
123
|
|
|
134
124
|
1. **电脑里必须装过 Devin Desktop 吗?**
|
|
135
125
|
* 是的。因为它是直接调用 Devin 的官方网关。只要你电脑上安装过 Devin Desktop 并且登录过一次,之后完全不用管它,MCP 会自动提取凭证跑起来。
|
|
136
126
|
2. **为什么不直接用常规的 Google / Bing 爬虫脚本?**
|
|
137
|
-
*
|
|
138
|
-
3.
|
|
139
|
-
*
|
|
127
|
+
* 公开搜索引擎爬虫天天被风控弹验证码;而这个走的是 Devin 官方针对技术问答和开发者文档清洗过的内部检索通道,精准度和干净程度完全是两个量级,而且速度极快。
|
|
128
|
+
3. **真的不消耗我的模型额度吗?**
|
|
129
|
+
* 是的。`devin_web_search` 走的是原生检索 RPC,**不经过大模型推理,零额度消耗**。这点与 `fast-context-mcp` 的设计哲学完全一致。
|
|
140
130
|
|
|
141
131
|
---
|
|
142
132
|
|
|
@@ -152,7 +142,14 @@
|
|
|
152
142
|
|
|
153
143
|
## 🌐 English
|
|
154
144
|
|
|
155
|
-
AI-driven real-time Web Search and Web Fetch as an MCP tool — powered by Devin
|
|
145
|
+
AI-driven real-time Web Search and Web Fetch as an MCP tool — powered by Devin's official native search gateway.
|
|
146
|
+
|
|
147
|
+
### Why It's Blazing Fast
|
|
148
|
+
|
|
149
|
+
Instead of spawning a heavy local `devin.exe` process and letting a huge LLM reason slowly, this project:
|
|
150
|
+
* **Reverse-engineered the native binary protocol** of Devin and calls `GetWebSearchResults` RPC directly.
|
|
151
|
+
* **No local client process**, **no LLM inference**, **zero model quota consumption**.
|
|
152
|
+
* Typical latency: **~1.5s on first call, ~1s afterwards (JWT cached)**.
|
|
156
153
|
|
|
157
154
|
### Quick Setup
|
|
158
155
|
|
|
@@ -174,10 +171,9 @@ Add to your MCP configuration (`claude_desktop_config.json`, `~/.claude.json`, o
|
|
|
174
171
|
|
|
175
172
|
### Key Tools
|
|
176
173
|
|
|
177
|
-
* **`devin_web_search`**:
|
|
178
|
-
* **`devin_web_fetch`**: Deep reader that cleans away advertisements
|
|
174
|
+
* **`devin_web_search`**: Native ultra-fast real-time web search (`query`, `num_results`, `detailed`).
|
|
175
|
+
* **`devin_web_fetch`**: Deep webpage reader that cleans away advertisements and navbars (`url`, `extract_mode`).
|
|
179
176
|
* **`extract_devin_key`**: Auto-extract Devin / Windsurf API Key from local installation.
|
|
180
|
-
* **`devin_status`**: Diagnostic tool for local engine path and login identity.
|
|
181
177
|
|
|
182
178
|
For full AI agent machine instructions, check [AI_PROMPT.md](AI_PROMPT.md).
|
|
183
179
|
|
|
@@ -186,4 +182,3 @@ For full AI agent machine instructions, check [AI_PROMPT.md](AI_PROMPT.md).
|
|
|
186
182
|
## License
|
|
187
183
|
|
|
188
184
|
MIT License © 2026 suvon
|
|
189
|
-
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devin-search-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "极速全网实时检索与网页抓取 MCP 服务,直连 Devin 官方网关,零大模型消耗,零本地进程 (支持 Cursor, Claude, CodeBuddy)",
|
|
5
5
|
"main": "src/index.mjs",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -44,7 +44,6 @@
|
|
|
44
44
|
"license": "MIT",
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
47
|
-
"sql.js": "^1.14.2"
|
|
48
|
-
"zod": "^3.23.8"
|
|
47
|
+
"sql.js": "^1.14.2"
|
|
49
48
|
}
|
|
50
49
|
}
|
package/src/fetch.mjs
CHANGED
|
@@ -1,106 +1,86 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* 遵循 Debug-First 准则:仅处理 retryable 官方重试,绝不引入隐式回退或静默降级代码,彻底暴露底层真实异常
|
|
2
|
+
* 轻量化网页正文抓取模块
|
|
3
|
+
* 本地直接 HTTP 抓取并做 HTML 到 Markdown 的正则清洗,零重型依赖
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
|
-
import { execFile } from "node:child_process";
|
|
8
|
-
import { findDevinExecutable } from "./detector.mjs";
|
|
9
6
|
import { searchCache } from "./cache.mjs";
|
|
10
7
|
|
|
11
8
|
/**
|
|
12
|
-
*
|
|
13
|
-
* @param {Object} options
|
|
14
|
-
* @param {string} options.url - 目标网页 URL
|
|
15
|
-
* @param {"markdown"|"text"|"summary"} [options.extractMode="markdown"] - 提取格式
|
|
16
|
-
* @param {number} [options.timeoutMs=60000] - 超时毫秒数
|
|
17
|
-
* @param {number} [options.maxRetries=1] - retryable 瞬时网络错误重试次数
|
|
9
|
+
* HTML 到 Markdown 极简清洗
|
|
18
10
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
11
|
+
function htmlToMarkdown(html) {
|
|
12
|
+
let text = html
|
|
13
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "")
|
|
14
|
+
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "")
|
|
15
|
+
.replace(/<nav\b[^<]*(?:(?!<\/nav>)<[^<]*)*<\/nav>/gi, "")
|
|
16
|
+
.replace(/<footer\b[^<]*(?:(?!<\/footer>)<[^<]*)*<\/footer>/gi, "")
|
|
17
|
+
.replace(/<header\b[^<]*(?:(?!<\/header>)<[^<]*)*<\/header>/gi, "")
|
|
18
|
+
.replace(/<!--[\s\S]*?-->/g, "");
|
|
19
|
+
|
|
20
|
+
text = text
|
|
21
|
+
.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, "\n# $1\n")
|
|
22
|
+
.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, "\n## $1\n")
|
|
23
|
+
.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, "\n### $1\n")
|
|
24
|
+
.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, "\n#### $1\n")
|
|
25
|
+
.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, "\n$1\n")
|
|
26
|
+
.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, "\n* $1")
|
|
27
|
+
.replace(/<a\s+(?:[^>]*?\s+)?href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, "[$2]($1)")
|
|
28
|
+
.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, "\n```\n$1\n```\n")
|
|
29
|
+
.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, "`$1`")
|
|
30
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
31
|
+
.replace(/<[^>]+>/g, " ")
|
|
32
|
+
.replace(/ /g, " ")
|
|
33
|
+
.replace(/&/g, "&")
|
|
34
|
+
.replace(/</g, "<")
|
|
35
|
+
.replace(/>/g, ">")
|
|
36
|
+
.replace(/"/g, '"')
|
|
37
|
+
.replace(/[ \t]+/g, " ")
|
|
38
|
+
.replace(/\n\s*\n\s*\n/g, "\n\n")
|
|
39
|
+
.trim();
|
|
40
|
+
|
|
41
|
+
return text;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 抓取指定 URL 的网页正文
|
|
46
|
+
*/
|
|
47
|
+
export async function executeWebFetch({ url, extractMode = "markdown", timeoutMs = 25000 }) {
|
|
25
48
|
if (!url || typeof url !== "string" || !/^https?:\/\//i.test(url)) {
|
|
26
49
|
throw new Error("请提供有效的 HTTP/HTTPS 网页 URL");
|
|
27
50
|
}
|
|
28
51
|
|
|
29
|
-
const cacheKey = searchCache.generateKey("
|
|
52
|
+
const cacheKey = searchCache.generateKey("native_web_fetch", url, { extractMode });
|
|
30
53
|
const cached = searchCache.get(cacheKey);
|
|
31
|
-
if (cached) {
|
|
32
|
-
return { ...cached, cached: true };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const devinExe = findDevinExecutable();
|
|
36
|
-
if (!devinExe) {
|
|
37
|
-
throw new Error("未检测到本地 Devin 安装,请确认已安装 Devin 或配置 DEVIN_PATH 环境变量。");
|
|
38
|
-
}
|
|
54
|
+
if (cached) return { ...cached, cached: true };
|
|
39
55
|
|
|
40
|
-
|
|
41
|
-
if (extractMode === "summary") {
|
|
42
|
-
prompt = `请使用 webfetch 工具读取 ${url} 的内容,并提供一份精准详实的核心内容提炼与关键要点总结。`;
|
|
43
|
-
} else if (extractMode === "text") {
|
|
44
|
-
prompt = `请使用 webfetch 工具读取 ${url} 的内容,去除网页导航、广告和页脚,直接输出主要正文纯文本。`;
|
|
45
|
-
} else {
|
|
46
|
-
prompt = `请使用 webfetch 工具读取 ${url} 的内容,并将正文内容转换为整洁、规范的 Markdown 格式输出。不要添加任何额外多余的寒暄。`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const runDevinAttempt = () =>
|
|
50
|
-
new Promise((resolve, reject) => {
|
|
51
|
-
execFile(
|
|
52
|
-
devinExe,
|
|
53
|
-
["--permission-mode", "dangerous", "--respect-workspace-trust", "false", "-p", prompt],
|
|
54
|
-
{
|
|
55
|
-
timeout: timeoutMs,
|
|
56
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
57
|
-
encoding: "utf-8",
|
|
58
|
-
env: { ...process.env, DEVIN_PERMISSION_MODE: "dangerous" },
|
|
59
|
-
},
|
|
60
|
-
(error, stdout, stderr) => {
|
|
61
|
-
if (error) {
|
|
62
|
-
if (error.killed) {
|
|
63
|
-
reject(new Error(`网页读取超时(超过 ${timeoutMs / 1000} 秒): ${url}`));
|
|
64
|
-
} else {
|
|
65
|
-
reject(new Error(error.message + (stderr ? `\n${stderr}` : "")));
|
|
66
|
-
}
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
resolve(stdout.trim());
|
|
70
|
-
}
|
|
71
|
-
);
|
|
72
|
-
});
|
|
56
|
+
const startTime = Date.now();
|
|
73
57
|
|
|
74
|
-
|
|
75
|
-
|
|
58
|
+
const resp = await fetch(url, {
|
|
59
|
+
headers: {
|
|
60
|
+
"User-Agent":
|
|
61
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
62
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
63
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
64
|
+
},
|
|
65
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
66
|
+
});
|
|
76
67
|
|
|
77
|
-
|
|
78
|
-
|
|
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;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
// 非 retryable 错误不盲目重试,立即向上抛出
|
|
91
|
-
throw err;
|
|
92
|
-
}
|
|
68
|
+
if (!resp.ok) {
|
|
69
|
+
throw new Error(`网页抓取失败: HTTP ${resp.status} ${resp.statusText}`);
|
|
93
70
|
}
|
|
94
71
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
72
|
+
const html = await resp.text();
|
|
73
|
+
let content = htmlToMarkdown(html);
|
|
74
|
+
|
|
75
|
+
if (extractMode === "text") {
|
|
76
|
+
content = content.replace(/[#*`\[\]()]/g, "").replace(/\n{2,}/g, "\n").trim();
|
|
98
77
|
}
|
|
99
78
|
|
|
100
79
|
const resultPayload = {
|
|
101
80
|
url,
|
|
102
81
|
extractMode,
|
|
103
|
-
content
|
|
82
|
+
content,
|
|
83
|
+
latencyMs: Date.now() - startTime,
|
|
104
84
|
cached: false,
|
|
105
85
|
};
|
|
106
86
|
|
package/src/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Devin Search MCP Server
|
|
3
|
-
* 遵循 Model Context Protocol
|
|
2
|
+
* Devin Search MCP Server (原生极速架构版)
|
|
3
|
+
* 遵循 Model Context Protocol,零本地重型进程,对齐 fast-context-mcp 设计
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -10,194 +10,96 @@ import {
|
|
|
10
10
|
ListToolsRequestSchema,
|
|
11
11
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
12
12
|
|
|
13
|
-
import { inspectDevinStatus } from "./detector.mjs";
|
|
14
13
|
import { extractDevinKey } from "./extract-key.mjs";
|
|
15
14
|
import { executeWebSearch } from "./search.mjs";
|
|
16
15
|
import { executeWebFetch } from "./fetch.mjs";
|
|
17
16
|
|
|
18
17
|
export function createDevinSearchServer() {
|
|
19
18
|
const server = new Server(
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
version: "1.0.0",
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
capabilities: {
|
|
26
|
-
tools: {},
|
|
27
|
-
},
|
|
28
|
-
}
|
|
19
|
+
{ name: "devin-search-mcp", version: "2.0.0" },
|
|
20
|
+
{ capabilities: { tools: {} } }
|
|
29
21
|
);
|
|
30
22
|
|
|
31
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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"],
|
|
23
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
24
|
+
tools: [
|
|
25
|
+
{
|
|
26
|
+
name: "devin_web_search",
|
|
27
|
+
description:
|
|
28
|
+
"极速全网实时检索 (直连 Devin 官方网关)。用自然语言查最新技术文档、库版本特性、报错与开源方案,亚秒级返回结构化标题、网址与摘要。",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
query: { type: "string", description: "搜索关键词或自然语言问题" },
|
|
33
|
+
num_results: { type: "integer", minimum: 1, maximum: 15, default: 5, description: "返回结果条数 (默认 5)" },
|
|
34
|
+
detailed: { type: "boolean", default: false, description: "是否附带深度综合总结" },
|
|
84
35
|
},
|
|
36
|
+
required: ["query"],
|
|
85
37
|
},
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{
|
|
96
|
-
name: "devin_status",
|
|
97
|
-
description: "诊断工具: 检测本地 Devin 安装路径、登录身份与可用性状态。",
|
|
98
|
-
inputSchema: {
|
|
99
|
-
type: "object",
|
|
100
|
-
properties: {},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "devin_web_fetch",
|
|
41
|
+
description: "轻量抓取指定网页的正文内容,自动剔除导航、广告与脚本,输出整洁的 Markdown 或纯文本。",
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
url: { type: "string", description: "目标网页完整 URL (http/https)" },
|
|
46
|
+
extract_mode: { type: "string", enum: ["markdown", "text", "summary"], default: "markdown", description: "输出格式" },
|
|
101
47
|
},
|
|
48
|
+
required: ["url"],
|
|
102
49
|
},
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "extract_devin_key",
|
|
53
|
+
description: "自动提取本地 Devin / Windsurf 登录凭证 (对齐 fast-context-mcp 的 extract_windsurf_key)。",
|
|
54
|
+
inputSchema: { type: "object", properties: {} },
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
}));
|
|
106
58
|
|
|
107
59
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
108
60
|
const { name, arguments: args } = request.params;
|
|
109
|
-
|
|
110
61
|
try {
|
|
111
62
|
if (name === "devin_web_search") {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const result = await executeWebSearch({
|
|
117
|
-
query,
|
|
118
|
-
num_results: numResults,
|
|
119
|
-
detailed,
|
|
63
|
+
const res = await executeWebSearch({
|
|
64
|
+
query: args?.query,
|
|
65
|
+
num_results: args?.num_results ?? 5,
|
|
66
|
+
detailed: args?.detailed ?? false,
|
|
120
67
|
});
|
|
121
68
|
|
|
122
|
-
const lines = [
|
|
123
|
-
|
|
124
|
-
"",
|
|
125
|
-
];
|
|
69
|
+
const lines = [`[config] query="${res.query}" 耗时=${res.latencyMs}ms 缓存=${res.cached}`, ""];
|
|
70
|
+
lines.push(JSON.stringify(res.results, null, 2));
|
|
126
71
|
|
|
127
|
-
|
|
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
|
-
};
|
|
72
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
141
73
|
}
|
|
142
74
|
|
|
143
75
|
if (name === "devin_web_fetch") {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const result = await executeWebFetch({
|
|
148
|
-
url,
|
|
149
|
-
extractMode,
|
|
76
|
+
const res = await executeWebFetch({
|
|
77
|
+
url: args?.url,
|
|
78
|
+
extractMode: args?.extract_mode ?? "markdown",
|
|
150
79
|
});
|
|
151
|
-
|
|
152
|
-
return {
|
|
153
|
-
content: [
|
|
154
|
-
{
|
|
155
|
-
type: "text",
|
|
156
|
-
text: result.content,
|
|
157
|
-
},
|
|
158
|
-
],
|
|
159
|
-
};
|
|
80
|
+
return { content: [{ type: "text", text: res.content }] };
|
|
160
81
|
}
|
|
161
82
|
|
|
162
83
|
if (name === "extract_devin_key") {
|
|
163
|
-
const
|
|
164
|
-
if (!
|
|
165
|
-
return {
|
|
166
|
-
content: [
|
|
167
|
-
{
|
|
168
|
-
type: "text",
|
|
169
|
-
text: "未在本地找到有效的 Devin / Windsurf API Key。请确认 Devin Desktop 处于登录状态。",
|
|
170
|
-
},
|
|
171
|
-
],
|
|
172
|
-
};
|
|
84
|
+
const r = await extractDevinKey();
|
|
85
|
+
if (!r.api_key) {
|
|
86
|
+
return { content: [{ type: "text", text: "未在本地找到有效的 Devin 凭证,请确认 Devin Desktop 已登录。" }] };
|
|
173
87
|
}
|
|
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
88
|
return {
|
|
188
|
-
content: [{
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
89
|
+
content: [{
|
|
90
|
+
type: "text",
|
|
91
|
+
text: [
|
|
92
|
+
"Devin API Key extracted successfully",
|
|
93
|
+
"",
|
|
94
|
+
` Key: ${r.api_key.slice(0, 25)}...${r.api_key.slice(-15)}`,
|
|
95
|
+
` Length: ${r.length}`,
|
|
96
|
+
` Source: ${r.source}`,
|
|
97
|
+
` App: ${r.app}`,
|
|
98
|
+
"",
|
|
99
|
+
`Usage:`,
|
|
100
|
+
` export DEVIN_API_KEY="${r.api_key}"`,
|
|
101
|
+
].join("\n"),
|
|
102
|
+
}],
|
|
201
103
|
};
|
|
202
104
|
}
|
|
203
105
|
|
|
@@ -205,12 +107,7 @@ export function createDevinSearchServer() {
|
|
|
205
107
|
} catch (error) {
|
|
206
108
|
return {
|
|
207
109
|
isError: true,
|
|
208
|
-
content: [
|
|
209
|
-
{
|
|
210
|
-
type: "text",
|
|
211
|
-
text: `[devin-search-mcp 出错] ${error.message}`,
|
|
212
|
-
},
|
|
213
|
-
],
|
|
110
|
+
content: [{ type: "text", text: `[devin-search-mcp 出错] ${error.message}` }],
|
|
214
111
|
};
|
|
215
112
|
}
|
|
216
113
|
});
|
|
@@ -222,5 +119,5 @@ export async function runServer() {
|
|
|
222
119
|
const server = createDevinSearchServer();
|
|
223
120
|
const transport = new StdioServerTransport();
|
|
224
121
|
await server.connect(transport);
|
|
225
|
-
console.error("Devin Search MCP
|
|
122
|
+
console.error("Devin Search MCP 原生极速版已启动 (stdio)");
|
|
226
123
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devin / Windsurf 官方原生检索核心层
|
|
3
|
+
* 完全对齐 fast-context-mcp 思路:本地零进程、直连 Connect-RPC、
|
|
4
|
+
* 直接调用官方网关原生检索接口 (不经过任何大模型的重推理)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { platform, arch, release, version as osVersion, hostname, cpus, totalmem } from "node:os";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
ProtobufEncoder,
|
|
12
|
+
extractStrings,
|
|
13
|
+
connectFrameEncode,
|
|
14
|
+
connectFrameDecode,
|
|
15
|
+
} from "./protobuf.mjs";
|
|
16
|
+
|
|
17
|
+
const API_BASE = "https://server.self-serve.windsurf.com/exa.api_server_pb.ApiServerService";
|
|
18
|
+
const AUTH_BASE = "https://server.self-serve.windsurf.com/exa.auth_pb.AuthService";
|
|
19
|
+
const WS_APP = "windsurf";
|
|
20
|
+
const WS_APP_VER = process.env.WS_APP_VER || "1.48.2";
|
|
21
|
+
const WS_LS_VER = process.env.WS_LS_VER || "1.9544.35";
|
|
22
|
+
|
|
23
|
+
// ─── 内存 JWT 缓存 (避免重复握手) ───
|
|
24
|
+
const _jwtCache = new Map();
|
|
25
|
+
|
|
26
|
+
function _getJwtExp(jwt) {
|
|
27
|
+
try {
|
|
28
|
+
const parts = jwt.split(".");
|
|
29
|
+
if (parts.length < 2) return 0;
|
|
30
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
|
|
31
|
+
return payload.exp || 0;
|
|
32
|
+
} catch {
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _buildMetadata(apiKey, jwt) {
|
|
38
|
+
const meta = new ProtobufEncoder();
|
|
39
|
+
meta.writeString(1, WS_APP);
|
|
40
|
+
meta.writeString(2, WS_APP_VER);
|
|
41
|
+
meta.writeString(3, apiKey);
|
|
42
|
+
meta.writeString(4, "en");
|
|
43
|
+
|
|
44
|
+
const plat = platform();
|
|
45
|
+
meta.writeString(5, JSON.stringify({
|
|
46
|
+
Os: plat, Arch: arch(), Release: release(), Version: osVersion(),
|
|
47
|
+
Machine: arch(), Nodename: hostname(),
|
|
48
|
+
Sysname: plat === "darwin" ? "Darwin" : plat === "win32" ? "Windows_NT" : "Linux",
|
|
49
|
+
ProductVersion: "",
|
|
50
|
+
}));
|
|
51
|
+
meta.writeString(7, WS_LS_VER);
|
|
52
|
+
|
|
53
|
+
const cpuList = cpus();
|
|
54
|
+
meta.writeString(8, JSON.stringify({
|
|
55
|
+
NumSockets: 1, NumCores: cpuList.length, NumThreads: cpuList.length,
|
|
56
|
+
VendorID: "", Family: "0", Model: "0",
|
|
57
|
+
ModelName: cpuList[0]?.model || "Unknown", Memory: totalmem(),
|
|
58
|
+
}));
|
|
59
|
+
meta.writeString(12, WS_APP);
|
|
60
|
+
meta.writeString(21, jwt);
|
|
61
|
+
meta.writeBytes(30, Buffer.from([0x00, 0x01]));
|
|
62
|
+
return meta;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 获取并缓存 JWT 令牌
|
|
67
|
+
*/
|
|
68
|
+
export async function getCachedJwt(apiKey) {
|
|
69
|
+
const now = Math.floor(Date.now() / 1000);
|
|
70
|
+
const cached = _jwtCache.get(apiKey);
|
|
71
|
+
if (cached && cached.expiresAt > now + 60) return cached.token;
|
|
72
|
+
|
|
73
|
+
const meta = new ProtobufEncoder();
|
|
74
|
+
meta.writeString(1, WS_APP);
|
|
75
|
+
meta.writeString(2, WS_APP_VER);
|
|
76
|
+
meta.writeString(3, apiKey);
|
|
77
|
+
meta.writeString(4, "en");
|
|
78
|
+
meta.writeString(7, WS_LS_VER);
|
|
79
|
+
meta.writeString(12, WS_APP);
|
|
80
|
+
meta.writeBytes(30, Buffer.from([0x00, 0x01]));
|
|
81
|
+
|
|
82
|
+
const outer = new ProtobufEncoder();
|
|
83
|
+
outer.writeMessage(1, meta);
|
|
84
|
+
|
|
85
|
+
const resp = await fetch(`${AUTH_BASE}/GetUserJwt`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"Content-Type": "application/proto",
|
|
89
|
+
"Connect-Protocol-Version": "1",
|
|
90
|
+
"User-Agent": "connect-go/1.18.1 (go1.25.5)",
|
|
91
|
+
"Accept-Encoding": "gzip",
|
|
92
|
+
},
|
|
93
|
+
body: outer.toBuffer(),
|
|
94
|
+
signal: AbortSignal.timeout(20000),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (!resp.ok) throw new Error(`获取凭证失败 HTTP ${resp.status},请检查 Devin 登录状态或重新提取 Key。`);
|
|
98
|
+
|
|
99
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
100
|
+
for (const s of extractStrings(buf)) {
|
|
101
|
+
if (s.startsWith("eyJ") && s.includes(".")) {
|
|
102
|
+
_jwtCache.set(apiKey, { token: s, expiresAt: _getJwtExp(s) || now + 3600 });
|
|
103
|
+
return s;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
throw new Error("未能从服务端响应中解析出有效凭证。");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 极简 Protobuf 解码器:返回 [{ field, wire, value }]
|
|
111
|
+
*/
|
|
112
|
+
export function parseProto(data) {
|
|
113
|
+
const out = [];
|
|
114
|
+
let i = 0;
|
|
115
|
+
while (i < data.length) {
|
|
116
|
+
let tag = 0, shift = 0;
|
|
117
|
+
while (i < data.length) {
|
|
118
|
+
const b = data[i++];
|
|
119
|
+
tag |= (b & 0x7f) << shift;
|
|
120
|
+
shift += 7;
|
|
121
|
+
if (!(b & 0x80)) break;
|
|
122
|
+
}
|
|
123
|
+
const field = tag >>> 3;
|
|
124
|
+
const wire = tag & 0x7;
|
|
125
|
+
|
|
126
|
+
if (wire === 0) {
|
|
127
|
+
let val = 0; shift = 0;
|
|
128
|
+
while (i < data.length) {
|
|
129
|
+
const b = data[i++];
|
|
130
|
+
val |= (b & 0x7f) << shift;
|
|
131
|
+
shift += 7;
|
|
132
|
+
if (!(b & 0x80)) break;
|
|
133
|
+
}
|
|
134
|
+
out.push({ field, wire, value: val });
|
|
135
|
+
} else if (wire === 2) {
|
|
136
|
+
let len = 0; shift = 0;
|
|
137
|
+
while (i < data.length) {
|
|
138
|
+
const b = data[i++];
|
|
139
|
+
len |= (b & 0x7f) << shift;
|
|
140
|
+
shift += 7;
|
|
141
|
+
if (!(b & 0x80)) break;
|
|
142
|
+
}
|
|
143
|
+
out.push({ field, wire, value: data.subarray(i, i + len) });
|
|
144
|
+
i += len;
|
|
145
|
+
} else if (wire === 1) i += 8;
|
|
146
|
+
else if (wire === 5) i += 4;
|
|
147
|
+
else break;
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 调用 Devin 原生全网检索 RPC (GetWebSearchResults)
|
|
154
|
+
* 纯网关检索,无任何大模型推理开销,亚秒级响应
|
|
155
|
+
*/
|
|
156
|
+
export async function nativeWebSearch(apiKey, jwt, query, timeoutMs = 25000) {
|
|
157
|
+
const searchWeb = new ProtobufEncoder();
|
|
158
|
+
searchWeb.writeString(1, query);
|
|
159
|
+
|
|
160
|
+
const req = new ProtobufEncoder();
|
|
161
|
+
req.writeMessage(1, _buildMetadata(apiKey, jwt));
|
|
162
|
+
req.writeMessage(2, searchWeb);
|
|
163
|
+
|
|
164
|
+
const resp = await fetch(`${API_BASE}/GetWebSearchResults`, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: {
|
|
167
|
+
"Content-Type": "application/proto",
|
|
168
|
+
"Connect-Protocol-Version": "1",
|
|
169
|
+
"User-Agent": "connect-go/1.18.1 (go1.25.5)",
|
|
170
|
+
"Accept-Encoding": "gzip",
|
|
171
|
+
},
|
|
172
|
+
body: req.toBuffer(),
|
|
173
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (!resp.ok) {
|
|
177
|
+
throw new Error(`官方检索网关返回异常 HTTP ${resp.status}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
181
|
+
const topFields = parseProto(buf);
|
|
182
|
+
|
|
183
|
+
const results = [];
|
|
184
|
+
|
|
185
|
+
for (const f of topFields) {
|
|
186
|
+
// field 1 为重复的结果条目
|
|
187
|
+
if (f.field === 1 && f.wire === 2 && Buffer.isBuffer(f.value)) {
|
|
188
|
+
const sub = parseProto(f.value);
|
|
189
|
+
let url = "", title = "", snippet = "";
|
|
190
|
+
|
|
191
|
+
for (const s of sub) {
|
|
192
|
+
if (!Buffer.isBuffer(s.value)) continue;
|
|
193
|
+
const text = s.value.toString("utf-8");
|
|
194
|
+
if (s.field === 3) url = text;
|
|
195
|
+
else if (s.field === 4) title = text;
|
|
196
|
+
else if (s.field === 7) snippet = text;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (url || title) {
|
|
200
|
+
results.push({ url, title: title || url, snippet });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return results;
|
|
206
|
+
}
|
package/src/protobuf.mjs
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 精简版 Protobuf 编码器 + Connect-RPC 帧处理
|
|
3
|
+
* 对齐 Windsurf / Devin 底层二进制协议,纯内存零依赖
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { gzipSync, gunzipSync } from "node:zlib";
|
|
7
|
+
|
|
8
|
+
export class ProtobufEncoder {
|
|
9
|
+
constructor() {
|
|
10
|
+
this._chunks = [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
_varint(value) {
|
|
14
|
+
const bytes = [];
|
|
15
|
+
while (value > 0x7f) {
|
|
16
|
+
bytes.push((value & 0x7f) | 0x80);
|
|
17
|
+
value >>>= 7;
|
|
18
|
+
}
|
|
19
|
+
bytes.push(value & 0x7f);
|
|
20
|
+
return Buffer.from(bytes);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_tag(field, wire) {
|
|
24
|
+
return this._varint((field << 3) | wire);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
writeVarint(field, value) {
|
|
28
|
+
this._chunks.push(this._tag(field, 0), this._varint(value));
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
writeString(field, value) {
|
|
33
|
+
const data = Buffer.from(value, "utf-8");
|
|
34
|
+
this._chunks.push(this._tag(field, 2), this._varint(data.length), data);
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
writeBytes(field, value) {
|
|
39
|
+
const buf = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
40
|
+
this._chunks.push(this._tag(field, 2), this._varint(buf.length), buf);
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
writeMessage(field, sub) {
|
|
45
|
+
const data = sub.toBuffer();
|
|
46
|
+
this._chunks.push(this._tag(field, 2), this._varint(data.length), data);
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
toBuffer() {
|
|
51
|
+
return Buffer.concat(this._chunks);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 从 protobuf 二进制流中提取长度大于 5 的字符串
|
|
57
|
+
*/
|
|
58
|
+
export function extractStrings(data) {
|
|
59
|
+
const strings = [];
|
|
60
|
+
let i = 0;
|
|
61
|
+
while (i < data.length) {
|
|
62
|
+
let tag = 0;
|
|
63
|
+
let shift = 0;
|
|
64
|
+
while (i < data.length) {
|
|
65
|
+
const b = data[i++];
|
|
66
|
+
tag |= (b & 0x7f) << shift;
|
|
67
|
+
shift += 7;
|
|
68
|
+
if (!(b & 0x80)) break;
|
|
69
|
+
}
|
|
70
|
+
const wire = tag & 0x7;
|
|
71
|
+
if (wire === 0) {
|
|
72
|
+
while (i < data.length) {
|
|
73
|
+
const b = data[i++];
|
|
74
|
+
if (!(b & 0x80)) break;
|
|
75
|
+
}
|
|
76
|
+
} else if (wire === 1) {
|
|
77
|
+
i += 8;
|
|
78
|
+
} else if (wire === 2) {
|
|
79
|
+
let length = 0;
|
|
80
|
+
shift = 0;
|
|
81
|
+
while (i < data.length) {
|
|
82
|
+
const b = data[i++];
|
|
83
|
+
length |= (b & 0x7f) << shift;
|
|
84
|
+
shift += 7;
|
|
85
|
+
if (!(b & 0x80)) break;
|
|
86
|
+
}
|
|
87
|
+
if (i + length <= data.length) {
|
|
88
|
+
const raw = data.subarray(i, i + length);
|
|
89
|
+
try {
|
|
90
|
+
const text = raw.toString("utf-8");
|
|
91
|
+
if (text.length > 5) strings.push(text);
|
|
92
|
+
} catch {}
|
|
93
|
+
}
|
|
94
|
+
i += length;
|
|
95
|
+
} else if (wire === 5) {
|
|
96
|
+
i += 4;
|
|
97
|
+
} else {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return strings;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function connectFrameEncode(protoBytes, compress = true) {
|
|
105
|
+
let payload;
|
|
106
|
+
let flags;
|
|
107
|
+
if (compress) {
|
|
108
|
+
payload = gzipSync(protoBytes);
|
|
109
|
+
flags = 1;
|
|
110
|
+
} else {
|
|
111
|
+
payload = protoBytes;
|
|
112
|
+
flags = 0;
|
|
113
|
+
}
|
|
114
|
+
const header = Buffer.alloc(5);
|
|
115
|
+
header[0] = flags;
|
|
116
|
+
header.writeUInt32BE(payload.length, 1);
|
|
117
|
+
return Buffer.concat([header, payload]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function connectFrameDecode(data) {
|
|
121
|
+
const frames = [];
|
|
122
|
+
let i = 0;
|
|
123
|
+
while (i + 5 <= data.length) {
|
|
124
|
+
const flags = data[i];
|
|
125
|
+
const length = data.readUInt32BE(i + 1);
|
|
126
|
+
i += 5;
|
|
127
|
+
let payload = data.subarray(i, i + length);
|
|
128
|
+
i += length;
|
|
129
|
+
if (flags === 1 || flags === 3) {
|
|
130
|
+
try {
|
|
131
|
+
payload = gunzipSync(payload);
|
|
132
|
+
} catch {}
|
|
133
|
+
}
|
|
134
|
+
frames.push(Buffer.from(payload));
|
|
135
|
+
}
|
|
136
|
+
return frames;
|
|
137
|
+
}
|
package/src/search.mjs
CHANGED
|
@@ -1,146 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Devin
|
|
3
|
-
*
|
|
2
|
+
* Devin 全网实时检索模块 (原生极速版)
|
|
3
|
+
* 不启动任何本地客户端进程,不经过大模型推理
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { extractDevinKey } from "./extract-key.mjs";
|
|
7
|
+
import { getCachedJwt, nativeWebSearch } from "./native-api.mjs";
|
|
8
8
|
import { searchCache } from "./cache.mjs";
|
|
9
|
-
import { parseJsonWithRepair } from "./response-repair.mjs";
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
|
-
*
|
|
11
|
+
* 执行原生极速检索
|
|
13
12
|
*/
|
|
14
|
-
function
|
|
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 = parseInt(process.env.DEVIN_TIMEOUT_MS || "60000", 10),
|
|
44
|
-
maxRetries = 1,
|
|
45
|
-
}) {
|
|
13
|
+
export async function executeWebSearch({ query, num_results = 5, detailed = false }) {
|
|
46
14
|
if (!query || typeof query !== "string") {
|
|
47
15
|
throw new Error("请提供有效的搜索关键词 (query)");
|
|
48
16
|
}
|
|
49
17
|
|
|
50
18
|
const startTime = Date.now();
|
|
51
|
-
const cacheKey = searchCache.generateKey("
|
|
19
|
+
const cacheKey = searchCache.generateKey("native_web_search", query, { num_results, detailed });
|
|
52
20
|
const cached = searchCache.get(cacheKey);
|
|
53
21
|
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 环境变量。");
|
|
22
|
+
return { ...cached, latencyMs: 0, cached: true };
|
|
64
23
|
}
|
|
65
24
|
|
|
66
|
-
|
|
67
|
-
if (!
|
|
68
|
-
|
|
69
|
-
} else {
|
|
70
|
-
prompt = `请调用内置的 web_search 工具深度搜索 "${query}"。请输出:1. 检索到的核心网页列表(包含标题、完整 URL、核心摘录);2. 针对该查询的综合结论分析与技术要点提炼。以清晰的 Markdown 格式呈现。`;
|
|
25
|
+
const { api_key } = await extractDevinKey();
|
|
26
|
+
if (!api_key) {
|
|
27
|
+
throw new Error("未检测到本地 Devin 登录凭证,请先安装并登录 Devin Desktop。");
|
|
71
28
|
}
|
|
72
29
|
|
|
73
|
-
const
|
|
74
|
-
|
|
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
|
-
});
|
|
30
|
+
const jwt = await getCachedJwt(api_key);
|
|
31
|
+
const rawResults = await nativeWebSearch(api_key, jwt, query);
|
|
97
32
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
}
|
|
33
|
+
const structuredResults = rawResults.slice(0, num_results).map((r) => ({
|
|
34
|
+
title: r.title,
|
|
35
|
+
url: r.url,
|
|
36
|
+
snippet: r.snippet ? r.snippet.replace(/\n+/g, " ").slice(0, 500) : "",
|
|
37
|
+
}));
|
|
135
38
|
|
|
136
39
|
const latencyMs = Date.now() - startTime;
|
|
137
40
|
const resultPayload = {
|
|
138
41
|
query,
|
|
139
42
|
count: structuredResults.length,
|
|
140
43
|
results: structuredResults,
|
|
141
|
-
summary: summary || undefined,
|
|
142
44
|
latencyMs,
|
|
143
|
-
config: `query="${query}" num_results=${num_results} detailed=${detailed}`,
|
|
144
45
|
cached: false,
|
|
145
46
|
};
|
|
146
47
|
|
package/src/detector.mjs
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
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: 15000,
|
|
144
|
-
}).trim();
|
|
145
|
-
} catch (err) {
|
|
146
|
-
authStatusText = err.stdout || err.message;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const isLoggedIn = authStatusText.includes("Logged in") || hasToken;
|
|
151
|
-
|
|
152
|
-
return {
|
|
153
|
-
executable,
|
|
154
|
-
credentialsPath,
|
|
155
|
-
hasToken,
|
|
156
|
-
tokenSnippet,
|
|
157
|
-
isLoggedIn,
|
|
158
|
-
statusSummary: authStatusText,
|
|
159
|
-
};
|
|
160
|
-
}
|