research-assistant 0.1.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/LICENSE +21 -0
- package/README-ZH.md +115 -0
- package/README.md +124 -0
- package/npm/bin/research-assistant.js +70 -0
- package/npm/scripts/postinstall.js +112 -0
- package/package.json +38 -0
- package/pyproject.toml +84 -0
- package/src/research_assistant/__init__.py +7 -0
- package/src/research_assistant/__main__.py +6 -0
- package/src/research_assistant/assets/researcher_persona.md +94 -0
- package/src/research_assistant/assets/skills/research-assistant/SKILL.md +248 -0
- package/src/research_assistant/cli.py +155 -0
- package/src/research_assistant/commands/__init__.py +7 -0
- package/src/research_assistant/commands/ask.py +54 -0
- package/src/research_assistant/commands/doctor.py +201 -0
- package/src/research_assistant/commands/fetch.py +99 -0
- package/src/research_assistant/commands/locate.py +71 -0
- package/src/research_assistant/commands/search.py +161 -0
- package/src/research_assistant/commands/setup.py +249 -0
- package/src/research_assistant/commands/skills.py +60 -0
- package/src/research_assistant/config.py +258 -0
- package/src/research_assistant/errors.py +106 -0
- package/src/research_assistant/fetch/__init__.py +20 -0
- package/src/research_assistant/fetch/browser.py +613 -0
- package/src/research_assistant/fetch/cfbypass.py +318 -0
- package/src/research_assistant/fetch/html2md.py +44 -0
- package/src/research_assistant/fetch/normal.py +69 -0
- package/src/research_assistant/fetch/search_engine.py +324 -0
- package/src/research_assistant/fetch/snapshot.py +50 -0
- package/src/research_assistant/http.py +118 -0
- package/src/research_assistant/installer.py +270 -0
- package/src/research_assistant/locate/__init__.py +5 -0
- package/src/research_assistant/locate/engine.py +289 -0
- package/src/research_assistant/loginstate/__init__.py +10 -0
- package/src/research_assistant/loginstate/daemon.py +145 -0
- package/src/research_assistant/loginstate/daemon_cli.py +21 -0
- package/src/research_assistant/loginstate/daemonctl.py +116 -0
- package/src/research_assistant/loginstate/extension/background.js +167 -0
- package/src/research_assistant/loginstate/extension/manifest.json +15 -0
- package/src/research_assistant/loginstate/extension/popup.html +31 -0
- package/src/research_assistant/loginstate/extension/popup.js +33 -0
- package/src/research_assistant/output.py +140 -0
- package/src/research_assistant/providers/__init__.py +10 -0
- package/src/research_assistant/providers/base.py +93 -0
- package/src/research_assistant/providers/browser.py +91 -0
- package/src/research_assistant/providers/context7.py +171 -0
- package/src/research_assistant/providers/exa.py +353 -0
- package/src/research_assistant/providers/firecrawl.py +644 -0
- package/src/research_assistant/providers/openai_compat.py +293 -0
- package/src/research_assistant/providers/registry.py +62 -0
- package/src/research_assistant/providers/tavily.py +346 -0
- package/src/research_assistant/proxy.py +58 -0
- package/src/research_assistant/targets.py +66 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""Tavily provider(自写 HTTP,ADR-0010)。
|
|
2
|
+
|
|
3
|
+
API(webReader 查证 docs.tavily.com,2026-07):
|
|
4
|
+
POST https://api.tavily.com/search 鉴权 Authorization: Bearer <key>
|
|
5
|
+
body: query / search_depth(ultra-fast|fast|basic|advanced) /
|
|
6
|
+
topic(general|news|finance) / time_range(day|week|month|year) /
|
|
7
|
+
max_results / chunks_per_source(1-3) / days /
|
|
8
|
+
include_answer(basic|true|advanced) /
|
|
9
|
+
include_raw_content(markdown|text) /
|
|
10
|
+
include_images / include_image_descriptions / include_favicon /
|
|
11
|
+
country / auto_parameters / start_date / end_date /
|
|
12
|
+
include_domains[] / exclude_domains[]
|
|
13
|
+
resp: {results[]{title,url,content,raw_content?,score?}, answer?, images?}
|
|
14
|
+
POST /extract
|
|
15
|
+
body: urls[] / extract_depth(basic|advanced) / format(markdown|text)
|
|
16
|
+
resp: {results[]{url,raw_content,...}, failed_results[]{url,error}}
|
|
17
|
+
POST /map 同步阻塞,最长 150s
|
|
18
|
+
body: url / instructions? / max_depth(1-5) / max_breadth(1-500) / limit /
|
|
19
|
+
select_paths[] / select_domains[] / exclude_paths[] / exclude_domains[] /
|
|
20
|
+
allow_external / timeout(10-150) / include_usage?
|
|
21
|
+
resp: {base_url, results[<url:str>], response_time, usage?, request_id?}
|
|
22
|
+
POST /crawl 同步阻塞,最长 150s
|
|
23
|
+
body: url / instructions? / chunks_per_source(1-5) / max_depth(1-5) /
|
|
24
|
+
max_breadth(1-500) / limit / select_paths[]/select_domains[]/
|
|
25
|
+
exclude_paths[]/exclude_domains[] / allow_external /
|
|
26
|
+
include_images / extract_depth(basic|advanced) / format(markdown|text) /
|
|
27
|
+
include_favicon / timeout(10-150) / include_usage?
|
|
28
|
+
resp: {base_url, results[]{url,raw_content,favicon?}, response_time, usage?, request_id?}
|
|
29
|
+
|
|
30
|
+
对齐 api-contract.md §2.1/§3:raw_content → content。
|
|
31
|
+
map/crawl 是服务端同步阻塞调用,handler 用 server timeout + 15s 覆盖客户端 timeout。
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
from ..config import ProviderConfig
|
|
39
|
+
from ..errors import ArgsError
|
|
40
|
+
from .. import http as http_mod
|
|
41
|
+
from ..proxy import resolve_proxy
|
|
42
|
+
from .base import ArgSpec, Capability, Provider
|
|
43
|
+
from .registry import register
|
|
44
|
+
|
|
45
|
+
DEFAULT_BASE_URL = "https://api.tavily.com"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _split_csv(values: list[str] | None) -> list[str]:
|
|
49
|
+
out: list[str] = []
|
|
50
|
+
for v in values or []:
|
|
51
|
+
for part in str(v).split(","):
|
|
52
|
+
part = part.strip()
|
|
53
|
+
if part:
|
|
54
|
+
out.append(part)
|
|
55
|
+
return out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@register
|
|
59
|
+
class TavilyProvider(Provider):
|
|
60
|
+
type = "tavily"
|
|
61
|
+
command = "tavily"
|
|
62
|
+
aliases = ["tvly"]
|
|
63
|
+
help = "Tavily web search, URL extract, site map, and recursive crawl."
|
|
64
|
+
|
|
65
|
+
def capabilities(self) -> list[Capability]:
|
|
66
|
+
return [
|
|
67
|
+
Capability(
|
|
68
|
+
name="search",
|
|
69
|
+
help="Search the web with Tavily.",
|
|
70
|
+
args=[
|
|
71
|
+
ArgSpec(["query"], kind="positional", help="Search query."),
|
|
72
|
+
ArgSpec(
|
|
73
|
+
["--depth"],
|
|
74
|
+
choices=["ultra-fast", "fast", "basic", "advanced"],
|
|
75
|
+
default="basic",
|
|
76
|
+
help="Search depth (default basic).",
|
|
77
|
+
),
|
|
78
|
+
ArgSpec(["--max-results"], type=int, default=10, help="Max results (default 10)."),
|
|
79
|
+
ArgSpec(
|
|
80
|
+
["--topic"],
|
|
81
|
+
choices=["general", "news", "finance"],
|
|
82
|
+
default="general",
|
|
83
|
+
help="Search topic (default general).",
|
|
84
|
+
),
|
|
85
|
+
ArgSpec(
|
|
86
|
+
["--time-range"],
|
|
87
|
+
choices=["day", "week", "month", "year"],
|
|
88
|
+
help="Restrict to a time range.",
|
|
89
|
+
),
|
|
90
|
+
ArgSpec(
|
|
91
|
+
["--include-answer"],
|
|
92
|
+
choices=["basic", "true", "advanced"],
|
|
93
|
+
help="Include a synthesized answer.",
|
|
94
|
+
),
|
|
95
|
+
ArgSpec(["--chunks-per-source"], type=int, help="Snippets per source (advanced, 1-3)."),
|
|
96
|
+
ArgSpec(["--include-raw-content"], choices=["markdown", "text"], help="Return full body per result."),
|
|
97
|
+
ArgSpec(["--include-domains"], nargs="+", metavar="DOMAIN", help="Restrict to domains."),
|
|
98
|
+
ArgSpec(["--exclude-domains"], nargs="+", metavar="DOMAIN", help="Exclude domains."),
|
|
99
|
+
ArgSpec(["--start-date"], metavar="YYYY-MM-DD", help="Results published on/after this date."),
|
|
100
|
+
ArgSpec(["--end-date"], metavar="YYYY-MM-DD", help="Results published on/before this date."),
|
|
101
|
+
ArgSpec(["--country"], metavar="COUNTRY", help="Boost results from a country (topic=general only)."),
|
|
102
|
+
ArgSpec(["--days"], type=int, help="For news topic: limit to last N days."),
|
|
103
|
+
ArgSpec(["--include-images"], action="store_true", help="Include query-related images."),
|
|
104
|
+
ArgSpec(["--include-image-descriptions"], action="store_true", help="Add a description per image."),
|
|
105
|
+
ArgSpec(["--include-favicon"], action="store_true", help="Include the favicon URL per result."),
|
|
106
|
+
ArgSpec(["--auto-parameters"], action="store_true", help="Let Tavily auto-tune params (2 credits)."),
|
|
107
|
+
],
|
|
108
|
+
handler=self.search,
|
|
109
|
+
),
|
|
110
|
+
Capability(
|
|
111
|
+
name="extract",
|
|
112
|
+
help="Extract clean content from one or more URLs.",
|
|
113
|
+
args=[
|
|
114
|
+
ArgSpec(["urls"], kind="positional", nargs="+", metavar="URL", help="URL(s) to extract."),
|
|
115
|
+
ArgSpec(["--extract-depth"], choices=["basic", "advanced"], default="basic", help="Extract depth."),
|
|
116
|
+
ArgSpec(["--format"], choices=["markdown", "text"], default="markdown", help="Output format."),
|
|
117
|
+
],
|
|
118
|
+
handler=self.extract,
|
|
119
|
+
),
|
|
120
|
+
Capability(
|
|
121
|
+
name="map",
|
|
122
|
+
help="Map a site: list reachable URLs (graph traversal).",
|
|
123
|
+
args=[
|
|
124
|
+
ArgSpec(["url"], kind="positional", help="Root URL to map."),
|
|
125
|
+
ArgSpec(["--instructions"], metavar="TEXT", help="Natural-language guidance for the crawler."),
|
|
126
|
+
ArgSpec(["--max-depth"], type=int, default=1, help="Max crawl depth (1-5)."),
|
|
127
|
+
ArgSpec(["--max-breadth"], type=int, default=10, help="Max links to follow per page (1-500)."),
|
|
128
|
+
ArgSpec(["--limit"], type=int, default=50, help="Total links to process."),
|
|
129
|
+
ArgSpec(["--select-paths"], nargs="+", metavar="REGEX", help="Keep only URLs matching these path regexes."),
|
|
130
|
+
ArgSpec(["--select-domains"], nargs="+", metavar="REGEX", help="Keep only these domains/subdomains."),
|
|
131
|
+
ArgSpec(["--exclude-paths"], nargs="+", metavar="REGEX", help="Drop URLs matching these path regexes."),
|
|
132
|
+
ArgSpec(["--exclude-domains"], nargs="+", metavar="REGEX", help="Drop these domains/subdomains."),
|
|
133
|
+
ArgSpec(["--allow-external"], action="store_true", help="Include external-domain links."),
|
|
134
|
+
ArgSpec(["--timeout"], type=int, default=60, help="Server-side timeout in seconds (10-150)."),
|
|
135
|
+
ArgSpec(["--include-usage"], action="store_true", help="Include credit usage in the response."),
|
|
136
|
+
],
|
|
137
|
+
handler=self.map,
|
|
138
|
+
),
|
|
139
|
+
Capability(
|
|
140
|
+
name="crawl",
|
|
141
|
+
help="Crawl a site recursively and extract each page's content.",
|
|
142
|
+
args=[
|
|
143
|
+
ArgSpec(["url"], kind="positional", help="Root URL to crawl."),
|
|
144
|
+
ArgSpec(["--instructions"], metavar="TEXT", help="Natural-language guidance for the crawler."),
|
|
145
|
+
ArgSpec(["--chunks-per-source"], type=int, default=3, help="Chunks per source (1-5, needs --instructions)."),
|
|
146
|
+
ArgSpec(["--max-depth"], type=int, default=1, help="Max crawl depth (1-5)."),
|
|
147
|
+
ArgSpec(["--max-breadth"], type=int, default=20, help="Max links to follow per page (1-500)."),
|
|
148
|
+
ArgSpec(["--limit"], type=int, default=50, help="Total pages to process."),
|
|
149
|
+
ArgSpec(["--select-paths"], nargs="+", metavar="REGEX", help="Keep only URLs matching these path regexes."),
|
|
150
|
+
ArgSpec(["--select-domains"], nargs="+", metavar="REGEX", help="Keep only these domains/subdomains."),
|
|
151
|
+
ArgSpec(["--exclude-paths"], nargs="+", metavar="REGEX", help="Drop URLs matching these path regexes."),
|
|
152
|
+
ArgSpec(["--exclude-domains"], nargs="+", metavar="REGEX", help="Drop these domains/subdomains."),
|
|
153
|
+
ArgSpec(["--no-external"], action="store_false", default=True, dest="allow_external", help="Follow external links by default; pass to keep crawl site-local."),
|
|
154
|
+
ArgSpec(["--include-images"], action="store_true", help="Include images in results."),
|
|
155
|
+
ArgSpec(["--extract-depth"], choices=["basic", "advanced"], default="basic", help="Per-page extraction depth."),
|
|
156
|
+
ArgSpec(["--format"], choices=["markdown", "text"], default="markdown", help="Content format."),
|
|
157
|
+
ArgSpec(["--include-favicon"], action="store_true", help="Include favicon URL per result."),
|
|
158
|
+
ArgSpec(["--timeout"], type=int, default=60, help="Server-side timeout in seconds (10-150)."),
|
|
159
|
+
ArgSpec(["--include-usage"], action="store_true", help="Include credit usage in the response."),
|
|
160
|
+
],
|
|
161
|
+
handler=self.crawl,
|
|
162
|
+
),
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
def _cfg(self) -> ProviderConfig:
|
|
166
|
+
cfg = self.config.require_provider("tavily")
|
|
167
|
+
if not cfg.api_key:
|
|
168
|
+
raise ArgsError("tavily: 缺少 api_key", provider="tavily")
|
|
169
|
+
if not cfg.base_url:
|
|
170
|
+
cfg.base_url = DEFAULT_BASE_URL
|
|
171
|
+
return cfg
|
|
172
|
+
|
|
173
|
+
async def search(self, args: Any) -> dict[str, Any]:
|
|
174
|
+
cfg = self._cfg()
|
|
175
|
+
body: dict[str, Any] = {
|
|
176
|
+
"query": args.query,
|
|
177
|
+
"search_depth": args.depth,
|
|
178
|
+
"max_results": args.max_results,
|
|
179
|
+
"topic": args.topic,
|
|
180
|
+
}
|
|
181
|
+
if args.time_range:
|
|
182
|
+
body["time_range"] = args.time_range
|
|
183
|
+
if args.include_answer:
|
|
184
|
+
ia = args.include_answer
|
|
185
|
+
body["include_answer"] = True if ia == "true" else ia
|
|
186
|
+
if args.chunks_per_source:
|
|
187
|
+
body["chunks_per_source"] = args.chunks_per_source
|
|
188
|
+
if args.include_raw_content:
|
|
189
|
+
body["include_raw_content"] = args.include_raw_content
|
|
190
|
+
if args.include_domains:
|
|
191
|
+
body["include_domains"] = _split_csv(args.include_domains)
|
|
192
|
+
if args.exclude_domains:
|
|
193
|
+
body["exclude_domains"] = _split_csv(args.exclude_domains)
|
|
194
|
+
if args.start_date:
|
|
195
|
+
body["start_date"] = args.start_date
|
|
196
|
+
if args.end_date:
|
|
197
|
+
body["end_date"] = args.end_date
|
|
198
|
+
if args.country:
|
|
199
|
+
body["country"] = args.country
|
|
200
|
+
if args.days is not None:
|
|
201
|
+
body["days"] = args.days
|
|
202
|
+
if args.include_images:
|
|
203
|
+
body["include_images"] = True
|
|
204
|
+
if args.include_image_descriptions:
|
|
205
|
+
body["include_image_descriptions"] = True
|
|
206
|
+
if args.include_favicon:
|
|
207
|
+
body["include_favicon"] = True
|
|
208
|
+
if args.auto_parameters:
|
|
209
|
+
body["auto_parameters"] = True
|
|
210
|
+
|
|
211
|
+
async with http_mod.make_client(
|
|
212
|
+
proxy_url=resolve_proxy(self.config.proxy.url),
|
|
213
|
+
timeout=cfg.timeout,
|
|
214
|
+
base_url=cfg.base_url,
|
|
215
|
+
headers={"Authorization": f"Bearer {cfg.api_key}"},
|
|
216
|
+
) as client:
|
|
217
|
+
data = await http_mod.request_json(client, "POST", "search", provider="tavily", json_body=body)
|
|
218
|
+
|
|
219
|
+
results = []
|
|
220
|
+
for r in data.get("results", []) or []:
|
|
221
|
+
item: dict[str, Any] = {"url": r.get("url"), "title": r.get("title")}
|
|
222
|
+
if r.get("content"):
|
|
223
|
+
item["content"] = r.get("content")
|
|
224
|
+
if r.get("raw_content"):
|
|
225
|
+
item["raw_content"] = r.get("raw_content")
|
|
226
|
+
if r.get("score") is not None:
|
|
227
|
+
item["score"] = r.get("score")
|
|
228
|
+
if args.include_images and r.get("images"):
|
|
229
|
+
item["images"] = r.get("images")
|
|
230
|
+
results.append(item)
|
|
231
|
+
out: dict[str, Any] = {"results": results}
|
|
232
|
+
if data.get("answer"):
|
|
233
|
+
out["answer"] = data.get("answer")
|
|
234
|
+
if data.get("images"):
|
|
235
|
+
out["images"] = data.get("images")
|
|
236
|
+
return out
|
|
237
|
+
|
|
238
|
+
async def extract(self, args: Any) -> dict[str, Any]:
|
|
239
|
+
cfg = self._cfg()
|
|
240
|
+
urls = _split_csv(args.urls)
|
|
241
|
+
if not urls:
|
|
242
|
+
raise ArgsError("tavily extract: 至少提供一个 url", provider="tavily")
|
|
243
|
+
body = {"urls": urls, "extract_depth": args.extract_depth, "format": args.format}
|
|
244
|
+
async with http_mod.make_client(
|
|
245
|
+
proxy_url=resolve_proxy(self.config.proxy.url),
|
|
246
|
+
timeout=cfg.timeout,
|
|
247
|
+
base_url=cfg.base_url,
|
|
248
|
+
headers={"Authorization": f"Bearer {cfg.api_key}"},
|
|
249
|
+
) as client:
|
|
250
|
+
data = await http_mod.request_json(client, "POST", "extract", provider="tavily", json_body=body)
|
|
251
|
+
|
|
252
|
+
results = []
|
|
253
|
+
for r in data.get("results", []) or []:
|
|
254
|
+
item: dict[str, Any] = {"url": r.get("url"), "content": r.get("raw_content") or ""}
|
|
255
|
+
results.append(item)
|
|
256
|
+
out: dict[str, Any] = {"results": results}
|
|
257
|
+
if data.get("failed_results"):
|
|
258
|
+
out["failed_results"] = data.get("failed_results")
|
|
259
|
+
return out
|
|
260
|
+
|
|
261
|
+
async def map(self, args: Any) -> dict[str, Any]:
|
|
262
|
+
cfg = self._cfg()
|
|
263
|
+
body: dict[str, Any] = {
|
|
264
|
+
"url": args.url,
|
|
265
|
+
"max_depth": args.max_depth,
|
|
266
|
+
"max_breadth": args.max_breadth,
|
|
267
|
+
"limit": args.limit,
|
|
268
|
+
"timeout": args.timeout,
|
|
269
|
+
}
|
|
270
|
+
if args.instructions:
|
|
271
|
+
body["instructions"] = args.instructions
|
|
272
|
+
if args.select_paths:
|
|
273
|
+
body["select_paths"] = args.select_paths
|
|
274
|
+
if args.select_domains:
|
|
275
|
+
body["select_domains"] = args.select_domains
|
|
276
|
+
if args.exclude_paths:
|
|
277
|
+
body["exclude_paths"] = args.exclude_paths
|
|
278
|
+
if args.exclude_domains:
|
|
279
|
+
body["exclude_domains"] = args.exclude_domains
|
|
280
|
+
if args.allow_external:
|
|
281
|
+
body["allow_external"] = True
|
|
282
|
+
if args.include_usage:
|
|
283
|
+
body["include_usage"] = True
|
|
284
|
+
|
|
285
|
+
# map 服务端会阻塞到爬完或 timeout;客户端多给 15s 缓冲,且不低于 provider 默认 timeout
|
|
286
|
+
client_timeout = max(cfg.timeout, float(args.timeout) + 15.0)
|
|
287
|
+
async with http_mod.make_client(
|
|
288
|
+
proxy_url=resolve_proxy(self.config.proxy.url),
|
|
289
|
+
timeout=client_timeout,
|
|
290
|
+
base_url=cfg.base_url,
|
|
291
|
+
headers={"Authorization": f"Bearer {cfg.api_key}"},
|
|
292
|
+
) as client:
|
|
293
|
+
data = await http_mod.request_json(client, "POST", "map", provider="tavily", json_body=body)
|
|
294
|
+
|
|
295
|
+
out: dict[str, Any] = {"base_url": data.get("base_url"), "results": list(data.get("results", []) or [])}
|
|
296
|
+
if data.get("response_time") is not None:
|
|
297
|
+
out["response_time"] = data.get("response_time")
|
|
298
|
+
return out
|
|
299
|
+
|
|
300
|
+
async def crawl(self, args: Any) -> dict[str, Any]:
|
|
301
|
+
cfg = self._cfg()
|
|
302
|
+
body: dict[str, Any] = {
|
|
303
|
+
"url": args.url,
|
|
304
|
+
"chunks_per_source": args.chunks_per_source,
|
|
305
|
+
"max_depth": args.max_depth,
|
|
306
|
+
"max_breadth": args.max_breadth,
|
|
307
|
+
"limit": args.limit,
|
|
308
|
+
"allow_external": bool(args.allow_external),
|
|
309
|
+
"include_images": bool(args.include_images),
|
|
310
|
+
"extract_depth": args.extract_depth,
|
|
311
|
+
"format": args.format,
|
|
312
|
+
"include_favicon": bool(args.include_favicon),
|
|
313
|
+
"timeout": args.timeout,
|
|
314
|
+
}
|
|
315
|
+
if args.instructions:
|
|
316
|
+
body["instructions"] = args.instructions
|
|
317
|
+
if args.select_paths:
|
|
318
|
+
body["select_paths"] = args.select_paths
|
|
319
|
+
if args.select_domains:
|
|
320
|
+
body["select_domains"] = args.select_domains
|
|
321
|
+
if args.exclude_paths:
|
|
322
|
+
body["exclude_paths"] = args.exclude_paths
|
|
323
|
+
if args.exclude_domains:
|
|
324
|
+
body["exclude_domains"] = args.exclude_domains
|
|
325
|
+
if args.include_usage:
|
|
326
|
+
body["include_usage"] = True
|
|
327
|
+
|
|
328
|
+
client_timeout = max(cfg.timeout, float(args.timeout) + 15.0)
|
|
329
|
+
async with http_mod.make_client(
|
|
330
|
+
proxy_url=resolve_proxy(self.config.proxy.url),
|
|
331
|
+
timeout=client_timeout,
|
|
332
|
+
base_url=cfg.base_url,
|
|
333
|
+
headers={"Authorization": f"Bearer {cfg.api_key}"},
|
|
334
|
+
) as client:
|
|
335
|
+
data = await http_mod.request_json(client, "POST", "crawl", provider="tavily", json_body=body)
|
|
336
|
+
|
|
337
|
+
results = []
|
|
338
|
+
for r in data.get("results", []) or []:
|
|
339
|
+
item: dict[str, Any] = {"url": r.get("url"), "content": r.get("raw_content") or ""}
|
|
340
|
+
if r.get("favicon"):
|
|
341
|
+
item["favicon"] = r.get("favicon")
|
|
342
|
+
results.append(item)
|
|
343
|
+
out: dict[str, Any] = {"base_url": data.get("base_url"), "results": results}
|
|
344
|
+
if data.get("response_time") is not None:
|
|
345
|
+
out["response_time"] = data.get("response_time")
|
|
346
|
+
return out
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""代理横切(ADR-0007):所有网络工具前置。
|
|
2
|
+
|
|
3
|
+
逻辑:
|
|
4
|
+
config proxy.url 空(默认)→ 自动检测系统代理并带上
|
|
5
|
+
config proxy.url 显式填 → 用配置代理覆盖自动检测
|
|
6
|
+
|
|
7
|
+
避免"填了代理端口但代理软件未开 → 工具走死代理不通"陷阱——默认零配置即可用。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from urllib.request import getproxies
|
|
14
|
+
|
|
15
|
+
# CLI --proxy 优先级最高,进程内缓存
|
|
16
|
+
_override: str | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def set_override(url: str | None) -> None:
|
|
20
|
+
"""设置本次 CLI 调用的代理覆盖(--proxy flag,横切)。"""
|
|
21
|
+
global _override
|
|
22
|
+
_override = url.strip() if url else None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def detect_system_proxy() -> str:
|
|
26
|
+
"""自动检测系统代理:返回单个 proxy URL(优先 https)。
|
|
27
|
+
|
|
28
|
+
覆盖 Windows/macOS/Linux 的环境变量(HTTP_PROXY/HTTPS_PROXY)与系统设置。
|
|
29
|
+
检测不到返回空串。
|
|
30
|
+
"""
|
|
31
|
+
# 环境变量优先(httpx/requests 同款约定)
|
|
32
|
+
for env_key in ("HTTPS_PROXY", "https_proxy", "ALL_PROXY", "all_proxy"):
|
|
33
|
+
val = os.getenv(env_key)
|
|
34
|
+
if val and val.strip():
|
|
35
|
+
return val.strip()
|
|
36
|
+
sys_proxies = getproxies() or {}
|
|
37
|
+
# getproxies 在 Windows 返回 {'http': '...', 'https': '...'}(已是完整 URL)
|
|
38
|
+
for key in ("https", "http"):
|
|
39
|
+
val = sys_proxies.get(key)
|
|
40
|
+
if val and val.strip():
|
|
41
|
+
return val.strip()
|
|
42
|
+
return ""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def resolve_proxy(config_proxy_url: str = "") -> str:
|
|
46
|
+
"""解析本次应使用的代理 URL(横切入口,所有网络工具调用)。
|
|
47
|
+
|
|
48
|
+
优先级:CLI --proxy 覆盖 > config 显式 > 系统自动检测。
|
|
49
|
+
"none" 为特殊值:强制直连(不走代理)——用于免 key 服务(如 firecrawl)经标记代理出口 IP 被拦时。
|
|
50
|
+
返回空串表示不走代理(直连)。
|
|
51
|
+
"""
|
|
52
|
+
if _override is not None:
|
|
53
|
+
if _override.lower() == "none":
|
|
54
|
+
return "" # 强制直连
|
|
55
|
+
return _override
|
|
56
|
+
if config_proxy_url.strip():
|
|
57
|
+
return config_proxy_url.strip()
|
|
58
|
+
return detect_system_proxy()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""安装目标矩阵(四家 AI Agent,ADR-0008 / data-model §2.3)。
|
|
2
|
+
|
|
3
|
+
每家是一个 Family:skill 路径 + agent 路径 + agent 格式(md/toml/none)。
|
|
4
|
+
managed skill 文件(SKILL.md)相对通用;managed agent 定义因家而异:
|
|
5
|
+
Claude / Cursor → md + frontmatter
|
|
6
|
+
Codex → toml
|
|
7
|
+
Hermes → 无独立文件(researcher 人设融入 skill)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Family:
|
|
17
|
+
name: str
|
|
18
|
+
label: str
|
|
19
|
+
skill_relative: str # 相对 $HOME 的 skill 目录
|
|
20
|
+
agent_relative: str | None # 相对 $HOME 的 agent 文件(None=无独立文件)
|
|
21
|
+
agent_format: str # "md" | "toml" | "none"
|
|
22
|
+
agent_support: str # "static" | "runtime" | "none"
|
|
23
|
+
md_fields: dict # md frontmatter 额外字段(tools/readonly 等)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
SKILL_NAME = "research-assistant"
|
|
27
|
+
AGENT_NAME = "researcher"
|
|
28
|
+
|
|
29
|
+
ALL_FAMILIES: dict[str, Family] = {
|
|
30
|
+
"claude": Family(
|
|
31
|
+
name="claude",
|
|
32
|
+
label="Claude Code",
|
|
33
|
+
skill_relative=f".claude/skills/{SKILL_NAME}",
|
|
34
|
+
agent_relative=".claude/agents/researcher.md",
|
|
35
|
+
agent_format="md",
|
|
36
|
+
agent_support="static",
|
|
37
|
+
md_fields={"tools": "WebFetch, Bash", "model": "inherit"},
|
|
38
|
+
),
|
|
39
|
+
"cursor": Family(
|
|
40
|
+
name="cursor",
|
|
41
|
+
label="Cursor",
|
|
42
|
+
skill_relative=f".cursor/skills/{SKILL_NAME}",
|
|
43
|
+
agent_relative=".cursor/agents/researcher.md",
|
|
44
|
+
agent_format="md",
|
|
45
|
+
agent_support="static",
|
|
46
|
+
md_fields={"readonly": "false", "model": "inherit"},
|
|
47
|
+
),
|
|
48
|
+
"codex": Family(
|
|
49
|
+
name="codex",
|
|
50
|
+
label="Codex",
|
|
51
|
+
skill_relative=f".agents/skills/{SKILL_NAME}", # ~/.agents/skills/(~/.codex/skills deprecated)
|
|
52
|
+
agent_relative=".codex/agents/researcher.toml",
|
|
53
|
+
agent_format="toml",
|
|
54
|
+
agent_support="static",
|
|
55
|
+
md_fields={},
|
|
56
|
+
),
|
|
57
|
+
"hermes": Family(
|
|
58
|
+
name="hermes",
|
|
59
|
+
label="Hermes Agent",
|
|
60
|
+
skill_relative=f".hermes/skills/{SKILL_NAME}",
|
|
61
|
+
agent_relative=None, # 无独立 agent 文件
|
|
62
|
+
agent_format="none",
|
|
63
|
+
agent_support="runtime", # 运行时 delegate_task
|
|
64
|
+
md_fields={},
|
|
65
|
+
),
|
|
66
|
+
}
|