argo-search 2.9.7 → 2.9.8
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/README.en.md +58 -145
- package/README.es.md +58 -145
- package/README.ja.md +64 -151
- package/README.ko.md +63 -150
- package/README.md +65 -154
- package/SKILL.md +3 -3
- package/assets/readme/hero.svg +2 -2
- package/backends/domain_profiles.json +48 -0
- package/backends/engine_registry.yaml +118 -4
- package/backends/quota_profiles.json +92 -12
- package/backends/source_types_cn.json +27 -1
- package/bin/argo +74 -5
- package/bin/argo.js +33 -2
- package/config.yaml +219 -60
- package/docs/ENGINE_CATALOG.md +50 -39
- package/docs/MCP_SETUP.md +3 -1
- package/docs/RELEASE_NOTES_v2.9.8.md +73 -0
- package/engines/specs/cfets.yaml +41 -0
- package/engines/specs/defillama.yaml +38 -0
- package/engines/specs/firecrawl.yaml +1 -1
- package/engines/specs/fxtwitter.yaml +5 -0
- package/engines/specs/gleif.yaml +55 -0
- package/engines/specs/hkex.yaml +40 -0
- package/engines/specs/keenable_free.yaml +36 -0
- package/engines/specs/shfe.yaml +39 -0
- package/engines/specs/stackexchange.yaml +2 -2
- package/engines/specs/tavily_free.yaml +56 -0
- package/engines/specs/yuanbao.yaml +93 -0
- package/mcp/clients.yaml +32 -0
- package/package.json +6 -2
- package/packages/dsh-plugin/README.md +23 -5
- package/packages/dsh-plugin/dsh/client.js +304 -15
- package/packages/dsh-plugin/dsh/diagnostics.js +54 -0
- package/packages/dsh-plugin/dsh/index.js +112 -1875
- package/packages/dsh-plugin/dsh/native-tools.mjs +7 -1
- package/packages/dsh-plugin/dsh/runner.js +666 -0
- package/packages/dsh-plugin/dsh/wide-research.js +878 -0
- package/packages/dsh-plugin/package.json +2 -2
- package/packages/dsh-plugin/scripts/sync-local.sh +70 -0
- package/packages/dsh-plugin/test/client.test.js +175 -1
- package/packages/dsh-plugin/test/diagnostics.test.js +28 -0
- package/packages/dsh-plugin/test/index.test.js +44 -0
- package/scripts/adaptive.py +24 -1
- package/scripts/article_meta.py +131 -0
- package/scripts/budget.py +247 -0
- package/scripts/budget_cli.py +93 -0
- package/scripts/build_release_assets.sh +65 -0
- package/scripts/cache.py +60 -72
- package/scripts/ci_drift_gate.py +90 -0
- package/scripts/content_boundary.py +46 -0
- package/scripts/content_signals.py +26 -5
- package/scripts/cross_lingual_trace.py +293 -0
- package/scripts/dispatch_result.py +98 -0
- package/scripts/engine_dispatch.py +49 -86
- package/scripts/engine_env.py +5 -0
- package/scripts/engine_families.py +6 -0
- package/scripts/engine_policy.py +7 -7
- package/scripts/engines.py +21 -0
- package/scripts/engines_base.py +3 -3
- package/scripts/engines_builders.py +12 -0
- package/scripts/engines_builders_batch10.py +48 -3
- package/scripts/engines_builders_batch14.py +656 -0
- package/scripts/engines_builders_batch15.py +249 -0
- package/scripts/engines_builders_keyless.py +111 -0
- package/scripts/engines_builders_tech.py +0 -1
- package/scripts/evidence.py +31 -3
- package/scripts/evidence_loop.py +54 -22
- package/scripts/fetch_v3.py +25 -0
- package/scripts/gen_routing_snapshot.py +11 -0
- package/scripts/install.ps1 +18 -0
- package/scripts/install.sh +16 -1
- package/scripts/job.py +3 -3
- package/scripts/local_seek.py +41 -5
- package/scripts/macro_countries.py +15 -0
- package/scripts/matrix_search_eval.py +82 -0
- package/scripts/mcp_compact.py +234 -0
- package/scripts/mcp_handlers.py +60 -642
- package/scripts/mcp_launch.sh +10 -8
- package/scripts/mcp_local.py +241 -0
- package/scripts/mcp_server.py +19 -14
- package/scripts/mcp_setup.py +33 -9
- package/scripts/mcp_tools.py +1 -0
- package/scripts/mcp_transport.py +1 -1
- package/scripts/mcp_warm.py +127 -0
- package/scripts/output_transparency.py +200 -0
- package/scripts/paid_policy.py +98 -0
- package/scripts/quota.py +158 -13
- package/scripts/recovery_runner.py +150 -0
- package/scripts/replay_eval.py +18 -0
- package/scripts/research.py +37 -21
- package/scripts/research_expand.py +24 -1
- package/scripts/retry_policy.py +76 -0
- package/scripts/route.py +6 -6
- package/scripts/route_cache.py +18 -0
- package/scripts/route_combo.py +7 -0
- package/scripts/route_domains.py +32 -19
- package/scripts/search.py +15 -27
- package/scripts/search_attribution.py +100 -3
- package/scripts/search_cli.py +24 -11
- package/scripts/search_entry.py +7 -1
- package/scripts/search_output.py +99 -15
- package/scripts/search_pipeline.py +42 -193
- package/scripts/search_types.py +107 -9
- package/scripts/semantic_evidence.py +179 -3
- package/scripts/serp_guard.py +115 -7
- package/scripts/serp_kill_log.py +25 -2
- package/scripts/stats_cli.py +13 -1
- package/scripts/wide_research.py +45 -0
- package/scripts/zhihu_quota.py +139 -0
- package/sub-skills/local-search/bm25.py +27 -3
- package/sub-skills/local-search/cli_runner.py +331 -0
- package/sub-skills/local-search/parsers.py +328 -0
- package/sub-skills/local-search/search_v3.py +31 -674
- package/sub-skills/local-seek/scripts/seek_rank.py +7 -3
- package/assets/readme/settings-keys.png +0 -0
- package/assets/readme/settings-tools.png +0 -0
- package/scripts/image_vision/vision_probe +0 -0
- package/scripts/local_engines.py +0 -321
- package/sub-skills/ego-search/references/browser-runtime.md +0 -43
- package/sub-skills/ego-search/references/install.md +0 -67
- package/sub-skills/ego-search/references/original-ego-upgrade.md +0 -51
- package/sub-skills/ego-search/tests/test_query_normalize.py +0 -68
- package/sub-skills/js-run/tests/fixtures/challenge_like.js +0 -27
- package/sub-skills/js-run/tests/test_jsrun.py +0 -165
- package/sub-skills/local-seek/references/auto-behaviors.md +0 -24
- package/sub-skills/local-seek/references/fd-mdfind-recipes.md +0 -62
- package/sub-skills/local-seek/references/git-integration.md +0 -54
- package/sub-skills/local-seek/references/remote-handoff.md +0 -46
- package/sub-skills/local-seek/references/rg-recipes.md +0 -96
- package/sub-skills/local-seek/references/strategies.md +0 -52
- package/sub-skills/local-seek/references/structural-search.md +0 -59
- package/sub-skills/local-seek/tests/test_content_rank.py +0 -222
- package/sub-skills/local-seek/tests/test_determinism_20261006.py +0 -145
- package/sub-skills/local-seek/tests/test_noise_tier.py +0 -101
- package/sub-skills/local-seek/tests/test_pcre2_probe.py +0 -136
- package/sub-skills/local-seek/tests/test_rank_pinyin.py +0 -77
- package/sub-skills/local-seek/tests/test_regression_20261003.py +0 -169
- package/sub-skills/local-seek/tests/test_regression_20261006.py +0 -146
- package/sub-skills/local-seek/tests/test_seek_degradation.py +0 -111
- package/sub-skills/local-seek/tests/test_seek_fts.py +0 -313
package/scripts/mcp_launch.sh
CHANGED
|
@@ -38,15 +38,17 @@ set +a
|
|
|
38
38
|
_ARGO_PASSTHROUGH_KEYS="ALL_PROXY ANYSEARCH_API_KEY ARGO_ANYSEARCH_API_KEY ARGO_BOCHA_API_KEY\
|
|
39
39
|
ARGO_BRAVE_API_KEY ARGO_BYTED_API_KEY ARGO_EASTMONEY_APIKEY\
|
|
40
40
|
ARGO_EXA_API_KEY ARGO_FELO_API_KEY ARGO_FIRECRAWL_API_KEY\
|
|
41
|
-
ARGO_GITHUB_TOKEN ARGO_METASO_API_KEY
|
|
42
|
-
ARGO_PROXY ARGO_QWEATHER_KEY
|
|
41
|
+
ARGO_GITHUB_TOKEN ARGO_JEV_API_KEY ARGO_METASO_API_KEY\
|
|
42
|
+
ARGO_OCTEN_API_KEY ARGO_PROXY ARGO_QWEATHER_KEY\
|
|
43
|
+
ARGO_TAVILY_API_KEY TYPESAFE_API_KEY\
|
|
43
44
|
ARGO_WEB_SEARCH_API_KEY ARGO_WEREAD_API_KEY\
|
|
44
|
-
ARGO_WOLFRAM_APPID ARGO_ZHIHU_ACCESS_SECRET
|
|
45
|
-
BRAVE_API_KEY EASTMONEY_APIKEY EXA_API_KEY
|
|
46
|
-
FIRECRAWL_API_KEY GITHUB_TOKEN HTTPS_PROXY
|
|
47
|
-
METASO_API_KEY NO_PROXY OCTEN_API_KEY
|
|
48
|
-
TAVILY_API_KEY
|
|
49
|
-
WOLFRAM_APPID
|
|
45
|
+
ARGO_WOLFRAM_APPID ARGO_YUANBAO_API_KEY ARGO_ZHIHU_ACCESS_SECRET\
|
|
46
|
+
BOCHA_API_KEY BRAVE_API_KEY EASTMONEY_APIKEY EXA_API_KEY\
|
|
47
|
+
FELO_API_KEY FIRECRAWL_API_KEY GITHUB_TOKEN HTTPS_PROXY\
|
|
48
|
+
HTTP_PROXY METASO_API_KEY NO_PROXY OCTEN_API_KEY\
|
|
49
|
+
QWEATHER_KEY TAVILY_API_KEY TENCENTCLOUD_WSA_APIKEY\
|
|
50
|
+
WEB_SEARCH_API_KEY WEREAD_API_KEY WOLFRAM_APPID\
|
|
51
|
+
YUANBAO_API_KEY ZHIHU_ACCESS_SECRET"
|
|
50
52
|
|
|
51
53
|
# launchctl 兜底(macOS):旧实现逐键 spawn `launchctl getenv`——实测每次
|
|
52
54
|
# 4-20ms,名单里 ~30 个未设键让每个宿主会话白付百毫秒级纯开销。改为一次
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""mcp_local.py — MCP 本地文件检索、受限预览与可复算沙箱。
|
|
3
|
+
|
|
4
|
+
拆分自 mcp_handlers.py:隔离本地文件系统访问、白名单审查、seek 定位与安全执行沙箱。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from mcp_compact import _trim_snippet
|
|
16
|
+
|
|
17
|
+
_LOCAL_READ_ALLOWED_EXTS = {
|
|
18
|
+
".md", ".txt", ".csv", ".tsv", ".json", ".yaml", ".yml", ".log",
|
|
19
|
+
".py", ".js", ".ts", ".html", ".xml", ".rst", ".ipynb",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _local_read_whitelist() -> list[str]:
|
|
24
|
+
"""白名单目录:ARGO_LOCAL_READ_DIRS(逗号分隔);没配置就返回空,不放行。"""
|
|
25
|
+
raw = (os.environ.get("ARGO_LOCAL_READ_DIRS") or "").strip()
|
|
26
|
+
if not raw:
|
|
27
|
+
return []
|
|
28
|
+
return [os.path.realpath(os.path.expanduser(d))
|
|
29
|
+
for d in raw.split(",") if d.strip()]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _local_read_preview(
|
|
33
|
+
path: str,
|
|
34
|
+
max_chars: int = 4000,
|
|
35
|
+
line_start: int | None = None,
|
|
36
|
+
line_end: int | None = None,
|
|
37
|
+
) -> tuple[str, dict[str, Any]]:
|
|
38
|
+
"""白名单内文本文件预览(行窗口或字符截断);越权/二进制拒绝。"""
|
|
39
|
+
root = os.path.realpath(os.path.expanduser(path))
|
|
40
|
+
allowed = _local_read_whitelist()
|
|
41
|
+
if not allowed:
|
|
42
|
+
raise PermissionError(
|
|
43
|
+
"本地读取未配置白名单:设 ARGO_LOCAL_READ_DIRS=目录1,目录2(没配置就不放行)"
|
|
44
|
+
)
|
|
45
|
+
if not any(root == d or root.startswith(d + os.sep) for d in allowed):
|
|
46
|
+
raise PermissionError(f"路径不在白名单目录内: {path}")
|
|
47
|
+
if not os.path.isfile(root):
|
|
48
|
+
raise FileNotFoundError(f"文件不存在: {path}")
|
|
49
|
+
ext = os.path.splitext(root)[1].lower()
|
|
50
|
+
if ext not in _LOCAL_READ_ALLOWED_EXTS:
|
|
51
|
+
raise PermissionError(f"仅支持文本类文件预览({ext} 不支持;pdf 走其他路径)")
|
|
52
|
+
size = os.path.getsize(root)
|
|
53
|
+
if size > 4 * 1024 * 1024:
|
|
54
|
+
raise PermissionError("文件超过 4MB,拒绝预览(请缩小范围或改用搜索)")
|
|
55
|
+
|
|
56
|
+
with open(root, encoding="utf-8", errors="replace") as f:
|
|
57
|
+
full = f.read()
|
|
58
|
+
total_lines = full.count("\n") + (1 if full and not full.endswith("\n") else 0)
|
|
59
|
+
if line_start is not None or line_end is not None:
|
|
60
|
+
start = max(1, int(line_start or 1))
|
|
61
|
+
end = int(line_end) if line_end else start + 200
|
|
62
|
+
lines = full.splitlines(keepends=True)[(start - 1): end]
|
|
63
|
+
content = "".join(lines)
|
|
64
|
+
lines_slice = (start, start + len(lines) - 1)
|
|
65
|
+
else:
|
|
66
|
+
content = full
|
|
67
|
+
lines_slice = None
|
|
68
|
+
|
|
69
|
+
truncated = len(content) > max_chars
|
|
70
|
+
if truncated:
|
|
71
|
+
content = content[:max_chars]
|
|
72
|
+
return content, {
|
|
73
|
+
"path": root,
|
|
74
|
+
"chars": len(content),
|
|
75
|
+
"lines": (lines_slice[1] - lines_slice[0] + 1) if lines_slice else None,
|
|
76
|
+
"total_lines": total_lines,
|
|
77
|
+
"is_preview": truncated or lines_slice is not None,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _seek_py() -> str:
|
|
82
|
+
"""定位 local-seek 的 seek.py(安装感知、唯一来源,见 seek_locator)。"""
|
|
83
|
+
from seek_locator import resolve_seek_py
|
|
84
|
+
return resolve_seek_py()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def handle_local_search(
|
|
88
|
+
arguments: dict[str, Any],
|
|
89
|
+
pretty: bool = False,
|
|
90
|
+
ok_fn=None,
|
|
91
|
+
clamp_int_fn=None,
|
|
92
|
+
lazy_cached_fn=None,
|
|
93
|
+
) -> dict[str, Any]:
|
|
94
|
+
"""argo_local_search 实现:本地文件搜索。"""
|
|
95
|
+
query = str(arguments.get("query", "")).strip()
|
|
96
|
+
|
|
97
|
+
def _local_err(msg: str) -> dict:
|
|
98
|
+
return ok_fn({"query": query, "engine": "local_files", "count": 0, "results": [],
|
|
99
|
+
"errors": [msg]}, pretty=pretty)
|
|
100
|
+
|
|
101
|
+
if not query:
|
|
102
|
+
return _local_err("query 不能为空")
|
|
103
|
+
path = str(arguments.get("path", "~"))
|
|
104
|
+
max_results = clamp_int_fn(arguments.get("max_results", 5), 5, 1, 20)
|
|
105
|
+
exact = bool(arguments.get("exact", False))
|
|
106
|
+
|
|
107
|
+
# 宽泛根守卫
|
|
108
|
+
try:
|
|
109
|
+
if lazy_cached_fn("local_seek")._is_broad_local_root(os.path.expanduser(path)):
|
|
110
|
+
return _local_err("路径过宽(home / 根目录),扫全盘既慢又失准,已拒绝;请传具体目录")
|
|
111
|
+
except Exception as e:
|
|
112
|
+
sys.stderr.write(
|
|
113
|
+
f"[argo-mcp] local_search 宽泛根守卫不可用({type(e).__name__}),fail-open\n")
|
|
114
|
+
|
|
115
|
+
payload = None
|
|
116
|
+
try:
|
|
117
|
+
from local_seek import seek_query_payload
|
|
118
|
+
payload = seek_query_payload(query, os.path.expanduser(path),
|
|
119
|
+
max_results, exact=exact, time_budget=15)
|
|
120
|
+
except Exception:
|
|
121
|
+
payload = None
|
|
122
|
+
|
|
123
|
+
if payload is None:
|
|
124
|
+
seek_py = _seek_py()
|
|
125
|
+
|
|
126
|
+
cmd = [sys.executable, seek_py, query, "--path", path, "--json",
|
|
127
|
+
"--max", str(max_results)]
|
|
128
|
+
if exact:
|
|
129
|
+
cmd.append("--exact")
|
|
130
|
+
try:
|
|
131
|
+
# 兼容测试 patch mcp_handlers.subprocess.run
|
|
132
|
+
runner = subprocess.run
|
|
133
|
+
try:
|
|
134
|
+
import mcp_handlers
|
|
135
|
+
runner = getattr(mcp_handlers.subprocess, "run", subprocess.run)
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
proc = runner(
|
|
140
|
+
cmd, capture_output=True, text=True,
|
|
141
|
+
encoding="utf-8", errors="replace", timeout=15,
|
|
142
|
+
env={**os.environ, "PYTHONUTF8": "1"})
|
|
143
|
+
except Exception as e:
|
|
144
|
+
return _local_err(f"本地搜索执行失败: {e}")
|
|
145
|
+
if proc.returncode != 0:
|
|
146
|
+
msg = (proc.stdout or proc.stderr or "").strip() or "本地搜索无结果"
|
|
147
|
+
return _local_err(msg)
|
|
148
|
+
try:
|
|
149
|
+
payload = json.loads(proc.stdout)
|
|
150
|
+
except Exception:
|
|
151
|
+
return _local_err("本地搜索输出解析失败")
|
|
152
|
+
|
|
153
|
+
mode = payload.get("mode", "fast")
|
|
154
|
+
score = 0.9 if mode == "fast" else 0.7
|
|
155
|
+
results = []
|
|
156
|
+
for r in payload.get("results") or []:
|
|
157
|
+
abspath = os.path.abspath(r.get("path", ""))
|
|
158
|
+
line = r.get("line") or 0
|
|
159
|
+
results.append({
|
|
160
|
+
"title": abspath,
|
|
161
|
+
"url": f"file://{abspath}" + (f"#L{line}" if line else ""),
|
|
162
|
+
"snippet": _trim_snippet(r.get("snippet"), 120),
|
|
163
|
+
"source": "local_files", "score": score,
|
|
164
|
+
})
|
|
165
|
+
return ok_fn({
|
|
166
|
+
"query": query, "engine": "local_files", "engines_used": ["local_files"],
|
|
167
|
+
"count": len(results), "elapsed_ms": payload.get("elapsed_ms"),
|
|
168
|
+
"cached": False, "mode": mode, "depth": None,
|
|
169
|
+
"results": results, "errors": [],
|
|
170
|
+
}, pretty=pretty)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def handle_local_read(
|
|
174
|
+
arguments: dict[str, Any],
|
|
175
|
+
pretty: bool = False,
|
|
176
|
+
ok_fn=None,
|
|
177
|
+
int_param_fn=None,
|
|
178
|
+
) -> dict[str, Any]:
|
|
179
|
+
"""argo_local_read 实现:白名单本地文本预览。"""
|
|
180
|
+
raw_path = str(arguments.get("path", "")).strip()
|
|
181
|
+
max_chars = max(200, min(int_param_fn(arguments, "max_chars", 4000), 20000))
|
|
182
|
+
line_start = arguments.get("line_start")
|
|
183
|
+
line_end = arguments.get("line_end")
|
|
184
|
+
if not raw_path:
|
|
185
|
+
return ok_fn({"path": "", "content": "", "errors": ["path 不能为空"]}, pretty=pretty)
|
|
186
|
+
try:
|
|
187
|
+
content, meta = _local_read_preview(
|
|
188
|
+
raw_path, max_chars=max_chars,
|
|
189
|
+
line_start=line_start, line_end=line_end,
|
|
190
|
+
)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
return ok_fn({"path": raw_path, "content": "",
|
|
193
|
+
"errors": [str(e)]}, pretty=pretty)
|
|
194
|
+
return ok_fn({
|
|
195
|
+
"path": meta["path"],
|
|
196
|
+
"content": content,
|
|
197
|
+
"chars": meta["chars"],
|
|
198
|
+
"lines": meta["lines"],
|
|
199
|
+
"total_lines": meta["total_lines"],
|
|
200
|
+
"is_preview": meta["is_preview"],
|
|
201
|
+
"errors": [],
|
|
202
|
+
}, pretty=pretty)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def handle_recompute(
|
|
206
|
+
arguments: dict[str, Any],
|
|
207
|
+
pretty: bool = False,
|
|
208
|
+
ok_fn=None,
|
|
209
|
+
clamp_int_fn=None,
|
|
210
|
+
) -> dict[str, Any]:
|
|
211
|
+
"""argo_recompute 实现:可复算执行器。"""
|
|
212
|
+
script = str(arguments.get("script", "")).strip()
|
|
213
|
+
inputs_raw = arguments.get("file_inputs")
|
|
214
|
+
inputs = []
|
|
215
|
+
if isinstance(inputs_raw, str) and inputs_raw.strip():
|
|
216
|
+
try:
|
|
217
|
+
inputs = json.loads(inputs_raw)
|
|
218
|
+
except ValueError:
|
|
219
|
+
inputs = []
|
|
220
|
+
elif isinstance(inputs_raw, list):
|
|
221
|
+
inputs = inputs_raw
|
|
222
|
+
elif inputs_raw:
|
|
223
|
+
try:
|
|
224
|
+
inputs = json.loads(inputs_raw)
|
|
225
|
+
except (ValueError, TypeError):
|
|
226
|
+
inputs = []
|
|
227
|
+
timeout_s = clamp_int_fn(arguments.get("timeout_s", 30), 30, 1, 120)
|
|
228
|
+
max_mem_mb = clamp_int_fn(arguments.get("max_mem_mb", 512), 512, 64, 4096)
|
|
229
|
+
allow_exec = bool(arguments.get("allow_exec", False))
|
|
230
|
+
if not script:
|
|
231
|
+
return ok_fn({"ok": False, "skipped_reason": "script 为空"}, pretty=pretty)
|
|
232
|
+
try:
|
|
233
|
+
from recompute import run_recompute
|
|
234
|
+
result = run_recompute(
|
|
235
|
+
script, inputs or None,
|
|
236
|
+
timeout_s=timeout_s, max_mem_mb=max_mem_mb,
|
|
237
|
+
allow_exec=allow_exec,
|
|
238
|
+
)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
result = {"ok": False, "skipped_reason": f"执行器异常: {e}"}
|
|
241
|
+
return ok_fn(result, pretty=pretty)
|
package/scripts/mcp_server.py
CHANGED
|
@@ -137,18 +137,17 @@ def call_mode(tool: str, payload: str) -> int:
|
|
|
137
137
|
|
|
138
138
|
# ── 入口 ──────────────────────────────────────────────────────────────────────
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
def main(argv: list[str] | None = None) -> int:
|
|
141
|
+
args = sys.argv[1:] if argv is None else argv
|
|
141
142
|
# Windows GBK 防线(PEP 540):非 UTF-8 模式直接跑本文件时,重新以
|
|
142
143
|
# -X utf8 启动自己,保证中文 JSON 读取与 stderr 输出不因控制台编码崩。
|
|
143
144
|
if not sys.flags.utf8_mode and os.name == "nt":
|
|
144
145
|
import subprocess as _sp
|
|
145
146
|
_rc = _sp.run(
|
|
146
|
-
[sys.executable, "-X", "utf8", __file__, *
|
|
147
|
+
[sys.executable, "-X", "utf8", __file__, *args],
|
|
147
148
|
env={**os.environ, "PYTHONUTF8": "1"},
|
|
148
149
|
).returncode
|
|
149
|
-
|
|
150
|
-
# 伪装成成功,调用方(DSH 原生工具/脚本)据此渲染必然说谎
|
|
151
|
-
sys.exit(_rc)
|
|
150
|
+
return _rc
|
|
152
151
|
# env 文件 → os.environ 同步(只填缺失,不覆盖已有值):非 mcp_launch.sh
|
|
153
152
|
# 启动(直跑本文件/裸宿主直连)时密钥仍能到位,读取方零改动
|
|
154
153
|
try:
|
|
@@ -165,7 +164,7 @@ if __name__ == "__main__":
|
|
|
165
164
|
# 未知旗标显式报错(exit 2):此前任何拼错的 flag 都静默落进 stdio 模式
|
|
166
165
|
# ——stdio 挂着等 stdin,调用方看到的是「卡死」而不是「参数错了」。
|
|
167
166
|
_skip = 0
|
|
168
|
-
for _a in
|
|
167
|
+
for _a in args:
|
|
169
168
|
if _skip:
|
|
170
169
|
_skip -= 1
|
|
171
170
|
continue
|
|
@@ -174,16 +173,22 @@ if __name__ == "__main__":
|
|
|
174
173
|
continue
|
|
175
174
|
if _a.startswith("-") and _a != "--test":
|
|
176
175
|
print(_dumps({"error": f"未知参数: {_a}(支持 --test / --call <tool> [json];无参数 = stdio 服务)"}))
|
|
177
|
-
|
|
178
|
-
if "--test" in
|
|
176
|
+
return 2
|
|
177
|
+
if "--test" in args:
|
|
179
178
|
test_mode()
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
179
|
+
return 0
|
|
180
|
+
elif "--call" in args:
|
|
181
|
+
_i = args.index("--call")
|
|
182
|
+
_tool = args[_i + 1] if len(args) > _i + 1 else ""
|
|
183
183
|
if not _tool:
|
|
184
184
|
print(_dumps({"error": "usage: mcp_server.py --call <tool> [json-args]"}))
|
|
185
|
-
|
|
186
|
-
_payload =
|
|
187
|
-
|
|
185
|
+
return 2
|
|
186
|
+
_payload = args[_i + 2] if len(args) > _i + 2 else "{}"
|
|
187
|
+
return call_mode(_tool, _payload)
|
|
188
188
|
else:
|
|
189
189
|
run_stdio()
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
sys.exit(main())
|
package/scripts/mcp_setup.py
CHANGED
|
@@ -87,12 +87,36 @@ def _resolve(path_str: str) -> Path:
|
|
|
87
87
|
return p
|
|
88
88
|
|
|
89
89
|
|
|
90
|
+
def _client_config_path(client: dict[str, Any]) -> str:
|
|
91
|
+
"""获取当前操作系统对应的配置文件路径声明。"""
|
|
92
|
+
plat = sys.platform
|
|
93
|
+
if plat == "darwin" and "config_path_darwin" in client:
|
|
94
|
+
return client["config_path_darwin"]
|
|
95
|
+
if plat == "win32" and "config_path_win32" in client:
|
|
96
|
+
return client["config_path_win32"]
|
|
97
|
+
if plat.startswith("linux") and "config_path_linux" in client:
|
|
98
|
+
return client["config_path_linux"]
|
|
99
|
+
return client.get("config_path", "")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _client_detects(client: dict[str, Any]) -> list[str]:
|
|
103
|
+
"""获取当前操作系统对应的客户端探测路径列表。"""
|
|
104
|
+
plat = sys.platform
|
|
105
|
+
if plat == "darwin" and "detect_darwin" in client:
|
|
106
|
+
return client["detect_darwin"]
|
|
107
|
+
if plat == "win32" and "detect_win32" in client:
|
|
108
|
+
return client["detect_win32"]
|
|
109
|
+
if plat.startswith("linux") and "detect_linux" in client:
|
|
110
|
+
return client["detect_linux"]
|
|
111
|
+
return client.get("detect") or []
|
|
112
|
+
|
|
113
|
+
|
|
90
114
|
def detect(client: dict[str, Any]) -> bool:
|
|
91
115
|
"""客户端是否已安装(detect 路径任一存在)。无 detect 字段则看 config_path 父目录。"""
|
|
92
|
-
detects = client
|
|
116
|
+
detects = _client_detects(client)
|
|
93
117
|
if detects:
|
|
94
118
|
return any(_resolve(d).exists() for d in detects)
|
|
95
|
-
return _resolve(client
|
|
119
|
+
return _resolve(_client_config_path(client)).parent.exists()
|
|
96
120
|
|
|
97
121
|
|
|
98
122
|
# ── MCP 命令模板 ────────────────────────────────────────────────────────
|
|
@@ -168,7 +192,7 @@ def _json_read(path: Path) -> dict[str, Any]:
|
|
|
168
192
|
|
|
169
193
|
def _inject_json(client: dict[str, Any], entry: dict[str, Any]) -> str:
|
|
170
194
|
"""JSON 客户端注入:合并 entry,保留未改动子树。"""
|
|
171
|
-
path = _resolve(client
|
|
195
|
+
path = _resolve(_client_config_path(client))
|
|
172
196
|
config = _json_read(path)
|
|
173
197
|
servers_key = client["servers_key"]
|
|
174
198
|
if not isinstance(config.get(servers_key), dict):
|
|
@@ -179,7 +203,7 @@ def _inject_json(client: dict[str, Any], entry: dict[str, Any]) -> str:
|
|
|
179
203
|
|
|
180
204
|
def _remove_json(client: dict[str, Any]) -> str | None:
|
|
181
205
|
"""JSON 客户端移除 argo entry。返回新内容;无 entry 返回 None。"""
|
|
182
|
-
path = _resolve(client
|
|
206
|
+
path = _resolve(_client_config_path(client))
|
|
183
207
|
if not path.exists():
|
|
184
208
|
return None
|
|
185
209
|
config = _json_read(path)
|
|
@@ -216,7 +240,7 @@ def _toml_block(client: dict[str, Any], entry: dict[str, Any]) -> str:
|
|
|
216
240
|
|
|
217
241
|
def _inject_toml(client: dict[str, Any], entry: dict[str, Any]) -> str:
|
|
218
242
|
"""TOML 客户端注入:行级 append section。已存在则报错(不覆盖,避免破坏)。"""
|
|
219
|
-
path = _resolve(client
|
|
243
|
+
path = _resolve(_client_config_path(client))
|
|
220
244
|
existing = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
221
245
|
servers_key = client["servers_key"]
|
|
222
246
|
marker = f"[{servers_key}.{ENTRY_NAME}]"
|
|
@@ -232,7 +256,7 @@ def _inject_toml(client: dict[str, Any], entry: dict[str, Any]) -> str:
|
|
|
232
256
|
|
|
233
257
|
def _remove_toml(client: dict[str, Any]) -> str | None:
|
|
234
258
|
"""TOML 客户端移除:删除 [mcp_servers.argo] section。"""
|
|
235
|
-
path = _resolve(client
|
|
259
|
+
path = _resolve(_client_config_path(client))
|
|
236
260
|
if not path.exists():
|
|
237
261
|
return None
|
|
238
262
|
text = path.read_text(encoding="utf-8")
|
|
@@ -260,7 +284,7 @@ def _remove_toml(client: dict[str, Any]) -> str | None:
|
|
|
260
284
|
# ── 诊断 ────────────────────────────────────────────────────────────────
|
|
261
285
|
|
|
262
286
|
def _entry_present(client: dict[str, Any]) -> bool:
|
|
263
|
-
path = _resolve(client
|
|
287
|
+
path = _resolve(_client_config_path(client))
|
|
264
288
|
if not path.exists():
|
|
265
289
|
return False
|
|
266
290
|
fmt = client.get("format", "json")
|
|
@@ -322,7 +346,7 @@ def cmd_inject(clients: list[dict[str, Any]], flags: list[str], dry_run: bool) -
|
|
|
322
346
|
return
|
|
323
347
|
print(f"将注入 {', '.join(c['name'] for c in targets)} ...\n")
|
|
324
348
|
for c in targets:
|
|
325
|
-
path = _resolve(c
|
|
349
|
+
path = _resolve(_client_config_path(c))
|
|
326
350
|
fmt = c.get("format", "json")
|
|
327
351
|
entry = _mcp_command(c)
|
|
328
352
|
try:
|
|
@@ -348,7 +372,7 @@ def cmd_undo(clients: list[dict[str, Any]], flags: list[str], dry_run: bool) ->
|
|
|
348
372
|
return
|
|
349
373
|
print(f"将还原 {', '.join(c['name'] for c in targets)} ...\n")
|
|
350
374
|
for c in targets:
|
|
351
|
-
path = _resolve(c
|
|
375
|
+
path = _resolve(_client_config_path(c))
|
|
352
376
|
fmt = c.get("format", "json")
|
|
353
377
|
try:
|
|
354
378
|
if dry_run:
|
package/scripts/mcp_tools.py
CHANGED
|
@@ -164,6 +164,7 @@ TOOLS = [
|
|
|
164
164
|
"focus": {"type": "string", "description": "BM25 聚焦查询词:只返回相关段落,省 token(仅 mode=text)"},
|
|
165
165
|
"max_chars": {"type": "integer", "description": "返回正文最大字符数", "default": 8000, "minimum": 500, "maximum": 50000},
|
|
166
166
|
"use_browser": {"type": "boolean", "description": "强制用反检测浏览器(HTTP 失败时已自动升级)", "default": False},
|
|
167
|
+
"include_links": {"type": "boolean", "description": "附带正文内链接(最多 30 条;默认只回 links_count,省 token)", "default": False},
|
|
167
168
|
},
|
|
168
169
|
"required": ["url"],
|
|
169
170
|
},
|
package/scripts/mcp_transport.py
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""mcp_warm.py — MCP 核心预热、连接池预建与社交抓取并发编排。
|
|
3
|
+
|
|
4
|
+
拆分自 mcp_handlers.py:隔离后台异步预热线程模型与社交平台并发调度。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import threading
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
_warm_started = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _prewarm_anysearch_connection() -> None:
|
|
18
|
+
"""在 warm 线程里预建 anysearch 的隧道连接(2026-10-18)。"""
|
|
19
|
+
if os.environ.get("ARGO_NO_PREWARM", "").strip() not in ("", "0"):
|
|
20
|
+
return
|
|
21
|
+
try:
|
|
22
|
+
import urllib.parse
|
|
23
|
+
from conn_pool import give_back, key_for, stats as pool_stats
|
|
24
|
+
from net_proxy import open_connection, resolve_proxy
|
|
25
|
+
if not pool_stats().get("enabled"):
|
|
26
|
+
return
|
|
27
|
+
url = "https://api.anysearch.com/mcp"
|
|
28
|
+
parsed = urllib.parse.urlparse(url)
|
|
29
|
+
proxy_url = resolve_proxy(url)
|
|
30
|
+
conn, _via = open_connection(parsed, 8.0, proxy_url)
|
|
31
|
+
conn.connect()
|
|
32
|
+
give_back(key_for(parsed, proxy_url), conn)
|
|
33
|
+
except Exception as e:
|
|
34
|
+
sys.stderr.write(f"[argo-mcp] prewarm-skip: {type(e).__name__}: {e}\n")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _warm_core_async(lazy_cached_fn, get_cache_fn) -> None:
|
|
38
|
+
"""initialize 后后台预热 search+cache,摊平首次 tools/call 延迟。"""
|
|
39
|
+
global _warm_started
|
|
40
|
+
if _warm_started:
|
|
41
|
+
return
|
|
42
|
+
_warm_started = True
|
|
43
|
+
|
|
44
|
+
def _run() -> None:
|
|
45
|
+
t0 = __import__("time").time()
|
|
46
|
+
try:
|
|
47
|
+
lazy_cached_fn("search")
|
|
48
|
+
get_cache_fn()
|
|
49
|
+
try:
|
|
50
|
+
from route_domains import warm_compiled_domains
|
|
51
|
+
warm_compiled_domains()
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
_prewarm_anysearch_connection()
|
|
55
|
+
ms = int((__import__("time").time() - t0) * 1000)
|
|
56
|
+
sys.stderr.write(f"[argo-mcp] warm-core ok {ms}ms\n")
|
|
57
|
+
except Exception as e:
|
|
58
|
+
sys.stderr.write(f"[argo-mcp] warm-core fail: {type(e).__name__}: {e}\n")
|
|
59
|
+
sys.stderr.flush()
|
|
60
|
+
|
|
61
|
+
threading.Thread(target=_run, name="argo-mcp-warm", daemon=True).start()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _resolve_research_profile(arguments: dict[str, Any], lazy_cached_fn) -> tuple[dict[str, Any] | None, str | None]:
|
|
65
|
+
"""解析 topic profile;全部在 argo 内,不加载外部 skill。"""
|
|
66
|
+
try:
|
|
67
|
+
profiles = lazy_cached_fn("topic_research_profiles")
|
|
68
|
+
except Exception:
|
|
69
|
+
return None, None
|
|
70
|
+
if not profiles:
|
|
71
|
+
return None, None
|
|
72
|
+
topic = arguments.get("topic")
|
|
73
|
+
key = None
|
|
74
|
+
if topic:
|
|
75
|
+
prof = profiles.get_profile(str(topic))
|
|
76
|
+
if prof:
|
|
77
|
+
raw = str(topic).strip()
|
|
78
|
+
key = profiles.ALIASES.get(raw) or profiles.ALIASES.get(raw.lower()) or raw.lower()
|
|
79
|
+
return prof, key
|
|
80
|
+
return None, None
|
|
81
|
+
if arguments.get("auto_topic", True):
|
|
82
|
+
key = profiles.detect_topic_from_query(arguments.get("query") or "")
|
|
83
|
+
if key:
|
|
84
|
+
return profiles.get_profile(key), key
|
|
85
|
+
return None, None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _search_social_platforms(
|
|
89
|
+
platforms: list[str], query: str, n: int, timeout: float = 20.0,
|
|
90
|
+
lazy_cached_fn=None,
|
|
91
|
+
) -> tuple[dict[str, list], list[str], list[str]]:
|
|
92
|
+
"""并行抓取多社交平台(MCP 热路径)。"""
|
|
93
|
+
platform_results: dict[str, list] = {}
|
|
94
|
+
errors: list[str] = []
|
|
95
|
+
engines_used: list[str] = []
|
|
96
|
+
|
|
97
|
+
def _one(platform: str) -> tuple[str, list, str | None]:
|
|
98
|
+
module_name = platform.replace("-", "_") + "_engine"
|
|
99
|
+
try:
|
|
100
|
+
mod = lazy_cached_fn(f"social_engines.{module_name}")
|
|
101
|
+
return platform, mod.search(query, n=n), None
|
|
102
|
+
except ImportError:
|
|
103
|
+
return platform, [], f"Platform {platform} not available (module social_engines.{module_name})"
|
|
104
|
+
except Exception as e:
|
|
105
|
+
return platform, [], f"{platform}: {str(e)[:100]}"
|
|
106
|
+
|
|
107
|
+
workers = min(max(len(platforms), 1), 4)
|
|
108
|
+
import concurrent.futures
|
|
109
|
+
ex = concurrent.futures.ThreadPoolExecutor(max_workers=workers)
|
|
110
|
+
futs = {ex.submit(_one, p): p for p in platforms}
|
|
111
|
+
try:
|
|
112
|
+
for fut in concurrent.futures.as_completed(futs, timeout=timeout):
|
|
113
|
+
platform, results, err = fut.result()
|
|
114
|
+
platform_results[platform] = results
|
|
115
|
+
if err:
|
|
116
|
+
errors.append(err)
|
|
117
|
+
elif results is not None:
|
|
118
|
+
engines_used.append(platform)
|
|
119
|
+
except concurrent.futures.TimeoutError:
|
|
120
|
+
for fut, platform in futs.items():
|
|
121
|
+
if not fut.done():
|
|
122
|
+
fut.cancel()
|
|
123
|
+
platform_results[platform] = []
|
|
124
|
+
errors.append(f"{platform}: timeout (>{timeout}s)")
|
|
125
|
+
finally:
|
|
126
|
+
ex.shutdown(wait=False, cancel_futures=True)
|
|
127
|
+
return platform_results, engines_used, errors
|