@dsh-plus/web-search-services 0.1.20 → 0.2.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/lib/client.js +1203 -178
- package/lib/index.d.ts +10 -83
- package/lib/index.js +16 -649
- package/package.json +15 -15
- package/src/client/card.tsx +130 -72
- package/src/client/client.ts +19 -7
- package/src/client/draft.ts +29 -89
- package/src/client/i18n.ts +109 -52
- package/src/client/styles.ts +10 -7
- package/src/config.ts +24 -74
- package/src/index.ts +29 -14
- package/scripts/README.md +0 -34
- package/scripts/context7.py +0 -259
- package/scripts/env_loader.py +0 -46
- package/scripts/exa.py +0 -251
- package/scripts/fetch.py +0 -938
- package/scripts/fetch_playwright.py +0 -525
- package/scripts/openai_chat.py +0 -570
- package/scripts/search.py +0 -962
- package/scripts/tavily.py +0 -609
- package/src/args.ts +0 -30
- package/src/env-file.ts +0 -49
- package/src/normalize.ts +0 -173
- package/src/provider.ts +0 -188
- package/src/python.ts +0 -193
- package/src/runner.ts +0 -140
package/scripts/tavily.py
DELETED
|
@@ -1,609 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Tavily REST API helper.
|
|
3
|
-
|
|
4
|
-
不使用 MCP,仅通过 Tavily HTTP API 执行搜索、提取、爬取、映射和研究任务。
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import argparse
|
|
10
|
-
import json
|
|
11
|
-
import os
|
|
12
|
-
import re
|
|
13
|
-
import sys
|
|
14
|
-
import urllib.error
|
|
15
|
-
import urllib.parse
|
|
16
|
-
import urllib.request
|
|
17
|
-
from typing import Any, Dict, Optional
|
|
18
|
-
|
|
19
|
-
from env_loader import load_env_file
|
|
20
|
-
|
|
21
|
-
load_env_file()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class ServiceError(RuntimeError):
|
|
25
|
-
"""服务调用失败。"""
|
|
26
|
-
|
|
27
|
-
def __init__(
|
|
28
|
-
self,
|
|
29
|
-
message: str,
|
|
30
|
-
*,
|
|
31
|
-
status: Optional[int] = None,
|
|
32
|
-
details: Any = None,
|
|
33
|
-
key_attempts: Optional[list[Dict[str, Any]]] = None,
|
|
34
|
-
):
|
|
35
|
-
super().__init__(message)
|
|
36
|
-
self.status = status
|
|
37
|
-
self.details = details
|
|
38
|
-
self.key_attempts = key_attempts or []
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class MissingAPIKey(ServiceError):
|
|
42
|
-
"""缺少 API Key。"""
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
DEFAULT_BASE_URL = "https://api.tavily.com"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _timeout() -> float:
|
|
49
|
-
return float(os.getenv("SEARCH_TIMEOUT") or os.getenv("TAVILY_TIMEOUT") or "60")
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def _base_url() -> str:
|
|
53
|
-
return (os.getenv("TAVILY_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _split_key_list(value: str) -> list[str]:
|
|
57
|
-
"""解析逗号/分号/换行分隔的 key 列表,保持顺序。"""
|
|
58
|
-
|
|
59
|
-
return [part.strip() for part in re.split(r"[,;\n]+", value or "") if part.strip()]
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _api_keys() -> list[tuple[str, str]]:
|
|
63
|
-
"""返回按优先级排序的 Tavily key。
|
|
64
|
-
|
|
65
|
-
优先级:
|
|
66
|
-
1. TAVILY_API_KEYS 中的显式顺序
|
|
67
|
-
2. TAVILY_API_KEY_0、TAVILY_API_KEY_1 ... 的数字顺序
|
|
68
|
-
3. TAVILY_API_KEY 兼容单 key 配置
|
|
69
|
-
"""
|
|
70
|
-
|
|
71
|
-
candidates: list[tuple[str, str]] = []
|
|
72
|
-
|
|
73
|
-
for index, key in enumerate(_split_key_list(os.getenv("TAVILY_API_KEYS", "")), start=1):
|
|
74
|
-
candidates.append((f"TAVILY_API_KEYS[{index}]", key))
|
|
75
|
-
|
|
76
|
-
numbered: list[tuple[int, str, str]] = []
|
|
77
|
-
for name, value in os.environ.items():
|
|
78
|
-
match = re.fullmatch(r"TAVILY_API_KEY_(\d+)", name)
|
|
79
|
-
if match and value.strip():
|
|
80
|
-
numbered.append((int(match.group(1)), name, value.strip()))
|
|
81
|
-
for _, name, key in sorted(numbered):
|
|
82
|
-
candidates.append((name, key))
|
|
83
|
-
|
|
84
|
-
legacy_key = os.getenv("TAVILY_API_KEY", "").strip()
|
|
85
|
-
if legacy_key:
|
|
86
|
-
candidates.append(("TAVILY_API_KEY", legacy_key))
|
|
87
|
-
|
|
88
|
-
deduped: list[tuple[str, str]] = []
|
|
89
|
-
seen: set[str] = set()
|
|
90
|
-
for label, key in candidates:
|
|
91
|
-
if key in seen:
|
|
92
|
-
continue
|
|
93
|
-
seen.add(key)
|
|
94
|
-
deduped.append((label, key))
|
|
95
|
-
|
|
96
|
-
if not deduped:
|
|
97
|
-
raise MissingAPIKey("缺少 Tavily API key:请设置 TAVILY_API_KEYS、TAVILY_API_KEY_0... 或 TAVILY_API_KEY")
|
|
98
|
-
return deduped
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def _redact_value(value: Any, keys: list[tuple[str, str]]) -> Any:
|
|
102
|
-
"""递归脱敏,避免错误详情中意外回显 key。"""
|
|
103
|
-
|
|
104
|
-
if isinstance(value, str):
|
|
105
|
-
redacted = value
|
|
106
|
-
for _, key in keys:
|
|
107
|
-
if key:
|
|
108
|
-
redacted = redacted.replace(key, "<redacted-tavily-key>")
|
|
109
|
-
return redacted
|
|
110
|
-
if isinstance(value, list):
|
|
111
|
-
return [_redact_value(item, keys) for item in value]
|
|
112
|
-
if isinstance(value, dict):
|
|
113
|
-
return {k: _redact_value(v, keys) for k, v in value.items()}
|
|
114
|
-
return value
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def _should_try_next_key(status: Optional[int]) -> bool:
|
|
118
|
-
"""这些状态通常与 key、额度或限流相关,可尝试下一个 key。"""
|
|
119
|
-
|
|
120
|
-
return status in {401, 402, 429, 432, 433}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def _decode_response(raw: bytes) -> Any:
|
|
124
|
-
text = raw.decode("utf-8", "replace")
|
|
125
|
-
try:
|
|
126
|
-
return json.loads(text)
|
|
127
|
-
except json.JSONDecodeError:
|
|
128
|
-
return text
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def request(path: str, *, body: Optional[Dict[str, Any]] = None, method: str = "POST", timeout: Optional[float] = None) -> Dict[str, Any]:
|
|
132
|
-
url = f"{_base_url()}{path}"
|
|
133
|
-
keys = _api_keys()
|
|
134
|
-
data = None
|
|
135
|
-
if body is not None:
|
|
136
|
-
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
|
137
|
-
|
|
138
|
-
attempts: list[Dict[str, Any]] = []
|
|
139
|
-
last_error: Optional[ServiceError] = None
|
|
140
|
-
for key_index, (key_label, key) in enumerate(keys, start=1):
|
|
141
|
-
headers = {
|
|
142
|
-
"Authorization": f"Bearer {key}",
|
|
143
|
-
"Accept": "application/json",
|
|
144
|
-
"User-Agent": "pi-search-services-skill/1.0",
|
|
145
|
-
}
|
|
146
|
-
if body is not None:
|
|
147
|
-
headers["Content-Type"] = "application/json"
|
|
148
|
-
req = urllib.request.Request(url, data=data, headers=headers, method=method.upper())
|
|
149
|
-
try:
|
|
150
|
-
with urllib.request.urlopen(req, timeout=timeout if timeout is not None else _timeout()) as resp:
|
|
151
|
-
parsed = _decode_response(resp.read())
|
|
152
|
-
result: Dict[str, Any] = {
|
|
153
|
-
"ok": True,
|
|
154
|
-
"service": "tavily",
|
|
155
|
-
"status": resp.status,
|
|
156
|
-
"data": _redact_value(parsed, keys),
|
|
157
|
-
"keyLabel": key_label,
|
|
158
|
-
"keyIndex": key_index,
|
|
159
|
-
}
|
|
160
|
-
if attempts:
|
|
161
|
-
result["keyFallbackAttempts"] = attempts
|
|
162
|
-
return result
|
|
163
|
-
except urllib.error.HTTPError as exc:
|
|
164
|
-
details = _redact_value(_decode_response(exc.read()), keys)
|
|
165
|
-
attempt = {"keyLabel": key_label, "keyIndex": key_index, "status": exc.code, "error": f"Tavily HTTP {exc.code}"}
|
|
166
|
-
attempts.append(attempt)
|
|
167
|
-
last_error = ServiceError(f"Tavily HTTP {exc.code}", status=exc.code, details=details, key_attempts=attempts.copy())
|
|
168
|
-
if _should_try_next_key(exc.code) and key_index < len(keys):
|
|
169
|
-
continue
|
|
170
|
-
raise last_error from exc
|
|
171
|
-
except urllib.error.URLError as exc:
|
|
172
|
-
details = _redact_value(str(exc), keys)
|
|
173
|
-
attempt = {"keyLabel": key_label, "keyIndex": key_index, "status": None, "error": f"Tavily 网络错误: {exc.reason}"}
|
|
174
|
-
attempts.append(attempt)
|
|
175
|
-
last_error = ServiceError(f"Tavily 网络错误: {exc.reason}", details=details, key_attempts=attempts.copy())
|
|
176
|
-
# 网络错误通常与 key 无关,直接抛出,避免重复消耗请求。
|
|
177
|
-
raise last_error from exc
|
|
178
|
-
|
|
179
|
-
if last_error is not None:
|
|
180
|
-
raise last_error
|
|
181
|
-
raise MissingAPIKey("缺少 Tavily API key:请设置 TAVILY_API_KEYS、TAVILY_API_KEY_0... 或 TAVILY_API_KEY")
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def search(
|
|
185
|
-
query: str,
|
|
186
|
-
*,
|
|
187
|
-
search_depth: str = "basic",
|
|
188
|
-
max_results: int = 5,
|
|
189
|
-
topic: str = "general",
|
|
190
|
-
time_range: Optional[str] = None,
|
|
191
|
-
start_date: Optional[str] = None,
|
|
192
|
-
end_date: Optional[str] = None,
|
|
193
|
-
include_answer: Any = False,
|
|
194
|
-
include_raw_content: Any = False,
|
|
195
|
-
include_images: bool = False,
|
|
196
|
-
include_image_descriptions: bool = False,
|
|
197
|
-
include_favicon: bool = False,
|
|
198
|
-
include_domains: Optional[list[str]] = None,
|
|
199
|
-
exclude_domains: Optional[list[str]] = None,
|
|
200
|
-
country: Optional[str] = None,
|
|
201
|
-
chunks_per_source: Optional[int] = None,
|
|
202
|
-
auto_parameters: bool = False,
|
|
203
|
-
exact_match: bool = False,
|
|
204
|
-
include_usage: bool = False,
|
|
205
|
-
extra: Optional[Dict[str, Any]] = None,
|
|
206
|
-
) -> Dict[str, Any]:
|
|
207
|
-
body: Dict[str, Any] = {
|
|
208
|
-
"query": query,
|
|
209
|
-
"search_depth": search_depth,
|
|
210
|
-
"max_results": max_results,
|
|
211
|
-
"topic": topic,
|
|
212
|
-
"include_answer": include_answer,
|
|
213
|
-
"include_raw_content": include_raw_content,
|
|
214
|
-
"include_images": include_images,
|
|
215
|
-
"include_image_descriptions": include_image_descriptions,
|
|
216
|
-
"include_favicon": include_favicon,
|
|
217
|
-
"auto_parameters": auto_parameters,
|
|
218
|
-
"exact_match": exact_match,
|
|
219
|
-
"include_usage": include_usage,
|
|
220
|
-
}
|
|
221
|
-
optional = {
|
|
222
|
-
"time_range": time_range,
|
|
223
|
-
"start_date": start_date,
|
|
224
|
-
"end_date": end_date,
|
|
225
|
-
"include_domains": include_domains,
|
|
226
|
-
"exclude_domains": exclude_domains,
|
|
227
|
-
"country": country,
|
|
228
|
-
"chunks_per_source": chunks_per_source,
|
|
229
|
-
}
|
|
230
|
-
body.update({k: v for k, v in optional.items() if v is not None})
|
|
231
|
-
if extra:
|
|
232
|
-
body.update(extra)
|
|
233
|
-
return request("/search", body=body)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def extract(
|
|
237
|
-
urls: list[str] | str,
|
|
238
|
-
*,
|
|
239
|
-
query: Optional[str] = None,
|
|
240
|
-
chunks_per_source: Optional[int] = None,
|
|
241
|
-
extract_depth: str = "basic",
|
|
242
|
-
include_images: bool = False,
|
|
243
|
-
include_favicon: bool = False,
|
|
244
|
-
fmt: str = "markdown",
|
|
245
|
-
timeout: Optional[float] = None,
|
|
246
|
-
http_timeout: Optional[float] = None,
|
|
247
|
-
include_usage: bool = False,
|
|
248
|
-
extra: Optional[Dict[str, Any]] = None,
|
|
249
|
-
) -> Dict[str, Any]:
|
|
250
|
-
body: Dict[str, Any] = {
|
|
251
|
-
"urls": urls,
|
|
252
|
-
"extract_depth": extract_depth,
|
|
253
|
-
"include_images": include_images,
|
|
254
|
-
"include_favicon": include_favicon,
|
|
255
|
-
"format": fmt,
|
|
256
|
-
"include_usage": include_usage,
|
|
257
|
-
}
|
|
258
|
-
optional = {"query": query, "chunks_per_source": chunks_per_source, "timeout": timeout}
|
|
259
|
-
body.update({k: v for k, v in optional.items() if v is not None})
|
|
260
|
-
if extra:
|
|
261
|
-
body.update(extra)
|
|
262
|
-
return request("/extract", body=body, timeout=http_timeout)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
def crawl(
|
|
266
|
-
url: str,
|
|
267
|
-
*,
|
|
268
|
-
instructions: Optional[str] = None,
|
|
269
|
-
chunks_per_source: Optional[int] = None,
|
|
270
|
-
max_depth: int = 1,
|
|
271
|
-
max_breadth: int = 20,
|
|
272
|
-
limit: int = 50,
|
|
273
|
-
select_paths: Optional[list[str]] = None,
|
|
274
|
-
select_domains: Optional[list[str]] = None,
|
|
275
|
-
exclude_paths: Optional[list[str]] = None,
|
|
276
|
-
exclude_domains: Optional[list[str]] = None,
|
|
277
|
-
allow_external: bool = False,
|
|
278
|
-
include_images: bool = False,
|
|
279
|
-
extract_depth: str = "basic",
|
|
280
|
-
fmt: str = "markdown",
|
|
281
|
-
include_favicon: bool = False,
|
|
282
|
-
timeout: Optional[float] = None,
|
|
283
|
-
include_usage: bool = False,
|
|
284
|
-
extra: Optional[Dict[str, Any]] = None,
|
|
285
|
-
) -> Dict[str, Any]:
|
|
286
|
-
body: Dict[str, Any] = {
|
|
287
|
-
"url": url,
|
|
288
|
-
"max_depth": max_depth,
|
|
289
|
-
"max_breadth": max_breadth,
|
|
290
|
-
"limit": limit,
|
|
291
|
-
"allow_external": allow_external,
|
|
292
|
-
"include_images": include_images,
|
|
293
|
-
"extract_depth": extract_depth,
|
|
294
|
-
"format": fmt,
|
|
295
|
-
"include_favicon": include_favicon,
|
|
296
|
-
"include_usage": include_usage,
|
|
297
|
-
}
|
|
298
|
-
optional = {
|
|
299
|
-
"instructions": instructions,
|
|
300
|
-
"chunks_per_source": chunks_per_source,
|
|
301
|
-
"select_paths": select_paths,
|
|
302
|
-
"select_domains": select_domains,
|
|
303
|
-
"exclude_paths": exclude_paths,
|
|
304
|
-
"exclude_domains": exclude_domains,
|
|
305
|
-
"timeout": timeout,
|
|
306
|
-
}
|
|
307
|
-
body.update({k: v for k, v in optional.items() if v is not None})
|
|
308
|
-
if extra:
|
|
309
|
-
body.update(extra)
|
|
310
|
-
return request("/crawl", body=body)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
def map_site(
|
|
314
|
-
url: str,
|
|
315
|
-
*,
|
|
316
|
-
instructions: Optional[str] = None,
|
|
317
|
-
max_depth: int = 1,
|
|
318
|
-
max_breadth: int = 20,
|
|
319
|
-
limit: int = 50,
|
|
320
|
-
select_paths: Optional[list[str]] = None,
|
|
321
|
-
select_domains: Optional[list[str]] = None,
|
|
322
|
-
exclude_paths: Optional[list[str]] = None,
|
|
323
|
-
exclude_domains: Optional[list[str]] = None,
|
|
324
|
-
allow_external: bool = True,
|
|
325
|
-
timeout: Optional[float] = None,
|
|
326
|
-
include_usage: bool = False,
|
|
327
|
-
extra: Optional[Dict[str, Any]] = None,
|
|
328
|
-
) -> Dict[str, Any]:
|
|
329
|
-
body: Dict[str, Any] = {
|
|
330
|
-
"url": url,
|
|
331
|
-
"max_depth": max_depth,
|
|
332
|
-
"max_breadth": max_breadth,
|
|
333
|
-
"limit": limit,
|
|
334
|
-
"allow_external": allow_external,
|
|
335
|
-
"include_usage": include_usage,
|
|
336
|
-
}
|
|
337
|
-
optional = {
|
|
338
|
-
"instructions": instructions,
|
|
339
|
-
"select_paths": select_paths,
|
|
340
|
-
"select_domains": select_domains,
|
|
341
|
-
"exclude_paths": exclude_paths,
|
|
342
|
-
"exclude_domains": exclude_domains,
|
|
343
|
-
"timeout": timeout,
|
|
344
|
-
}
|
|
345
|
-
body.update({k: v for k, v in optional.items() if v is not None})
|
|
346
|
-
if extra:
|
|
347
|
-
body.update(extra)
|
|
348
|
-
return request("/map", body=body)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
def research(
|
|
352
|
-
input_text: str,
|
|
353
|
-
*,
|
|
354
|
-
model: str = "auto",
|
|
355
|
-
output_schema: Optional[Dict[str, Any]] = None,
|
|
356
|
-
citation_format: Optional[str] = None,
|
|
357
|
-
include_domains: Optional[list[str]] = None,
|
|
358
|
-
exclude_domains: Optional[list[str]] = None,
|
|
359
|
-
output_length: Optional[str] = None,
|
|
360
|
-
stream: bool = False,
|
|
361
|
-
extra: Optional[Dict[str, Any]] = None,
|
|
362
|
-
) -> Dict[str, Any]:
|
|
363
|
-
body: Dict[str, Any] = {"input": input_text, "model": model, "stream": stream}
|
|
364
|
-
optional = {
|
|
365
|
-
"output_schema": output_schema,
|
|
366
|
-
"citation_format": citation_format,
|
|
367
|
-
"include_domains": include_domains,
|
|
368
|
-
"exclude_domains": exclude_domains,
|
|
369
|
-
"output_length": output_length,
|
|
370
|
-
}
|
|
371
|
-
body.update({k: v for k, v in optional.items() if v is not None})
|
|
372
|
-
if extra:
|
|
373
|
-
body.update(extra)
|
|
374
|
-
return request("/research", body=body)
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
def research_status(request_id: str) -> Dict[str, Any]:
|
|
378
|
-
safe_id = urllib.parse.quote(request_id, safe="")
|
|
379
|
-
return request(f"/research/{safe_id}", method="GET")
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
def usage() -> Dict[str, Any]:
|
|
383
|
-
return request("/usage", method="GET")
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
def raw(path: str, *, method: str = "POST", body_json: Optional[str] = None) -> Dict[str, Any]:
|
|
387
|
-
body = json.loads(body_json) if body_json else None
|
|
388
|
-
return request(path, body=body, method=method)
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
def _split_csv(value: Optional[str]) -> Optional[list[str]]:
|
|
392
|
-
if not value:
|
|
393
|
-
return None
|
|
394
|
-
return [part.strip() for part in value.split(",") if part.strip()]
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
def _json_or_none(value: Optional[str]) -> Optional[Dict[str, Any]]:
|
|
398
|
-
if not value:
|
|
399
|
-
return None
|
|
400
|
-
return json.loads(value)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
def _json_scalar(value: Optional[str], default: Any) -> Any:
|
|
404
|
-
if value is None:
|
|
405
|
-
return default
|
|
406
|
-
try:
|
|
407
|
-
return json.loads(value)
|
|
408
|
-
except json.JSONDecodeError:
|
|
409
|
-
return value
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
def _print_json(value: Any) -> None:
|
|
413
|
-
print(json.dumps(value, ensure_ascii=False, indent=2))
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
def main(argv: Optional[list[str]] = None) -> int:
|
|
417
|
-
parser = argparse.ArgumentParser(description="Tavily REST API helper")
|
|
418
|
-
sub = parser.add_subparsers(dest="operation", required=True)
|
|
419
|
-
|
|
420
|
-
p = sub.add_parser("search", help="Tavily Web Search")
|
|
421
|
-
p.add_argument("--query", required=True)
|
|
422
|
-
p.add_argument("--search-depth", default="basic", choices=["basic", "advanced", "fast", "ultra-fast"])
|
|
423
|
-
p.add_argument("--max-results", type=int, default=5)
|
|
424
|
-
p.add_argument("--topic", default="general", choices=["general", "news", "finance"])
|
|
425
|
-
p.add_argument("--time-range", choices=["day", "week", "month", "year"])
|
|
426
|
-
p.add_argument("--start-date")
|
|
427
|
-
p.add_argument("--end-date")
|
|
428
|
-
p.add_argument("--include-answer-json", help="bool 或 \"basic\"/\"advanced\" 的 JSON")
|
|
429
|
-
p.add_argument("--include-raw-content-json", help="bool 或 \"markdown\"/\"text\" 的 JSON")
|
|
430
|
-
p.add_argument("--include-images", action="store_true")
|
|
431
|
-
p.add_argument("--include-image-descriptions", action="store_true")
|
|
432
|
-
p.add_argument("--include-favicon", action="store_true")
|
|
433
|
-
p.add_argument("--include-domains", help="逗号分隔")
|
|
434
|
-
p.add_argument("--exclude-domains", help="逗号分隔")
|
|
435
|
-
p.add_argument("--country")
|
|
436
|
-
p.add_argument("--chunks-per-source", type=int)
|
|
437
|
-
p.add_argument("--auto-parameters", action="store_true")
|
|
438
|
-
p.add_argument("--exact-match", action="store_true")
|
|
439
|
-
p.add_argument("--include-usage", action="store_true")
|
|
440
|
-
p.add_argument("--extra-json")
|
|
441
|
-
|
|
442
|
-
p = sub.add_parser("extract", help="Tavily Extract")
|
|
443
|
-
p.add_argument("--urls", required=True, help="逗号分隔 URL")
|
|
444
|
-
p.add_argument("--query")
|
|
445
|
-
p.add_argument("--chunks-per-source", type=int)
|
|
446
|
-
p.add_argument("--extract-depth", default="basic", choices=["basic", "advanced"])
|
|
447
|
-
p.add_argument("--include-images", action="store_true")
|
|
448
|
-
p.add_argument("--include-favicon", action="store_true")
|
|
449
|
-
p.add_argument("--format", dest="fmt", default="markdown", choices=["markdown", "text"])
|
|
450
|
-
p.add_argument("--timeout", type=float)
|
|
451
|
-
p.add_argument("--include-usage", action="store_true")
|
|
452
|
-
p.add_argument("--extra-json")
|
|
453
|
-
|
|
454
|
-
for name, help_text in [("crawl", "Tavily Crawl"), ("map", "Tavily Map")]:
|
|
455
|
-
p = sub.add_parser(name, help=help_text)
|
|
456
|
-
p.add_argument("--url", required=True)
|
|
457
|
-
p.add_argument("--instructions")
|
|
458
|
-
p.add_argument("--max-depth", type=int, default=1)
|
|
459
|
-
p.add_argument("--max-breadth", type=int, default=20)
|
|
460
|
-
p.add_argument("--limit", type=int, default=50)
|
|
461
|
-
p.add_argument("--select-paths", help="逗号分隔 regex")
|
|
462
|
-
p.add_argument("--select-domains", help="逗号分隔 regex")
|
|
463
|
-
p.add_argument("--exclude-paths", help="逗号分隔 regex")
|
|
464
|
-
p.add_argument("--exclude-domains", help="逗号分隔 regex")
|
|
465
|
-
p.add_argument("--allow-external", action="store_true")
|
|
466
|
-
p.add_argument("--timeout", type=float)
|
|
467
|
-
p.add_argument("--include-usage", action="store_true")
|
|
468
|
-
p.add_argument("--extra-json")
|
|
469
|
-
if name == "crawl":
|
|
470
|
-
p.add_argument("--chunks-per-source", type=int)
|
|
471
|
-
p.add_argument("--include-images", action="store_true")
|
|
472
|
-
p.add_argument("--extract-depth", default="basic", choices=["basic", "advanced"])
|
|
473
|
-
p.add_argument("--format", dest="fmt", default="markdown", choices=["markdown", "text"])
|
|
474
|
-
p.add_argument("--include-favicon", action="store_true")
|
|
475
|
-
|
|
476
|
-
p = sub.add_parser("research", help="创建 Tavily Research 任务")
|
|
477
|
-
p.add_argument("--input", "--query", dest="input_text", required=True)
|
|
478
|
-
p.add_argument("--model", default="auto", choices=["mini", "pro", "auto"])
|
|
479
|
-
p.add_argument("--output-schema-json")
|
|
480
|
-
p.add_argument("--citation-format")
|
|
481
|
-
p.add_argument("--include-domains", help="逗号分隔")
|
|
482
|
-
p.add_argument("--exclude-domains", help="逗号分隔")
|
|
483
|
-
p.add_argument("--output-length")
|
|
484
|
-
p.add_argument("--stream", action="store_true")
|
|
485
|
-
p.add_argument("--extra-json")
|
|
486
|
-
|
|
487
|
-
p = sub.add_parser("research-status", help="获取研究任务状态")
|
|
488
|
-
p.add_argument("--request-id", required=True)
|
|
489
|
-
|
|
490
|
-
sub.add_parser("usage", help="查询用量")
|
|
491
|
-
|
|
492
|
-
p = sub.add_parser("raw", help="原始 Tavily API 调用")
|
|
493
|
-
p.add_argument("--path", required=True)
|
|
494
|
-
p.add_argument("--method", default="POST")
|
|
495
|
-
p.add_argument("--body-json")
|
|
496
|
-
|
|
497
|
-
args = parser.parse_args(argv)
|
|
498
|
-
try:
|
|
499
|
-
if args.operation == "search":
|
|
500
|
-
out = search(
|
|
501
|
-
args.query,
|
|
502
|
-
search_depth=args.search_depth,
|
|
503
|
-
max_results=args.max_results,
|
|
504
|
-
topic=args.topic,
|
|
505
|
-
time_range=args.time_range,
|
|
506
|
-
start_date=args.start_date,
|
|
507
|
-
end_date=args.end_date,
|
|
508
|
-
include_answer=_json_scalar(args.include_answer_json, False),
|
|
509
|
-
include_raw_content=_json_scalar(args.include_raw_content_json, False),
|
|
510
|
-
include_images=args.include_images,
|
|
511
|
-
include_image_descriptions=args.include_image_descriptions,
|
|
512
|
-
include_favicon=args.include_favicon,
|
|
513
|
-
include_domains=_split_csv(args.include_domains),
|
|
514
|
-
exclude_domains=_split_csv(args.exclude_domains),
|
|
515
|
-
country=args.country,
|
|
516
|
-
chunks_per_source=args.chunks_per_source,
|
|
517
|
-
auto_parameters=args.auto_parameters,
|
|
518
|
-
exact_match=args.exact_match,
|
|
519
|
-
include_usage=args.include_usage,
|
|
520
|
-
extra=_json_or_none(args.extra_json),
|
|
521
|
-
)
|
|
522
|
-
elif args.operation == "extract":
|
|
523
|
-
urls = _split_csv(args.urls) or []
|
|
524
|
-
out = extract(
|
|
525
|
-
urls[0] if len(urls) == 1 else urls,
|
|
526
|
-
query=args.query,
|
|
527
|
-
chunks_per_source=args.chunks_per_source,
|
|
528
|
-
extract_depth=args.extract_depth,
|
|
529
|
-
include_images=args.include_images,
|
|
530
|
-
include_favicon=args.include_favicon,
|
|
531
|
-
fmt=args.fmt,
|
|
532
|
-
timeout=args.timeout,
|
|
533
|
-
include_usage=args.include_usage,
|
|
534
|
-
extra=_json_or_none(args.extra_json),
|
|
535
|
-
)
|
|
536
|
-
elif args.operation == "crawl":
|
|
537
|
-
out = crawl(
|
|
538
|
-
args.url,
|
|
539
|
-
instructions=args.instructions,
|
|
540
|
-
chunks_per_source=args.chunks_per_source,
|
|
541
|
-
max_depth=args.max_depth,
|
|
542
|
-
max_breadth=args.max_breadth,
|
|
543
|
-
limit=args.limit,
|
|
544
|
-
select_paths=_split_csv(args.select_paths),
|
|
545
|
-
select_domains=_split_csv(args.select_domains),
|
|
546
|
-
exclude_paths=_split_csv(args.exclude_paths),
|
|
547
|
-
exclude_domains=_split_csv(args.exclude_domains),
|
|
548
|
-
allow_external=args.allow_external,
|
|
549
|
-
include_images=args.include_images,
|
|
550
|
-
extract_depth=args.extract_depth,
|
|
551
|
-
fmt=args.fmt,
|
|
552
|
-
include_favicon=args.include_favicon,
|
|
553
|
-
timeout=args.timeout,
|
|
554
|
-
include_usage=args.include_usage,
|
|
555
|
-
extra=_json_or_none(args.extra_json),
|
|
556
|
-
)
|
|
557
|
-
elif args.operation == "map":
|
|
558
|
-
out = map_site(
|
|
559
|
-
args.url,
|
|
560
|
-
instructions=args.instructions,
|
|
561
|
-
max_depth=args.max_depth,
|
|
562
|
-
max_breadth=args.max_breadth,
|
|
563
|
-
limit=args.limit,
|
|
564
|
-
select_paths=_split_csv(args.select_paths),
|
|
565
|
-
select_domains=_split_csv(args.select_domains),
|
|
566
|
-
exclude_paths=_split_csv(args.exclude_paths),
|
|
567
|
-
exclude_domains=_split_csv(args.exclude_domains),
|
|
568
|
-
allow_external=args.allow_external,
|
|
569
|
-
timeout=args.timeout,
|
|
570
|
-
include_usage=args.include_usage,
|
|
571
|
-
extra=_json_or_none(args.extra_json),
|
|
572
|
-
)
|
|
573
|
-
elif args.operation == "research":
|
|
574
|
-
out = research(
|
|
575
|
-
args.input_text,
|
|
576
|
-
model=args.model,
|
|
577
|
-
output_schema=_json_or_none(args.output_schema_json),
|
|
578
|
-
citation_format=args.citation_format,
|
|
579
|
-
include_domains=_split_csv(args.include_domains),
|
|
580
|
-
exclude_domains=_split_csv(args.exclude_domains),
|
|
581
|
-
output_length=args.output_length,
|
|
582
|
-
stream=args.stream,
|
|
583
|
-
extra=_json_or_none(args.extra_json),
|
|
584
|
-
)
|
|
585
|
-
elif args.operation == "research-status":
|
|
586
|
-
out = research_status(args.request_id)
|
|
587
|
-
elif args.operation == "usage":
|
|
588
|
-
out = usage()
|
|
589
|
-
elif args.operation == "raw":
|
|
590
|
-
out = raw(args.path, method=args.method, body_json=args.body_json)
|
|
591
|
-
else:
|
|
592
|
-
parser.error("未知操作")
|
|
593
|
-
return 2
|
|
594
|
-
_print_json(out)
|
|
595
|
-
return 0
|
|
596
|
-
except ServiceError as exc:
|
|
597
|
-
_print_json({
|
|
598
|
-
"ok": False,
|
|
599
|
-
"service": "tavily",
|
|
600
|
-
"error": str(exc),
|
|
601
|
-
"status": exc.status,
|
|
602
|
-
"details": exc.details,
|
|
603
|
-
"keyFallbackAttempts": exc.key_attempts,
|
|
604
|
-
})
|
|
605
|
-
return 1
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
if __name__ == "__main__":
|
|
609
|
-
sys.exit(main())
|
package/src/args.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* search.py 子命令参数组装(纯函数)。
|
|
3
|
-
* 优先级直接透传 --service:search.py 自带链内 fallback 与 Tavily 多 key 轮转,
|
|
4
|
-
* 插件不重复实现回退编排。
|
|
5
|
-
* @module web-search-services/args
|
|
6
|
-
*/
|
|
7
|
-
import type { WebSearchRequest } from '@deepseek-ai/dsh-web'
|
|
8
|
-
|
|
9
|
-
import type { WebSearchServicesConfig } from './config.ts'
|
|
10
|
-
|
|
11
|
-
/** dsh-tool-web 之外的调用方未给 maxResults 时的请求层缺省。 */
|
|
12
|
-
export const FALLBACK_MAX_RESULTS = 8
|
|
13
|
-
|
|
14
|
-
/** 由 web 请求 + 插件配置生成 search.py 的 argv(不含解释器与脚本路径)。 */
|
|
15
|
-
export function buildSearchArgv(cfg: WebSearchServicesConfig, request: WebSearchRequest): string[] {
|
|
16
|
-
const query = request.query.trim()
|
|
17
|
-
if (query === '') {
|
|
18
|
-
throw new Error('query must be a non-empty string')
|
|
19
|
-
}
|
|
20
|
-
const maxResults = request.maxResults ?? FALLBACK_MAX_RESULTS
|
|
21
|
-
return [
|
|
22
|
-
'search',
|
|
23
|
-
'--query',
|
|
24
|
-
query,
|
|
25
|
-
'--service',
|
|
26
|
-
cfg.priority.join(','),
|
|
27
|
-
'--max-results',
|
|
28
|
-
String(maxResults),
|
|
29
|
-
]
|
|
30
|
-
}
|
package/src/env-file.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* search-services skill env 文件解析(纯函数)。
|
|
3
|
-
* 只解析不执行:按行提取 KEY=VALUE,不展开 $ 变量、不运行 shell,畸形行跳过;
|
|
4
|
-
* 文件缺失/不可读返回空表(envFile 是优化来源,不是硬依赖)。
|
|
5
|
-
* @module web-search-services/env-file
|
|
6
|
-
*/
|
|
7
|
-
import { readFileSync } from 'node:fs'
|
|
8
|
-
import { homedir } from 'node:os'
|
|
9
|
-
import { join } from 'node:path'
|
|
10
|
-
|
|
11
|
-
/** 展开路径开头的 ~(支持 `~`、`~/`、`~\` 三种形态,与官方 dsh-home-paths 对齐,不借 shell)。 */
|
|
12
|
-
export function expandHome(path: string): string {
|
|
13
|
-
if (path === '~') return homedir()
|
|
14
|
-
if (path.startsWith('~/') || path.startsWith('~\\')) return join(homedir(), path.slice(2))
|
|
15
|
-
return path
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function unquote(value: string): string {
|
|
19
|
-
const first = value.charAt(0)
|
|
20
|
-
const last = value.charAt(value.length - 1)
|
|
21
|
-
if (value.length >= 2 && first === last && (first === '"' || first === "'")) {
|
|
22
|
-
return value.slice(1, -1)
|
|
23
|
-
}
|
|
24
|
-
return value
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function parseEnvText(text: string): Record<string, string> {
|
|
28
|
-
const out: Record<string, string> = {}
|
|
29
|
-
for (const rawLine of text.split(/\r?\n/)) {
|
|
30
|
-
const line = rawLine.trim()
|
|
31
|
-
if (line === '' || line.startsWith('#')) continue
|
|
32
|
-
const body = line.startsWith('export ') ? line.slice('export '.length).trim() : line
|
|
33
|
-
const eq = body.indexOf('=')
|
|
34
|
-
if (eq <= 0) continue
|
|
35
|
-
const key = body.slice(0, eq).trim()
|
|
36
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue
|
|
37
|
-
out[key] = unquote(body.slice(eq + 1).trim())
|
|
38
|
-
}
|
|
39
|
-
return out
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function loadEnvFile(path: string): Record<string, string> {
|
|
43
|
-
if (path.trim() === '') return {}
|
|
44
|
-
try {
|
|
45
|
-
return parseEnvText(readFileSync(expandHome(path), 'utf8'))
|
|
46
|
-
} catch {
|
|
47
|
-
return {}
|
|
48
|
-
}
|
|
49
|
-
}
|