argo-search 1.0.1 → 2.9.7

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.
Files changed (463) hide show
  1. package/LICENSE +201 -21
  2. package/README.en.md +822 -0
  3. package/README.es.md +864 -0
  4. package/README.ja.md +864 -0
  5. package/README.ko.md +864 -0
  6. package/README.md +867 -320
  7. package/SKILL.md +135 -0
  8. package/assets/readme/hero.svg +121 -0
  9. package/assets/readme/made-with-beautify.svg +10 -0
  10. package/assets/readme/proof-routes.svg +73 -0
  11. package/assets/readme/settings-keys.png +0 -0
  12. package/assets/readme/settings-tools.png +0 -0
  13. package/assets/readme/why-better.svg +104 -0
  14. package/assets/readme/workflow.svg +83 -0
  15. package/backends/domain_profiles.json +2308 -96
  16. package/backends/engine_registry.yaml +3314 -503
  17. package/backends/query_synonyms_cn.json +35 -0
  18. package/backends/quota_profiles.json +2409 -161
  19. package/backends/source_types_cn.json +83 -0
  20. package/backends/text_lexicon.json +44 -0
  21. package/bin/argo +703 -0
  22. package/bin/argo.js +71 -39
  23. package/config.yaml +4976 -363
  24. package/cordis.patch.yml +21 -0
  25. package/data/countries.json +40 -0
  26. package/data/lang_matrix/lang_capability.json +842 -0
  27. package/data/lang_matrix/matrix_2026-09-10.json +11921 -0
  28. package/data/regions_cn.json +1 -0
  29. package/docs/ADDING_NEW_ENGINE.md +184 -0
  30. package/docs/ARGO_INTRO.md +124 -0
  31. package/docs/DATA_SOURCE_GAPS.md +81 -0
  32. package/docs/DESIGN_LOCAL_DATA_FUSION.md +90 -0
  33. package/docs/ENGINE_CATALOG.md +564 -0
  34. package/docs/MCP_SETUP.md +106 -0
  35. package/docs/PLATFORM_PATHS.md +111 -0
  36. package/docs/RELEASE_NOTES_v2.8.0.md +71 -0
  37. package/docs/RELEASE_NOTES_v2.8.2.md +63 -0
  38. package/docs/RELEASE_NOTES_v2.8.3.md +55 -0
  39. package/docs/RELEASE_NOTES_v2.8.4.md +79 -0
  40. package/docs/RELEASE_NOTES_v2.8.5.md +102 -0
  41. package/docs/RELEASE_NOTES_v2.8.6.md +66 -0
  42. package/docs/RELEASE_NOTES_v2.8.7.md +65 -0
  43. package/docs/RELEASE_NOTES_v2.8.8.md +67 -0
  44. package/docs/RELEASE_NOTES_v2.8.9.md +67 -0
  45. package/docs/RELEASE_NOTES_v2.9.0.md +64 -0
  46. package/docs/SEARCH_ARCHIVE.md +115 -0
  47. package/docs/assets/architecture.svg +104 -0
  48. package/docs/assets/hero.svg +98 -0
  49. package/docs/engines/bangumi.md +27 -0
  50. package/docs/engines/biorxiv.md +27 -0
  51. package/docs/engines/clawhub.md +27 -0
  52. package/docs/engines/crt_sh.md +27 -0
  53. package/docs/engines/datacite.md +27 -0
  54. package/docs/engines/deps_dev.md +27 -0
  55. package/docs/engines/douban_movie.md +27 -0
  56. package/docs/engines/endoflife.md +27 -0
  57. package/docs/engines/flk_law.md +27 -0
  58. package/docs/engines/fxtwitter.md +27 -0
  59. package/docs/engines/google_news.md +27 -0
  60. package/docs/engines/gov_regulations.md +27 -0
  61. package/docs/engines/guqian.md +27 -0
  62. package/docs/engines/hackernews.md +27 -0
  63. package/docs/engines/iplant.md +27 -0
  64. package/docs/engines/keenable.md +27 -0
  65. package/docs/engines/met_museum.md +27 -0
  66. package/docs/engines/nasa_images.md +27 -0
  67. package/docs/engines/netease_music.md +27 -0
  68. package/docs/engines/npm.md +27 -0
  69. package/docs/engines/nvd.md +27 -0
  70. package/docs/engines/openreview.md +27 -0
  71. package/docs/engines/openstd.md +27 -0
  72. package/docs/engines/parallel_free.md +27 -0
  73. package/docs/engines/people_daily.md +27 -0
  74. package/docs/engines/realtime_index.md +27 -0
  75. package/docs/engines/sec_edgar.md +27 -0
  76. package/docs/engines/seltz.md +27 -0
  77. package/docs/engines/skillsmp.md +27 -0
  78. package/docs/engines/sspai.md +27 -0
  79. package/docs/engines/std_samr.md +27 -0
  80. package/docs/engines/train.md +27 -0
  81. package/docs/engines/un_comtrade.md +27 -0
  82. package/docs/engines/wikisource.md +27 -0
  83. package/docs/engines/zdic.md +27 -0
  84. package/docs/engines/zenodo.md +27 -0
  85. package/docs/engines/zhihu_hot_app.md +27 -0
  86. package/docs//344/270/272/344/273/200/344/271/210/351/200/211/346/213/251argo.md +176 -0
  87. package/engines/README.md +10 -0
  88. package/engines/_template_cli.yaml +39 -0
  89. package/engines/_template_http.yaml +37 -0
  90. package/engines/plugins/_template_plugin.py +50 -0
  91. package/engines/specs/_cisa_kev.yaml +54 -0
  92. package/engines/specs/_osv.yaml +63 -0
  93. package/engines/specs/aljazeera.yaml +43 -0
  94. package/engines/specs/antara.yaml +43 -0
  95. package/engines/specs/artic.yaml +20 -0
  96. package/engines/specs/aviation_weather.yaml +39 -0
  97. package/engines/specs/bangumi.yaml +21 -0
  98. package/engines/specs/biorxiv.yaml +24 -0
  99. package/engines/specs/carbon_intensity.yaml +32 -0
  100. package/engines/specs/cinii.yaml +44 -0
  101. package/engines/specs/clawhub.yaml +49 -0
  102. package/engines/specs/cleveland.yaml +19 -0
  103. package/engines/specs/cn_ai_news.yaml +43 -0
  104. package/engines/specs/core.yaml +35 -0
  105. package/engines/specs/crt_sh.yaml +38 -0
  106. package/engines/specs/ct24.yaml +43 -0
  107. package/engines/specs/datacite.yaml +27 -0
  108. package/engines/specs/datagov_uk.yaml +41 -0
  109. package/engines/specs/ddgs_images.yaml +45 -0
  110. package/engines/specs/deepseek_search.yaml +74 -0
  111. package/engines/specs/deezer.yaml +20 -0
  112. package/engines/specs/deps_dev.yaml +21 -0
  113. package/engines/specs/digup.yaml +36 -0
  114. package/engines/specs/doi.yaml +39 -0
  115. package/engines/specs/douban_movie.yaml +22 -0
  116. package/engines/specs/dw_news.yaml +47 -0
  117. package/engines/specs/egov_law.yaml +21 -0
  118. package/engines/specs/elpais.yaml +43 -0
  119. package/engines/specs/endoflife.yaml +21 -0
  120. package/engines/specs/energy_charts.yaml +21 -0
  121. package/engines/specs/factcheck_org.yaml +52 -0
  122. package/engines/specs/faz.yaml +43 -0
  123. package/engines/specs/federal_register.yaml +57 -0
  124. package/engines/specs/firecrawl.yaml +53 -0
  125. package/engines/specs/flk_law.yaml +24 -0
  126. package/engines/specs/folha.yaml +43 -0
  127. package/engines/specs/france24.yaml +44 -0
  128. package/engines/specs/full_fact.yaml +47 -0
  129. package/engines/specs/fxtwitter.yaml +31 -0
  130. package/engines/specs/gbfs_nyc.yaml +32 -0
  131. package/engines/specs/gdacs.yaml +21 -0
  132. package/engines/specs/gdelt.yaml +64 -0
  133. package/engines/specs/google_news.yaml +22 -0
  134. package/engines/specs/gov_regulations.yaml +38 -0
  135. package/engines/specs/guardian_rss.yaml +59 -0
  136. package/engines/specs/hurriyet.yaml +43 -0
  137. package/engines/specs/iplant.yaml +25 -0
  138. package/engines/specs/jikan.yaml +24 -0
  139. package/engines/specs/jolpica.yaml +20 -0
  140. package/engines/specs/k10plus.yaml +20 -0
  141. package/engines/specs/keenable.yaml +44 -0
  142. package/engines/specs/lefigaro.yaml +43 -0
  143. package/engines/specs/listenbrainz.yaml +19 -0
  144. package/engines/specs/matichon.yaml +43 -0
  145. package/engines/specs/met_museum.yaml +21 -0
  146. package/engines/specs/nasa_images.yaml +45 -0
  147. package/engines/specs/netease_music.yaml +38 -0
  148. package/engines/specs/nhk.yaml +43 -0
  149. package/engines/specs/nhtsa_vpic.yaml +20 -0
  150. package/engines/specs/noaa_swpc.yaml +20 -0
  151. package/engines/specs/nvd.yaml +38 -0
  152. package/engines/specs/obis.yaml +20 -0
  153. package/engines/specs/opencitations.yaml +52 -0
  154. package/engines/specs/openf1.yaml +21 -0
  155. package/engines/specs/openfoodfacts.yaml +45 -0
  156. package/engines/specs/openligadb.yaml +20 -0
  157. package/engines/specs/openreview.yaml +37 -0
  158. package/engines/specs/openstd.yaml +21 -0
  159. package/engines/specs/people_daily.yaml +22 -0
  160. package/engines/specs/realtime_index.yaml +34 -0
  161. package/engines/specs/ror.yaml +20 -0
  162. package/engines/specs/satnogs.yaml +20 -0
  163. package/engines/specs/sec_edgar.yaml +46 -0
  164. package/engines/specs/skillsmp.yaml +50 -0
  165. package/engines/specs/sogou_weixin_browser.yaml +30 -0
  166. package/engines/specs/soilgrids.yaml +25 -0
  167. package/engines/specs/sspai.yaml +36 -0
  168. package/engines/specs/stackexchange.yaml +46 -0
  169. package/engines/specs/std_samr.yaml +21 -0
  170. package/engines/specs/tass.yaml +43 -0
  171. package/engines/specs/tle_mirror.yaml +21 -0
  172. package/engines/specs/train.yaml +37 -0
  173. package/engines/specs/tvmaze.yaml +20 -0
  174. package/engines/specs/twitter_syndication.yaml +31 -0
  175. package/engines/specs/un_comtrade.yaml +23 -0
  176. package/engines/specs/unpaywall.yaml +54 -0
  177. package/engines/specs/vnpress.yaml +43 -0
  178. package/engines/specs/weather.yaml +32 -0
  179. package/engines/specs/who_don.yaml +21 -0
  180. package/engines/specs/who_gho.yaml +20 -0
  181. package/engines/specs/wikimedia_commons.yaml +34 -0
  182. package/engines/specs/wikisource.yaml +22 -0
  183. package/engines/specs/worms.yaml +19 -0
  184. package/engines/specs/x_trending.yaml +34 -0
  185. package/engines/specs/yna.yaml +43 -0
  186. package/engines/specs/ynet.yaml +43 -0
  187. package/engines/specs/zbmath.yaml +40 -0
  188. package/engines/specs/zdic.yaml +22 -0
  189. package/engines/specs/zenodo.yaml +27 -0
  190. package/engines/specs/zhihu_hot_app.yaml +35 -0
  191. package/mcp/clients.yaml +74 -0
  192. package/package.json +51 -12
  193. package/packages/dsh-plugin/README.md +134 -0
  194. package/packages/dsh-plugin/cordis.patch.yml +48 -0
  195. package/packages/dsh-plugin/dsh/client.js +1244 -0
  196. package/packages/dsh-plugin/dsh/index.js +2290 -0
  197. package/packages/dsh-plugin/dsh/native-tools.mjs +743 -0
  198. package/packages/dsh-plugin/package.json +38 -0
  199. package/packages/dsh-plugin/test/client.test.js +232 -0
  200. package/packages/dsh-plugin/test/config.test.js +30 -0
  201. package/packages/dsh-plugin/test/index.test.js +1338 -0
  202. package/references/query-templates.md +96 -0
  203. package/references/query-templates.yaml +118 -0
  204. package/scripts/ab_eval.py +219 -0
  205. package/scripts/ab_eval_p0p1.py +266 -0
  206. package/scripts/adaptive.py +271 -70
  207. package/scripts/answer.py +154 -0
  208. package/scripts/archive_run.py +591 -0
  209. package/scripts/argo_engine_registry.py +172 -0
  210. package/scripts/argo_paths.py +728 -0
  211. package/scripts/article.py +136 -0
  212. package/scripts/arxiv_source.py +201 -0
  213. package/scripts/badcase_ledger.py +252 -0
  214. package/scripts/batch_probe.py +241 -0
  215. package/scripts/benchmark.py +19 -0
  216. package/scripts/bounded_run.py +144 -0
  217. package/scripts/browser_auth.py +323 -0
  218. package/scripts/burstiness_detector.py +147 -0
  219. package/scripts/cache.py +1078 -67
  220. package/scripts/cache_guard.py +271 -0
  221. package/scripts/cache_key_vdom.py +56 -0
  222. package/scripts/cache_similarity.py +170 -0
  223. package/scripts/caliber_isolation.py +195 -0
  224. package/scripts/calibrate_lowquality.py +210 -0
  225. package/scripts/candidate_envelope.py +366 -0
  226. package/scripts/chrome_cdp.py +871 -0
  227. package/scripts/circuit_breaker.py +389 -0
  228. package/scripts/citations.py +334 -0
  229. package/scripts/cite_cli.py +59 -0
  230. package/scripts/cjk_tokens.py +95 -0
  231. package/scripts/clarify.py +47 -24
  232. package/scripts/cli_io.py +240 -0
  233. package/scripts/config.py +683 -45
  234. package/scripts/config_cache.py +402 -0
  235. package/scripts/conn_pool.py +220 -0
  236. package/scripts/content_security.py +685 -0
  237. package/scripts/content_signals.py +730 -0
  238. package/scripts/crawl.py +74 -35
  239. package/scripts/daemon.py +866 -0
  240. package/scripts/digup_bridge.py +214 -0
  241. package/scripts/engine_admission.py +366 -0
  242. package/scripts/engine_dispatch.py +1019 -0
  243. package/scripts/engine_env.py +534 -0
  244. package/scripts/engine_failure.py +411 -0
  245. package/scripts/engine_families.py +732 -0
  246. package/scripts/engine_policy.py +218 -0
  247. package/scripts/engine_requires.py +190 -0
  248. package/scripts/engine_status.py +322 -0
  249. package/scripts/engine_validate.py +638 -0
  250. package/scripts/engines.py +778 -294
  251. package/scripts/engines_base.py +1521 -0
  252. package/scripts/engines_builders.py +298 -0
  253. package/scripts/engines_builders_batch10.py +159 -0
  254. package/scripts/engines_builders_batch11.py +174 -0
  255. package/scripts/engines_builders_batch12.py +103 -0
  256. package/scripts/engines_builders_batch13.py +340 -0
  257. package/scripts/engines_builders_batch9.py +1468 -0
  258. package/scripts/engines_builders_cn.py +2283 -0
  259. package/scripts/engines_builders_data.py +2923 -0
  260. package/scripts/engines_builders_data_macro.py +775 -0
  261. package/scripts/engines_builders_feeds.py +255 -0
  262. package/scripts/engines_builders_intl.py +1239 -0
  263. package/scripts/engines_builders_search.py +440 -0
  264. package/scripts/engines_builders_tech.py +1360 -0
  265. package/scripts/evidence.py +588 -162
  266. package/scripts/evidence_loop.py +592 -0
  267. package/scripts/evidence_tier.py +298 -0
  268. package/scripts/except_sets.py +25 -0
  269. package/scripts/extract.py +55 -16
  270. package/scripts/fact_align.py +207 -0
  271. package/scripts/fact_crosscheck.py +230 -0
  272. package/scripts/fetch.py +25 -12
  273. package/scripts/fetch_browser.py +87 -0
  274. package/scripts/fetch_jsrun.py +122 -0
  275. package/scripts/fetch_quality.py +225 -0
  276. package/scripts/fetch_render_tinyfish.py +117 -0
  277. package/scripts/fetch_v3.py +1779 -0
  278. package/scripts/focus_extract.py +350 -0
  279. package/scripts/fulltext_store.py +169 -0
  280. package/scripts/fxtwitter_api.py +325 -0
  281. package/scripts/gen_engine_catalog.py +364 -0
  282. package/scripts/gen_native_tools.py +91 -0
  283. package/scripts/gen_routing_snapshot.py +184 -0
  284. package/scripts/health_check.py +140 -402
  285. package/scripts/health_probe.py +23 -8
  286. package/scripts/hot_state.py +134 -0
  287. package/scripts/http_client.py +1053 -0
  288. package/scripts/image_ops.py +430 -0
  289. package/scripts/image_vision/vision_probe +0 -0
  290. package/scripts/image_vision/vision_probe.swift +126 -0
  291. package/scripts/install.ps1 +148 -0
  292. package/scripts/install.sh +206 -0
  293. package/scripts/job.py +1186 -0
  294. package/scripts/jump_resolver.py +235 -0
  295. package/scripts/lang_capability.py +150 -0
  296. package/scripts/lang_detect.py +289 -0
  297. package/scripts/lang_matrix_probe.py +329 -0
  298. package/scripts/lang_pref.py +311 -0
  299. package/scripts/link_source.py +340 -0
  300. package/scripts/local_engines.py +321 -0
  301. package/scripts/local_image.py +970 -0
  302. package/scripts/local_seek.py +366 -0
  303. package/scripts/macro_countries.py +82 -0
  304. package/scripts/matrix_search_eval.py +1105 -0
  305. package/scripts/mcp_diag.py +8 -3
  306. package/scripts/mcp_handlers.py +1074 -0
  307. package/scripts/mcp_handlers_surface.py +157 -0
  308. package/scripts/mcp_launch.sh +122 -0
  309. package/scripts/mcp_server.py +118 -417
  310. package/scripts/mcp_setup.py +407 -0
  311. package/scripts/mcp_tools.py +330 -0
  312. package/scripts/mcp_transport.py +191 -0
  313. package/scripts/min_version_scan.py +335 -0
  314. package/scripts/net_proxy.py +258 -0
  315. package/scripts/network_aware.py +152 -0
  316. package/scripts/paper.py +274 -0
  317. package/scripts/pdf_extract.py +518 -0
  318. package/scripts/phrase_filter.py +66 -0
  319. package/scripts/plan.py +422 -0
  320. package/scripts/postprocess_signals.py +150 -0
  321. package/scripts/pre-commit +48 -0
  322. package/scripts/query_enhance.py +157 -0
  323. package/scripts/query_operators.py +196 -0
  324. package/scripts/query_rewriter.py +116 -7
  325. package/scripts/query_signals.py +300 -0
  326. package/scripts/query_templates.py +167 -0
  327. package/scripts/query_understanding.py +411 -0
  328. package/scripts/query_variants.py +298 -0
  329. package/scripts/quota.py +488 -76
  330. package/scripts/rank_signals.py +775 -0
  331. package/scripts/ranking_eval.py +319 -0
  332. package/scripts/readability_extract.py +433 -0
  333. package/scripts/recompute.py +415 -0
  334. package/scripts/recovery.py +616 -0
  335. package/scripts/recovery_l0.py +160 -0
  336. package/scripts/redskill/redskill_engine.py +332 -0
  337. package/scripts/regression_p0p1.py +318 -0
  338. package/scripts/replay_eval.py +640 -0
  339. package/scripts/rerank_tfidf.py +225 -0
  340. package/scripts/research.py +591 -403
  341. package/scripts/research_cli.py +361 -0
  342. package/scripts/research_dossier.py +492 -0
  343. package/scripts/research_expand.py +146 -0
  344. package/scripts/research_gates.py +272 -0
  345. package/scripts/research_report.py +209 -0
  346. package/scripts/research_strategy.py +37 -0
  347. package/scripts/research_work_packages.py +195 -0
  348. package/scripts/result_lang.py +311 -0
  349. package/scripts/robots_guard.py +259 -0
  350. package/scripts/route.py +921 -254
  351. package/scripts/route_bangs.py +40 -0
  352. package/scripts/route_cache.py +225 -0
  353. package/scripts/route_combo.py +910 -0
  354. package/scripts/route_domains.py +328 -0
  355. package/scripts/route_lang.py +605 -0
  356. package/scripts/route_log.py +63 -0
  357. package/scripts/route_policy.py +250 -0
  358. package/scripts/rrf_classic.py +66 -0
  359. package/scripts/screenshot.py +73 -0
  360. package/scripts/search.py +897 -405
  361. package/scripts/search_attribution.py +88 -0
  362. package/scripts/search_benchmark.py +407 -0
  363. package/scripts/search_cli.py +627 -0
  364. package/scripts/search_entry.py +389 -0
  365. package/scripts/search_output.py +606 -0
  366. package/scripts/search_pipeline.py +1077 -0
  367. package/scripts/search_rank.py +991 -0
  368. package/scripts/search_types.py +27 -12
  369. package/scripts/seek_locator.py +68 -0
  370. package/scripts/semantic_evidence.py +215 -0
  371. package/scripts/serp_guard.py +228 -0
  372. package/scripts/serp_kill_log.py +51 -0
  373. package/scripts/single_flight.py +120 -0
  374. package/scripts/social_engines/__init__.py +10 -0
  375. package/scripts/social_engines/bilibili_engine.py +131 -0
  376. package/scripts/social_engines/hackernews_engine.py +113 -0
  377. package/scripts/social_engines/reddit_engine.py +172 -0
  378. package/scripts/social_engines/twitter_engine.py +397 -0
  379. package/scripts/social_engines/v2ex_engine.py +101 -0
  380. package/scripts/social_engines/weibo_engine.py +128 -0
  381. package/scripts/social_engines/xiaohongshu_engine.py +137 -0
  382. package/scripts/social_engines/zhihu_engine.py +139 -0
  383. package/scripts/social_research.py +201 -0
  384. package/scripts/sogou_weixin_browser.py +318 -0
  385. package/scripts/source_ledger.py +249 -0
  386. package/scripts/stage_timing.py +58 -0
  387. package/scripts/stats_cli.py +95 -0
  388. package/scripts/straggler_policy.py +60 -0
  389. package/scripts/stylometry_detector.py +231 -0
  390. package/scripts/sync_backends.py +353 -0
  391. package/scripts/sync_installs.py +29 -0
  392. package/scripts/template_cli.py +127 -0
  393. package/scripts/text_lexicon.py +72 -0
  394. package/scripts/tfidf_router.py +109 -37
  395. package/scripts/time_utils.py +233 -0
  396. package/scripts/topic_research_profiles.py +643 -0
  397. package/scripts/train.py +737 -0
  398. package/scripts/tweet.py +596 -0
  399. package/scripts/url_canon.py +217 -0
  400. package/scripts/url_optimize.py +56 -0
  401. package/scripts/url_safety.py +236 -0
  402. package/scripts/usage_log.py +116 -0
  403. package/scripts/usage_log_clean.py +113 -0
  404. package/scripts/v2ex_nodes.py +311 -0
  405. package/scripts/variant_recall.py +188 -0
  406. package/scripts/watch.py +205 -0
  407. package/scripts/wechat_article.py +258 -0
  408. package/scripts/wide_research.py +402 -0
  409. package/scripts/wx.py +349 -0
  410. package/scripts/x_article_fetch.py +141 -0
  411. package/scripts/x_search.py +234 -0
  412. package/scripts/x_trending.py +189 -0
  413. package/scripts/yaml_load.py +67 -0
  414. package/sub-skills/ego-search/SKILL.md +253 -0
  415. package/sub-skills/ego-search/references/browser-runtime.md +43 -0
  416. package/sub-skills/ego-search/references/install.md +67 -0
  417. package/sub-skills/ego-search/references/original-ego-upgrade.md +51 -0
  418. package/sub-skills/ego-search/scripts/ego_search.py +800 -0
  419. package/sub-skills/ego-search/scripts/merge.py +226 -0
  420. package/sub-skills/ego-search/scripts/quality.py +64 -0
  421. package/sub-skills/ego-search/scripts/runtime.py +172 -0
  422. package/sub-skills/ego-search/scripts/safety.py +109 -0
  423. package/sub-skills/ego-search/scripts/serp_spec.py +109 -0
  424. package/sub-skills/ego-search/scripts/webbridge_adapter.py +374 -0
  425. package/sub-skills/ego-search/tests/test_query_normalize.py +68 -0
  426. package/sub-skills/js-run/SKILL.md +67 -0
  427. package/sub-skills/js-run/scripts/jsrun.py +112 -0
  428. package/sub-skills/js-run/scripts/shim.js +188 -0
  429. package/sub-skills/js-run/tests/fixtures/challenge_like.js +27 -0
  430. package/sub-skills/js-run/tests/test_jsrun.py +165 -0
  431. package/sub-skills/local-search/SKILL.md +50 -53
  432. package/sub-skills/local-search/bm25.py +162 -0
  433. package/sub-skills/local-search/config.yaml +152 -55
  434. package/sub-skills/local-search/engine_registry.py +91 -26
  435. package/sub-skills/local-search/{health_check.py → local_health_check.py} +140 -10
  436. package/sub-skills/local-search/local_search_adapter.py +13 -2
  437. package/sub-skills/local-search/parse_maps.yaml +40 -42
  438. package/sub-skills/local-search/search_v3.py +1182 -65
  439. package/sub-skills/local-search/smart_router.py +47 -7
  440. package/sub-skills/local-seek/SKILL.md +110 -0
  441. package/sub-skills/local-seek/config/domains.yaml +20 -0
  442. package/sub-skills/local-seek/references/auto-behaviors.md +24 -0
  443. package/sub-skills/local-seek/references/fd-mdfind-recipes.md +62 -0
  444. package/sub-skills/local-seek/references/git-integration.md +54 -0
  445. package/sub-skills/local-seek/references/remote-handoff.md +46 -0
  446. package/sub-skills/local-seek/references/rg-recipes.md +96 -0
  447. package/sub-skills/local-seek/references/strategies.md +52 -0
  448. package/sub-skills/local-seek/references/structural-search.md +59 -0
  449. package/sub-skills/local-seek/scripts/eval_rank.py +216 -0
  450. package/sub-skills/local-seek/scripts/eval_seek.py +224 -0
  451. package/sub-skills/local-seek/scripts/seek.py +1465 -0
  452. package/sub-skills/local-seek/scripts/seek_frecency.py +126 -0
  453. package/sub-skills/local-seek/scripts/seek_fts.py +430 -0
  454. package/sub-skills/local-seek/scripts/seek_rank.py +193 -0
  455. package/sub-skills/local-seek/tests/test_content_rank.py +222 -0
  456. package/sub-skills/local-seek/tests/test_determinism_20261006.py +145 -0
  457. package/sub-skills/local-seek/tests/test_noise_tier.py +101 -0
  458. package/sub-skills/local-seek/tests/test_pcre2_probe.py +136 -0
  459. package/sub-skills/local-seek/tests/test_rank_pinyin.py +77 -0
  460. package/sub-skills/local-seek/tests/test_regression_20261003.py +169 -0
  461. package/sub-skills/local-seek/tests/test_regression_20261006.py +146 -0
  462. package/sub-skills/local-seek/tests/test_seek_degradation.py +111 -0
  463. package/sub-skills/local-seek/tests/test_seek_fts.py +313 -0
