argo-search 2.9.7 → 2.9.9
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 +73 -150
- package/README.es.md +73 -150
- package/README.ja.md +79 -156
- package/README.ko.md +78 -155
- package/README.md +80 -159
- 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 +78 -5
- package/bin/argo.js +33 -2
- package/config.yaml +269 -63
- package/docs/ARGO_INTRO.md +5 -3
- 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/docs/RELEASE_NOTES_v2.9.9.md +78 -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 +7 -2
- package/packages/dsh-plugin/README.md +25 -6
- package/packages/dsh-plugin/dsh/client.js +402 -47
- package/packages/dsh-plugin/dsh/diagnostics.js +97 -0
- package/packages/dsh-plugin/dsh/index.js +135 -1876
- 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 +3 -3
- package/packages/dsh-plugin/scripts/sync-local.sh +70 -0
- package/packages/dsh-plugin/test/client.test.js +177 -2
- package/packages/dsh-plugin/test/diagnostics.test.js +28 -0
- package/packages/dsh-plugin/test/index.test.js +44 -0
- package/scripts/adaptive.py +126 -43
- package/scripts/article_meta.py +131 -0
- package/scripts/bench_tail_ab.py +281 -0
- package/scripts/bounded_run.py +91 -49
- 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/clock_bridge.py +22 -0
- package/scripts/content_boundary.py +46 -0
- package/scripts/content_security.py +119 -4
- package/scripts/content_signals.py +26 -5
- package/scripts/cross_lingual_trace.py +293 -0
- package/scripts/dispatch_result.py +105 -0
- package/scripts/engine_add.py +602 -0
- package/scripts/engine_dispatch.py +58 -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 +58 -22
- package/scripts/fetch_quality.py +13 -0
- package/scripts/fetch_v3.py +28 -3
- package/scripts/gen_routing_snapshot.py +11 -0
- package/scripts/http_client.py +24 -38
- 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 +192 -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/net_proxy.py +27 -0
- package/scripts/output_transparency.py +200 -0
- package/scripts/paid_policy.py +98 -0
- package/scripts/quota.py +158 -13
- package/scripts/recovery.py +7 -3
- 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 +26 -5
- package/scripts/route_domains.py +170 -31
- package/scripts/safe_conn.py +52 -0
- 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 +8 -1
- package/scripts/search_output.py +99 -15
- package/scripts/search_pipeline.py +62 -195
- package/scripts/search_rank.py +6 -0
- package/scripts/search_types.py +107 -9
- package/scripts/semantic_evidence.py +179 -3
- package/scripts/serp_guard.py +138 -7
- package/scripts/serp_kill_log.py +25 -2
- package/scripts/stats_cli.py +13 -1
- package/scripts/tail_completion.py +212 -0
- package/scripts/url_safety.py +126 -3
- package/scripts/usage_log.py +45 -0
- 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/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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""漂移阈值判定:把夜间「重录 vs 基线」的比较结果变成退出码。
|
|
3
|
+
|
|
4
|
+
## 为什么单独成文件
|
|
5
|
+
|
|
6
|
+
判定逻辑原来写在 nightly.yml 的 heredoc 里:语法对错只能在推上去之后知道,
|
|
7
|
+
阈值改松改紧也没人测。抽出来后有两个好处——本地能跑同一套判定,用例能锁住
|
|
8
|
+
「跌多少算跌」这条契约(tests/test_ci_workflows.py)。
|
|
9
|
+
|
|
10
|
+
## 判什么、为什么只判这两样
|
|
11
|
+
|
|
12
|
+
- **结果条数**:少 1 条允许(工具源单条抖动很常见),少 2 条以上算漂移;
|
|
13
|
+
- **输出体积**:超过基线 20% 算漂移(体积是 Agent 真金白银付的上下文)。
|
|
14
|
+
|
|
15
|
+
`not_recorded`(重放时要了一个当时没调过的引擎)**不计入**:录制只保留真正
|
|
16
|
+
被调到的引擎,而重放是瞬时完成,早停时机与真跑不同,这是录制机制的正常边界,
|
|
17
|
+
当成回归报会制造长期噪音。它仍然会打印出来,供人看。
|
|
18
|
+
|
|
19
|
+
用法:
|
|
20
|
+
python3 scripts/ci_drift_gate.py --baseline 基线报告.json --current 本次报告.json
|
|
21
|
+
退出码:0 通过;1 有漂移;2 输入缺失/格式不对(这两件事必须分开,混在一起
|
|
22
|
+
会让人以为「红了=质量掉了」)。
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
KEPT_SLACK = 1 # 允许少 1 条
|
|
34
|
+
BYTES_GROWTH = 1.2 # 体积上限 = 基线 × 1.2
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load(path: Path) -> dict[str, Any]:
|
|
38
|
+
doc = json.loads(path.read_text(encoding="utf-8"))
|
|
39
|
+
# save_baseline 存的是 {"_meta":…, "report":{…}};evaluate_all 直接给报告本体
|
|
40
|
+
return doc.get("report", doc)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def violations(base: dict[str, Any], cur: dict[str, Any]) -> list[str]:
|
|
44
|
+
by_id = {c.get("id"): c for c in (base.get("cases") or [])}
|
|
45
|
+
cur_ids = {c.get("id") for c in (cur.get("cases") or [])}
|
|
46
|
+
out: list[str] = []
|
|
47
|
+
for case in cur.get("cases") or []:
|
|
48
|
+
old = by_id.get(case.get("id"))
|
|
49
|
+
if not old:
|
|
50
|
+
continue # 新增的用例没有基线,不算漂移
|
|
51
|
+
cid = case.get("id")
|
|
52
|
+
kept, old_kept = case.get("kept", 0), old.get("kept", 0)
|
|
53
|
+
if kept < old_kept - KEPT_SLACK:
|
|
54
|
+
out.append(f"{cid}: 结果 {kept} 条,基线 {old_kept} 条")
|
|
55
|
+
old_bytes = old.get("bytes", 0)
|
|
56
|
+
if old_bytes and case.get("bytes", 0) > old_bytes * BYTES_GROWTH:
|
|
57
|
+
out.append(
|
|
58
|
+
f"{cid}: 输出 {case.get('bytes')}B,超过基线 {old_bytes}B×{BYTES_GROWTH}")
|
|
59
|
+
# 基线里有、本次没有:不是「通过」,是这条根本没被录到(查询集被改、
|
|
60
|
+
# 或录制整条失败)。2026-10-22 本地实跑踩过:只剩 1 条时判定照样退 0。
|
|
61
|
+
for cid in by_id:
|
|
62
|
+
if cid and cid not in cur_ids:
|
|
63
|
+
out.append(f"{cid}: 本次报告里没有这条(查询集被改了,还是录制失败?)")
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main() -> int:
|
|
68
|
+
ap = argparse.ArgumentParser(description="夜间漂移阈值判定")
|
|
69
|
+
ap.add_argument("--baseline", required=True)
|
|
70
|
+
ap.add_argument("--current", required=True)
|
|
71
|
+
args = ap.parse_args()
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
base = _load(Path(args.baseline))
|
|
75
|
+
cur = _load(Path(args.current))
|
|
76
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
77
|
+
print(f"读不到报告:{type(e).__name__}: {e}", file=sys.stderr)
|
|
78
|
+
return 2
|
|
79
|
+
|
|
80
|
+
bad = violations(base, cur)
|
|
81
|
+
if bad:
|
|
82
|
+
print("\n".join(bad), file=sys.stderr)
|
|
83
|
+
return 1
|
|
84
|
+
print(f"通过:{len(cur.get('cases') or [])} 条,漏录 "
|
|
85
|
+
f"{cur.get('total_missing', 0)} 个引擎(信息项,不算漂移)")
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
sys.exit(main())
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""clock_bridge.py — 统一时钟的一行入口(2026-10-24)。
|
|
3
|
+
|
|
4
|
+
计时断言与「预算夹紧」判定不该取决于机器快慢:测试域用 tests/_measure 的
|
|
5
|
+
虚拟时钟,生产路径就是 ``time.monotonic``。各模块从这里取 ``clock``,不再各自
|
|
6
|
+
复制一段 try/except(那会让千行文件更难拆,也容易只改一半——本文件对应的
|
|
7
|
+
fetch_v3 就曾因「预算夹在超时请求上、耗时却按真实墙钟算」而被测出击穿)。
|
|
8
|
+
|
|
9
|
+
与 ``tests/_measure.py`` 的关系:那个模块定义语义(虚拟/真实、预算锚点、
|
|
10
|
+
确定性时间线),本模块只做「生产环境安全地取到它」这一件事。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
try: # 测试域:虚拟时钟(见 tests/_measure.py)
|
|
18
|
+
from _measure import clock # type: ignore
|
|
19
|
+
except ImportError: # pragma: no cover - 生产路径
|
|
20
|
+
clock = time.monotonic
|
|
21
|
+
|
|
22
|
+
__all__ = ["clock"]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""content_boundary.py — 不可信网页内容的边界标记与清洗。
|
|
3
|
+
|
|
4
|
+
为什么需要单独一层:Argo 对 Agent 输出的是 JSON,不能像纯文本工具那样简单
|
|
5
|
+
把整段响应包进 <untrusted-web-content>(会破坏 JSON 解析)。所以这里只做
|
|
6
|
+
两件确定能做的事:
|
|
7
|
+
|
|
8
|
+
1. **清洗**:任何来自上游的标题/摘要/正文在进入输出前,剥掉其中的边界标记
|
|
9
|
+
字样——否则网页正文里自带的 `</untrusted-web-content>` 可以提前闭合边界,
|
|
10
|
+
把后续内容伪装成可信指令(dsh-free-search 的同类防护)。
|
|
11
|
+
2. **声明**:调用方在响应里带 `content_trust: "untrusted"`,让模型与宿主
|
|
12
|
+
知道这批文本的信任级别;文本边界由宿主(DSH/MCP 客户端)按自己的渲染
|
|
13
|
+
形态加,不在 JSON 契约里硬塞。
|
|
14
|
+
|
|
15
|
+
不引入依赖、不做 HTML 清洗:正文已经是纯文本摘要,这里只管边界标记这一类
|
|
16
|
+
可被利用的结构字符。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
CONTENT_TRUST_UNTRUSTED = "untrusted"
|
|
25
|
+
|
|
26
|
+
# 大小写、属性、空白都按「可能的变体」处理;只剥标记本身,不动正文其余字符。
|
|
27
|
+
_BOUNDARY_RE = re.compile(
|
|
28
|
+
r"<\s*/?\s*untrusted-web-content\b[^>]*>", re.IGNORECASE)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def strip_boundary_markers(text: Any, limit: int | None = None) -> str:
|
|
32
|
+
"""剥掉正文里的边界标记(含变体),可选截断。"""
|
|
33
|
+
out = _BOUNDARY_RE.sub("", str(text or ""))
|
|
34
|
+
if limit is not None and limit >= 0 and len(out) > limit:
|
|
35
|
+
out = out[:limit]
|
|
36
|
+
return out
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def sanitize_results_boundary(results: Any) -> None:
|
|
40
|
+
"""就地清洗结果列表每条的 title/snippet 边界标记(CLI/Agent/MCP 共用单一出口)。"""
|
|
41
|
+
for item in results or []:
|
|
42
|
+
if not isinstance(item, dict):
|
|
43
|
+
continue
|
|
44
|
+
for key in ("title", "snippet"):
|
|
45
|
+
if isinstance(item.get(key), str):
|
|
46
|
+
item[key] = strip_boundary_markers(item[key])
|
|
@@ -35,6 +35,7 @@ THREAT_NAMES = {
|
|
|
35
35
|
"payload_smuggling": "载荷走私",
|
|
36
36
|
"xss_injection": "XSS 注入",
|
|
37
37
|
"recursive_injection": "递归注入",
|
|
38
|
+
"credential_exposure": "凭证暴露",
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
|
|
@@ -181,11 +182,53 @@ INJECTION_PATTERNS_EL = [
|
|
|
181
182
|
r"υποδύσου\s+(?:τον\s+)?(?:άνθρωπο|χρήστη|διαχειριστή|σύστημα)",
|
|
182
183
|
]
|
|
183
184
|
|
|
185
|
+
# 西班牙语注入
|
|
186
|
+
INJECTION_PATTERNS_ES = [
|
|
187
|
+
r"(?i)ignora\s+(?:todas\s+)?(?:las\s+)?(?:instrucciones|órdenes|ordenes|reglas|indicaciones)\s+(?:anteriores|previas)",
|
|
188
|
+
r"(?i)olvida\s+(?:todas\s+)?(?:las\s+)?(?:instrucciones|órdenes|reglas)",
|
|
189
|
+
r"(?i)(?:muéstrame|muestrame|enséñame|ensename|dime|revela|imprime|exhibe)\s+(?:tu\s+)?(?:prompt\s+del\s+sistema|prompt|instrucciones|configuración|configuracion)",
|
|
190
|
+
r"(?i)ya\s+no\s+(?:eres|es)\s+(?:un\s+)?(?:asistente|bot|ia|agente)",
|
|
191
|
+
r"(?i)(?:eres|ahora\s+eres)\s+(?:una\s+)?(?:persona|humano|usuario|administrador|sistema)",
|
|
192
|
+
r"(?i)(?:actúa|actua|comporta|finge|simula)\s+(?:como|de\s+que)\s+(?:una\s+)?(?:persona|humano|usuario|administrador|sistema)",
|
|
193
|
+
r"(?i)(?:elude|evita|ignora)\s+(?:todas\s+)?(?:las\s+)?(?:restricciones|limitaciones|protecciones|reglas)",
|
|
194
|
+
r"(?i)te\s+llamas\s+(?:ChatGPT|Claude|GPT|asistente\s+de\s+ia)",
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
# 法语注入
|
|
198
|
+
INJECTION_PATTERNS_FR = [
|
|
199
|
+
r"(?i)ignore\s+(?:toutes\s+)?(?:les\s+)?(?:instructions|consignes|règles|regles|ordres)\s+(?:précédentes|precedentes)",
|
|
200
|
+
r"(?i)oublie\s+(?:toutes\s+)?(?:les\s+)?(?:instructions|consignes|règles|regles)",
|
|
201
|
+
r"(?i)(?:montre|affiche|dis|révèle|revel|imprime)\s+(?:ton|votre|ta)?\s*(?:prompt\s+système|prompt|instructions|configuration)",
|
|
202
|
+
r"(?i)tu\s+n'es\s+plus\s+(?:un\s+)?(?:assistant|bot|ia|agent)",
|
|
203
|
+
r"(?i)tu\s+es\s+(?:désormais|desormais\s+)?(?:une\s+)?(?:personne|humain|utilisateur|administrateur|système|systeme)",
|
|
204
|
+
r"(?i)(?:fais\s+semblant|joue|agis)\s+(?:comme|en\s+tant\s+que)\s+(?:une\s+)?(?:personne|humain|utilisateur|administrateur|système|systeme)",
|
|
205
|
+
r"(?i)(?:contourne|évite|evite)\s+(?:toutes\s+)?(?:les\s+)?(?:restrictions|limitations|protections|règles|regles)",
|
|
206
|
+
r"(?i)ton\s+nom\s+est\s+(?:ChatGPT|Claude|GPT|assistant\s+ia)",
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
# 德语注入
|
|
210
|
+
INJECTION_PATTERNS_DE = [
|
|
211
|
+
r"(?i)ignoriere\s+(?:alle\s+)?(?:vorherigen\s+)?(?:Anweisungen|Regeln|Instruktionen|Befehle)",
|
|
212
|
+
r"(?i)vergiss\s+(?:alle\s+)?(?:vorherigen\s+)?(?:Anweisungen|Regeln|Instruktionen)",
|
|
213
|
+
r"(?i)(?:zeig|zeige|erzähl|erzahl|gib\s+preis|offenbare|drucke)\s+(?:mir\s+)?(?:deinen\s+)?(?:System-?Prompt|Systemprompt|Anweisungen|Einstellungen)",
|
|
214
|
+
r"(?i)du\s+bist\s+(?:nicht\s+mehr|kein\s+mehr)\s+(?:ein\s+)?(?:Assistent|Bot|KI|Agent)",
|
|
215
|
+
r"(?i)du\s+bist\s+(?:nun\s+)?(?:ein\s+)?(?:Mensch|Benutzer|Administrator|System)",
|
|
216
|
+
r"(?i)(?:tu|gib\s+vor|verhalte\s+dich)\s+(?:als|wie)\s+(?:ein\s+)?(?:Mensch|Benutzer|Administrator|System)",
|
|
217
|
+
r"(?i)(?:umgehe|ignoriere)\s+(?:alle\s+)?(?:Einschränkungen|Einschrankungen|Beschränkungen|Beschrankungen|Schutzmaßnahmen|Schutzmassnahmen|Regeln)",
|
|
218
|
+
r"(?i)dein\s+Name\s+ist\s+(?:ChatGPT|Claude|GPT|KI-Assistent)",
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
# 拉丁语系专用表(不含英语通用,避免 en/latin 两条路重复匹配)
|
|
222
|
+
INJECTION_PATTERNS_LATIN = (
|
|
223
|
+
INJECTION_PATTERNS_ES + INJECTION_PATTERNS_FR + INJECTION_PATTERNS_DE
|
|
224
|
+
)
|
|
225
|
+
|
|
184
226
|
# 按语系分类的注入模式表(lang_detect 主语言标签 → 模式列表)
|
|
185
227
|
LANG_INJECTION_PATTERNS: dict[str, list[str]] = {
|
|
186
228
|
"zh": INJECTION_PATTERNS_ZH,
|
|
187
229
|
"ja": INJECTION_PATTERNS_JA,
|
|
188
230
|
"ko": INJECTION_PATTERNS_KO,
|
|
231
|
+
"latin": INJECTION_PATTERNS_LATIN,
|
|
189
232
|
"cyrillic": INJECTION_PATTERNS_RU,
|
|
190
233
|
"arabic": INJECTION_PATTERNS_AR,
|
|
191
234
|
"hebrew": INJECTION_PATTERNS_HE,
|
|
@@ -259,6 +302,21 @@ XSS_PATTERNS = [
|
|
|
259
302
|
r"(?i)window\.location",
|
|
260
303
|
]
|
|
261
304
|
|
|
305
|
+
# 凭证形态:出现在抓取正文里的密钥形状串——可能是页面自身的示例,也可能是
|
|
306
|
+
# 被注入/外泄的凭证。标 credential_exposure(权重 0.5,单独命中不触发隔离,
|
|
307
|
+
# 但进 threat_types 供上层判断)。
|
|
308
|
+
SECRET_FORM_PATTERNS = [
|
|
309
|
+
r"sk-[A-Za-z0-9_\-]{16,}",
|
|
310
|
+
r"sk-ant-[A-Za-z0-9_\-]{16,}",
|
|
311
|
+
r"ghp_[A-Za-z0-9]{20,}",
|
|
312
|
+
r"github_pat_[A-Za-z0-9_]{20,}",
|
|
313
|
+
r"AKIA[0-9A-Z]{16}",
|
|
314
|
+
r"ASIA[0-9A-Z]{16}",
|
|
315
|
+
r"xox[baprs]-[A-Za-z0-9\-]{10,}",
|
|
316
|
+
r"AIza[0-9A-Za-z_\-]{35}",
|
|
317
|
+
r"-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----",
|
|
318
|
+
]
|
|
319
|
+
|
|
262
320
|
# 检测但不过度脱敏的文件路径类模式
|
|
263
321
|
_NO_REDACT_PATTERNS: frozenset = frozenset({
|
|
264
322
|
r"(?i)/etc/passwd",
|
|
@@ -317,6 +375,7 @@ WEIGHTS = {
|
|
|
317
375
|
"payload_smuggling": 0.9,
|
|
318
376
|
"xss_injection": 0.9,
|
|
319
377
|
"recursive_injection": 1.0,
|
|
378
|
+
"credential_exposure": 0.5,
|
|
320
379
|
}
|
|
321
380
|
|
|
322
381
|
|
|
@@ -348,6 +407,7 @@ class ContentScrubber:
|
|
|
348
407
|
threats.extend(self._detect_exfiltration(normalized))
|
|
349
408
|
threats.extend(self._detect_impersonation(normalized))
|
|
350
409
|
threats.extend(self._detect_xss(normalized))
|
|
410
|
+
threats.extend(self._detect_secret_forms(normalized))
|
|
351
411
|
|
|
352
412
|
# 阶段 3:语义意图分析
|
|
353
413
|
threats.extend(self._semantic_analysis(normalized))
|
|
@@ -467,9 +527,10 @@ class ContentScrubber:
|
|
|
467
527
|
if lang == "zh" or lang == "mixed":
|
|
468
528
|
patterns.extend(INJECTION_PATTERNS_ZH)
|
|
469
529
|
for key, plist in LANG_INJECTION_PATTERNS.items():
|
|
470
|
-
|
|
530
|
+
# en 与 latin 共享拉丁模式:ES/FR/DE 注入多为 ASCII 或带扩展字符,
|
|
531
|
+
# 纯 ASCII 时 lang_detect 判 en,漏掉它们等于给拉丁注入留门
|
|
532
|
+
if lang == key or (lang == "en" and key == "latin"):
|
|
471
533
|
patterns.extend(plist)
|
|
472
|
-
# en/latin 落到英文通用(已有)
|
|
473
534
|
return tuple(patterns)
|
|
474
535
|
|
|
475
536
|
def _detect_injections(self, text: str, lang: str = "other") -> list[ThreatDetection]:
|
|
@@ -485,6 +546,11 @@ class ContentScrubber:
|
|
|
485
546
|
def _detect_xss(self, text: str) -> list[ThreatDetection]:
|
|
486
547
|
return self._match_patterns(text, XSS_PATTERNS, "xss_injection", 0.9)
|
|
487
548
|
|
|
549
|
+
def _detect_secret_forms(self, text: str) -> list[ThreatDetection]:
|
|
550
|
+
"""凭证形态检测(输出侧防护):正文里的密钥形状串。"""
|
|
551
|
+
return self._match_patterns(text, SECRET_FORM_PATTERNS,
|
|
552
|
+
"credential_exposure", 0.7)
|
|
553
|
+
|
|
488
554
|
@staticmethod
|
|
489
555
|
def _match_patterns(text: str, patterns: list[str],
|
|
490
556
|
threat_type: str, confidence: float) -> list[ThreatDetection]:
|
|
@@ -629,10 +695,51 @@ def scrub_content(content: str) -> ScrubResult:
|
|
|
629
695
|
return get_scrubber().scrub(content)
|
|
630
696
|
|
|
631
697
|
|
|
698
|
+
# ── 风险门禁(v2.9.8)──────────────────────────────────────────────────────
|
|
699
|
+
#
|
|
700
|
+
# 此前 scrub_to_dict 只回传元数据(risk_score 等),**清洗后的正文被丢弃**——
|
|
701
|
+
# 安全引擎只「评分」不「门禁」,高风险内容照样进证据流水线与 Agent 上下文。
|
|
702
|
+
# 门禁把「评分」变成「动作」:risk_score ≥ 阈值时,fetch 层用清洗后正文替换
|
|
703
|
+
# 原始正文并标 content_quarantined,使被注入污染的正文不进证据分、不进上下文。
|
|
704
|
+
#
|
|
705
|
+
# 开关与阈值都可配(见 references/usage.md 功能开关总表):
|
|
706
|
+
# ARGO_CONTENT_RISK_GATE 默认开;关掉 = 放行高风险内容,仅应急用
|
|
707
|
+
# ARGO_CONTENT_RISK_THRESHOLD 默认 0.5,与历史 content_clean 阈值一致
|
|
708
|
+
RISK_GATE_THRESHOLD = 0.5
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def risk_gate_enabled() -> bool:
|
|
712
|
+
"""内容风险门禁总开关(默认开)。"""
|
|
713
|
+
try:
|
|
714
|
+
from engine_env import env_flag
|
|
715
|
+
return env_flag("ARGO_CONTENT_RISK_GATE", default=True)
|
|
716
|
+
except Exception:
|
|
717
|
+
return True
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def risk_gate_threshold() -> float:
|
|
721
|
+
"""门禁阈值:risk_score ≥ 此值即隔离。默认 0.5,越界回落默认。"""
|
|
722
|
+
try:
|
|
723
|
+
from engine_env import get_env
|
|
724
|
+
val = float(str(get_env(["ARGO_CONTENT_RISK_THRESHOLD"])).strip())
|
|
725
|
+
except (TypeError, ValueError):
|
|
726
|
+
return RISK_GATE_THRESHOLD
|
|
727
|
+
if val <= 0.0 or val > 1.0:
|
|
728
|
+
return RISK_GATE_THRESHOLD
|
|
729
|
+
return val
|
|
730
|
+
|
|
731
|
+
|
|
632
732
|
def scrub_to_dict(content: str) -> dict:
|
|
633
|
-
"""清洗入口(dict 形态,供 fetch 层直接合并)。
|
|
733
|
+
"""清洗入口(dict 形态,供 fetch 层直接合并)。
|
|
734
|
+
|
|
735
|
+
除历史字段外,回传门禁三件套:gate_applied(门禁是否生效)、
|
|
736
|
+
quarantined(是否隔离)、cleaned_content(仅隔离时回传,供替换原始正文)。
|
|
737
|
+
"""
|
|
634
738
|
r = scrub_content(content)
|
|
635
|
-
|
|
739
|
+
gate_on = risk_gate_enabled()
|
|
740
|
+
threshold = risk_gate_threshold()
|
|
741
|
+
quarantined = gate_on and r.risk_score >= threshold
|
|
742
|
+
out = {
|
|
636
743
|
"content_clean": r.clean,
|
|
637
744
|
"risk_score": r.risk_score,
|
|
638
745
|
"threat_count": len(r.threats),
|
|
@@ -640,7 +747,15 @@ def scrub_to_dict(content: str) -> dict:
|
|
|
640
747
|
"redactions": r.redactions,
|
|
641
748
|
"encoding_layers_stripped": r.encoding_layers_stripped,
|
|
642
749
|
"content_lang": r.content_lang,
|
|
750
|
+
"gate_applied": gate_on,
|
|
751
|
+
"gate_threshold": threshold,
|
|
752
|
+
"quarantined": quarantined,
|
|
643
753
|
}
|
|
754
|
+
# 只有被隔离时才回传清洗后正文:它替换原始正文,是门禁的「动作」部分;
|
|
755
|
+
# 未命中时多带一份正文副本只是白占内存。
|
|
756
|
+
if quarantined:
|
|
757
|
+
out["cleaned_content"] = r.content
|
|
758
|
+
return out
|
|
644
759
|
|
|
645
760
|
|
|
646
761
|
# ── CLI ────────────────────────────────────────────────────────────────────
|
|
@@ -492,9 +492,16 @@ def score_clickbait(title: str) -> dict:
|
|
|
492
492
|
score += 0.08
|
|
493
493
|
hits.append("year_prefix")
|
|
494
494
|
# 利益承诺:「看完…你就会」「建议收藏」「一篇搞懂」
|
|
495
|
-
if re.search(r"(
|
|
495
|
+
if re.search(r"(看完|建议收藏|一篇(?:讲|说|搞懂)|全解析|必看)", t):
|
|
496
496
|
score += 0.10
|
|
497
497
|
hits.append("benefit_promise")
|
|
498
|
+
# 虚假实测 / 承诺尾钩
|
|
499
|
+
if re.search(r"自费\s*\d+\s*元?实测", t):
|
|
500
|
+
score += 0.25
|
|
501
|
+
hits.append("fake_testimony")
|
|
502
|
+
if re.search(r"看完.*(?:你就懂了|你就会)", t):
|
|
503
|
+
score += 0.15
|
|
504
|
+
hits.append("clickbait_promise")
|
|
498
505
|
|
|
499
506
|
# 标点:仅在已有命中时作**放大器**(PACLIC 的误杀教训)
|
|
500
507
|
if score > 0:
|
|
@@ -595,13 +602,27 @@ def score_template_repetition(content: str) -> dict:
|
|
|
595
602
|
这里是接入点,且标定集能立刻给出 before/after。
|
|
596
603
|
"""
|
|
597
604
|
text = (content or "").strip()
|
|
598
|
-
|
|
605
|
+
# ── 序数连接词与流水线套话检测(对抗现代批量模板/AI农场)──
|
|
606
|
+
connectors = ("首先", "其次", "然后", "接下来", "最后", "综上所述")
|
|
607
|
+
hit_conn = sum(1 for w in connectors if w in text)
|
|
608
|
+
ai_phrases = ("业内人士表示", "专家分析", "消费者认为", "数据显示", "有望成为")
|
|
609
|
+
hit_ai = sum(1 for w in ai_phrases if w in text)
|
|
610
|
+
conn_boost = 0.0
|
|
611
|
+
if hit_conn >= 4:
|
|
612
|
+
conn_boost = 0.85
|
|
613
|
+
elif hit_conn >= 3 and hit_ai >= 1:
|
|
614
|
+
conn_boost = 0.80
|
|
615
|
+
elif hit_ai >= 3:
|
|
616
|
+
conn_boost = 0.78
|
|
617
|
+
|
|
618
|
+
if len(text) < 120 and conn_boost == 0.0:
|
|
599
619
|
return {"score": 0.0, "templates": 0, "sentences": 0, "repetition": 0.0}
|
|
600
620
|
# 分句:中英标点 + 换行
|
|
601
621
|
sents = [s.strip() for s in re.split(r"[。!?!?;;\n]+", text) if len(s.strip()) >= 6]
|
|
602
622
|
if len(sents) < 4:
|
|
603
|
-
|
|
604
|
-
|
|
623
|
+
base_rep = conn_boost
|
|
624
|
+
return {"score": round(base_rep, 3), "templates": len(sents), "sentences": len(sents),
|
|
625
|
+
"repetition": round(base_rep, 3)}
|
|
605
626
|
sigs: list[str] = []
|
|
606
627
|
for s in sents:
|
|
607
628
|
head = s[:2]
|
|
@@ -610,7 +631,7 @@ def score_template_repetition(content: str) -> dict:
|
|
|
610
631
|
bucket = "S" if len(s) < 25 else ("M" if len(s) < 60 else "L")
|
|
611
632
|
sigs.append(f"{head}|{tail}|{bucket}")
|
|
612
633
|
uniq = len(set(sigs))
|
|
613
|
-
repetition = 1.0 - uniq / len(sigs)
|
|
634
|
+
repetition = max(1.0 - uniq / len(sigs), conn_boost)
|
|
614
635
|
return {
|
|
615
636
|
"score": round(min(repetition, 1.0), 3),
|
|
616
637
|
"templates": uniq,
|