@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
package/build-cli/cli.js
ADDED
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cli/cli.ts — EoSearch 命令行客户端入口(npm 包 @eogee-tech/eosearch)
|
|
3
|
+
// 薄客户端:本地只做参数解析与输出格式化,搜索与抓取全部在 eogee.com 服务端执行
|
|
4
|
+
// 退出码:0 成功;1 业务空结果;2 用法错误;3 鉴权失败;4 配额限流;5 服务不可用/执行失败
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync } from 'node:fs';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { join, resolve as resolvePath } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { resolveConfig, configFilePath, DEFAULT_SERVER_URL } from './config.js';
|
|
11
|
+
import { callTool, whoami } from './client.js';
|
|
12
|
+
import { notifyIfUpdateAvailable, refreshUpdateCache } from './updateCheck.js';
|
|
13
|
+
function readVersion() {
|
|
14
|
+
try {
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
return require('../package.json').version ?? '0.0.0';
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return '0.0.0';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// ---------- 用法文本 ----------
|
|
23
|
+
const MAIN_HELP = `eosearch — EoSearch 联网搜索命令行客户端(v${readVersion()})
|
|
24
|
+
|
|
25
|
+
本地只做参数解析与结果展示,搜索与抓取在服务端执行;需要 API 密钥。
|
|
26
|
+
|
|
27
|
+
用法: eosearch <子命令> [参数] [选项]
|
|
28
|
+
|
|
29
|
+
子命令:
|
|
30
|
+
search <query> 联网搜索:意图路由 + 多引擎融合评分
|
|
31
|
+
batch <q1> <q2> ... 并行执行 1-5 条独立搜索(配额计 1 次)
|
|
32
|
+
fetch <url> 抓取网页正文(自动适配 CSDN/掘金/知乎/GitHub)
|
|
33
|
+
deep <query> 深度调研:搜索 + 选源 + 抓正文 + 摘录
|
|
34
|
+
whoami 验证密钥与服务连接
|
|
35
|
+
skill install 安装技能文档到客户端技能目录(无需密钥)
|
|
36
|
+
help [子命令] 显示帮助
|
|
37
|
+
|
|
38
|
+
通用选项:
|
|
39
|
+
--json 机器可读 JSON 输出(stdout,字段与 MCP 工具一致)
|
|
40
|
+
--server-url <url> 覆盖服务地址(默认 ${DEFAULT_SERVER_URL})
|
|
41
|
+
--timeout <秒> 单次调用超时(默认 120)
|
|
42
|
+
--version, -V 显示 CLI 版本
|
|
43
|
+
--help, -h 显示帮助
|
|
44
|
+
|
|
45
|
+
密钥配置(环境变量优先,二选一):
|
|
46
|
+
环境变量 EOSEARCH_API_KEY=<你的API密钥>
|
|
47
|
+
配置文件 ${configFilePath()} 写入 {"apiKey":"你的API密钥","serverUrl":"可选服务地址"}
|
|
48
|
+
|
|
49
|
+
安全约定:密钥只从环境变量或配置文件读取,不提供命令行参数入口,避免进入 Shell 历史与进程列表。
|
|
50
|
+
|
|
51
|
+
更新提示:每日最多后台检查一次 npm 新版本,有更新时在 stderr 提示;
|
|
52
|
+
升级后执行 eosearch skill install 刷新技能文档;CI 或 NO_UPDATE_NOTIFIER=1 时关闭。
|
|
53
|
+
|
|
54
|
+
示例:
|
|
55
|
+
eosearch search "llama.cpp 新版本" --json --limit 5
|
|
56
|
+
eosearch deep "2026 国产大模型对比" --top-n 3 --timeout 300
|
|
57
|
+
eosearch batch "量子计算现状" "相关ETF" --json
|
|
58
|
+
eosearch fetch https://github.com/ggml-org/llama.cpp
|
|
59
|
+
eosearch whoami`;
|
|
60
|
+
function subcommandHelp(command) {
|
|
61
|
+
const shared = `通用选项: --json 机器可读输出 | --server-url <url> 覆盖服务地址 | --timeout <秒> 单次超时(默认 120)
|
|
62
|
+
|
|
63
|
+
密钥: 环境变量 EOSEARCH_API_KEY,或配置文件 ${configFilePath()} 的 {"apiKey":"..."} 字段`;
|
|
64
|
+
switch (command) {
|
|
65
|
+
case 'search':
|
|
66
|
+
return `eosearch search — 联网搜索:意图路由 + 多引擎融合评分
|
|
67
|
+
|
|
68
|
+
用法: eosearch search <query> [选项]
|
|
69
|
+
|
|
70
|
+
选项:
|
|
71
|
+
--limit <1-50> 返回条数上限(默认 10)
|
|
72
|
+
--engines <a,b,...> 指定引擎列表,绕过意图路由(如 bing,baidu,bilibili)
|
|
73
|
+
--zone <auto|cn|intl> 路由偏好:cn 偏中文引擎,intl 偏国际引擎(默认 auto)
|
|
74
|
+
--search-mode <模式> request|auto|playwright(Bing 异常时用 playwright 重试)
|
|
75
|
+
--freshness <窗口> day|week|month|year,只保留该时间窗内发布的结果
|
|
76
|
+
--include-domains <a,b> 只保留这些域名(含子域)的结果
|
|
77
|
+
--exclude-domains <a,b> 丢弃这些域名(含子域)的结果
|
|
78
|
+
--json 输出服务端 JSON(字段含 score/engines/reasons)
|
|
79
|
+
|
|
80
|
+
${shared}
|
|
81
|
+
|
|
82
|
+
示例: eosearch search "llama.cpp 新版本" --freshness month --json --limit 5`;
|
|
83
|
+
case 'batch':
|
|
84
|
+
return `eosearch batch — 并行执行 1-5 条独立搜索,配额计 1 次
|
|
85
|
+
|
|
86
|
+
用法: eosearch batch <query1> <query2> ... [选项]
|
|
87
|
+
|
|
88
|
+
选项:
|
|
89
|
+
--zone <auto|cn|intl> 共享路由偏好,应用到未单独指定的查询
|
|
90
|
+
--json 输出服务端 JSON(results 数组 + failures 数组)
|
|
91
|
+
|
|
92
|
+
${shared}
|
|
93
|
+
|
|
94
|
+
示例: eosearch batch "量子计算现状" "相关ETF" "IBM 股价" --json`;
|
|
95
|
+
case 'fetch':
|
|
96
|
+
return `eosearch fetch — 抓取网页并返回清洗后的正文
|
|
97
|
+
|
|
98
|
+
用法: eosearch fetch <url> [选项]
|
|
99
|
+
|
|
100
|
+
选项:
|
|
101
|
+
--max-chars <500-200000> 返回正文长度上限(默认 20000)
|
|
102
|
+
--no-readability 关闭正文主内容抽取(默认开启)
|
|
103
|
+
--render-mode <模式> request|auto|browser(auto 需服务端装有 Playwright)
|
|
104
|
+
--json 输出 {"url":"...","content":"..."} 结构
|
|
105
|
+
|
|
106
|
+
${shared}
|
|
107
|
+
|
|
108
|
+
示例: eosearch fetch https://github.com/ggml-org/llama.cpp --max-chars 8000`;
|
|
109
|
+
case 'deep':
|
|
110
|
+
return `eosearch deep — 深度调研:搜索、选源、抓正文、按 token 预算输出摘录
|
|
111
|
+
|
|
112
|
+
用法: eosearch deep <query> [选项]
|
|
113
|
+
|
|
114
|
+
选项:
|
|
115
|
+
--top-n <1-10> 实际读取的来源数(默认 5)
|
|
116
|
+
--token-budget <数值> 摘录 token 预算,2000-120000(默认 24000)
|
|
117
|
+
--engines <a,b,...> 指定引擎列表
|
|
118
|
+
--zone <auto|cn|intl> 路由偏好
|
|
119
|
+
--freshness <窗口> day|week|month|year
|
|
120
|
+
--include-domains <a,b> 只保留这些域名
|
|
121
|
+
--exclude-domains <a,b> 丢弃这些域名
|
|
122
|
+
--json 额外返回结构化 sources 数组(url/score/adapter)
|
|
123
|
+
|
|
124
|
+
${shared}
|
|
125
|
+
|
|
126
|
+
示例: eosearch deep "2026 国产大模型对比" --top-n 3 --timeout 300`;
|
|
127
|
+
case 'whoami':
|
|
128
|
+
return `eosearch whoami — 验证密钥与服务连接(握手不计配额)
|
|
129
|
+
|
|
130
|
+
用法: eosearch whoami [选项]
|
|
131
|
+
|
|
132
|
+
输出密钥有效性、服务端名称与版本、密钥来源。
|
|
133
|
+
退出码: 0 密钥有效;3 密钥被拒;4 限流;5 服务不可达。
|
|
134
|
+
|
|
135
|
+
${shared}
|
|
136
|
+
|
|
137
|
+
示例: eosearch whoami`;
|
|
138
|
+
case 'skill':
|
|
139
|
+
return `eosearch skill — 把随包分发的技能文档安装到客户端技能目录(纯本地操作,无需密钥)
|
|
140
|
+
|
|
141
|
+
用法: eosearch skill install [选项]
|
|
142
|
+
|
|
143
|
+
动作:
|
|
144
|
+
install 复制技能文档 SKILL.md 到 <技能根目录>/eosearch/SKILL.md
|
|
145
|
+
|
|
146
|
+
选项:
|
|
147
|
+
--dir <path> 技能根目录(默认 ~/.zcode/skills,即 ZCode 用户级技能目录)
|
|
148
|
+
--video 同时安装 eosearch-video 视频技能(整目录复制)
|
|
149
|
+
|
|
150
|
+
说明:
|
|
151
|
+
重复执行即把技能文档升级为当前 CLI 同版本(覆盖旧版);重启客户端会话后生效。
|
|
152
|
+
其他客户端(Codex 等)把 --dir 指向其技能目录即可。
|
|
153
|
+
|
|
154
|
+
示例: eosearch skill install --video`;
|
|
155
|
+
default:
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const SPECS = {
|
|
160
|
+
search: {
|
|
161
|
+
valueFlags: ['limit', 'engines', 'zone', 'search-mode', 'freshness', 'include-domains', 'exclude-domains', 'server-url', 'timeout'],
|
|
162
|
+
boolFlags: ['json'],
|
|
163
|
+
minPos: 1,
|
|
164
|
+
maxPos: 1
|
|
165
|
+
},
|
|
166
|
+
batch: {
|
|
167
|
+
valueFlags: ['zone', 'server-url', 'timeout'],
|
|
168
|
+
boolFlags: ['json'],
|
|
169
|
+
minPos: 1,
|
|
170
|
+
maxPos: 5
|
|
171
|
+
},
|
|
172
|
+
fetch: {
|
|
173
|
+
valueFlags: ['max-chars', 'render-mode', 'server-url', 'timeout'],
|
|
174
|
+
boolFlags: ['json', 'no-readability'],
|
|
175
|
+
minPos: 1,
|
|
176
|
+
maxPos: 1
|
|
177
|
+
},
|
|
178
|
+
deep: {
|
|
179
|
+
valueFlags: ['top-n', 'token-budget', 'engines', 'zone', 'freshness', 'include-domains', 'exclude-domains', 'server-url', 'timeout'],
|
|
180
|
+
boolFlags: ['json'],
|
|
181
|
+
minPos: 1,
|
|
182
|
+
maxPos: 1
|
|
183
|
+
},
|
|
184
|
+
whoami: {
|
|
185
|
+
valueFlags: ['server-url', 'timeout'],
|
|
186
|
+
boolFlags: ['json'],
|
|
187
|
+
minPos: 0,
|
|
188
|
+
maxPos: 0
|
|
189
|
+
},
|
|
190
|
+
skill: {
|
|
191
|
+
valueFlags: ['dir'],
|
|
192
|
+
boolFlags: ['video'],
|
|
193
|
+
minPos: 0,
|
|
194
|
+
maxPos: 1
|
|
195
|
+
},
|
|
196
|
+
help: {
|
|
197
|
+
valueFlags: [],
|
|
198
|
+
boolFlags: [],
|
|
199
|
+
minPos: 0,
|
|
200
|
+
maxPos: 1
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
class UsageError extends Error {
|
|
204
|
+
}
|
|
205
|
+
function parseArgs(argv) {
|
|
206
|
+
if (argv.length === 0)
|
|
207
|
+
return { kind: 'ok', command: 'help', positional: [], flags: new Map() };
|
|
208
|
+
const first = argv[0];
|
|
209
|
+
if (first === 'help' || first === '--help' || first === '-h') {
|
|
210
|
+
return { kind: 'ok', command: 'help', positional: argv.slice(1), flags: new Map() };
|
|
211
|
+
}
|
|
212
|
+
if (first.startsWith('-')) {
|
|
213
|
+
return { kind: 'usage', message: `未知参数 "${first}"。先写子命令,如 eosearch search <query>;eosearch help 查看全部子命令` };
|
|
214
|
+
}
|
|
215
|
+
if (!(first in SPECS)) {
|
|
216
|
+
return { kind: 'usage', message: `未知子命令 "${first}"。eosearch help 查看全部子命令` };
|
|
217
|
+
}
|
|
218
|
+
const command = first;
|
|
219
|
+
const spec = SPECS[command];
|
|
220
|
+
const rest = argv.slice(1);
|
|
221
|
+
const flags = new Map();
|
|
222
|
+
const positional = [];
|
|
223
|
+
for (let i = 0; i < rest.length; i++) {
|
|
224
|
+
const tok = rest[i];
|
|
225
|
+
if (tok === '--') {
|
|
226
|
+
positional.push(...rest.slice(i + 1));
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
if (tok.startsWith('--')) {
|
|
230
|
+
const eq = tok.indexOf('=');
|
|
231
|
+
if (eq >= 0) {
|
|
232
|
+
flags.set(tok.slice(2, eq), tok.slice(eq + 1));
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const key = tok.slice(2);
|
|
236
|
+
const next = rest[i + 1];
|
|
237
|
+
if (spec.valueFlags.includes(key) && next !== undefined && !next.startsWith('--')) {
|
|
238
|
+
flags.set(key, next);
|
|
239
|
+
i++;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
flags.set(key, true);
|
|
243
|
+
}
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (tok.startsWith('-') && tok.length === 2) {
|
|
247
|
+
flags.set(tok.slice(1), true);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
positional.push(tok);
|
|
251
|
+
}
|
|
252
|
+
for (const key of flags.keys()) {
|
|
253
|
+
if (!spec.valueFlags.includes(key) && !spec.boolFlags.includes(key) && !['help', 'h', 'version', 'V'].includes(key)) {
|
|
254
|
+
return {
|
|
255
|
+
kind: 'usage',
|
|
256
|
+
message: `子命令 ${command} 不支持选项 --${key}。eosearch ${command} --help 查看支持的选项`
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// 带 --help/-h 时跳过位置参数数量校验,交给主入口显示该子命令的帮助
|
|
261
|
+
if (flags.get('help') === true || flags.get('h') === true) {
|
|
262
|
+
return { kind: 'ok', command, positional, flags };
|
|
263
|
+
}
|
|
264
|
+
if (positional.length < spec.minPos) {
|
|
265
|
+
return { kind: 'usage', message: `${command} 缺少必填参数。eosearch ${command} --help 查看用法` };
|
|
266
|
+
}
|
|
267
|
+
if (positional.length > spec.maxPos) {
|
|
268
|
+
const hint = command === 'batch' ? `batch 一次最多 ${spec.maxPos} 条查询` : `${command} 只接受 ${spec.maxPos} 个位置参数`;
|
|
269
|
+
return { kind: 'usage', message: `${hint},收到 ${positional.length} 个。eosearch ${command} --help 查看用法` };
|
|
270
|
+
}
|
|
271
|
+
return { kind: 'ok', command, positional, flags };
|
|
272
|
+
}
|
|
273
|
+
// ---------- 选项读取与校验 ----------
|
|
274
|
+
function rawFlag(flags, key) {
|
|
275
|
+
const value = flags.get(key);
|
|
276
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
277
|
+
}
|
|
278
|
+
function intFlag(flags, key, min, max) {
|
|
279
|
+
const raw = rawFlag(flags, key);
|
|
280
|
+
if (raw === null)
|
|
281
|
+
return null;
|
|
282
|
+
if (!/^\d+$/.test(raw)) {
|
|
283
|
+
throw new UsageError(`--${key} 需要 ${min}-${max} 的整数,收到 "${raw}"`);
|
|
284
|
+
}
|
|
285
|
+
const n = parseInt(raw, 10);
|
|
286
|
+
if (n < min || n > max) {
|
|
287
|
+
throw new UsageError(`--${key} 需要在 ${min}-${max} 之间,收到 ${n}`);
|
|
288
|
+
}
|
|
289
|
+
return n;
|
|
290
|
+
}
|
|
291
|
+
function enumFlag(flags, key, allowed) {
|
|
292
|
+
const raw = rawFlag(flags, key);
|
|
293
|
+
if (raw === null)
|
|
294
|
+
return null;
|
|
295
|
+
if (!allowed.includes(raw)) {
|
|
296
|
+
throw new UsageError(`--${key} 只支持 ${allowed.join('|')},收到 "${raw}"`);
|
|
297
|
+
}
|
|
298
|
+
return raw;
|
|
299
|
+
}
|
|
300
|
+
function commaList(flags, key) {
|
|
301
|
+
const raw = rawFlag(flags, key);
|
|
302
|
+
if (raw === null)
|
|
303
|
+
return null;
|
|
304
|
+
const list = raw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
305
|
+
if (list.length === 0) {
|
|
306
|
+
throw new UsageError(`--${key} 需要逗号分隔的列表,如 a.com,b.com`);
|
|
307
|
+
}
|
|
308
|
+
return list;
|
|
309
|
+
}
|
|
310
|
+
function timeoutMsOf(flags) {
|
|
311
|
+
const seconds = intFlag(flags, 'timeout', 1, 600);
|
|
312
|
+
return (seconds ?? 120) * 1000;
|
|
313
|
+
}
|
|
314
|
+
// ---------- 输出与退出 ----------
|
|
315
|
+
function emitFailure(f) {
|
|
316
|
+
console.error(`错误:${f.problem}`);
|
|
317
|
+
console.error(`原因:${f.cause}`);
|
|
318
|
+
console.error(`下一步:${f.next}`);
|
|
319
|
+
}
|
|
320
|
+
function emitUsage(message) {
|
|
321
|
+
console.error(`错误:${message}`);
|
|
322
|
+
process.exit(2);
|
|
323
|
+
}
|
|
324
|
+
function missingKeyFailure() {
|
|
325
|
+
return {
|
|
326
|
+
kind: 'failure',
|
|
327
|
+
exitCode: 3,
|
|
328
|
+
problem: '未找到 EoSearch API 密钥',
|
|
329
|
+
cause: `环境变量 EOSEARCH_API_KEY 未设置,配置文件 ${configFilePath()} 中也没有 apiKey 字段`,
|
|
330
|
+
next: '设置环境变量 export EOSEARCH_API_KEY=<你的API密钥>,或在 ~/.eosearchrc 写入 {"apiKey":"..."};密钥在 eogee.com 个人中心获取'
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
async function callAndExit(opts) {
|
|
334
|
+
const outcome = await callTool({
|
|
335
|
+
serverUrl: opts.config.serverUrl,
|
|
336
|
+
apiKey: opts.config.apiKey,
|
|
337
|
+
clientVersion: readVersion(),
|
|
338
|
+
toolName: opts.toolName,
|
|
339
|
+
toolArgs: opts.toolArgs,
|
|
340
|
+
timeoutMs: opts.timeoutMs
|
|
341
|
+
});
|
|
342
|
+
if (outcome.kind === 'failure') {
|
|
343
|
+
emitFailure(outcome);
|
|
344
|
+
process.exit(outcome.exitCode);
|
|
345
|
+
}
|
|
346
|
+
if (opts.json) {
|
|
347
|
+
process.stdout.write(outcome.text + '\n');
|
|
348
|
+
let parsed = null;
|
|
349
|
+
try {
|
|
350
|
+
parsed = JSON.parse(outcome.text);
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
parsed = null;
|
|
354
|
+
}
|
|
355
|
+
if (parsed !== null) {
|
|
356
|
+
const empty = opts.emptyCheck(parsed);
|
|
357
|
+
if (empty) {
|
|
358
|
+
emitFailure(empty);
|
|
359
|
+
process.exit(empty.exitCode);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
process.exit(0);
|
|
363
|
+
}
|
|
364
|
+
process.stdout.write(outcome.text + '\n');
|
|
365
|
+
process.exit(0);
|
|
366
|
+
}
|
|
367
|
+
// ---------- 子命令实现 ----------
|
|
368
|
+
async function runSearch(config, positional, flags) {
|
|
369
|
+
const toolArgs = {
|
|
370
|
+
query: positional[0],
|
|
371
|
+
format: flags.get('json') === true ? 'json' : 'markdown'
|
|
372
|
+
};
|
|
373
|
+
const limit = intFlag(flags, 'limit', 1, 50);
|
|
374
|
+
if (limit !== null)
|
|
375
|
+
toolArgs.limit = limit;
|
|
376
|
+
const engines = commaList(flags, 'engines');
|
|
377
|
+
if (engines)
|
|
378
|
+
toolArgs.engines = engines;
|
|
379
|
+
const zone = enumFlag(flags, 'zone', ['auto', 'cn', 'intl']);
|
|
380
|
+
if (zone)
|
|
381
|
+
toolArgs.zone = zone;
|
|
382
|
+
const searchMode = enumFlag(flags, 'search-mode', ['request', 'auto', 'playwright']);
|
|
383
|
+
if (searchMode)
|
|
384
|
+
toolArgs.searchMode = searchMode;
|
|
385
|
+
const freshness = enumFlag(flags, 'freshness', ['day', 'week', 'month', 'year']);
|
|
386
|
+
if (freshness)
|
|
387
|
+
toolArgs.freshness = freshness;
|
|
388
|
+
const includeDomains = commaList(flags, 'include-domains');
|
|
389
|
+
if (includeDomains)
|
|
390
|
+
toolArgs.includeDomains = includeDomains;
|
|
391
|
+
const excludeDomains = commaList(flags, 'exclude-domains');
|
|
392
|
+
if (excludeDomains)
|
|
393
|
+
toolArgs.excludeDomains = excludeDomains;
|
|
394
|
+
return callAndExit({
|
|
395
|
+
config,
|
|
396
|
+
toolName: 'eosearch_search',
|
|
397
|
+
toolArgs,
|
|
398
|
+
timeoutMs: timeoutMsOf(flags),
|
|
399
|
+
json: flags.get('json') === true,
|
|
400
|
+
emptyCheck: (parsed) => {
|
|
401
|
+
const obj = parsed;
|
|
402
|
+
if (Array.isArray(obj.results) && obj.results.length === 0) {
|
|
403
|
+
return {
|
|
404
|
+
kind: 'failure',
|
|
405
|
+
exitCode: 1,
|
|
406
|
+
problem: '搜索无结果',
|
|
407
|
+
cause: '服务端多引擎融合后返回 0 条结果',
|
|
408
|
+
next: '放宽 --freshness、更换更具体的关键词,或用 --engines 指定引擎重试'
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
async function runBatch(config, positional, flags) {
|
|
416
|
+
const toolArgs = {
|
|
417
|
+
queries: positional,
|
|
418
|
+
format: flags.get('json') === true ? 'json' : 'markdown'
|
|
419
|
+
};
|
|
420
|
+
const zone = enumFlag(flags, 'zone', ['auto', 'cn', 'intl']);
|
|
421
|
+
if (zone)
|
|
422
|
+
toolArgs.zone = zone;
|
|
423
|
+
return callAndExit({
|
|
424
|
+
config,
|
|
425
|
+
toolName: 'eosearch_batch_search',
|
|
426
|
+
toolArgs,
|
|
427
|
+
timeoutMs: timeoutMsOf(flags),
|
|
428
|
+
json: flags.get('json') === true,
|
|
429
|
+
emptyCheck: (parsed) => {
|
|
430
|
+
const obj = parsed;
|
|
431
|
+
if (typeof obj.failed === 'number' && obj.failed > 0 && obj.failed < (obj.totalQueries ?? 0)) {
|
|
432
|
+
console.error(`提示:${obj.failed} 条查询失败,详见输出中 failures 字段`);
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
if (typeof obj.totalQueries === 'number' && typeof obj.failed === 'number' && obj.totalQueries > 0 && obj.failed === obj.totalQueries) {
|
|
436
|
+
return {
|
|
437
|
+
kind: 'failure',
|
|
438
|
+
exitCode: 1,
|
|
439
|
+
problem: `全部 ${obj.totalQueries} 条查询都失败`,
|
|
440
|
+
cause: '服务端执行批量搜索时每条查询都未返回结果',
|
|
441
|
+
next: '检查查询词是否过偏,减少条数或更换关键词后重试'
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
async function runFetch(config, positional, flags) {
|
|
449
|
+
const url = positional[0];
|
|
450
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
451
|
+
emitUsage(`fetch 需要 http(s) 网址,收到 "${url}"`);
|
|
452
|
+
}
|
|
453
|
+
const toolArgs = { url };
|
|
454
|
+
const maxChars = intFlag(flags, 'max-chars', 500, 200000);
|
|
455
|
+
if (maxChars !== null)
|
|
456
|
+
toolArgs.maxChars = maxChars;
|
|
457
|
+
if (flags.get('no-readability') === true)
|
|
458
|
+
toolArgs.readability = false;
|
|
459
|
+
const renderMode = enumFlag(flags, 'render-mode', ['request', 'auto', 'browser']);
|
|
460
|
+
if (renderMode)
|
|
461
|
+
toolArgs.renderMode = renderMode;
|
|
462
|
+
const json = flags.get('json') === true;
|
|
463
|
+
if (!json) {
|
|
464
|
+
return callAndExit({
|
|
465
|
+
config,
|
|
466
|
+
toolName: 'eosearch_fetch',
|
|
467
|
+
toolArgs,
|
|
468
|
+
timeoutMs: timeoutMsOf(flags),
|
|
469
|
+
json: false,
|
|
470
|
+
emptyCheck: () => null
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
// fetch 的 --json:工具返回纯文本(首行元信息 + 正文),包装为结构化输出
|
|
474
|
+
const outcome = await callTool({
|
|
475
|
+
serverUrl: config.serverUrl,
|
|
476
|
+
apiKey: config.apiKey,
|
|
477
|
+
clientVersion: readVersion(),
|
|
478
|
+
toolName: 'eosearch_fetch',
|
|
479
|
+
toolArgs,
|
|
480
|
+
timeoutMs: timeoutMsOf(flags)
|
|
481
|
+
});
|
|
482
|
+
if (outcome.kind === 'failure') {
|
|
483
|
+
emitFailure(outcome);
|
|
484
|
+
process.exit(outcome.exitCode);
|
|
485
|
+
}
|
|
486
|
+
const firstLine = (outcome.text.split('\n', 1)[0] ?? '').trim();
|
|
487
|
+
const match = /^URL: (\S+)$/.exec(firstLine);
|
|
488
|
+
const payload = JSON.stringify({ url: match ? match[1] : url, content: outcome.text }, null, 2);
|
|
489
|
+
process.stdout.write(payload + '\n');
|
|
490
|
+
process.exit(0);
|
|
491
|
+
}
|
|
492
|
+
async function runDeep(config, positional, flags) {
|
|
493
|
+
const toolArgs = {
|
|
494
|
+
query: positional[0],
|
|
495
|
+
format: flags.get('json') === true ? 'json' : 'text'
|
|
496
|
+
};
|
|
497
|
+
const topN = intFlag(flags, 'top-n', 1, 10);
|
|
498
|
+
if (topN !== null)
|
|
499
|
+
toolArgs.topN = topN;
|
|
500
|
+
const tokenBudget = intFlag(flags, 'token-budget', 2000, 120000);
|
|
501
|
+
if (tokenBudget !== null)
|
|
502
|
+
toolArgs.tokenBudget = tokenBudget;
|
|
503
|
+
const engines = commaList(flags, 'engines');
|
|
504
|
+
if (engines)
|
|
505
|
+
toolArgs.engines = engines;
|
|
506
|
+
const zone = enumFlag(flags, 'zone', ['auto', 'cn', 'intl']);
|
|
507
|
+
if (zone)
|
|
508
|
+
toolArgs.zone = zone;
|
|
509
|
+
const freshness = enumFlag(flags, 'freshness', ['day', 'week', 'month', 'year']);
|
|
510
|
+
if (freshness)
|
|
511
|
+
toolArgs.freshness = freshness;
|
|
512
|
+
const includeDomains = commaList(flags, 'include-domains');
|
|
513
|
+
if (includeDomains)
|
|
514
|
+
toolArgs.includeDomains = includeDomains;
|
|
515
|
+
const excludeDomains = commaList(flags, 'exclude-domains');
|
|
516
|
+
if (excludeDomains)
|
|
517
|
+
toolArgs.excludeDomains = excludeDomains;
|
|
518
|
+
return callAndExit({
|
|
519
|
+
config,
|
|
520
|
+
toolName: 'eosearch_deep_search',
|
|
521
|
+
toolArgs,
|
|
522
|
+
timeoutMs: timeoutMsOf(flags),
|
|
523
|
+
json: flags.get('json') === true,
|
|
524
|
+
emptyCheck: () => null
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
async function runWhoami(flags) {
|
|
528
|
+
const config = resolveConfig({ serverUrl: rawFlag(flags, 'server-url') });
|
|
529
|
+
if (!config.apiKey) {
|
|
530
|
+
const f = missingKeyFailure();
|
|
531
|
+
emitFailure(f);
|
|
532
|
+
process.exit(f.exitCode);
|
|
533
|
+
}
|
|
534
|
+
const result = await whoami({
|
|
535
|
+
serverUrl: config.serverUrl,
|
|
536
|
+
apiKey: config.apiKey,
|
|
537
|
+
clientVersion: readVersion()
|
|
538
|
+
});
|
|
539
|
+
if ('kind' in result && result.kind === 'failure') {
|
|
540
|
+
const f = result;
|
|
541
|
+
emitFailure(f);
|
|
542
|
+
process.exit(f.exitCode);
|
|
543
|
+
}
|
|
544
|
+
const info = result;
|
|
545
|
+
if (flags.get('json') === true) {
|
|
546
|
+
process.stdout.write(JSON.stringify({
|
|
547
|
+
ok: true,
|
|
548
|
+
server: { name: info.serverName ?? null, version: info.serverVersion ?? null },
|
|
549
|
+
serverUrl: config.serverUrl,
|
|
550
|
+
apiKeySource: config.apiKeySource,
|
|
551
|
+
instructions: info.instructions ?? null
|
|
552
|
+
}, null, 2) + '\n');
|
|
553
|
+
process.exit(0);
|
|
554
|
+
}
|
|
555
|
+
console.log(`密钥有效,服务连接正常(密钥来自${config.apiKeySource === 'environment' ? '环境变量' : '配置文件 ~/.eosearchrc'})`);
|
|
556
|
+
console.log(`服务端:${info.serverName ?? 'eosearch'} ${info.serverVersion ?? ''}`.trimEnd());
|
|
557
|
+
console.log(`地址:${config.serverUrl}`);
|
|
558
|
+
process.exit(0);
|
|
559
|
+
}
|
|
560
|
+
// ---------- skill 子命令(纯本地操作,无需密钥) ----------
|
|
561
|
+
function skillPackError(src) {
|
|
562
|
+
console.error(`错误:安装包内缺少技能源文件(${src})`);
|
|
563
|
+
console.error('原因:npm 包内容不完整,files 配置异常或包安装损坏');
|
|
564
|
+
console.error('下一步:重装 npm 包(npm install -g @eogee-tech/eosearch)后重试');
|
|
565
|
+
process.exit(5);
|
|
566
|
+
}
|
|
567
|
+
function runSkill(positional, flags) {
|
|
568
|
+
const action = positional[0];
|
|
569
|
+
if (action !== 'install') {
|
|
570
|
+
emitUsage(action === undefined
|
|
571
|
+
? 'skill 缺少动作,用法:eosearch skill install [--dir <path>] [--video]。eosearch skill --help 查看用法'
|
|
572
|
+
: `未知 skill 动作 "${action}",目前只支持 install。eosearch skill --help 查看用法`);
|
|
573
|
+
}
|
|
574
|
+
const targetRoot = resolvePath(rawFlag(flags, 'dir') ?? join(homedir(), '.zcode', 'skills'));
|
|
575
|
+
const installVideo = flags.get('video') === true;
|
|
576
|
+
const version = readVersion();
|
|
577
|
+
const lines = [];
|
|
578
|
+
// 主技能:单文件复制到 <技能根目录>/eosearch/SKILL.md(build-cli 与 skill 同在包根)
|
|
579
|
+
const mainSrc = fileURLToPath(new URL('../skill/SKILL.md', import.meta.url));
|
|
580
|
+
if (!existsSync(mainSrc))
|
|
581
|
+
skillPackError(mainSrc);
|
|
582
|
+
const mainDestDir = join(targetRoot, 'eosearch');
|
|
583
|
+
const mainDest = join(mainDestDir, 'SKILL.md');
|
|
584
|
+
const mainExisted = existsSync(mainDest);
|
|
585
|
+
mkdirSync(mainDestDir, { recursive: true });
|
|
586
|
+
copyFileSync(mainSrc, mainDest);
|
|
587
|
+
lines.push(`${mainDest}${mainExisted ? '(已覆盖旧版)' : ''}`);
|
|
588
|
+
// 视频技能:整目录复制到 <技能根目录>/eosearch-video/
|
|
589
|
+
if (installVideo) {
|
|
590
|
+
const videoSrc = fileURLToPath(new URL('../skill/eosearch-video', import.meta.url));
|
|
591
|
+
if (!existsSync(videoSrc))
|
|
592
|
+
skillPackError(videoSrc);
|
|
593
|
+
const videoDest = join(targetRoot, 'eosearch-video');
|
|
594
|
+
const videoExisted = existsSync(videoDest);
|
|
595
|
+
if (videoExisted && !existsSync(join(videoDest, 'SKILL.md'))) {
|
|
596
|
+
console.error(`错误:目标目录 ${videoDest} 已存在但不像已安装的 eosearch-video 技能`);
|
|
597
|
+
console.error('原因:为避免覆盖非技能内容,安装已中止');
|
|
598
|
+
console.error('下一步:确认该目录可删除后手动移除,再重新执行本命令');
|
|
599
|
+
process.exit(5);
|
|
600
|
+
}
|
|
601
|
+
cpSync(videoSrc, videoDest, { recursive: true });
|
|
602
|
+
if (process.platform !== 'win32') {
|
|
603
|
+
try {
|
|
604
|
+
chmodSync(join(videoDest, 'helpers', 'grab.sh'), 0o755);
|
|
605
|
+
}
|
|
606
|
+
catch { /* 执行权限设置失败不阻塞安装 */ }
|
|
607
|
+
}
|
|
608
|
+
lines.push(`${videoDest}${videoExisted ? '(已覆盖旧版)' : ''}`);
|
|
609
|
+
}
|
|
610
|
+
console.log(`EoSearch 技能安装完成(v${version}):`);
|
|
611
|
+
for (const line of lines)
|
|
612
|
+
console.log(` ${line}`);
|
|
613
|
+
console.log('提示:重启客户端会话后技能生效;目录不对时用 --dir <path> 指定技能根目录。');
|
|
614
|
+
process.exit(0);
|
|
615
|
+
}
|
|
616
|
+
// ---------- 主入口 ----------
|
|
617
|
+
async function main() {
|
|
618
|
+
const argv = process.argv.slice(2);
|
|
619
|
+
// 后台子进程入口:只刷新版本缓存,不执行用户命令
|
|
620
|
+
if (argv[0] === '--update-check') {
|
|
621
|
+
await refreshUpdateCache();
|
|
622
|
+
process.exit(0);
|
|
623
|
+
}
|
|
624
|
+
if (argv.includes('--version') || argv.includes('-V')) {
|
|
625
|
+
console.log(readVersion());
|
|
626
|
+
process.exit(0);
|
|
627
|
+
}
|
|
628
|
+
const parsed = parseArgs(argv);
|
|
629
|
+
if (parsed.kind === 'usage')
|
|
630
|
+
emitUsage(parsed.message);
|
|
631
|
+
const { command, positional, flags } = parsed;
|
|
632
|
+
if (command === 'help' || flags.get('help') === true || flags.get('h') === true) {
|
|
633
|
+
if (command === 'help' && positional.length > 0) {
|
|
634
|
+
const sub = subcommandHelp(positional[0]);
|
|
635
|
+
if (sub === null)
|
|
636
|
+
emitUsage(`未知子命令 "${positional[0]}"。eosearch help 查看全部子命令`);
|
|
637
|
+
console.log(sub);
|
|
638
|
+
}
|
|
639
|
+
else if (command !== 'help') {
|
|
640
|
+
// eosearch <子命令> --help:显示该子命令的帮助
|
|
641
|
+
const sub = subcommandHelp(command);
|
|
642
|
+
if (sub === null)
|
|
643
|
+
emitUsage(`未知子命令 "${command}"。eosearch help 查看全部子命令`);
|
|
644
|
+
console.log(sub);
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
console.log(MAIN_HELP);
|
|
648
|
+
}
|
|
649
|
+
process.exit(0);
|
|
650
|
+
}
|
|
651
|
+
notifyIfUpdateAvailable(readVersion(), { quiet: Boolean(flags.get('json')) });
|
|
652
|
+
const config = resolveConfig({ serverUrl: rawFlag(flags, 'server-url') });
|
|
653
|
+
const needsKey = command !== 'help' && command !== 'skill';
|
|
654
|
+
if (needsKey && !config.apiKey) {
|
|
655
|
+
const f = missingKeyFailure();
|
|
656
|
+
emitFailure(f);
|
|
657
|
+
process.exit(f.exitCode);
|
|
658
|
+
}
|
|
659
|
+
try {
|
|
660
|
+
switch (command) {
|
|
661
|
+
case 'search':
|
|
662
|
+
return await runSearch(config, positional, flags);
|
|
663
|
+
case 'batch':
|
|
664
|
+
return await runBatch(config, positional, flags);
|
|
665
|
+
case 'fetch':
|
|
666
|
+
return await runFetch(config, positional, flags);
|
|
667
|
+
case 'deep':
|
|
668
|
+
return await runDeep(config, positional, flags);
|
|
669
|
+
case 'whoami':
|
|
670
|
+
return await runWhoami(flags);
|
|
671
|
+
case 'skill':
|
|
672
|
+
return runSkill(positional, flags);
|
|
673
|
+
default:
|
|
674
|
+
emitUsage(`未知子命令 "${command}"。eosearch help 查看全部子命令`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
if (error instanceof UsageError)
|
|
679
|
+
emitUsage(error.message);
|
|
680
|
+
console.error('错误:CLI 内部异常');
|
|
681
|
+
console.error(`原因:${error instanceof Error ? error.stack ?? error.message : String(error)}`);
|
|
682
|
+
console.error('下一步:重试一次;持续出现请带上上面的堆栈反馈');
|
|
683
|
+
process.exit(5);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
main().catch((error) => {
|
|
687
|
+
console.error('错误:CLI 未捕获异常');
|
|
688
|
+
console.error(`原因:${error instanceof Error ? error.message : String(error)}`);
|
|
689
|
+
console.error('下一步:重试一次;持续出现请带上该信息反馈');
|
|
690
|
+
process.exit(5);
|
|
691
|
+
});
|