@eogee-tech/eosearch 0.0.0-stage → 0.2.1
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 +201 -0
- package/NOTICE +37 -0
- package/README.md +210 -3
- package/build-cli/cli.js +691 -0
- package/build-cli/client.js +115 -0
- package/build-cli/config.js +33 -0
- package/build-cli/updateCheck.js +140 -0
- package/package.json +63 -4
- package/skill/SKILL.md +214 -0
- package/skill/eosearch-video/SKILL.md +119 -0
- package/skill/eosearch-video/helpers/grab.sh +58 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// cli/client.ts — 通过 MCP Streamable HTTP 调用 eosearch 服务端
|
|
2
|
+
// 每次进程:initialize 握手(不计费)→ tools/call(计费 1 次)→ 关闭会话
|
|
3
|
+
// 薄客户端边界:只封装 HTTP 调用,不含任何搜索/抓取逻辑
|
|
4
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
5
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
6
|
+
function failure(exitCode, problem, cause, next) {
|
|
7
|
+
return { kind: 'failure', exitCode, problem, cause, next };
|
|
8
|
+
}
|
|
9
|
+
function serverMessage(message) {
|
|
10
|
+
const match = /"message"\s*:\s*"([^"]+)"/.exec(message);
|
|
11
|
+
return match ? match[1] : null;
|
|
12
|
+
}
|
|
13
|
+
function probe(error) {
|
|
14
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
15
|
+
const httpMatch = /HTTP (\d{3})/.exec(message);
|
|
16
|
+
if (httpMatch)
|
|
17
|
+
return { status: parseInt(httpMatch[1], 10), message };
|
|
18
|
+
// 服务端 deny() 约定:-32001 = 401(鉴权拒绝),-32002 = 429(配额限流);
|
|
19
|
+
// SDK 抛错时可能只带 JSON body(McpError code 或文本),按约定码还原 HTTP 状态
|
|
20
|
+
const rpcCode = error?.code;
|
|
21
|
+
if (rpcCode === -32001)
|
|
22
|
+
return { status: 401, message };
|
|
23
|
+
if (rpcCode === -32002)
|
|
24
|
+
return { status: 429, message };
|
|
25
|
+
if (/"code"\s*:\s*-32001/.test(message))
|
|
26
|
+
return { status: 401, message };
|
|
27
|
+
if (/"code"\s*:\s*-32002/.test(message))
|
|
28
|
+
return { status: 429, message };
|
|
29
|
+
return { status: null, message };
|
|
30
|
+
}
|
|
31
|
+
function isNetworkError(message) {
|
|
32
|
+
return /ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|fetch failed|socket hang up/i.test(message);
|
|
33
|
+
}
|
|
34
|
+
function isTimeoutError(message) {
|
|
35
|
+
return /timed?\s*out|timeout|aborted/i.test(message);
|
|
36
|
+
}
|
|
37
|
+
function connectFailure(error, serverUrl) {
|
|
38
|
+
const { status, message } = probe(error);
|
|
39
|
+
if (status === 401) {
|
|
40
|
+
const detail = serverMessage(message) ? `(服务端:${serverMessage(message)})` : '';
|
|
41
|
+
return failure(3, '服务端拒绝了这把密钥(HTTP 401)', `向 ${serverUrl} 握手时返回 401,密钥无效或请求未携带${detail}`, '到 eogee.com 个人中心核对密钥(注意无多余空格),修正 EOSEARCH_API_KEY 或 ~/.eosearchrc 后重试 eosearch whoami');
|
|
42
|
+
}
|
|
43
|
+
if (status === 429) {
|
|
44
|
+
const detail = serverMessage(message) ?? 'rate limit';
|
|
45
|
+
return failure(4, '触发限流(HTTP 429)', detail, '等待数十秒后重试;每分钟限速与每日配额档位见 eogee.com 个人中心');
|
|
46
|
+
}
|
|
47
|
+
if (status === 404) {
|
|
48
|
+
return failure(5, '服务地址不存在(HTTP 404)', `${serverUrl} 未找到`, '确认服务地址是否为 https://eogee.com/search/v1/mcp,或用 --server-url 指定正确地址');
|
|
49
|
+
}
|
|
50
|
+
if (isNetworkError(message)) {
|
|
51
|
+
return failure(5, '无法连接服务端', message, '检查本机网络与代理设置;若怀疑服务端故障,稍后用 eosearch whoami 探测恢复情况');
|
|
52
|
+
}
|
|
53
|
+
if (status !== null && status >= 500) {
|
|
54
|
+
return failure(5, `服务端错误(HTTP ${status})`, message, '服务端临时故障,稍后重试');
|
|
55
|
+
}
|
|
56
|
+
return failure(5, '与服务端通信失败', message, '检查 --server-url 与网络环境后重试');
|
|
57
|
+
}
|
|
58
|
+
async function withClient(serverUrl, apiKey, clientVersion, fn) {
|
|
59
|
+
const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
|
|
60
|
+
requestInit: { headers: { 'X-API-Key': apiKey } }
|
|
61
|
+
});
|
|
62
|
+
const client = new Client({ name: 'eosearch-cli', version: clientVersion }, { capabilities: {} });
|
|
63
|
+
try {
|
|
64
|
+
await client.connect(transport);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
try {
|
|
68
|
+
await client.close();
|
|
69
|
+
}
|
|
70
|
+
catch { /* 关闭失败不影响错误分类 */ }
|
|
71
|
+
return { fail: connectFailure(error, serverUrl) };
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return { result: await fn(client) };
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const { message } = probe(error);
|
|
78
|
+
if (isTimeoutError(message)) {
|
|
79
|
+
return {
|
|
80
|
+
fail: failure(5, '调用超时', message, '加大 --timeout(秒,默认 120)后重试;deep 与 batch 任务较慢时建议 --timeout 300')
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return { fail: connectFailure(error, serverUrl) };
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
try {
|
|
87
|
+
await client.close();
|
|
88
|
+
}
|
|
89
|
+
catch { /* 忽略关闭错误 */ }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export async function callTool(args) {
|
|
93
|
+
const outcome = await withClient(args.serverUrl, args.apiKey, args.clientVersion, async (client) => client.callTool({ name: args.toolName, arguments: args.toolArgs }, undefined, { timeout: args.timeoutMs }));
|
|
94
|
+
if ('fail' in outcome)
|
|
95
|
+
return outcome.fail;
|
|
96
|
+
const result = outcome.result;
|
|
97
|
+
const text = (result.content ?? [])
|
|
98
|
+
.filter((c) => c.type === 'text' && typeof c.text === 'string')
|
|
99
|
+
.map((c) => c.text)
|
|
100
|
+
.join('\n');
|
|
101
|
+
if (result.isError) {
|
|
102
|
+
return failure(5, '服务端执行工具失败', text || '工具返回 isError 但无错误详情', '按错误信息调整参数重试;若持续失败,用 eosearch whoami 确认服务状态');
|
|
103
|
+
}
|
|
104
|
+
return { kind: 'success', text };
|
|
105
|
+
}
|
|
106
|
+
export async function whoami(args) {
|
|
107
|
+
const outcome = await withClient(args.serverUrl, args.apiKey, args.clientVersion, async (client) => ({
|
|
108
|
+
serverName: client.getServerVersion()?.name,
|
|
109
|
+
serverVersion: client.getServerVersion()?.version,
|
|
110
|
+
instructions: client.getInstructions()
|
|
111
|
+
}));
|
|
112
|
+
if ('fail' in outcome)
|
|
113
|
+
return outcome.fail;
|
|
114
|
+
return outcome.result;
|
|
115
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// cli/config.ts — 密钥与服务地址解析
|
|
2
|
+
// 优先级(高到低):环境变量 > 用户级配置文件 ~/.eosearchrc
|
|
3
|
+
// 安全约定:密钥不提供命令行参数入口,避免进入 Shell 历史与进程列表
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
export const DEFAULT_SERVER_URL = 'https://eogee.com/search/v1/mcp';
|
|
8
|
+
export function configFilePath() {
|
|
9
|
+
return join(homedir(), '.eosearchrc');
|
|
10
|
+
}
|
|
11
|
+
function readRc() {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(readFileSync(configFilePath(), 'utf8'));
|
|
14
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function asTrimmedString(value) {
|
|
21
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
22
|
+
}
|
|
23
|
+
export function resolveConfig(overrides) {
|
|
24
|
+
const rc = readRc();
|
|
25
|
+
const envKey = asTrimmedString(process.env.EOSEARCH_API_KEY);
|
|
26
|
+
const rcKey = asTrimmedString(rc.apiKey);
|
|
27
|
+
const envUrl = asTrimmedString(process.env.EOSEARCH_SERVER_URL);
|
|
28
|
+
const rcUrl = asTrimmedString(rc.serverUrl);
|
|
29
|
+
const apiKey = envKey ?? rcKey;
|
|
30
|
+
const apiKeySource = envKey ? 'environment' : rcKey ? 'config-file' : 'none';
|
|
31
|
+
const serverUrl = overrides.serverUrl ?? envUrl ?? rcUrl ?? DEFAULT_SERVER_URL;
|
|
32
|
+
return { apiKey, apiKeySource, serverUrl };
|
|
33
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// cli/updateCheck.ts — npm 版本发现与更新提示
|
|
2
|
+
// 策略:每次启动读本地缓存并提示;缓存过期时由后台子进程刷新,不阻塞当前命令
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
export const PACKAGE_NAME = '@eogee-tech/eosearch';
|
|
9
|
+
export const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
10
|
+
const FETCH_TIMEOUT_MS = 4000;
|
|
11
|
+
const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
|
|
12
|
+
export function updateCachePath() {
|
|
13
|
+
return process.env.EOSEARCH_UPDATE_CACHE || join(homedir(), '.eosearch-update.json');
|
|
14
|
+
}
|
|
15
|
+
export function updateCheckDisabled(env = process.env) {
|
|
16
|
+
return Boolean(env.CI) || Boolean(env.NO_UPDATE_NOTIFIER) || Boolean(env.EOSEARCH_NO_UPDATE_CHECK);
|
|
17
|
+
}
|
|
18
|
+
function readCache() {
|
|
19
|
+
try {
|
|
20
|
+
const raw = JSON.parse(readFileSync(updateCachePath(), 'utf8'));
|
|
21
|
+
return {
|
|
22
|
+
checkedAt: typeof raw.checkedAt === 'number' && Number.isFinite(raw.checkedAt) ? raw.checkedAt : 0,
|
|
23
|
+
latest: typeof raw.latest === 'string' && raw.latest.trim() ? raw.latest.trim() : null
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return { checkedAt: 0, latest: null };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function writeCache(cache) {
|
|
31
|
+
try {
|
|
32
|
+
writeFileSync(updateCachePath(), `${JSON.stringify(cache)}\n`, 'utf8');
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// 缓存写入失败不影响命令
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function parseVersion(value) {
|
|
39
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value.trim());
|
|
40
|
+
if (!match)
|
|
41
|
+
return null;
|
|
42
|
+
return {
|
|
43
|
+
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
44
|
+
pre: match[4] ? match[4].split('.') : []
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function compareVersions(a, b) {
|
|
48
|
+
const pa = parseVersion(a);
|
|
49
|
+
const pb = parseVersion(b);
|
|
50
|
+
if (!pa || !pb)
|
|
51
|
+
return 0;
|
|
52
|
+
for (let i = 0; i < 3; i++) {
|
|
53
|
+
if (pa.core[i] !== pb.core[i])
|
|
54
|
+
return pa.core[i] > pb.core[i] ? 1 : -1;
|
|
55
|
+
}
|
|
56
|
+
if (pa.pre.length === 0 || pb.pre.length === 0) {
|
|
57
|
+
if (pa.pre.length === pb.pre.length)
|
|
58
|
+
return 0;
|
|
59
|
+
return pa.pre.length === 0 ? 1 : -1;
|
|
60
|
+
}
|
|
61
|
+
const length = Math.max(pa.pre.length, pb.pre.length);
|
|
62
|
+
for (let i = 0; i < length; i++) {
|
|
63
|
+
const x = pa.pre[i];
|
|
64
|
+
const y = pb.pre[i];
|
|
65
|
+
if (x === undefined)
|
|
66
|
+
return -1;
|
|
67
|
+
if (y === undefined)
|
|
68
|
+
return 1;
|
|
69
|
+
const nx = /^\d+$/.test(x) ? Number(x) : null;
|
|
70
|
+
const ny = /^\d+$/.test(y) ? Number(y) : null;
|
|
71
|
+
if (nx !== null && ny !== null) {
|
|
72
|
+
if (nx !== ny)
|
|
73
|
+
return nx > ny ? 1 : -1;
|
|
74
|
+
}
|
|
75
|
+
else if (x !== y) {
|
|
76
|
+
if (nx !== null)
|
|
77
|
+
return -1;
|
|
78
|
+
if (ny !== null)
|
|
79
|
+
return 1;
|
|
80
|
+
return x > y ? 1 : -1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
export function isCheckDue(checkedAt, now = Date.now()) {
|
|
86
|
+
return now - checkedAt >= CHECK_INTERVAL_MS;
|
|
87
|
+
}
|
|
88
|
+
export function formatUpdateNotice(current, latest) {
|
|
89
|
+
return `提示:EoSearch CLI 有新版本 ${latest}(当前 ${current})。升级:npm i -g ${PACKAGE_NAME},再执行 eosearch skill install 刷新技能文档(装了视频技能再加 --video)`;
|
|
90
|
+
}
|
|
91
|
+
export async function refreshUpdateCache() {
|
|
92
|
+
const previous = readCache();
|
|
93
|
+
const registry = (process.env.EOSEARCH_UPDATE_REGISTRY || DEFAULT_REGISTRY).replace(/\/+$/, '');
|
|
94
|
+
const controller = new AbortController();
|
|
95
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
96
|
+
let latest = previous.latest;
|
|
97
|
+
try {
|
|
98
|
+
const response = await fetch(`${registry}/${PACKAGE_NAME}/latest`, {
|
|
99
|
+
headers: { accept: 'application/json' },
|
|
100
|
+
signal: controller.signal
|
|
101
|
+
});
|
|
102
|
+
if (response.ok) {
|
|
103
|
+
const body = (await response.json());
|
|
104
|
+
if (typeof body.version === 'string' && body.version.trim())
|
|
105
|
+
latest = body.version.trim();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// 网络失败静默,保留上次结果
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
writeCache({ checkedAt: Date.now(), latest });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function spawnBackgroundRefresh() {
|
|
117
|
+
try {
|
|
118
|
+
const cliPath = fileURLToPath(new URL('./cli.js', import.meta.url));
|
|
119
|
+
const child = spawn(process.execPath, [cliPath, '--update-check'], {
|
|
120
|
+
detached: true,
|
|
121
|
+
stdio: 'ignore',
|
|
122
|
+
windowsHide: true
|
|
123
|
+
});
|
|
124
|
+
child.on('error', () => { });
|
|
125
|
+
child.unref();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// 无法启动后台检查时静默
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export function notifyIfUpdateAvailable(currentVersion, options = {}) {
|
|
132
|
+
if (updateCheckDisabled())
|
|
133
|
+
return;
|
|
134
|
+
const cache = readCache();
|
|
135
|
+
if (!options.quiet && cache.latest && compareVersions(cache.latest, currentVersion) > 0) {
|
|
136
|
+
console.error(formatUpdateNotice(currentVersion, cache.latest));
|
|
137
|
+
}
|
|
138
|
+
if (isCheckDue(cache.checkedAt))
|
|
139
|
+
spawnBackgroundRefresh();
|
|
140
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eogee-tech/eosearch",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "EoSearch command-line client for AI agents — web search, page fetch and deep research via eogee.com (thin client; engines run server-side)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"eosearch": "build-cli/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"build-cli",
|
|
11
|
+
"skill",
|
|
12
|
+
"NOTICE",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"build:cli": "tsc -p tsconfig.cli.json",
|
|
18
|
+
"prepublishOnly": "npm run build:cli",
|
|
19
|
+
"watch": "tsc --watch",
|
|
20
|
+
"start": "node build/index.js",
|
|
21
|
+
"inspector": "npx @modelcontextprotocol/inspector build/index.js",
|
|
22
|
+
"smoke": "node build/eosearch/smoke.js",
|
|
23
|
+
"test": "npm run build && node --test \"test/unit/*.test.mjs\""
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"mcp",
|
|
27
|
+
"cli",
|
|
28
|
+
"search",
|
|
29
|
+
"web-search",
|
|
30
|
+
"agent",
|
|
31
|
+
"rag"
|
|
32
|
+
],
|
|
33
|
+
"license": "Apache-2.0",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.11.2",
|
|
36
|
+
"@mozilla/readability": "^0.6.0",
|
|
37
|
+
"@types/cheerio": "^0.22.35",
|
|
38
|
+
"axios": "^1.16.0",
|
|
39
|
+
"cheerio": "^1.0.0",
|
|
40
|
+
"cors": "^2.8.5",
|
|
41
|
+
"express": "^5.2.1",
|
|
42
|
+
"https-proxy-agent": "^7.0.6",
|
|
43
|
+
"ipaddr.js": "^2.3.0",
|
|
44
|
+
"jsdom": "^26.1.0",
|
|
45
|
+
"koffi": "^2.16.0",
|
|
46
|
+
"request-filtering-agent": "^3.2.0",
|
|
47
|
+
"zod": "^3.25.76"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/cors": "^2.8.19",
|
|
51
|
+
"@types/express": "^5.0.6",
|
|
52
|
+
"@types/jsdom": "^21.1.7",
|
|
53
|
+
"@types/node": "^20.17.10",
|
|
54
|
+
"typescript": "^5.3.3"
|
|
55
|
+
},
|
|
56
|
+
"main": "index.js",
|
|
57
|
+
"directories": {
|
|
58
|
+
"test": "test"
|
|
59
|
+
},
|
|
60
|
+
"repository": {
|
|
61
|
+
"type": "git",
|
|
62
|
+
"url": "git@gitee.com:eogee/eosearch.git"
|
|
63
|
+
},
|
|
64
|
+
"author": ""
|
|
65
|
+
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: eosearch
|
|
3
|
+
description: EoSearch(eosearch)联网搜索服务接入与使用指南。当用户要求联网搜索、查资料、查最新信息、深度调研某个主题,或提到 EoSearch / eosearch 搜索服务时使用本技能完成服务接入与工具调用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# EoSearch 搜索服务接入与使用
|
|
7
|
+
|
|
8
|
+
EoSearch 是面向 AI Agent 的联网搜索服务:意图路由自动选择搜索引擎,多引擎结果融合去重并给出透明评分(score + 排序理由),支持一步到位的深度检索(搜索 + 正文抓取 + token 预算控制)。
|
|
9
|
+
|
|
10
|
+
本技能即 EoSearch 能力的开通入口:安装本技能并完成一次配置(填入API密钥)后,Agent 即具备完整的联网搜索能力。服务提供两种接口面:CLI 命令行客户端(推荐,`eosearch` 命令)与 MCP(Streamable HTTP),底层为同一个服务端、共用同一份配额,按场景任选其一即可。
|
|
11
|
+
|
|
12
|
+
## 第一步:准备接入信息
|
|
13
|
+
|
|
14
|
+
| 项目 | 说明 |
|
|
15
|
+
|------|------|
|
|
16
|
+
| 服务地址 | `https://eogee.com/search/v1/mcp`(以服务公告为准) |
|
|
17
|
+
| API Key | API密钥,在 eogee.com 个人中心获取 |
|
|
18
|
+
| 接入方式 | CLI 命令行(推荐)或 MCP 客户端 |
|
|
19
|
+
|
|
20
|
+
## 第二步:完成接入(CLI 优先,MCP 备选)
|
|
21
|
+
|
|
22
|
+
若用户尚未提供密钥,先请其到 eogee.com 个人中心「API密钥」申请,把密钥发给你后再完成配置。注册名统一使用小写 `eosearch`,产品展示名写作 EoSearch。
|
|
23
|
+
|
|
24
|
+
### 方式一:CLI 命令行客户端(推荐)
|
|
25
|
+
|
|
26
|
+
适合所有能用 Shell 的环境(WorkBuddy、ZCode、Codex、CI、一次性沙箱等),不依赖任何 MCP 客户端配置,装完即用。
|
|
27
|
+
|
|
28
|
+
1. **安装**:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g @eogee-tech/eosearch
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
无法全局安装时可用免安装形式 `npx @eogee-tech/eosearch <子命令>`(每次调用有解包延迟,多轮使用建议全局安装)。
|
|
35
|
+
|
|
36
|
+
2. **技能落位**(推荐,让支持技能发现的客户端在会话中自动触发本指引):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
eosearch skill install
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
把随包分发的技能文档复制到客户端技能目录:默认 `~/.zcode/skills/eosearch/`(ZCode 用户级技能目录),其他客户端用 `--dir <path>` 指定其技能目录;加 `--video` 同时安装 eosearch-video 视频技能。重复执行即把文档升级为与 CLI 同版本。若你正在阅读本文档,说明技能已就位,此步可跳过。
|
|
43
|
+
|
|
44
|
+
3. **配置密钥**(二选一,环境变量优先):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# bash / zsh
|
|
48
|
+
export EOSEARCH_API_KEY=你的API密钥
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```powershell
|
|
52
|
+
# Windows PowerShell(当前会话)
|
|
53
|
+
$env:EOSEARCH_API_KEY = "你的API密钥"
|
|
54
|
+
# 永久生效
|
|
55
|
+
setx EOSEARCH_API_KEY "你的API密钥"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
或在用户目录建配置文件 `~/.eosearchrc`:`{"apiKey":"你的API密钥"}`。密钥只从环境变量或该配置文件读取,CLI 不提供密钥的命令行参数入口,避免密钥进入 Shell 历史与进程列表。
|
|
59
|
+
|
|
60
|
+
4. **自检**:`eosearch whoami`,输出"密钥有效,服务连接正常"即接入完成;401 回到上一步核对密钥。
|
|
61
|
+
|
|
62
|
+
### 方式二:MCP 客户端配置
|
|
63
|
+
|
|
64
|
+
请求头 `X-API-Key` 用于身份认证。把下方示例中的「你的API密钥」替换为真实密钥即可。以下方式任选其一。
|
|
65
|
+
|
|
66
|
+
#### ZCode / Claude Code(命令行注册)
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
claude mcp add --transport http eosearch https://eogee.com/search/v1/mcp --header "X-API-Key: 你的API密钥"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### 通用 JSON 配置(Cherry Studio、Cursor、VS Code 等)
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"mcpServers": {
|
|
77
|
+
"eosearch": {
|
|
78
|
+
"type": "http",
|
|
79
|
+
"url": "https://eogee.com/search/v1/mcp",
|
|
80
|
+
"headers": {
|
|
81
|
+
"X-API-Key": "你的API密钥"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
配置完成后重启客户端,确认 `eosearch_search`、`eosearch_batch_search`、`eosearch_fetch`、`eosearch_deep_search` 四个工具已出现。
|
|
89
|
+
|
|
90
|
+
## 第三步:自检(配置完成后立即执行)
|
|
91
|
+
|
|
92
|
+
1. **CLI 接入自检**:`eosearch whoami` 返回"密钥有效"即完成(退出码 0)。
|
|
93
|
+
2. **MCP 接入自检**:确认客户端工具列表已出现四个 `eosearch_*` 工具;或做握手检查(验证服务可达、密钥有效):
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
curl -s -o /dev/null -w "%{http_code}" https://eogee.com/search/v1/mcp \
|
|
97
|
+
-H "Content-Type: application/json" \
|
|
98
|
+
-H "Accept: application/json, text/event-stream" \
|
|
99
|
+
-H "X-API-Key: 你的API密钥" \
|
|
100
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"selfcheck","version":"0.0.0"}}}'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
返回 `200` 即密钥有效、服务正常;`401` 表示密钥无效或请求头拼写有误,回到第二步核对;超时或 5xx 稍后重试。
|
|
104
|
+
|
|
105
|
+
3. **实调检查**:CLI 跑 `eosearch search "hello" --limit 3`,或 MCP 实际调用一次 `eosearch_search`(如 `query: "hello"`),返回带 `score` 的结果列表即接入完成。
|
|
106
|
+
|
|
107
|
+
## 第四步:选择正确的调用方式与工具
|
|
108
|
+
|
|
109
|
+
CLI 接入时按场景直接执行命令(均支持 `--json` 获取机器可读输出):
|
|
110
|
+
|
|
111
|
+
| 场景 | CLI 命令 | 说明 |
|
|
112
|
+
|------|---------|------|
|
|
113
|
+
| 找资料、找来源 | `eosearch search "<关键词>"` | 加 `--freshness week` 做时效过滤、`--limit 5` 控制条数 |
|
|
114
|
+
| 多个独立查询 | `eosearch batch "<q1>" "<q2>"` | 一次并行至多 5 条,配额计 1 次 |
|
|
115
|
+
| 已有网址,读正文 | `eosearch fetch <url>` | 自动适配 CSDN/掘金/知乎/GitHub,`--json` 时输出 `{url, content}` |
|
|
116
|
+
| 要可直接引用的调研结论 | `eosearch deep "<主题>"` | `--top-n` 控制读源数量,任务慢可加 `--timeout 300` |
|
|
117
|
+
| 验证密钥/服务状态 | `eosearch whoami` | 握手不计配额 |
|
|
118
|
+
|
|
119
|
+
CLI 退出码约定:0 成功;1 业务空结果;2 用法错误(参数写错,读 stderr 修正);3 鉴权失败(核对密钥);4 配额限流(稍后重试);5 服务不可用或执行失败。报错固定三段式(错误/原因/下一步),按"下一步"提示修正后重试即可。
|
|
120
|
+
|
|
121
|
+
MCP 接入时按场景选择工具:
|
|
122
|
+
|
|
123
|
+
| 场景 | 使用工具 | 说明 |
|
|
124
|
+
|------|---------|------|
|
|
125
|
+
| 找资料、找来源、看大家都怎么说 | `eosearch_search` | 快,返回链接列表带评分 |
|
|
126
|
+
| 多个独立查询(多意图调研、核查多个说法、横向对比) | `eosearch_batch_search` | 一次调用并行跑多条搜索(至多 5 条),省轮次省 token,配额只计 1 次 |
|
|
127
|
+
| 已有具体网址,要读正文 | `eosearch_fetch` | 自动适配 CSDN/掘金/知乎/GitHub,返回清洗后的正文 |
|
|
128
|
+
| 要直接可用的调研结论/上下文 | `eosearch_deep_search` | 一步完成搜索、选源、抓正文、按预算输出摘录 |
|
|
129
|
+
| 找视频源(后续下载到本机) | `eosearch_search` | "视频"类查询自动走视频意图路由;已验证平台的视频页结果带 `isVideoPage`/`videoPlatform` 标记 |
|
|
130
|
+
|
|
131
|
+
调用原则:
|
|
132
|
+
|
|
133
|
+
1. 需要背景知识或结论时**优先用 `eosearch_deep_search`**,一次调用胜过多次 search + fetch 往返,省时间也省 token。
|
|
134
|
+
2. 有多个独立查询时**合并成一次 `eosearch_batch_search`**(如 `queries: ["量子计算现状", "IBM 股价", "相关ETF"]`),不要拆成多次 search。
|
|
135
|
+
3. 只要链接列表时用 `eosearch_search` 或 `eosearch_batch_search`,配 `format: "markdown"` 更省 token。
|
|
136
|
+
4. 关注时效(新闻、版本发布、近期讨论)时带 `freshness` 参数(`day`/`week`/`month`/`year`);需要限定或排除某些站点时用 `includeDomains` / `excludeDomains`(如 `excludeDomains: ["csdn.net"]`)。
|
|
137
|
+
5. 从 search 结果里挑目标后,用 `eosearch_fetch` 读具体页面。
|
|
138
|
+
|
|
139
|
+
## 保持更新
|
|
140
|
+
|
|
141
|
+
CLI 每次启动会在后台最多每日检查一次 npm 新版本(缓存 `~/.eosearch-update.json`),有更新时在 stderr 提示。看到提示后执行:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npm i -g @eogee-tech/eosearch
|
|
145
|
+
eosearch skill install # 刷新主技能文档;装了视频技能再加 --video
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`CI` 或 `NO_UPDATE_NOTIFIER=1` 环境不检查,`--json` 调用不提示;技能文档无需自己再写版本检查。
|
|
149
|
+
|
|
150
|
+
## 结果解读
|
|
151
|
+
|
|
152
|
+
### search 结果字段
|
|
153
|
+
|
|
154
|
+
- `score`:0-100 的综合质量分,排序依据,分数越高越值得先读。
|
|
155
|
+
- `engines`:哪些搜索引擎返回了这条结果。多个引擎同时返回(如 `["csdn", "bing"]`)说明该来源可信度较高。
|
|
156
|
+
- `reasons`:人类可读的排序理由,例如:
|
|
157
|
+
- `#1 on csdn` — 在 csdn 引擎中排名第一
|
|
158
|
+
- `also found by bing` — 同时被 bing 收录(多引擎佐证)
|
|
159
|
+
- `title matches 75% of query terms` — 标题与查询词高度相关
|
|
160
|
+
- `download-page penalty` — 下载站页面被降权(谨慎参考)
|
|
161
|
+
|
|
162
|
+
### deep_search 返回
|
|
163
|
+
|
|
164
|
+
`digest` 是按 token 预算组装好的检索摘录,可直接作为回答依据;`sources` 列出实际读取的来源与得分。引用时注明来源 URL。需要可追溯的结构化信源清单(url/score/adapter/字符数/是否截断)时,带 `format: "json"` 调用,返回体中包含完整 `sources` 数组。
|
|
165
|
+
|
|
166
|
+
## 委派抓取:服务端抓不到时的本地接力
|
|
167
|
+
|
|
168
|
+
部分页面(反爬拦截、需 JS 渲染、非文本内容)服务端无法直接提取正文:`eosearch_fetch` 会返回带 `[委派]` 标记的接力指引,`eosearch_deep_search` 会在摘录末尾标注失败来源及原因。注意:JS 渲染页可能返回成功但只有壳内容(一两百字的公司简介/导航壳,不含目标正文;实测例 stepfun.com/news 资讯页只回简介壳),此时不出现 `[委派]` 标记——`Chars` 异常少且与页面主题不符时,同样按下方接力流程转本机处理,勿把壳内容当正文。收到上述任一信号时,按以下顺序在本机完成读取,再把内容并入原任务:
|
|
169
|
+
|
|
170
|
+
1. **浏览器直读(首选)**:用本地的 Playwright MCP 打开该 URL,渲染成功则直接提取正文,多数页面到此即可;
|
|
171
|
+
2. **PDF 接力**:版面复杂(多栏/表格/论文)时,先滚动到底触发懒加载,再用 Playwright MCP 将页面保存为 PDF,交给 MinerU 免 Key 极速版解析为 Markdown(`mineru-open-api flash-extract <文件>`,未安装先按下方说明安装;单文件 ≤10MB 且 ≤20 页;PDF 为空或过短则改回第 1 步);
|
|
172
|
+
3. **兜底**:只有截图能力时用你的视觉能力读取截图;超过 Flash 限制或需登录的页面,告知用户无法自动读取,请其手动阅读或提供内容。
|
|
173
|
+
|
|
174
|
+
Playwright MCP 未安装时先征得用户同意再装(需 Node.js 18+):客户端 MCP 配置写入 `{"mcpServers":{"playwright":{"command":"npx","args":["@playwright/mcp@latest","--caps=pdf"]}}}`,其中 `--caps=pdf` 用于第 2 步保存 PDF;说明见 https://github.com/microsoft/playwright-mcp。装完重启客户端,工具列表能调到 `browser_navigate` 后,再从第 1 步继续。
|
|
175
|
+
|
|
176
|
+
非文本内容(PDF/图片)不必开浏览器:直接下载文件走 MinerU Flash(免 Key,`mineru-open-api flash-extract <文件>`),图片直接用视觉能力读取。涉及登录态的内容由你在本机读取,动作对用户可见可控。
|
|
177
|
+
|
|
178
|
+
MinerU CLI 未安装时先装再解析:`npm install -g mineru-open-api`(需 Node.js 16+),装完用 `mineru-open-api --version` 自检,能输出版本即就绪;无法全局安装时用 `npx mineru-open-api flash-extract <文件>` 免安装运行;仍不可用时告知用户该文档无法自动解析。
|
|
179
|
+
|
|
180
|
+
## 视频搜索与下载
|
|
181
|
+
|
|
182
|
+
需要"搜视频并下载到本机"时,EoSearch 负责发现、用户本机负责下载,两段配合:
|
|
183
|
+
|
|
184
|
+
1. **发现**:用 `eosearch_search` 搜视频主题(如「XX 教学视频」),视频类查询自动优先 B站垂直引擎(直连站内搜索,返回真实 `/video/` 页链接,描述含 UP主/时长/播放数)。站点定向检索(如"去B站搜")传 `includeDomains: ["bilibili.com"]`,已支持站点自动接入站内引擎。结果中带 `isVideoPage: true` 与 `videoPlatform`(如 `bilibili`、`douyin`)标记的,是已验证下载配方的视频页,优先选择;百度/搜狗的跳转链接已由服务端解包为真实地址,标记不失明。
|
|
185
|
+
2. **下载**:交给「eosearch-video」技能完成——在用户本机用 Playwright MCP 打开视频页拦截流地址,ffmpeg 合成 mp4。下载是点对点直连:视频字节从平台 CDN 直接进用户本机,不经过 EoSearch 服务器,不占服务带宽,也不消耗配额(配额只计工具调用)。
|
|
186
|
+
|
|
187
|
+
「eosearch-video」技能的获取方式:执行 `eosearch skill install --video`(推荐,随 npm 包分发,直接落位到客户端技能目录);或下载 [eosearch-video 技能包](https://eogee.com/downloads/eosearch-video-skill.zip),解压后把 `eosearch-video` 文件夹整体放入客户端技能目录(如 ZCode 的 `.zcode/skills/eosearch-video/`)。技能内含环境检查(ffmpeg 与 Playwright MCP 检测)、缺失时的安装指引与降级方案,未装齐前置条件时不会硬跑。
|
|
188
|
+
|
|
189
|
+
## 常见问题
|
|
190
|
+
|
|
191
|
+
| 现象 | 原因与处理 |
|
|
192
|
+
|------|-----------|
|
|
193
|
+
| `eosearch: command not found` | CLI 未安装或 npm 全局 bin 不在 PATH:重跑 `npm install -g @eogee-tech/eosearch`,或改用 `npx @eogee-tech/eosearch` |
|
|
194
|
+
| 技能文档缺失或版本落后 | 执行 `eosearch skill install` 重新落位(随 CLI 升级保持文档同版本) |
|
|
195
|
+
| CLI 报"未找到 EoSearch API 密钥" | 设置 `EOSEARCH_API_KEY` 环境变量,或在 `~/.eosearchrc` 写入 `{"apiKey":"..."}` |
|
|
196
|
+
| 401 Invalid membership API key | 密钥无效或已过期,到 eogee.com 个人中心核对;确认请求头为 `X-API-Key` |
|
|
197
|
+
| 401 This service requires a membership API key | 请求没带 Key,检查客户端配置的 headers |
|
|
198
|
+
| 429 Daily quota exhausted | 当日配额用完,次日 00:00 UTC 重置;升级会员可提升配额 |
|
|
199
|
+
| 429 Rate limit exceeded | 请求过快,等待数秒后重试 |
|
|
200
|
+
| 搜索结果为空或部分引擎失败 | 服务会自动熔断故障引擎并换道重试;可带 `engines` 参数显式指定其他引擎重试一次 |
|
|
201
|
+
| 返回 `[委派]` 或"无法获取正文" | 页面反爬/需渲染/非文本,按「委派抓取」一节的本地接力流程完成读取 |
|
|
202
|
+
| 只想在某个站内搜(如去B站) | `includeDomains: ["bilibili.com"]`:B站站内引擎自动接入并返回站内直链;未接入站内引擎的站点只有通用结果的后置过滤,产出可能偏少 |
|
|
203
|
+
| Bing 结果异常 | 带 `searchMode: "playwright"` 参数重试一次 |
|
|
204
|
+
|
|
205
|
+
## 配额说明
|
|
206
|
+
|
|
207
|
+
| 档位 | 每日配额 | 每分钟限速 |
|
|
208
|
+
|------|---------|-----------|
|
|
209
|
+
| 极会员(会员 Key) | 不限次数 | 60 次 |
|
|
210
|
+
| 普通用户 Key | 100 次 | 10 次 |
|
|
211
|
+
|
|
212
|
+
服务不提供匿名通道:未携带有效 Key 的请求一律返回 401。
|
|
213
|
+
|
|
214
|
+
工具调用才计费;连接握手与工具列表查询不消耗配额。响应头 `X-Quota-Remaining-Today` 指示当日剩余额度(极会员显示 unlimited)。
|