@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/context7.py
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Context7 REST API helper.
|
|
3
|
-
|
|
4
|
-
不使用 MCP,仅通过 Context7 HTTP API 获取库文档、代码片段和相关上下文。
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import argparse
|
|
10
|
-
import json
|
|
11
|
-
import os
|
|
12
|
-
import sys
|
|
13
|
-
import urllib.error
|
|
14
|
-
import urllib.parse
|
|
15
|
-
import urllib.request
|
|
16
|
-
from typing import Any, Dict, Optional
|
|
17
|
-
|
|
18
|
-
from env_loader import load_env_file
|
|
19
|
-
|
|
20
|
-
load_env_file()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class ServiceError(RuntimeError):
|
|
24
|
-
"""服务调用失败。"""
|
|
25
|
-
|
|
26
|
-
def __init__(self, message: str, *, status: Optional[int] = None, details: Any = None):
|
|
27
|
-
super().__init__(message)
|
|
28
|
-
self.status = status
|
|
29
|
-
self.details = details
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class MissingAPIKey(ServiceError):
|
|
33
|
-
"""缺少 API Key。"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
DEFAULT_BASE_URL = "https://context7.com/api"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _timeout() -> float:
|
|
40
|
-
return float(os.getenv("SEARCH_TIMEOUT") or os.getenv("CONTEXT7_TIMEOUT") or "30")
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _base_url() -> str:
|
|
44
|
-
return (os.getenv("CONTEXT7_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _api_key() -> str:
|
|
48
|
-
key = os.getenv("CONTEXT7_API_KEY", "").strip()
|
|
49
|
-
if not key:
|
|
50
|
-
raise MissingAPIKey("缺少 CONTEXT7_API_KEY")
|
|
51
|
-
return key
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _decode_response(raw: bytes) -> Any:
|
|
55
|
-
text = raw.decode("utf-8", "replace")
|
|
56
|
-
try:
|
|
57
|
-
return json.loads(text)
|
|
58
|
-
except json.JSONDecodeError:
|
|
59
|
-
return text
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def request(
|
|
63
|
-
method: str,
|
|
64
|
-
path: str,
|
|
65
|
-
*,
|
|
66
|
-
params: Optional[Dict[str, Any]] = None,
|
|
67
|
-
body: Optional[Dict[str, Any]] = None,
|
|
68
|
-
) -> Dict[str, Any]:
|
|
69
|
-
"""调用 Context7 API,返回带元信息的 JSON。"""
|
|
70
|
-
|
|
71
|
-
query = urllib.parse.urlencode({k: v for k, v in (params or {}).items() if v is not None})
|
|
72
|
-
url = f"{_base_url()}{path}"
|
|
73
|
-
if query:
|
|
74
|
-
url = f"{url}?{query}"
|
|
75
|
-
|
|
76
|
-
headers = {
|
|
77
|
-
"Authorization": f"Bearer {_api_key()}",
|
|
78
|
-
"Accept": "application/json",
|
|
79
|
-
"User-Agent": "pi-search-services-skill/1.0",
|
|
80
|
-
}
|
|
81
|
-
data = None
|
|
82
|
-
if body is not None:
|
|
83
|
-
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
|
84
|
-
headers["Content-Type"] = "application/json"
|
|
85
|
-
|
|
86
|
-
req = urllib.request.Request(url, data=data, headers=headers, method=method.upper())
|
|
87
|
-
try:
|
|
88
|
-
with urllib.request.urlopen(req, timeout=_timeout()) as resp:
|
|
89
|
-
parsed = _decode_response(resp.read())
|
|
90
|
-
return {"ok": True, "service": "context7", "status": resp.status, "data": parsed}
|
|
91
|
-
except urllib.error.HTTPError as exc:
|
|
92
|
-
details = _decode_response(exc.read())
|
|
93
|
-
raise ServiceError(f"Context7 HTTP {exc.code}", status=exc.code, details=details) from exc
|
|
94
|
-
except urllib.error.URLError as exc:
|
|
95
|
-
raise ServiceError(f"Context7 网络错误: {exc.reason}", details=str(exc)) from exc
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def search_libraries(library_name: str, query: Optional[str] = None, fast: bool = False) -> Dict[str, Any]:
|
|
99
|
-
"""搜索 Context7 库 ID。"""
|
|
100
|
-
|
|
101
|
-
return request(
|
|
102
|
-
"GET",
|
|
103
|
-
"/v2/libs/search",
|
|
104
|
-
params={"libraryName": library_name, "query": query, "fast": str(fast).lower() if fast else None},
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def get_context(
|
|
109
|
-
library_id: str,
|
|
110
|
-
query: str,
|
|
111
|
-
*,
|
|
112
|
-
response_type: str = "json",
|
|
113
|
-
fast: bool = False,
|
|
114
|
-
) -> Dict[str, Any]:
|
|
115
|
-
"""获取某个库的文档上下文。"""
|
|
116
|
-
|
|
117
|
-
return request(
|
|
118
|
-
"GET",
|
|
119
|
-
"/v2/context",
|
|
120
|
-
params={
|
|
121
|
-
"libraryId": library_id,
|
|
122
|
-
"query": query,
|
|
123
|
-
"type": response_type,
|
|
124
|
-
"fast": str(fast).lower() if fast else None,
|
|
125
|
-
},
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def lookup(
|
|
130
|
-
library: str,
|
|
131
|
-
query: str,
|
|
132
|
-
*,
|
|
133
|
-
response_type: str = "json",
|
|
134
|
-
fast: bool = False,
|
|
135
|
-
) -> Dict[str, Any]:
|
|
136
|
-
"""先解析库 ID,再获取文档上下文。library 可为 /owner/repo 或库名。"""
|
|
137
|
-
|
|
138
|
-
if library.startswith("/"):
|
|
139
|
-
library_id = library
|
|
140
|
-
search_result = None
|
|
141
|
-
else:
|
|
142
|
-
search_result = search_libraries(library, query=query, fast=fast)
|
|
143
|
-
results = (search_result.get("data") or {}).get("results") or []
|
|
144
|
-
if not results:
|
|
145
|
-
raise ServiceError("Context7 未找到匹配库", status=404, details=search_result.get("data"))
|
|
146
|
-
library_id = results[0].get("id")
|
|
147
|
-
if not library_id:
|
|
148
|
-
raise ServiceError("Context7 搜索结果缺少 library id", details=results[0])
|
|
149
|
-
|
|
150
|
-
context = get_context(library_id, query, response_type=response_type, fast=fast)
|
|
151
|
-
context["resolvedLibraryId"] = library_id
|
|
152
|
-
if search_result is not None:
|
|
153
|
-
context["librarySearch"] = search_result.get("data")
|
|
154
|
-
return context
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def refresh(library_name: str, *, branch: Optional[str] = None, git_token: Optional[str] = None) -> Dict[str, Any]:
|
|
158
|
-
body: Dict[str, Any] = {"libraryName": library_name}
|
|
159
|
-
if branch:
|
|
160
|
-
body["branch"] = branch
|
|
161
|
-
if git_token:
|
|
162
|
-
body["gitToken"] = git_token
|
|
163
|
-
return request("POST", "/v1/refresh", body=body)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def add_website(website_url: str) -> Dict[str, Any]:
|
|
167
|
-
return request("POST", "/v2/add/website", body={"websiteUrl": website_url})
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def add_llmstxt(llmstxt_url: str) -> Dict[str, Any]:
|
|
171
|
-
return request("POST", "/v2/add/llmstxt", body={"llmstxtUrl": llmstxt_url})
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def add_openapi(openapi_url: str) -> Dict[str, Any]:
|
|
175
|
-
return request("POST", "/v2/add/openapi", body={"openApiUrl": openapi_url})
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def raw(method: str, path: str, *, params_json: Optional[str] = None, body_json: Optional[str] = None) -> Dict[str, Any]:
|
|
179
|
-
params = json.loads(params_json) if params_json else None
|
|
180
|
-
body = json.loads(body_json) if body_json else None
|
|
181
|
-
return request(method, path, params=params, body=body)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def _print_json(value: Any) -> None:
|
|
185
|
-
print(json.dumps(value, ensure_ascii=False, indent=2))
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def main(argv: Optional[list[str]] = None) -> int:
|
|
189
|
-
parser = argparse.ArgumentParser(description="Context7 REST API helper")
|
|
190
|
-
sub = parser.add_subparsers(dest="operation", required=True)
|
|
191
|
-
|
|
192
|
-
p = sub.add_parser("search-libraries", help="搜索库 ID")
|
|
193
|
-
p.add_argument("--library", "--library-name", dest="library", required=True)
|
|
194
|
-
p.add_argument("--query")
|
|
195
|
-
p.add_argument("--fast", action="store_true")
|
|
196
|
-
|
|
197
|
-
p = sub.add_parser("context", help="获取文档上下文")
|
|
198
|
-
p.add_argument("--library-id", required=True)
|
|
199
|
-
p.add_argument("--query", required=True)
|
|
200
|
-
p.add_argument("--type", default="json")
|
|
201
|
-
p.add_argument("--fast", action="store_true")
|
|
202
|
-
|
|
203
|
-
p = sub.add_parser("lookup", help="按库名/库 ID 查询文档上下文")
|
|
204
|
-
p.add_argument("--library", required=True)
|
|
205
|
-
p.add_argument("--query", required=True)
|
|
206
|
-
p.add_argument("--type", default="json")
|
|
207
|
-
p.add_argument("--fast", action="store_true")
|
|
208
|
-
|
|
209
|
-
p = sub.add_parser("refresh", help="刷新库文档")
|
|
210
|
-
p.add_argument("--library-name", required=True)
|
|
211
|
-
p.add_argument("--branch")
|
|
212
|
-
p.add_argument("--git-token-env", help="从指定环境变量读取 gitToken")
|
|
213
|
-
|
|
214
|
-
p = sub.add_parser("add-website", help="提交网站文档源")
|
|
215
|
-
p.add_argument("--url", required=True)
|
|
216
|
-
|
|
217
|
-
p = sub.add_parser("add-llmstxt", help="提交 llms.txt 文档源")
|
|
218
|
-
p.add_argument("--url", required=True)
|
|
219
|
-
|
|
220
|
-
p = sub.add_parser("add-openapi", help="提交 OpenAPI 文档源")
|
|
221
|
-
p.add_argument("--url", required=True)
|
|
222
|
-
|
|
223
|
-
p = sub.add_parser("raw", help="原始 Context7 API 调用")
|
|
224
|
-
p.add_argument("--method", required=True)
|
|
225
|
-
p.add_argument("--path", required=True)
|
|
226
|
-
p.add_argument("--params-json")
|
|
227
|
-
p.add_argument("--body-json")
|
|
228
|
-
|
|
229
|
-
args = parser.parse_args(argv)
|
|
230
|
-
try:
|
|
231
|
-
if args.operation == "search-libraries":
|
|
232
|
-
out = search_libraries(args.library, query=args.query, fast=args.fast)
|
|
233
|
-
elif args.operation == "context":
|
|
234
|
-
out = get_context(args.library_id, args.query, response_type=args.type, fast=args.fast)
|
|
235
|
-
elif args.operation == "lookup":
|
|
236
|
-
out = lookup(args.library, args.query, response_type=args.type, fast=args.fast)
|
|
237
|
-
elif args.operation == "refresh":
|
|
238
|
-
git_token = os.getenv(args.git_token_env, "") if args.git_token_env else None
|
|
239
|
-
out = refresh(args.library_name, branch=args.branch, git_token=git_token)
|
|
240
|
-
elif args.operation == "add-website":
|
|
241
|
-
out = add_website(args.url)
|
|
242
|
-
elif args.operation == "add-llmstxt":
|
|
243
|
-
out = add_llmstxt(args.url)
|
|
244
|
-
elif args.operation == "add-openapi":
|
|
245
|
-
out = add_openapi(args.url)
|
|
246
|
-
elif args.operation == "raw":
|
|
247
|
-
out = raw(args.method, args.path, params_json=args.params_json, body_json=args.body_json)
|
|
248
|
-
else:
|
|
249
|
-
parser.error("未知操作")
|
|
250
|
-
return 2
|
|
251
|
-
_print_json(out)
|
|
252
|
-
return 0
|
|
253
|
-
except ServiceError as exc:
|
|
254
|
-
_print_json({"ok": False, "service": "context7", "error": str(exc), "status": exc.status, "details": exc.details})
|
|
255
|
-
return 1
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if __name__ == "__main__":
|
|
259
|
-
sys.exit(main())
|
package/scripts/env_loader.py
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
"""Search Services skill 环境变量加载器。
|
|
2
|
-
|
|
3
|
-
从用户私有 env 文件加载变量到当前 Python 进程环境,避免在脚本中硬编码密钥。
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
import os
|
|
9
|
-
import re
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
DEFAULT_ENV_FILE = Path.home() / ".config" / "search-services" / "env"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _strip_shell_quotes(value: str) -> str:
|
|
17
|
-
value = value.strip()
|
|
18
|
-
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
|
|
19
|
-
value = value[1:-1]
|
|
20
|
-
return value
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def load_env_file(path: str | None = None, *, override: bool = False) -> None:
|
|
24
|
-
"""加载 export KEY=value 格式的私有 env 文件。
|
|
25
|
-
|
|
26
|
-
默认不覆盖已存在的环境变量;可用 SEARCH_SERVICES_ENV_FILE 指定路径。
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
env_path = Path(path or os.getenv("SEARCH_SERVICES_ENV_FILE", "") or DEFAULT_ENV_FILE).expanduser()
|
|
30
|
-
if not env_path.is_file():
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
pattern = re.compile(r"^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$")
|
|
34
|
-
try:
|
|
35
|
-
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
|
|
36
|
-
line = raw_line.strip()
|
|
37
|
-
if not line or line.startswith("#"):
|
|
38
|
-
continue
|
|
39
|
-
match = pattern.match(line)
|
|
40
|
-
if not match:
|
|
41
|
-
continue
|
|
42
|
-
key, value = match.group(1), _strip_shell_quotes(match.group(2))
|
|
43
|
-
if override or key not in os.environ:
|
|
44
|
-
os.environ[key] = value
|
|
45
|
-
except OSError:
|
|
46
|
-
return
|
package/scripts/exa.py
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Exa REST API helper.
|
|
3
|
-
|
|
4
|
-
不使用 MCP,仅通过 Exa HTTP API 执行语义搜索和内容获取。
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import argparse
|
|
10
|
-
import json
|
|
11
|
-
import os
|
|
12
|
-
import sys
|
|
13
|
-
import urllib.error
|
|
14
|
-
import urllib.request
|
|
15
|
-
from typing import Any, Dict, Optional
|
|
16
|
-
|
|
17
|
-
from env_loader import load_env_file
|
|
18
|
-
|
|
19
|
-
load_env_file()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class ServiceError(RuntimeError):
|
|
23
|
-
"""服务调用失败。"""
|
|
24
|
-
|
|
25
|
-
def __init__(self, message: str, *, status: Optional[int] = None, details: Any = None):
|
|
26
|
-
super().__init__(message)
|
|
27
|
-
self.status = status
|
|
28
|
-
self.details = details
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class MissingAPIKey(ServiceError):
|
|
32
|
-
"""缺少 API Key。"""
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
DEFAULT_BASE_URL = "https://api.exa.ai"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _timeout() -> float:
|
|
39
|
-
return float(os.getenv("SEARCH_TIMEOUT") or os.getenv("EXA_TIMEOUT") or "30")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _base_url() -> str:
|
|
43
|
-
return (os.getenv("EXA_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _api_key() -> str:
|
|
47
|
-
key = os.getenv("EXA_API_KEY", "").strip()
|
|
48
|
-
if not key:
|
|
49
|
-
raise MissingAPIKey("缺少 EXA_API_KEY")
|
|
50
|
-
return key
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _decode_response(raw: bytes) -> Any:
|
|
54
|
-
text = raw.decode("utf-8", "replace")
|
|
55
|
-
try:
|
|
56
|
-
return json.loads(text)
|
|
57
|
-
except json.JSONDecodeError:
|
|
58
|
-
return text
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def request(path: str, body: Dict[str, Any], *, method: str = "POST", timeout: Optional[float] = None) -> Dict[str, Any]:
|
|
62
|
-
url = f"{_base_url()}{path}"
|
|
63
|
-
headers = {
|
|
64
|
-
"x-api-key": _api_key(),
|
|
65
|
-
"Content-Type": "application/json",
|
|
66
|
-
"Accept": "application/json",
|
|
67
|
-
"User-Agent": "pi-search-services-skill/1.0",
|
|
68
|
-
}
|
|
69
|
-
data = json.dumps(body, ensure_ascii=False).encode("utf-8") if body is not None else None
|
|
70
|
-
req = urllib.request.Request(url, data=data, headers=headers, method=method.upper())
|
|
71
|
-
try:
|
|
72
|
-
with urllib.request.urlopen(req, timeout=timeout if timeout is not None else _timeout()) as resp:
|
|
73
|
-
parsed = _decode_response(resp.read())
|
|
74
|
-
return {"ok": True, "service": "exa", "status": resp.status, "data": parsed}
|
|
75
|
-
except urllib.error.HTTPError as exc:
|
|
76
|
-
details = _decode_response(exc.read())
|
|
77
|
-
raise ServiceError(f"Exa HTTP {exc.code}", status=exc.code, details=details) from exc
|
|
78
|
-
except urllib.error.URLError as exc:
|
|
79
|
-
raise ServiceError(f"Exa 网络错误: {exc.reason}", details=str(exc)) from exc
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def search(
|
|
83
|
-
query: str,
|
|
84
|
-
*,
|
|
85
|
-
search_type: str = "auto",
|
|
86
|
-
num_results: int = 10,
|
|
87
|
-
category: Optional[str] = None,
|
|
88
|
-
include_domains: Optional[list[str]] = None,
|
|
89
|
-
exclude_domains: Optional[list[str]] = None,
|
|
90
|
-
start_published_date: Optional[str] = None,
|
|
91
|
-
end_published_date: Optional[str] = None,
|
|
92
|
-
user_location: Optional[str] = None,
|
|
93
|
-
contents: Optional[Dict[str, Any]] = None,
|
|
94
|
-
output_schema: Optional[Dict[str, Any]] = None,
|
|
95
|
-
system_prompt: Optional[str] = None,
|
|
96
|
-
extra: Optional[Dict[str, Any]] = None,
|
|
97
|
-
) -> Dict[str, Any]:
|
|
98
|
-
"""调用 Exa /search。REST API 使用 camelCase 参数。"""
|
|
99
|
-
|
|
100
|
-
body: Dict[str, Any] = {
|
|
101
|
-
"query": query,
|
|
102
|
-
"type": search_type,
|
|
103
|
-
"numResults": num_results,
|
|
104
|
-
}
|
|
105
|
-
optional = {
|
|
106
|
-
"category": category,
|
|
107
|
-
"includeDomains": include_domains,
|
|
108
|
-
"excludeDomains": exclude_domains,
|
|
109
|
-
"startPublishedDate": start_published_date,
|
|
110
|
-
"endPublishedDate": end_published_date,
|
|
111
|
-
"userLocation": user_location,
|
|
112
|
-
"contents": contents if contents is not None else {"highlights": True},
|
|
113
|
-
"outputSchema": output_schema,
|
|
114
|
-
"systemPrompt": system_prompt,
|
|
115
|
-
}
|
|
116
|
-
body.update({k: v for k, v in optional.items() if v is not None})
|
|
117
|
-
if extra:
|
|
118
|
-
body.update(extra)
|
|
119
|
-
return request("/search", body)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def contents(
|
|
123
|
-
ids: list[str],
|
|
124
|
-
*,
|
|
125
|
-
text: Any = True,
|
|
126
|
-
highlights: Any = None,
|
|
127
|
-
summary: Any = None,
|
|
128
|
-
extra: Optional[Dict[str, Any]] = None,
|
|
129
|
-
timeout: Optional[float] = None,
|
|
130
|
-
) -> Dict[str, Any]:
|
|
131
|
-
"""调用 Exa /contents。
|
|
132
|
-
|
|
133
|
-
Exa 搜索结果 id 通常就是 URL。/contents 与 /search 不同:text/highlights/summary 是顶层参数。
|
|
134
|
-
"""
|
|
135
|
-
|
|
136
|
-
body: Dict[str, Any] = {"ids": ids}
|
|
137
|
-
if text is not None:
|
|
138
|
-
body["text"] = text
|
|
139
|
-
if highlights is not None:
|
|
140
|
-
body["highlights"] = highlights
|
|
141
|
-
if summary is not None:
|
|
142
|
-
body["summary"] = summary
|
|
143
|
-
if extra:
|
|
144
|
-
body.update(extra)
|
|
145
|
-
return request("/contents", body, timeout=timeout)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def raw(path: str, *, body_json: str, method: str = "POST") -> Dict[str, Any]:
|
|
149
|
-
return request(path, json.loads(body_json), method=method)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def _split_csv(value: Optional[str]) -> Optional[list[str]]:
|
|
153
|
-
if not value:
|
|
154
|
-
return None
|
|
155
|
-
return [part.strip() for part in value.split(",") if part.strip()]
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def _json_or_none(value: Optional[str]) -> Optional[Dict[str, Any]]:
|
|
159
|
-
if not value:
|
|
160
|
-
return None
|
|
161
|
-
return json.loads(value)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def _print_json(value: Any) -> None:
|
|
165
|
-
print(json.dumps(value, ensure_ascii=False, indent=2))
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def main(argv: Optional[list[str]] = None) -> int:
|
|
169
|
-
parser = argparse.ArgumentParser(description="Exa REST API helper")
|
|
170
|
-
sub = parser.add_subparsers(dest="operation", required=True)
|
|
171
|
-
|
|
172
|
-
p = sub.add_parser("search", help="Exa 语义搜索")
|
|
173
|
-
p.add_argument("--query", required=True)
|
|
174
|
-
p.add_argument("--type", default="auto", choices=["auto", "fast", "instant", "deep-lite", "deep", "deep-reasoning"])
|
|
175
|
-
p.add_argument("--num-results", type=int, default=10)
|
|
176
|
-
p.add_argument("--category")
|
|
177
|
-
p.add_argument("--include-domains", help="逗号分隔")
|
|
178
|
-
p.add_argument("--exclude-domains", help="逗号分隔")
|
|
179
|
-
p.add_argument("--start-published-date")
|
|
180
|
-
p.add_argument("--end-published-date")
|
|
181
|
-
p.add_argument("--user-location")
|
|
182
|
-
p.add_argument("--contents-json", help="完整 contents JSON;默认 {\"highlights\": true}")
|
|
183
|
-
p.add_argument("--text", action="store_true", help="请求 contents.text")
|
|
184
|
-
p.add_argument("--summary", action="store_true", help="请求 contents.summary")
|
|
185
|
-
p.add_argument("--fresh", action="store_true", help="设置 contents.maxAgeHours=0 强制实时抓取")
|
|
186
|
-
p.add_argument("--output-schema-json")
|
|
187
|
-
p.add_argument("--system-prompt")
|
|
188
|
-
p.add_argument("--extra-json", help="合并到请求体的额外 JSON")
|
|
189
|
-
|
|
190
|
-
p = sub.add_parser("contents", help="按 URL/ID 获取内容")
|
|
191
|
-
p.add_argument("--ids", required=True, help="逗号分隔 URL 或 Exa result id")
|
|
192
|
-
p.add_argument("--text-json", help="text 顶层参数 JSON;未提供则为 true")
|
|
193
|
-
p.add_argument("--highlights-json")
|
|
194
|
-
p.add_argument("--summary-json")
|
|
195
|
-
p.add_argument("--extra-json")
|
|
196
|
-
|
|
197
|
-
p = sub.add_parser("raw", help="原始 Exa API 调用")
|
|
198
|
-
p.add_argument("--path", required=True)
|
|
199
|
-
p.add_argument("--method", default="POST")
|
|
200
|
-
p.add_argument("--body-json", required=True)
|
|
201
|
-
|
|
202
|
-
args = parser.parse_args(argv)
|
|
203
|
-
try:
|
|
204
|
-
if args.operation == "search":
|
|
205
|
-
contents_value = _json_or_none(args.contents_json)
|
|
206
|
-
if contents_value is None:
|
|
207
|
-
contents_value = {"highlights": True}
|
|
208
|
-
if args.text:
|
|
209
|
-
contents_value["text"] = {"maxCharacters": 8000}
|
|
210
|
-
if args.summary:
|
|
211
|
-
contents_value["summary"] = True
|
|
212
|
-
if args.fresh:
|
|
213
|
-
contents_value["maxAgeHours"] = 0
|
|
214
|
-
out = search(
|
|
215
|
-
args.query,
|
|
216
|
-
search_type=args.type,
|
|
217
|
-
num_results=args.num_results,
|
|
218
|
-
category=args.category,
|
|
219
|
-
include_domains=_split_csv(args.include_domains),
|
|
220
|
-
exclude_domains=_split_csv(args.exclude_domains),
|
|
221
|
-
start_published_date=args.start_published_date,
|
|
222
|
-
end_published_date=args.end_published_date,
|
|
223
|
-
user_location=args.user_location,
|
|
224
|
-
contents=contents_value,
|
|
225
|
-
output_schema=_json_or_none(args.output_schema_json),
|
|
226
|
-
system_prompt=args.system_prompt,
|
|
227
|
-
extra=_json_or_none(args.extra_json),
|
|
228
|
-
)
|
|
229
|
-
elif args.operation == "contents":
|
|
230
|
-
text_value = json.loads(args.text_json) if args.text_json else True
|
|
231
|
-
out = contents(
|
|
232
|
-
_split_csv(args.ids) or [],
|
|
233
|
-
text=text_value,
|
|
234
|
-
highlights=json.loads(args.highlights_json) if args.highlights_json else None,
|
|
235
|
-
summary=json.loads(args.summary_json) if args.summary_json else None,
|
|
236
|
-
extra=_json_or_none(args.extra_json),
|
|
237
|
-
)
|
|
238
|
-
elif args.operation == "raw":
|
|
239
|
-
out = raw(args.path, body_json=args.body_json, method=args.method)
|
|
240
|
-
else:
|
|
241
|
-
parser.error("未知操作")
|
|
242
|
-
return 2
|
|
243
|
-
_print_json(out)
|
|
244
|
-
return 0
|
|
245
|
-
except ServiceError as exc:
|
|
246
|
-
_print_json({"ok": False, "service": "exa", "error": str(exc), "status": exc.status, "details": exc.details})
|
|
247
|
-
return 1
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if __name__ == "__main__":
|
|
251
|
-
sys.exit(main())
|