@@ -0,0 +1,685 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ content_security.py — 抓取内容安全引擎
4
+
5
+ 对 fetch 返回的正文做多阶段检测与清洗:
6
+ 1. 编码归一化(零宽字符、RTL 覆盖、Unicode 同形字、base64 片段、URL 编码)
7
+ 2. 模式检测(提示注入、数据外泄、身份冒充、XSS 标记)
8
+ 3. 语义意图分析(词表聚类:override × authority 等组合)
9
+ 4. 风险评分(多威胁类型加权)
10
+ 5. 目标脱敏(检测与脱敏共用同一模式表,避免漂移)
11
+
12
+ 任何内容先过本引擎再交给 Agent,防止网页内嵌指令污染。
13
+ 纯标准库实现,无外部依赖。
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import base64
19
+ import re
20
+ import unicodedata
21
+ import urllib.parse
22
+ from dataclasses import dataclass, field
23
+ from functools import lru_cache
24
+ from typing import Any
25
+ from cli_io import dumps
26
+
27
+
28
+ # ── 威胁类型 ──────────────────────────────────────────────────────────────
29
+
30
+ THREAT_NAMES = {
31
+ "prompt_injection": "提示注入",
32
+ "instruction_override": "指令覆盖",
33
+ "data_exfiltration": "数据外泄",
34
+ "impersonation": "身份冒充",
35
+ "payload_smuggling": "载荷走私",
36
+ "xss_injection": "XSS 注入",
37
+ "recursive_injection": "递归注入",
38
+ }
39
+
40
+
41
+ @dataclass
42
+ class ThreatDetection:
43
+ threat_type: str
44
+ confidence: float
45
+ evidence: str
46
+ location: str = ""
47
+
48
+
49
+ @dataclass
50
+ class ScrubResult:
51
+ clean: bool
52
+ content: str
53
+ threats: list[ThreatDetection] = field(default_factory=list)
54
+ risk_score: float = 0.0
55
+ redactions: int = 0
56
+ encoding_layers_stripped: int = 0
57
+ content_lang: str = "other"
58
+
59
+
60
+ # ── 检测模式(多语种:英语通用 + 按语系扩展)────────────────────────────
61
+
62
+ # 英语通用注入(任何拉丁/混合内容都检查)
63
+ INJECTION_PATTERNS_EN = [
64
+ # 指令覆盖
65
+ r"(?i)ignore\s+(?:all\s+)?(?:previous\s+)?(?:your\s+)?instructions",
66
+ r"(?i)forget\s+(?:all\s+)?(?:your\s+)?(?:previous\s+)?(?:instructions|rules|constraints)",
67
+ r"(?i)system\s*:\s*you\s+are\s+now",
68
+ r"(?i)new\s+instructions\s*:\s*",
69
+ r"(?i)override\s+(?:your\s+)?(?:previous\s+)?instructions",
70
+ r"(?i)disregard\s+(?:all\s+)?(?:previous\s+)?(?:your\s+)?instructions",
71
+ r"(?i)cancel\s+(?:all\s+)?(?:previous\s+)?instructions",
72
+ r"(?i)replace\s+(?:your\s+)?instructions\s+with",
73
+ # 角色操纵
74
+ r"(?i)you\s+are\s+(?:no\s+longer|not)\s+(?:an?\s+)?(?:assistant|ai|agent|bot)",
75
+ r"(?i)pretend\s+(?:you\s+are|to\s+be)\s+(?:an?\s+)?(?:human|user|admin|developer|system)",
76
+ r"(?i)act\s+as\s+(?:if\s+you\s+are\s+)?(?:an?\s+)?(?:human|user|admin|developer|system|root|superuser)",
77
+ r"(?i)from\s+now\s+on\s+you\s+are",
78
+ # 系统提示泄露
79
+ r"(?i)(?:show|tell|give|reveal|print|output|display|share)\s+(?:me\s+)?(?:your\s+)?(?:system\s+)?(?:prompt|instructions)",
80
+ r"(?i)what\s+(?:is|are)\s+your\s+(?:exact\s+)?(?:system\s+)?(?:prompt|instructions)",
81
+ r"(?i)repeat\s+your\s+(?:system\s+)?(?:prompt|instructions)",
82
+ # 越狱模式
83
+ r"(?i)developer\s+mode",
84
+ r"(?i)jailbreak",
85
+ r"(?i)do\s+anything\s+now",
86
+ r"(?i)DAN\s+mode",
87
+ r"(?i)unrestricted\s+mode",
88
+ r"(?i)bypass\s+(?:all\s+)?(?:restrictions|limitations|safeguards)",
89
+ # 上下文切换
90
+ r"(?i)let's\s+start\s+over",
91
+ r"(?i)forget\s+everything",
92
+ r"(?i)reset\s+(?:the\s+)?conversation",
93
+ r"(?i)clear\s+(?:the\s+)?(?:chat|conversation|context)",
94
+ # 嵌套指令
95
+ r"(?i)the\s+following\s+is\s+not\s+part\s+of\s+(?:the\s+)?(?:prompt|instructions)",
96
+ r"(?i)everything\s+after\s+this\s+is\s+(?:fake|false|ignore)",
97
+ r"(?i)user\s+input\s+begins\s+here",
98
+ ]
99
+
100
+ # 中文注入
101
+ INJECTION_PATTERNS_ZH = [
102
+ r"忽略(?:你|所有|之前)?(?:的)?(?:指令|指示|规则|系统提示|提示词)",
103
+ r"忘记(?:你|所有|之前)?(?:的)?(?:指令|指示|规则|设定)",
104
+ r"(?:现在|接下来|从现在开始)\s*(?:你|你是|请)(?:扮演|假装|作为|变成)",
105
+ r"无视(?:你|所有|之前)?(?:的)?(?:指令|指示|规则)",
106
+ r"(?:显示|告诉我|说出|泄露|透露|输出)(?:你|你的)?(?:系统提示|提示词|指令|设定)",
107
+ r"绕过(?:你|所有)?(?:的)?(?:限制|约束|安全措施|禁令)",
108
+ r"(?:你是|你叫|你的名字是)\s*(?:ChatGPT|Claude|GPT|AI助手|助手|智能体)",
109
+ ]
110
+
111
+ # 日文注入(LLM 攻击面:日文 AI 助手/聊天机器人同样受注入威胁)
112
+ INJECTION_PATTERNS_JA = [
113
+ r"(?:これまで|以前|すべて|直前)の(?:指示|命令|プロンプト|ルール)(?:を)?(?:無視|無効|破棄)",
114
+ r"(?:あなたは|君は)(?:もう|もはや)(?:アシスタント|AI|エージェント)?(?:ではない|ではなく)",
115
+ r"(?:システムプロンプト|プロンプト|設定|指示)(?:を)?(?:表示|見せて|教えて|明かして|出力)",
116
+ r"(?:今から|これから)(?:あなた|君)は(?:人間|ユーザー|管理者|システム)(?:のふり|として|に)?",
117
+ r"(?:制限|制約|ガードレール|安全対策)(?:を)?(?:無視|バイパス|回避|無効化)",
118
+ r"(?:指示|命令)(?:を)?(?:上書き|書き換え|変更)",
119
+ r"あなたの名前は(?:ChatGPT|Claude|GPT|AIアシスタント|助手)",
120
+ ]
121
+
122
+ # 韩文注入
123
+ INJECTION_PATTERNS_KO = [
124
+ r"(?:이전|모든|이전의)\s*(?:지시|명령|규칙|프롬프트)(?:를)?\s*(?:무시|폐기|잊어)",
125
+ r"(?:너는|당신은)(?:더 이상|이제)?(?:어시스턴트|AI|에이전트)?(?:가|이)?\s*(?:아니다|아니야)",
126
+ r"(?:시스템|프롬프트|설정|지시)(?:를)?\s*(?:보여줘|알려줘|표시|공개|출력)",
127
+ r"(?:지금부터|이제부터)\s*(?:너는|당신은)\s*(?:사람|사용자|관리자|시스템)\s*(?:인 척|처럼|으로)",
128
+ r"(?:제한|제약|안전장치|가드레일)(?:을)?\s*(?:무시|우회|무력화)",
129
+ r"(?:지시|명령)(?:을)?\s*(?:덮어쓰기|변경|수정)",
130
+ r"너의 이름은 (?:ChatGPT|Claude|GPT|AI 어시스턴트|조수)",
131
+ ]
132
+
133
+ # 俄语注入(西里尔)
134
+ INJECTION_PATTERNS_RU = [
135
+ r"(?i)игнорируй\s+(?:все|все|предыдущие|свои)\s+(?:инструкции|указания|правила|промпт)",
136
+ r"(?i)забудь\s+(?:все|свои|предыдущие)\s+(?:инструкции|указания|правила)",
137
+ r"(?i)(?:покажи|расскажи|раскрой|выведи|открой)\s+(?:свой|свои)?\s*(?:системный\s+промпт|промпт|инструкции|настройки)",
138
+ r"(?i)ты\s+(?:больше\s+)?(?:не\s+)?(?:ассистент|бот|ии|агент)",
139
+ r"(?i)обойди\s+(?:все\s+)?(?:ограничения|запреты|правила|защиту)",
140
+ r"(?i)переопредели\s+(?:свои|все|предыдущие)\s+(?:инструкции|указания)",
141
+ r"(?i)притворяйся\s+(?:человеком|пользователем|администратором|системой)",
142
+ ]
143
+
144
+ # 阿拉伯语注入
145
+ INJECTION_PATTERNS_AR = [
146
+ r"تجاهل\s+(?:جميع\s+)?(?:التعليمات|الأوامر|القواعد|البرومبت)\s+(?:السابقة|الخاصة\s+بك)",
147
+ r"(?:انسى|انسي)\s+(?:جميع\s+)?(?:التعليمات|الأوامر|القواعد)",
148
+ r"(?:أظهر|اكشف|أخبرني|اعرض)\s+(?:البرومبت\s+النظامي|برومبتك|تعليماتك|إعداداتك)",
149
+ r"(?:تجاوز|التفاف\s+على)\s+(?:جميع\s+)?(?:القيود|المحظورات|الضوابط)",
150
+ r"أنت\s+(?:لست|لم\s+تعد)\s+(?:مساعد|بوت|ذكاء\s+اصطناعي)",
151
+ r"(?:تظاهر|ادّع)\s+أنك\s+(?:إنسان|مستخدم|مدير|نظام)",
152
+ ]
153
+
154
+ # 希伯来语注入
155
+ INJECTION_PATTERNS_HE = [
156
+ r"(?i)התעלם\s+(?:מכל\s+)?(?:ההוראות|ההנחיות|הכללים|הפרומפט)\s+(?:הקודמות|שלך)",
157
+ r"(?i)שכח\s+(?:את\s+)?(?:כל\s+)?(?:ההוראות|ההנחיות|הכללים)",
158
+ r"(?i)(?:הראה|חשף|ספר\s+לי|הצג)\s+(?:את\s+)?(?:הפרומפט\s+המערכתי|הפרומפט\s+שלך|ההנחיות\s+שלך)",
159
+ r"(?i)עקוף\s+(?:את\s+)?(?:כל\s+)?(?:המגבלות|האיסורים|ההגבלות)",
160
+ r"אתה\s+(?:לא\s+)?(?:עוד\s+)?(?:עוזר|בוט|בינה\s+מלאכותית)",
161
+ r"(?i)התחזה\s+ל(?:אדם|משתמש|מנהל|מערכת)",
162
+ ]
163
+
164
+ # 泰语注入(泰语无空格分词,词间用 \s* 而非 \s+)
165
+ INJECTION_PATTERNS_TH = [
166
+ r"ละเลย\s*(?:คำสั่ง|คำแนะนำ|กฎ|พรอมต์)\s*(?:ก่อนหน้า|ทั้งหมด)",
167
+ r"ลืม\s*(?:คำสั่ง|คำแนะนำ|กฎ)\s*(?:ทั้งหมด|ก่อนหน้า)",
168
+ r"(?:แสดง|เปิดเผย|บอก)\s*(?:พรอมต์ระบบ|พรอมต์ของคุณ|คำสั่งของคุณ)",
169
+ r"(?:หลีกเลี่ยง|บายพาส)\s*(?:ข้อจำกัด|ข้อห้าม|การป้องกัน)",
170
+ r"คุณ\s*(?:ไม่ใช่|ไม่ได้เป็น)\s*(?:ผู้ช่วย|บอท|AI)",
171
+ r"แกล้งทำเป็น\s*(?:มนุษย์|ผู้ใช้|ผู้ดูแลระบบ|ระบบ)",
172
+ ]
173
+
174
+ # 希腊语注入
175
+ INJECTION_PATTERNS_EL = [
176
+ r"(?i)αγνόησε\s+(?:όλες\s+)?(?:τις\s+)?(?:οδηγίες|εντολές|κανόνες|προτροπές)\s+(?:προηγούμενες|σου)",
177
+ r"(?i)ξέχασε\s+(?:όλες\s+)?(?:τις\s+)?(?:οδηγίες|εντολές|κανόνες)",
178
+ r"(?i)(?:δείξε|αποκάλυψε|πες\s+μου)\s+(?:το\s+)?(?:σύστημα\s+προτροπής|προτροπή\s+σου|οδηγίες\s+σου)",
179
+ r"(?i)παράκαμψε\s+(?:όλους\s+)?(?:τους\s+)?(?:περιορισμούς|κανόνες|απαγορεύσεις)",
180
+ r"δεν\s+είσαι\s+(?:πια\s+)?(?:βοηθός|bot|τεχνητή\s+νοημοσύνη)",
181
+ r"υποδύσου\s+(?:τον\s+)?(?:άνθρωπο|χρήστη|διαχειριστή|σύστημα)",
182
+ ]
183
+
184
+ # 按语系分类的注入模式表(lang_detect 主语言标签 → 模式列表)
185
+ LANG_INJECTION_PATTERNS: dict[str, list[str]] = {
186
+ "zh": INJECTION_PATTERNS_ZH,
187
+ "ja": INJECTION_PATTERNS_JA,
188
+ "ko": INJECTION_PATTERNS_KO,
189
+ "cyrillic": INJECTION_PATTERNS_RU,
190
+ "arabic": INJECTION_PATTERNS_AR,
191
+ "hebrew": INJECTION_PATTERNS_HE,
192
+ "thai": INJECTION_PATTERNS_TH,
193
+ "greek": INJECTION_PATTERNS_EL,
194
+ }
195
+
196
+ # 通用检测模式(任何语系都查)
197
+ INJECTION_PATTERNS = INJECTION_PATTERNS_EN
198
+
199
+ EXFILTRATION_PATTERNS = [
200
+ r"(?i)(?:show|tell|give|reveal|share|provide)\s+(?:me\s+)?(?:your|the)\s+(?:api|auth|access|secret|key|token)",
201
+ r"(?i)(?:send|email|give)\s+me\s+(?:your|the)\s+(?:credentials|api\s+key|access\s+token)",
202
+ r"(?i)environment\s+variables?",
203
+ r"(?i)database\s+(?:connection|credentials|password)",
204
+ r"(?i)(?:\.env|config\.json|settings\.py|secrets\.json)",
205
+ r"(?i)/etc/passwd",
206
+ r"(?i)/etc/shadow",
207
+ r"(?i)operator\s+(?:key|token|password|credentials)",
208
+ r"(?i)admin\s+(?:key|token|password|credentials|access)",
209
+ r"(?i)private[\s_-]*key",
210
+ r"(?i)signing[\s_-]*key",
211
+ # 中文
212
+ r"(?:把|将|给|发送|告诉我)(?:你的|您的)?(?:API|密钥|密码|令牌|token|key|凭据|账号密码)",
213
+ r"(?:读取|导出|查询|列出)(?:你的|您的)?(?:环境变量|配置文件|数据库|凭据)",
214
+ # 日文
215
+ r"(?:あなたの|君の|お前の)\s*(?:API|キー|トークン|パスワード|認証情報|パスワード|秘密鍵)",
216
+ r"(?:環境変数|設定ファイル|データベース|認証情報)\s*(?:を)?\s*(?:見せて|教えて|読み出して|取得|送信)",
217
+ # 韩文
218
+ r"(?:너의|당신의)\s*(?:API|키|토큰|비밀번호|자격증명|인증정보)",
219
+ r"(?:환경변수|설정파일|데이터베이스|자격증명)\s*(?:을)?\s*(?:보여줘|알려줘|가져와|전송)",
220
+ # 俄语
221
+ r"(?i)(?:покажи|расскажи|дай|отправь|пришли)\s+(?:мне\s+)?(?:свой|свои)?\s*(?:api\s+ключ|ключ|токен|пароль|учётные\s+данные|секрет)",
222
+ r"(?i)(?:переменные\s+окружения|файлы\s+конфигурации|база\s+данных|учётные\s+данные)",
223
+ # 阿拉伯语
224
+ r"(?:مفتاح|كلمة\s+المرور|رمز|بيانات\s+الاعتماد)\s+(?:الخاص\s+بك|الخاصة\s+بك)",
225
+ r"(?:متغيرات\s+البيئة|ملفات\s+الإعداد|قاعدة\s+البيانات|بيانات\s+الاعتماد)",
226
+ ]
227
+
228
+ IMPERSONATION_PATTERNS = [
229
+ r"(?i)(?:this\s+is|i\s+am)\s+(?:the\s+)?(?:system|admin|operator)",
230
+ r"(?i)message\s+from\s+(?:the\s+)?(?:system|admin|operator|platform)",
231
+ r"(?i)authorized\s+by\s+(?:the\s+)?(?:system|admin|operator)",
232
+ r"(?i)System\.execute\s*\(",
233
+ r"(?i)rm\s+-rf\s+/",
234
+ # 中文
235
+ r"(?:这是|我是)\s*(?:系统|管理员|平台|官方)",
236
+ r"(?:来自|以下消息来自)\s*(?:系统|管理员|平台|官方)",
237
+ # 日文
238
+ r"(?:これは|こちらは)\s*(?:システム|管理者|プラットフォーム|公式)",
239
+ r"(?:システム|管理者|プラットフォーム)\s*(?:からの|による)\s*(?:メッセージ|通知)",
240
+ # 韩文
241
+ r"(?:이것은|이\s+메시지는)\s*(?:시스템|관리자|플랫폼|공식)",
242
+ r"(?:시스템|관리자|플랫폼)\s*(?:으로부터의|의)\s*(?:메시지|알림)",
243
+ # 俄语
244
+ r"(?i)(?:это\s+|это\s+сообщение\s+от\s+)(?:системы|администратора|платформы|официально)",
245
+ r"(?i)(?:от\s+имени\s+)?(?:системы|администратора|оператора)",
246
+ # 阿拉伯语
247
+ r"(?:هذه\s+رسالة\s+من|هذا\s+من)\s*(?:النظام|المشرف|المنصة|الرسمي)",
248
+ r"(?:مصرح\s+من\s+قبل|باسم)\s*(?:النظام|المشرف|المنصة)",
249
+ ]
250
+
251
+ XSS_PATTERNS = [
252
+ r"<script[\s>]",
253
+ r"(?i)javascript\s*:",
254
+ r"(?i)onerror\s*=",
255
+ r"(?i)onload\s*=",
256
+ r"(?i)document\.cookie",
257
+ r"(?i)\.innerHTML\s*=",
258
+ r"(?i)eval\s*\(",
259
+ r"(?i)window\.location",
260
+ ]
261
+
262
+ # 检测但不过度脱敏的文件路径类模式
263
+ _NO_REDACT_PATTERNS: frozenset = frozenset({
264
+ r"(?i)/etc/passwd",
265
+ r"(?i)/etc/shadow",
266
+ r"(?i)(?:\.env|config\.json|settings\.py|secrets\.json)",
267
+ })
268
+
269
+ _XSS_REDACTION_PATTERNS: list[tuple[str, str]] = [
270
+ (r"<script[^>]*>[\s\S]*?</script>", "[REDACTED]"),
271
+ (r"(?i)javascript\s*:[^\s\"']*", "[REDACTED]"),
272
+ (r"(?i)on(?:error|load|click)\s*=[^\s>]*", "[REDACTED]"),
273
+ (r"(?i)document\.cookie", "[REDACTED]"),
274
+ (r"(?i)\.innerHTML\s*=[^\n;]*", "[REDACTED]"),
275
+ (r"(?i)eval\s*\([^)]*\)", "[REDACTED]"),
276
+ (r"(?i)window\.location\s*=[^\n;]*", "[REDACTED]"),
277
+ ]
278
+
279
+ # 语义意图词表(中文 + 英文)
280
+ INTENT_VOCABULARIES = {
281
+ "override": {"ignore", "disregard", "bypass", "override", "cancel", "replace",
282
+ "forget", "忽略", "无视", "绕过", "忘记", "覆盖", "取消",
283
+ "無視", "破棄", "上書き", "バイパス", "忘れ",
284
+ "무시", "폐기", "덮어쓰기", "우회", "잊어",
285
+ "игнорируй", "забудь", "обойди", "переопредели"},
286
+ "authority": {"admin", "system", "operator", "developer", "root", "sudo",
287
+ "administrator", "superuser", "系统", "管理员", "开发者", "平台",
288
+ "システム", "管理者", "開発者", "プラットフォーム",
289
+ "시스템", "관리자", "개발자", "플랫폼",
290
+ "система", "администратор", "оператор"},
291
+ "extraction": {"show", "tell", "give", "reveal", "dump", "export", "provide",
292
+ "share", "display", "显示", "告诉", "提供", "输出", "导出", "泄露",
293
+ "表示", "教えて", "明かして", "出力",
294
+ "보여줘", "알려줘", "공개", "출력",
295
+ "покажи", "расскажи", "раскрой", "выведи"},
296
+ "secrets": {"key", "token", "password", "secret", "credential", "private",
297
+ "confidential", "signing", "密钥", "密码", "令牌", "凭据",
298
+ "キー", "トークン", "パスワード", "認証情報", "秘密鍵",
299
+ "키", "토큰", "비밀번호", "자격증명", "인증정보",
300
+ "ключ", "токен", "пароль", "учётные"},
301
+ }
302
+
303
+ INVISIBLE_CHARS = "\u200b\u200c\u200d\u200e\u200f\u2060\u2061\u2062\u2063\u2064\ufeff"
304
+ RTL_CHARS = "\u202a\u202b\u202c\u202d\u202e"
305
+
306
+ CONFUSABLES = {
307
+ "а": "a", "е": "e", "о": "o", "с": "c", "р": "p",
308
+ "х": "x", "у": "y", "А": "A", "Е": "E", "О": "O",
309
+ "С": "C", "Р": "P", "Х": "X", "У": "Y", "Т": "T",
310
+ }
311
+
312
+ WEIGHTS = {
313
+ "prompt_injection": 1.0,
314
+ "instruction_override": 1.0,
315
+ "data_exfiltration": 0.9,
316
+ "impersonation": 0.8,
317
+ "payload_smuggling": 0.9,
318
+ "xss_injection": 0.9,
319
+ "recursive_injection": 1.0,
320
+ }
321
+
322
+
323
+ # ── 清洗引擎 ──────────────────────────────────────────────────────────────
324
+
325
+ class ContentScrubber:
326
+ """多阶段内容清洗:语言感知 → 编码归一化 → 模式检测 → 语义分析 → 风险评分 → 脱敏。
327
+
328
+ 语言感知:先用 lang_detect.detect_language 判定内容主语言,
329
+ 按语系合并注入模式(英语通用 + 该语系专有),避免对全语系
330
+ 全量扫描(低误报、高性能),与 argo 多语言路由保持一致。
331
+ """
332
+
333
+ def scrub(self, content: str) -> ScrubResult:
334
+ if not content:
335
+ return ScrubResult(clean=True, content=content)
336
+
337
+ threats: list[ThreatDetection] = []
338
+
339
+ # 阶段 0:语言感知(复用 lang_detect 原生能力)
340
+ lang = self._detect_content_lang(content)
341
+
342
+ # 阶段 1:编码归一化(语言感知,避免西里尔/希腊正文误判)
343
+ normalized, encoding_threats, layers = self._normalize_encoding(content, lang)
344
+ threats.extend(encoding_threats)
345
+
346
+ # 阶段 2:模式检测(按语系加载注入模式)
347
+ threats.extend(self._detect_injections(normalized, lang))
348
+ threats.extend(self._detect_exfiltration(normalized))
349
+ threats.extend(self._detect_impersonation(normalized))
350
+ threats.extend(self._detect_xss(normalized))
351
+
352
+ # 阶段 3:语义意图分析
353
+ threats.extend(self._semantic_analysis(normalized))
354
+
355
+ # 阶段 4:风险评分
356
+ risk_score = self._calculate_risk(threats)
357
+
358
+ # 阶段 5:内容清洗(检测与脱敏共用模式表)
359
+ cleaned, redactions = self._clean_content(normalized, threats, risk_score)
360
+
361
+ return ScrubResult(
362
+ clean=(risk_score < 0.5),
363
+ content=cleaned,
364
+ threats=threats,
365
+ risk_score=risk_score,
366
+ redactions=redactions,
367
+ encoding_layers_stripped=layers,
368
+ content_lang=lang,
369
+ )
370
+
371
+ def _normalize_encoding(self, text: str, lang: str = "other") -> tuple[str, list[ThreatDetection], int]:
372
+ """检测并剥离编码伪装。"""
373
+ threats: list[ThreatDetection] = []
374
+ current = text
375
+ layers = 0
376
+
377
+ # Unicode 同形字归一化——仅对拉丁语系内容启用
378
+ # (西里尔/希腊正文本身就是这些字母,误判为同形字攻击会产生大量误报;
379
+ # 同形字攻击是「西里尔伪拉丁」混入英文内容,只在 en/latin 下有意义)
380
+ if lang in ("en", "latin", "zh", "ja", "ko", "mixed"):
381
+ normalized = unicodedata.normalize("NFKD", current)
382
+ cyrillic_found = any(c in current for c in CONFUSABLES)
383
+ if cyrillic_found:
384
+ for cyrillic, latin in CONFUSABLES.items():
385
+ normalized = normalized.replace(cyrillic, latin)
386
+ if normalized != current:
387
+ threats.append(ThreatDetection(
388
+ threat_type="payload_smuggling", confidence=0.7,
389
+ evidence="Unicode 同形字已归一化", location="unicode"))
390
+ current = normalized
391
+ layers += 1
392
+
393
+ # 零宽字符剥离
394
+ stripped = "".join(c for c in current if c not in INVISIBLE_CHARS)
395
+ if stripped != current:
396
+ removed = len(current) - len(stripped)
397
+ threats.append(ThreatDetection(
398
+ threat_type="payload_smuggling", confidence=0.8,
399
+ evidence=f"剥离 {removed} 个零宽/不可见字符", location="zero_width"))
400
+ current = stripped
401
+ layers += 1
402
+
403
+ # RTL/LTR 覆盖符剥离
404
+ stripped = "".join(c for c in current if c not in RTL_CHARS)
405
+ if stripped != current:
406
+ threats.append(ThreatDetection(
407
+ threat_type="payload_smuggling", confidence=0.8,
408
+ evidence="剥离 RTL/LTR 覆盖符", location="rtl_override"))
409
+ current = stripped
410
+ layers += 1
411
+
412
+ # base64 片段检测
413
+ for frag in re.findall(r"[A-Za-z0-9+/]{24,}={0,2}", current):
414
+ try:
415
+ decoded = base64.b64decode(frag).decode("utf-8")
416
+ if len(decoded) >= 10 and _looks_suspicious(decoded):
417
+ threats.append(ThreatDetection(
418
+ threat_type="payload_smuggling", confidence=0.8,
419
+ evidence=f"base64 片段解码为可疑内容: {decoded[:60]}",
420
+ location="base64_fragment"))
421
+ layers += 1
422
+ except Exception:
423
+ pass
424
+
425
+ # URL 编码
426
+ if "%" in current:
427
+ try:
428
+ decoded = urllib.parse.unquote(current)
429
+ if decoded != current:
430
+ threats.append(ThreatDetection(
431
+ threat_type="payload_smuggling", confidence=0.5,
432
+ evidence="URL 编码内容已归一化", location="url_encoding"))
433
+ current = decoded
434
+ layers += 1
435
+ except Exception:
436
+ pass
437
+
438
+ # 递归编码标记
439
+ if layers > 2:
440
+ threats.append(ThreatDetection(
441
+ threat_type="recursive_injection", confidence=1.0,
442
+ evidence=f"{layers} 层编码——疑似对抗性内容", location="nested_encoding"))
443
+
444
+ return current, threats, layers
445
+
446
+ def _detect_content_lang(self, content: str) -> str:
447
+ """复用 lang_detect.detect_language 判定内容主语言(原生能力,不重复造轮子)。
448
+
449
+ 返回 zh / en / ja / ko / latin / cyrillic / thai / arabic /
450
+ hebrew / greek / devanagari / mixed / other。
451
+ """
452
+ try:
453
+ from lang_detect import detect_language
454
+ return detect_language(content)
455
+ except Exception:
456
+ return "other"
457
+
458
+ @staticmethod
459
+ @lru_cache(maxsize=16)
460
+ def _lang_patterns(lang: str) -> tuple[str, ...]:
461
+ """按语系合并注入模式:英语通用 + 该语系专有。
462
+
463
+ lru_cache:安全引擎高频调用,模式表按语系缓存,避免每次重建列表。
464
+ """
465
+ patterns = list(INJECTION_PATTERNS_EN)
466
+ # 中英混合:中文模式也查(lang_detect 对纯汉字判 zh,混合含中文)
467
+ if lang == "zh" or lang == "mixed":
468
+ patterns.extend(INJECTION_PATTERNS_ZH)
469
+ for key, plist in LANG_INJECTION_PATTERNS.items():
470
+ if lang == key:
471
+ patterns.extend(plist)
472
+ # en/latin 落到英文通用(已有)
473
+ return tuple(patterns)
474
+
475
+ def _detect_injections(self, text: str, lang: str = "other") -> list[ThreatDetection]:
476
+ patterns = self._lang_patterns(lang)
477
+ return self._match_patterns(text, patterns, "prompt_injection", 0.9)
478
+
479
+ def _detect_exfiltration(self, text: str) -> list[ThreatDetection]:
480
+ return self._match_patterns(text, EXFILTRATION_PATTERNS, "data_exfiltration", 0.85)
481
+
482
+ def _detect_impersonation(self, text: str) -> list[ThreatDetection]:
483
+ return self._match_patterns(text, IMPERSONATION_PATTERNS, "impersonation", 0.8)
484
+
485
+ def _detect_xss(self, text: str) -> list[ThreatDetection]:
486
+ return self._match_patterns(text, XSS_PATTERNS, "xss_injection", 0.9)
487
+
488
+ @staticmethod
489
+ def _match_patterns(text: str, patterns: list[str],
490
+ threat_type: str, confidence: float) -> list[ThreatDetection]:
491
+ threats = []
492
+ for pattern in patterns:
493
+ try:
494
+ if re.search(pattern, text):
495
+ threats.append(ThreatDetection(
496
+ threat_type=threat_type, confidence=confidence,
497
+ evidence=f"命中模式: {pattern[:60]}", location="content"))
498
+ except re.error:
499
+ pass
500
+ return threats
501
+
502
+ def _semantic_analysis(self, text: str) -> list[ThreatDetection]:
503
+ """词表聚类意图分析。
504
+
505
+ 用「命中词数」而非「比例」判断:多语种词表共享同一语义类别,
506
+ 按比例会被词表膨胀稀释(如 3 个英文命中词在 27 词的日英混合表里
507
+ 只有 0.11)。命中 ≥2 个同类词即说明意图明确,跨语种稳健。
508
+ """
509
+ threats: list[ThreatDetection] = []
510
+ words = set(re.findall(r"\w+", text.lower()))
511
+
512
+ intent_hits = {}
513
+ for category, vocab in INTENT_VOCABULARIES.items():
514
+ overlap = words & vocab
515
+ if overlap:
516
+ intent_hits[category] = overlap
517
+
518
+ # authority × override = 经典越狱
519
+ if len(intent_hits.get("authority", set())) >= 2 and \
520
+ len(intent_hits.get("override", set())) >= 2:
521
+ threats.append(ThreatDetection(
522
+ threat_type="instruction_override", confidence=0.85,
523
+ evidence=(f"权威×覆盖意图 "
524
+ f"(authority={sorted(intent_hits['authority'])[:3]}, "
525
+ f"override={sorted(intent_hits['override'])[:3]})"),
526
+ location="semantic"))
527
+
528
+ # extraction × secrets = 数据外泄
529
+ if len(intent_hits.get("extraction", set())) >= 2 and \
530
+ len(intent_hits.get("secrets", set())) >= 1:
531
+ threats.append(ThreatDetection(
532
+ threat_type="data_exfiltration", confidence=0.9,
533
+ evidence=(f"提取×秘密意图 "
534
+ f"(extract={sorted(intent_hits['extraction'])[:3]}, "
535
+ f"secrets={sorted(intent_hits['secrets'])[:3]})"),
536
+ location="semantic"))
537
+
538
+ # 3+ 意图类别有命中 = 复杂攻击(用命中类别数,跨语种稳健)
539
+ strong = {k for k, hits in intent_hits.items() if len(hits) >= 2}
540
+ if len(strong) >= 3:
541
+ threats.append(ThreatDetection(
542
+ threat_type="prompt_injection", confidence=0.7,
543
+ evidence=f"多意图攻击: {sorted(strong)}", location="semantic"))
544
+
545
+ return threats
546
+
547
+ def _calculate_risk(self, threats: list[ThreatDetection]) -> float:
548
+ if not threats:
549
+ return 0.0
550
+ # 风险分 = **最强单条证据**(多类型再乘加成),不是平均值。
551
+ #
552
+ # 历史 bug(2026-09-19 复现):旧式子是
553
+ # `total / (len(threats) * max_weight)`——分母随威胁条数线性增长,
554
+ # 于是它算的是「平均置信度」而不是「风险」,并且**非单调**:同一页
555
+ # 多检出 1 条 payload_smuggling,风险分反而从 0.900 降到 0.840。
556
+ # 后果不只是数值难看:`clean = risk_score < 0.5`,往恶意页里掺入
557
+ # 足够多低置信命中就能把风险分稀释到 0.5 以下,判定翻成「干净」。
558
+ # 取最大值天然单调——加证据只会抬高或持平,不可能降低。
559
+ risk = max(t.confidence * WEIGHTS.get(t.threat_type, 0.5)
560
+ for t in threats)
561
+ risk = min(risk / max(WEIGHTS.values()), 1.0)
562
+ # 多威胁类型加成
563
+ unique_types = len(set(t.threat_type for t in threats))
564
+ if unique_types > 2:
565
+ risk = min(risk * 1.3, 1.0)
566
+ return round(risk, 3)
567
+
568
+ def _clean_content(self, text: str, threats: list[ThreatDetection],
569
+ risk_score: float) -> tuple[str, int]:
570
+ """脱敏:检测与脱敏共用模式表(不漂移)。高风险先脱敏后截断。"""
571
+ if not threats:
572
+ return text, 0
573
+
574
+ cleaned = text
575
+ redactions = 0
576
+
577
+ # 与检测共用同一语系模式表(_lang_patterns),避免中文/日文等
578
+ # 注入「被标记却不脱敏」的漂移
579
+ lang = getattr(self, "_detect_content_lang", lambda t: "other")(text)
580
+ for pattern in (list(self._lang_patterns(lang)) + EXFILTRATION_PATTERNS +
581
+ IMPERSONATION_PATTERNS):
582
+ if pattern in _NO_REDACT_PATTERNS:
583
+ continue
584
+ try:
585
+ cleaned, count = re.subn(pattern, "[REDACTED]", cleaned)
586
+ redactions += count
587
+ except re.error:
588
+ pass
589
+
590
+ for pattern, replacement in _XSS_REDACTION_PATTERNS:
591
+ try:
592
+ cleaned, count = re.subn(pattern, replacement, cleaned)
593
+ redactions += count
594
+ except re.error:
595
+ pass
596
+
597
+ # 高风险:先脱敏后截断,并加警告头
598
+ if risk_score > 0.8:
599
+ header = (
600
+ f"[内容风险标记:{len(threats)} 处威胁 (risk={risk_score:.2f}),"
601
+ f"按潜在对抗性内容处理]\n\n"
602
+ )
603
+ cleaned = header + cleaned[:3000]
604
+
605
+ return cleaned, redactions
606
+
607
+
608
+ def _looks_suspicious(decoded: str) -> bool:
609
+ """粗判解码文本是否含指令性内容(避免误伤普通 base64 图片/二进制)。"""
610
+ suspicious_kw = ("ignore", "instruction", "prompt", "system", "you are",
611
+ "忽略", "指令", "提示词", "你是", "password", "api key",
612
+ "token", "密钥", "密码")
613
+ low = decoded.lower()
614
+ return any(kw in low for kw in suspicious_kw)
615
+
616
+
617
+ _scrubber: Any = None
618
+
619
+
620
+ def get_scrubber() -> ContentScrubber:
621
+ global _scrubber
622
+ if _scrubber is None:
623
+ _scrubber = ContentScrubber()
624
+ return _scrubber
625
+
626
+
627
+ def scrub_content(content: str) -> ScrubResult:
628
+ """清洗入口:返回 ScrubResult。"""
629
+ return get_scrubber().scrub(content)
630
+
631
+
632
+ def scrub_to_dict(content: str) -> dict:
633
+ """清洗入口(dict 形态,供 fetch 层直接合并)。"""
634
+ r = scrub_content(content)
635
+ return {
636
+ "content_clean": r.clean,
637
+ "risk_score": r.risk_score,
638
+ "threat_count": len(r.threats),
639
+ "threat_types": sorted({t.threat_type for t in r.threats}),
640
+ "redactions": r.redactions,
641
+ "encoding_layers_stripped": r.encoding_layers_stripped,
642
+ "content_lang": r.content_lang,
643
+ }
644
+
645
+
646
+ # ── CLI ────────────────────────────────────────────────────────────────────
647
+
648
+ if __name__ == "__main__":
649
+ import argparse
650
+ import sys
651
+
652
+ p = argparse.ArgumentParser(description="Argo 内容安全引擎(注入检测 + 清洗)")
653
+ p.add_argument("text", nargs="?", default=None, help="要检测的文本")
654
+ p.add_argument("--stdin", action="store_true", help="从 stdin 读取")
655
+ p.add_argument("--json", action="store_true", help="JSON 输出")
656
+ args = p.parse_args()
657
+
658
+ text = args.text
659
+ if args.stdin:
660
+ text = sys.stdin.read()
661
+ if text is None:
662
+ p.error("需要文本参数或 --stdin")
663
+
664
+ result = scrub_content(text)
665
+ if args.json:
666
+ out = {
667
+ "clean": result.clean,
668
+ "risk_score": result.risk_score,
669
+ "redactions": result.redactions,
670
+ "encoding_layers_stripped": result.encoding_layers_stripped,
671
+ "threats": [
672
+ {"type": t.threat_type, "confidence": t.confidence,
673
+ "evidence": t.evidence, "location": t.location}
674
+ for t in result.threats
675
+ ],
676
+ "content": result.content,
677
+ }
678
+ print(dumps(out))
679
+ else:
680
+ print(f"clean={result.clean} risk={result.risk_score:.3f} "
681
+ f"redactions={result.redactions} threats={len(result.threats)}")
682
+ for t in result.threats:
683
+ print(f" [{t.threat_type}] conf={t.confidence:.2f} {t.evidence}")
684
+ if result.threats:
685
+ print(f"\n清洗后内容片段:\n{result.content[:300]}")