argo-search 1.0.1 → 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.
Files changed (472) hide show
  1. package/LICENSE +201 -21
  2. package/README.en.md +735 -0
  3. package/README.es.md +777 -0
  4. package/README.ja.md +777 -0
  5. package/README.ko.md +777 -0
  6. package/README.md +778 -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/why-better.svg +104 -0
  12. package/assets/readme/workflow.svg +83 -0
  13. package/backends/domain_profiles.json +2356 -96
  14. package/backends/engine_registry.yaml +3428 -503
  15. package/backends/query_synonyms_cn.json +35 -0
  16. package/backends/quota_profiles.json +2488 -160
  17. package/backends/source_types_cn.json +109 -0
  18. package/backends/text_lexicon.json +44 -0
  19. package/bin/argo +772 -0
  20. package/bin/argo.js +102 -39
  21. package/config.yaml +5139 -367
  22. package/cordis.patch.yml +21 -0
  23. package/data/countries.json +40 -0
  24. package/data/lang_matrix/lang_capability.json +842 -0
  25. package/data/lang_matrix/matrix_2026-09-10.json +11921 -0
  26. package/data/regions_cn.json +1 -0
  27. package/docs/ADDING_NEW_ENGINE.md +184 -0
  28. package/docs/ARGO_INTRO.md +124 -0
  29. package/docs/DATA_SOURCE_GAPS.md +81 -0
  30. package/docs/DESIGN_LOCAL_DATA_FUSION.md +90 -0
  31. package/docs/ENGINE_CATALOG.md +575 -0
  32. package/docs/MCP_SETUP.md +108 -0
  33. package/docs/PLATFORM_PATHS.md +111 -0
  34. package/docs/RELEASE_NOTES_v2.8.0.md +71 -0
  35. package/docs/RELEASE_NOTES_v2.8.2.md +63 -0
  36. package/docs/RELEASE_NOTES_v2.8.3.md +55 -0
  37. package/docs/RELEASE_NOTES_v2.8.4.md +79 -0
  38. package/docs/RELEASE_NOTES_v2.8.5.md +102 -0
  39. package/docs/RELEASE_NOTES_v2.8.6.md +66 -0
  40. package/docs/RELEASE_NOTES_v2.8.7.md +65 -0
  41. package/docs/RELEASE_NOTES_v2.8.8.md +67 -0
  42. package/docs/RELEASE_NOTES_v2.8.9.md +67 -0
  43. package/docs/RELEASE_NOTES_v2.9.0.md +64 -0
  44. package/docs/RELEASE_NOTES_v2.9.8.md +73 -0
  45. package/docs/SEARCH_ARCHIVE.md +115 -0
  46. package/docs/assets/architecture.svg +104 -0
  47. package/docs/assets/hero.svg +98 -0
  48. package/docs/engines/bangumi.md +27 -0
  49. package/docs/engines/biorxiv.md +27 -0
  50. package/docs/engines/clawhub.md +27 -0
  51. package/docs/engines/crt_sh.md +27 -0
  52. package/docs/engines/datacite.md +27 -0
  53. package/docs/engines/deps_dev.md +27 -0
  54. package/docs/engines/douban_movie.md +27 -0
  55. package/docs/engines/endoflife.md +27 -0
  56. package/docs/engines/flk_law.md +27 -0
  57. package/docs/engines/fxtwitter.md +27 -0
  58. package/docs/engines/google_news.md +27 -0
  59. package/docs/engines/gov_regulations.md +27 -0
  60. package/docs/engines/guqian.md +27 -0
  61. package/docs/engines/hackernews.md +27 -0
  62. package/docs/engines/iplant.md +27 -0
  63. package/docs/engines/keenable.md +27 -0
  64. package/docs/engines/met_museum.md +27 -0
  65. package/docs/engines/nasa_images.md +27 -0
  66. package/docs/engines/netease_music.md +27 -0
  67. package/docs/engines/npm.md +27 -0
  68. package/docs/engines/nvd.md +27 -0
  69. package/docs/engines/openreview.md +27 -0
  70. package/docs/engines/openstd.md +27 -0
  71. package/docs/engines/parallel_free.md +27 -0
  72. package/docs/engines/people_daily.md +27 -0
  73. package/docs/engines/realtime_index.md +27 -0
  74. package/docs/engines/sec_edgar.md +27 -0
  75. package/docs/engines/seltz.md +27 -0
  76. package/docs/engines/skillsmp.md +27 -0
  77. package/docs/engines/sspai.md +27 -0
  78. package/docs/engines/std_samr.md +27 -0
  79. package/docs/engines/train.md +27 -0
  80. package/docs/engines/un_comtrade.md +27 -0
  81. package/docs/engines/wikisource.md +27 -0
  82. package/docs/engines/zdic.md +27 -0
  83. package/docs/engines/zenodo.md +27 -0
  84. package/docs/engines/zhihu_hot_app.md +27 -0
  85. package/docs//344/270/272/344/273/200/344/271/210/351/200/211/346/213/251argo.md +176 -0
  86. package/engines/README.md +10 -0
  87. package/engines/_template_cli.yaml +39 -0
  88. package/engines/_template_http.yaml +37 -0
  89. package/engines/plugins/_template_plugin.py +50 -0
  90. package/engines/specs/_cisa_kev.yaml +54 -0
  91. package/engines/specs/_osv.yaml +63 -0
  92. package/engines/specs/aljazeera.yaml +43 -0
  93. package/engines/specs/antara.yaml +43 -0
  94. package/engines/specs/artic.yaml +20 -0
  95. package/engines/specs/aviation_weather.yaml +39 -0
  96. package/engines/specs/bangumi.yaml +21 -0
  97. package/engines/specs/biorxiv.yaml +24 -0
  98. package/engines/specs/carbon_intensity.yaml +32 -0
  99. package/engines/specs/cfets.yaml +41 -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/defillama.yaml +38 -0
  113. package/engines/specs/deps_dev.yaml +21 -0
  114. package/engines/specs/digup.yaml +36 -0
  115. package/engines/specs/doi.yaml +39 -0
  116. package/engines/specs/douban_movie.yaml +22 -0
  117. package/engines/specs/dw_news.yaml +47 -0
  118. package/engines/specs/egov_law.yaml +21 -0
  119. package/engines/specs/elpais.yaml +43 -0
  120. package/engines/specs/endoflife.yaml +21 -0
  121. package/engines/specs/energy_charts.yaml +21 -0
  122. package/engines/specs/factcheck_org.yaml +52 -0
  123. package/engines/specs/faz.yaml +43 -0
  124. package/engines/specs/federal_register.yaml +57 -0
  125. package/engines/specs/firecrawl.yaml +53 -0
  126. package/engines/specs/flk_law.yaml +24 -0
  127. package/engines/specs/folha.yaml +43 -0
  128. package/engines/specs/france24.yaml +44 -0
  129. package/engines/specs/full_fact.yaml +47 -0
  130. package/engines/specs/fxtwitter.yaml +36 -0
  131. package/engines/specs/gbfs_nyc.yaml +32 -0
  132. package/engines/specs/gdacs.yaml +21 -0
  133. package/engines/specs/gdelt.yaml +64 -0
  134. package/engines/specs/gleif.yaml +55 -0
  135. package/engines/specs/google_news.yaml +22 -0
  136. package/engines/specs/gov_regulations.yaml +38 -0
  137. package/engines/specs/guardian_rss.yaml +59 -0
  138. package/engines/specs/hkex.yaml +40 -0
  139. package/engines/specs/hurriyet.yaml +43 -0
  140. package/engines/specs/iplant.yaml +25 -0
  141. package/engines/specs/jikan.yaml +24 -0
  142. package/engines/specs/jolpica.yaml +20 -0
  143. package/engines/specs/k10plus.yaml +20 -0
  144. package/engines/specs/keenable.yaml +44 -0
  145. package/engines/specs/keenable_free.yaml +36 -0
  146. package/engines/specs/lefigaro.yaml +43 -0
  147. package/engines/specs/listenbrainz.yaml +19 -0
  148. package/engines/specs/matichon.yaml +43 -0
  149. package/engines/specs/met_museum.yaml +21 -0
  150. package/engines/specs/nasa_images.yaml +45 -0
  151. package/engines/specs/netease_music.yaml +38 -0
  152. package/engines/specs/nhk.yaml +43 -0
  153. package/engines/specs/nhtsa_vpic.yaml +20 -0
  154. package/engines/specs/noaa_swpc.yaml +20 -0
  155. package/engines/specs/nvd.yaml +38 -0
  156. package/engines/specs/obis.yaml +20 -0
  157. package/engines/specs/opencitations.yaml +52 -0
  158. package/engines/specs/openf1.yaml +21 -0
  159. package/engines/specs/openfoodfacts.yaml +45 -0
  160. package/engines/specs/openligadb.yaml +20 -0
  161. package/engines/specs/openreview.yaml +37 -0
  162. package/engines/specs/openstd.yaml +21 -0
  163. package/engines/specs/people_daily.yaml +22 -0
  164. package/engines/specs/realtime_index.yaml +34 -0
  165. package/engines/specs/ror.yaml +20 -0
  166. package/engines/specs/satnogs.yaml +20 -0
  167. package/engines/specs/sec_edgar.yaml +46 -0
  168. package/engines/specs/shfe.yaml +39 -0
  169. package/engines/specs/skillsmp.yaml +50 -0
  170. package/engines/specs/sogou_weixin_browser.yaml +30 -0
  171. package/engines/specs/soilgrids.yaml +25 -0
  172. package/engines/specs/sspai.yaml +36 -0
  173. package/engines/specs/stackexchange.yaml +46 -0
  174. package/engines/specs/std_samr.yaml +21 -0
  175. package/engines/specs/tass.yaml +43 -0
  176. package/engines/specs/tavily_free.yaml +56 -0
  177. package/engines/specs/tle_mirror.yaml +21 -0
  178. package/engines/specs/train.yaml +37 -0
  179. package/engines/specs/tvmaze.yaml +20 -0
  180. package/engines/specs/twitter_syndication.yaml +31 -0
  181. package/engines/specs/un_comtrade.yaml +23 -0
  182. package/engines/specs/unpaywall.yaml +54 -0
  183. package/engines/specs/vnpress.yaml +43 -0
  184. package/engines/specs/weather.yaml +32 -0
  185. package/engines/specs/who_don.yaml +21 -0
  186. package/engines/specs/who_gho.yaml +20 -0
  187. package/engines/specs/wikimedia_commons.yaml +34 -0
  188. package/engines/specs/wikisource.yaml +22 -0
  189. package/engines/specs/worms.yaml +19 -0
  190. package/engines/specs/x_trending.yaml +34 -0
  191. package/engines/specs/yna.yaml +43 -0
  192. package/engines/specs/ynet.yaml +43 -0
  193. package/engines/specs/yuanbao.yaml +93 -0
  194. package/engines/specs/zbmath.yaml +40 -0
  195. package/engines/specs/zdic.yaml +22 -0
  196. package/engines/specs/zenodo.yaml +27 -0
  197. package/engines/specs/zhihu_hot_app.yaml +35 -0
  198. package/mcp/clients.yaml +106 -0
  199. package/package.json +55 -12
  200. package/packages/dsh-plugin/README.md +152 -0
  201. package/packages/dsh-plugin/cordis.patch.yml +48 -0
  202. package/packages/dsh-plugin/dsh/client.js +1533 -0
  203. package/packages/dsh-plugin/dsh/diagnostics.js +54 -0
  204. package/packages/dsh-plugin/dsh/index.js +527 -0
  205. package/packages/dsh-plugin/dsh/native-tools.mjs +749 -0
  206. package/packages/dsh-plugin/dsh/runner.js +666 -0
  207. package/packages/dsh-plugin/dsh/wide-research.js +878 -0
  208. package/packages/dsh-plugin/package.json +38 -0
  209. package/packages/dsh-plugin/scripts/sync-local.sh +70 -0
  210. package/packages/dsh-plugin/test/client.test.js +406 -0
  211. package/packages/dsh-plugin/test/config.test.js +30 -0
  212. package/packages/dsh-plugin/test/diagnostics.test.js +28 -0
  213. package/packages/dsh-plugin/test/index.test.js +1382 -0
  214. package/references/query-templates.md +96 -0
  215. package/references/query-templates.yaml +118 -0
  216. package/scripts/ab_eval.py +219 -0
  217. package/scripts/ab_eval_p0p1.py +266 -0
  218. package/scripts/adaptive.py +294 -70
  219. package/scripts/answer.py +154 -0
  220. package/scripts/archive_run.py +591 -0
  221. package/scripts/argo_engine_registry.py +172 -0
  222. package/scripts/argo_paths.py +728 -0
  223. package/scripts/article.py +136 -0
  224. package/scripts/article_meta.py +131 -0
  225. package/scripts/arxiv_source.py +201 -0
  226. package/scripts/badcase_ledger.py +252 -0
  227. package/scripts/batch_probe.py +241 -0
  228. package/scripts/benchmark.py +19 -0
  229. package/scripts/bounded_run.py +144 -0
  230. package/scripts/browser_auth.py +323 -0
  231. package/scripts/budget.py +247 -0
  232. package/scripts/budget_cli.py +93 -0
  233. package/scripts/build_release_assets.sh +65 -0
  234. package/scripts/burstiness_detector.py +147 -0
  235. package/scripts/cache.py +1066 -67
  236. package/scripts/cache_guard.py +271 -0
  237. package/scripts/cache_key_vdom.py +56 -0
  238. package/scripts/cache_similarity.py +170 -0
  239. package/scripts/caliber_isolation.py +195 -0
  240. package/scripts/calibrate_lowquality.py +210 -0
  241. package/scripts/candidate_envelope.py +366 -0
  242. package/scripts/chrome_cdp.py +871 -0
  243. package/scripts/ci_drift_gate.py +90 -0
  244. package/scripts/circuit_breaker.py +389 -0
  245. package/scripts/citations.py +334 -0
  246. package/scripts/cite_cli.py +59 -0
  247. package/scripts/cjk_tokens.py +95 -0
  248. package/scripts/clarify.py +47 -24
  249. package/scripts/cli_io.py +240 -0
  250. package/scripts/config.py +683 -45
  251. package/scripts/config_cache.py +402 -0
  252. package/scripts/conn_pool.py +220 -0
  253. package/scripts/content_boundary.py +46 -0
  254. package/scripts/content_security.py +685 -0
  255. package/scripts/content_signals.py +751 -0
  256. package/scripts/crawl.py +74 -35
  257. package/scripts/cross_lingual_trace.py +293 -0
  258. package/scripts/daemon.py +866 -0
  259. package/scripts/digup_bridge.py +214 -0
  260. package/scripts/dispatch_result.py +98 -0
  261. package/scripts/engine_admission.py +366 -0
  262. package/scripts/engine_dispatch.py +982 -0
  263. package/scripts/engine_env.py +539 -0
  264. package/scripts/engine_failure.py +411 -0
  265. package/scripts/engine_families.py +738 -0
  266. package/scripts/engine_policy.py +218 -0
  267. package/scripts/engine_requires.py +190 -0
  268. package/scripts/engine_status.py +322 -0
  269. package/scripts/engine_validate.py +638 -0
  270. package/scripts/engines.py +799 -294
  271. package/scripts/engines_base.py +1521 -0
  272. package/scripts/engines_builders.py +310 -0
  273. package/scripts/engines_builders_batch10.py +204 -0
  274. package/scripts/engines_builders_batch11.py +174 -0
  275. package/scripts/engines_builders_batch12.py +103 -0
  276. package/scripts/engines_builders_batch13.py +340 -0
  277. package/scripts/engines_builders_batch14.py +656 -0
  278. package/scripts/engines_builders_batch15.py +249 -0
  279. package/scripts/engines_builders_batch9.py +1468 -0
  280. package/scripts/engines_builders_cn.py +2283 -0
  281. package/scripts/engines_builders_data.py +2923 -0
  282. package/scripts/engines_builders_data_macro.py +775 -0
  283. package/scripts/engines_builders_feeds.py +255 -0
  284. package/scripts/engines_builders_intl.py +1239 -0
  285. package/scripts/engines_builders_keyless.py +111 -0
  286. package/scripts/engines_builders_search.py +440 -0
  287. package/scripts/engines_builders_tech.py +1359 -0
  288. package/scripts/evidence.py +620 -166
  289. package/scripts/evidence_loop.py +624 -0
  290. package/scripts/evidence_tier.py +298 -0
  291. package/scripts/except_sets.py +25 -0
  292. package/scripts/extract.py +55 -16
  293. package/scripts/fact_align.py +207 -0
  294. package/scripts/fact_crosscheck.py +230 -0
  295. package/scripts/fetch.py +25 -12
  296. package/scripts/fetch_browser.py +87 -0
  297. package/scripts/fetch_jsrun.py +122 -0
  298. package/scripts/fetch_quality.py +225 -0
  299. package/scripts/fetch_render_tinyfish.py +117 -0
  300. package/scripts/fetch_v3.py +1804 -0
  301. package/scripts/focus_extract.py +350 -0
  302. package/scripts/fulltext_store.py +169 -0
  303. package/scripts/fxtwitter_api.py +325 -0
  304. package/scripts/gen_engine_catalog.py +364 -0
  305. package/scripts/gen_native_tools.py +91 -0
  306. package/scripts/gen_routing_snapshot.py +195 -0
  307. package/scripts/health_check.py +140 -402
  308. package/scripts/health_probe.py +23 -8
  309. package/scripts/hot_state.py +134 -0
  310. package/scripts/http_client.py +1053 -0
  311. package/scripts/image_ops.py +430 -0
  312. package/scripts/image_vision/vision_probe.swift +126 -0
  313. package/scripts/install.ps1 +166 -0
  314. package/scripts/install.sh +221 -0
  315. package/scripts/job.py +1186 -0
  316. package/scripts/jump_resolver.py +235 -0
  317. package/scripts/lang_capability.py +150 -0
  318. package/scripts/lang_detect.py +289 -0
  319. package/scripts/lang_matrix_probe.py +329 -0
  320. package/scripts/lang_pref.py +311 -0
  321. package/scripts/link_source.py +340 -0
  322. package/scripts/local_image.py +970 -0
  323. package/scripts/local_seek.py +402 -0
  324. package/scripts/macro_countries.py +97 -0
  325. package/scripts/matrix_search_eval.py +1187 -0
  326. package/scripts/mcp_compact.py +234 -0
  327. package/scripts/mcp_diag.py +8 -3
  328. package/scripts/mcp_handlers.py +492 -0
  329. package/scripts/mcp_handlers_surface.py +157 -0
  330. package/scripts/mcp_launch.sh +124 -0
  331. package/scripts/mcp_local.py +241 -0
  332. package/scripts/mcp_server.py +125 -419
  333. package/scripts/mcp_setup.py +431 -0
  334. package/scripts/mcp_tools.py +331 -0
  335. package/scripts/mcp_transport.py +191 -0
  336. package/scripts/mcp_warm.py +127 -0
  337. package/scripts/min_version_scan.py +335 -0
  338. package/scripts/net_proxy.py +258 -0
  339. package/scripts/network_aware.py +152 -0
  340. package/scripts/output_transparency.py +200 -0
  341. package/scripts/paid_policy.py +98 -0
  342. package/scripts/paper.py +274 -0
  343. package/scripts/pdf_extract.py +518 -0
  344. package/scripts/phrase_filter.py +66 -0
  345. package/scripts/plan.py +422 -0
  346. package/scripts/postprocess_signals.py +150 -0
  347. package/scripts/pre-commit +48 -0
  348. package/scripts/query_enhance.py +157 -0
  349. package/scripts/query_operators.py +196 -0
  350. package/scripts/query_rewriter.py +116 -7
  351. package/scripts/query_signals.py +300 -0
  352. package/scripts/query_templates.py +167 -0
  353. package/scripts/query_understanding.py +411 -0
  354. package/scripts/query_variants.py +298 -0
  355. package/scripts/quota.py +633 -76
  356. package/scripts/rank_signals.py +775 -0
  357. package/scripts/ranking_eval.py +319 -0
  358. package/scripts/readability_extract.py +433 -0
  359. package/scripts/recompute.py +415 -0
  360. package/scripts/recovery.py +616 -0
  361. package/scripts/recovery_l0.py +160 -0
  362. package/scripts/recovery_runner.py +150 -0
  363. package/scripts/redskill/redskill_engine.py +332 -0
  364. package/scripts/regression_p0p1.py +318 -0
  365. package/scripts/replay_eval.py +658 -0
  366. package/scripts/rerank_tfidf.py +225 -0
  367. package/scripts/research.py +607 -403
  368. package/scripts/research_cli.py +361 -0
  369. package/scripts/research_dossier.py +492 -0
  370. package/scripts/research_expand.py +169 -0
  371. package/scripts/research_gates.py +272 -0
  372. package/scripts/research_report.py +209 -0
  373. package/scripts/research_strategy.py +37 -0
  374. package/scripts/research_work_packages.py +195 -0
  375. package/scripts/result_lang.py +311 -0
  376. package/scripts/retry_policy.py +76 -0
  377. package/scripts/robots_guard.py +259 -0
  378. package/scripts/route.py +921 -254
  379. package/scripts/route_bangs.py +40 -0
  380. package/scripts/route_cache.py +243 -0
  381. package/scripts/route_combo.py +917 -0
  382. package/scripts/route_domains.py +341 -0
  383. package/scripts/route_lang.py +605 -0
  384. package/scripts/route_log.py +63 -0
  385. package/scripts/route_policy.py +250 -0
  386. package/scripts/rrf_classic.py +66 -0
  387. package/scripts/screenshot.py +73 -0
  388. package/scripts/search.py +886 -406
  389. package/scripts/search_attribution.py +185 -0
  390. package/scripts/search_benchmark.py +407 -0
  391. package/scripts/search_cli.py +640 -0
  392. package/scripts/search_entry.py +395 -0
  393. package/scripts/search_output.py +690 -0
  394. package/scripts/search_pipeline.py +926 -0
  395. package/scripts/search_rank.py +991 -0
  396. package/scripts/search_types.py +129 -16
  397. package/scripts/seek_locator.py +68 -0
  398. package/scripts/semantic_evidence.py +391 -0
  399. package/scripts/serp_guard.py +336 -0
  400. package/scripts/serp_kill_log.py +74 -0
  401. package/scripts/single_flight.py +120 -0
  402. package/scripts/social_engines/__init__.py +10 -0
  403. package/scripts/social_engines/bilibili_engine.py +131 -0
  404. package/scripts/social_engines/hackernews_engine.py +113 -0
  405. package/scripts/social_engines/reddit_engine.py +172 -0
  406. package/scripts/social_engines/twitter_engine.py +397 -0
  407. package/scripts/social_engines/v2ex_engine.py +101 -0
  408. package/scripts/social_engines/weibo_engine.py +128 -0
  409. package/scripts/social_engines/xiaohongshu_engine.py +137 -0
  410. package/scripts/social_engines/zhihu_engine.py +139 -0
  411. package/scripts/social_research.py +201 -0
  412. package/scripts/sogou_weixin_browser.py +318 -0
  413. package/scripts/source_ledger.py +249 -0
  414. package/scripts/stage_timing.py +58 -0
  415. package/scripts/stats_cli.py +107 -0
  416. package/scripts/straggler_policy.py +60 -0
  417. package/scripts/stylometry_detector.py +231 -0
  418. package/scripts/sync_backends.py +353 -0
  419. package/scripts/sync_installs.py +29 -0
  420. package/scripts/template_cli.py +127 -0
  421. package/scripts/text_lexicon.py +72 -0
  422. package/scripts/tfidf_router.py +109 -37
  423. package/scripts/time_utils.py +233 -0
  424. package/scripts/topic_research_profiles.py +643 -0
  425. package/scripts/train.py +737 -0
  426. package/scripts/tweet.py +596 -0
  427. package/scripts/url_canon.py +217 -0
  428. package/scripts/url_optimize.py +56 -0
  429. package/scripts/url_safety.py +236 -0
  430. package/scripts/usage_log.py +116 -0
  431. package/scripts/usage_log_clean.py +113 -0
  432. package/scripts/v2ex_nodes.py +311 -0
  433. package/scripts/variant_recall.py +188 -0
  434. package/scripts/watch.py +205 -0
  435. package/scripts/wechat_article.py +258 -0
  436. package/scripts/wide_research.py +447 -0
  437. package/scripts/wx.py +349 -0
  438. package/scripts/x_article_fetch.py +141 -0
  439. package/scripts/x_search.py +234 -0
  440. package/scripts/x_trending.py +189 -0
  441. package/scripts/yaml_load.py +67 -0
  442. package/scripts/zhihu_quota.py +139 -0
  443. package/sub-skills/ego-search/SKILL.md +253 -0
  444. package/sub-skills/ego-search/scripts/ego_search.py +800 -0
  445. package/sub-skills/ego-search/scripts/merge.py +226 -0
  446. package/sub-skills/ego-search/scripts/quality.py +64 -0
  447. package/sub-skills/ego-search/scripts/runtime.py +172 -0
  448. package/sub-skills/ego-search/scripts/safety.py +109 -0
  449. package/sub-skills/ego-search/scripts/serp_spec.py +109 -0
  450. package/sub-skills/ego-search/scripts/webbridge_adapter.py +374 -0
  451. package/sub-skills/js-run/SKILL.md +67 -0
  452. package/sub-skills/js-run/scripts/jsrun.py +112 -0
  453. package/sub-skills/js-run/scripts/shim.js +188 -0
  454. package/sub-skills/local-search/SKILL.md +50 -53
  455. package/sub-skills/local-search/bm25.py +186 -0
  456. package/sub-skills/local-search/cli_runner.py +331 -0
  457. package/sub-skills/local-search/config.yaml +152 -55
  458. package/sub-skills/local-search/engine_registry.py +91 -26
  459. package/sub-skills/local-search/{health_check.py → local_health_check.py} +140 -10
  460. package/sub-skills/local-search/local_search_adapter.py +13 -2
  461. package/sub-skills/local-search/parse_maps.yaml +40 -42
  462. package/sub-skills/local-search/parsers.py +328 -0
  463. package/sub-skills/local-search/search_v3.py +716 -242
  464. package/sub-skills/local-search/smart_router.py +47 -7
  465. package/sub-skills/local-seek/SKILL.md +110 -0
  466. package/sub-skills/local-seek/config/domains.yaml +20 -0
  467. package/sub-skills/local-seek/scripts/eval_rank.py +216 -0
  468. package/sub-skills/local-seek/scripts/eval_seek.py +224 -0
  469. package/sub-skills/local-seek/scripts/seek.py +1465 -0
  470. package/sub-skills/local-seek/scripts/seek_frecency.py +126 -0
  471. package/sub-skills/local-seek/scripts/seek_fts.py +430 -0
  472. package/sub-skills/local-seek/scripts/seek_rank.py +197 -0
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env python3
2
+ """批次十二构建器:CN 通用搜索补源(2026-09-28,9044621)。
3
+
4
+ so 360 搜索(so.com/s?q=,HTML 解析)
5
+ shenma 神马搜索(m.sm.cn/s?q=,移动端 HTML 解析)
6
+
7
+ 为什么放 batch 而不是 engines_builders_cn:cn 是登记上限 2319 的祖父
8
+ 文件(中文源声明表,逐源一段),新引擎的下一站按既有惯例是 batch 模块
9
+ (engines_builders_tech.py 的登记理由明写了「新引擎的下一站是 batch 模块」)。
10
+ 两源在 chinese_general 域尾部(位次 8/9)观察期,台账见
11
+ tests/test_new_source_reachability.py::_DORMANT_ALLOWLIST。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ import re
18
+ import urllib.request
19
+ from typing import Any
20
+
21
+ from engines_base import http_open, safe_search
22
+
23
+ logger = logging.getLogger("unified_search.engines")
24
+
25
+
26
+ # ── 360 搜索(so.com)───────────────────────────────────────────────────────
27
+
28
+ def _build_so_engine(spec: dict[str, Any]) -> Any:
29
+ """360 搜索(so.com/s?q=,HTML 解析)"""
30
+ timeout = spec.get("timeout", 10)
31
+
32
+ @safe_search
33
+ def _engine(query: str, n: int = 5, _timeout: float | None = None, **kwargs) -> list[dict[str, Any]]:
34
+ import urllib.parse as up
35
+ to = _timeout or timeout
36
+ url = "https://www.so.com/s?q=" + up.quote(query)
37
+ headers = {"User-Agent": "Mozilla/5.0", "Referer": "https://www.so.com/"}
38
+ req = urllib.request.Request(url, headers=headers)
39
+ try:
40
+ with http_open(req, timeout=to, engine=spec.get("_name", "")) as resp:
41
+ page = resp.read().decode("utf-8", "replace")
42
+ results = []
43
+ # 360 搜索结果块:<li class="res-list">...<h3><a href="...">title</a>
44
+ for m in re.finditer(r'<h3[^>]*>\s*<a[^>]*href="([^"]+)"[^>]*>(.*?)</a>', page, re.S):
45
+ url_m, title_m = m.group(1), re.sub(r'<[^>]+>', '', m.group(2)).strip()
46
+ if not title_m or not url_m:
47
+ continue
48
+ results.append({
49
+ "title": title_m[:80],
50
+ "url": url_m,
51
+ "snippet": "",
52
+ "source": "so",
53
+ "score": max(1.0 - len(results) * 0.1, 0.1),
54
+ })
55
+ if len(results) >= n:
56
+ break
57
+ return results
58
+ except Exception as e:
59
+ logger.warning(f"360 搜索失败: {e}")
60
+ return []
61
+ return _engine
62
+
63
+
64
+ # ── 神马搜索(sm.cn,移动端)────────────────────────────────────────────────
65
+
66
+ def _build_shenma_engine(spec: dict[str, Any]) -> Any:
67
+ """神马搜索(m.sm.cn/s?q=,移动端 HTML 解析)"""
68
+ timeout = spec.get("timeout", 10)
69
+
70
+ @safe_search
71
+ def _engine(query: str, n: int = 5, _timeout: float | None = None, **kwargs) -> list[dict[str, Any]]:
72
+ import urllib.parse as up
73
+ to = _timeout or timeout
74
+ url = "https://m.sm.cn/s?q=" + up.quote(query)
75
+ headers = {
76
+ "User-Agent": ("Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) "
77
+ "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"),
78
+ "Referer": "https://m.sm.cn/",
79
+ }
80
+ req = urllib.request.Request(url, headers=headers)
81
+ try:
82
+ with http_open(req, timeout=to, engine=spec.get("_name", "")) as resp:
83
+ page = resp.read().decode("utf-8", "replace")
84
+ results = []
85
+ # 神马结果块:<a href="..." class="...">title</a> + snippet
86
+ for m in re.finditer(r'<a[^>]*href="(https?://[^"]+)"[^>]*class="[^"]*result[^"]*"[^>]*>(.*?)</a>', page, re.S):
87
+ url_m, title_m = m.group(1), re.sub(r'<[^>]+>', '', m.group(2)).strip()
88
+ if not title_m or not url_m or "sm.cn" in url_m:
89
+ continue
90
+ results.append({
91
+ "title": title_m[:80],
92
+ "url": url_m,
93
+ "snippet": "",
94
+ "source": "shenma",
95
+ "score": max(1.0 - len(results) * 0.1, 0.1),
96
+ })
97
+ if len(results) >= n:
98
+ break
99
+ return results
100
+ except Exception as e:
101
+ logger.warning(f"神马搜索失败: {e}")
102
+ return []
103
+ return _engine
@@ -0,0 +1,340 @@
1
+ #!/usr/bin/env python3
2
+ """批次十三构建器:美股/汇率数据源(2026-10-06)。
3
+
4
+ yahoo_finance Yahoo Finance 代码解析 + 实时行情(search/chart 免认证)
5
+ sec_companyfacts SEC XBRL companyfacts 年度结构化财务(免认证)
6
+ frankfurter ECB 官方参考汇率(免认证)
7
+
8
+ 为什么放 batch 而不是 engines_builders_data:data 是登记上限 2923 的
9
+ 祖父文件,新引擎的下一站按既有惯例是 batch 模块。三源均经实测验证
10
+ (health + quality 准入通过)后入库;frankfurter 与 sec_companyfacts
11
+ 的探针 UA 必须是合法邮箱域(SEC 对 @local 返 403)。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import re
18
+ import time
19
+ import urllib.request
20
+ from typing import Any
21
+
22
+ from engines_base import http_open, safe_search
23
+
24
+
25
+ # ── Yahoo Finance 美股代码解析 + 实时行情(免认证)──────────────────────────────
26
+
27
+ def _build_yahoo_finance_engine(spec: dict[str, Any]) -> Any:
28
+ """Yahoo Finance:query → 代码/公司解析(search API)→ 实时行情(chart API)。
29
+
30
+ 2026-10-06 接入。两个端点均免认证免 crumb(实测本机 search ~0.5s、
31
+ chart ~0.3s)。search 解决「这家公司美股代码是什么/哪个交易所」,
32
+ chart 补实时价与涨跌幅——finviz 是筛选器不带实时价,本引擎补上。
33
+ """
34
+ timeout = spec.get("timeout", 10)
35
+ ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
36
+
37
+ def _get_json(url: str, to: float) -> Any:
38
+ req = urllib.request.Request(url, headers={
39
+ "User-Agent": ua, "Accept": "application/json"})
40
+ with http_open(req, timeout=to, engine=spec.get("_name", "")) as resp:
41
+ return json.loads(resp.read().decode("utf-8", "replace"))
42
+
43
+ @safe_search
44
+ def _engine(query: str, n: int = 5, _timeout: float | None = None, **kwargs) -> list[dict[str, Any]]:
45
+ import urllib.parse as up
46
+ to = _timeout or timeout
47
+ q = (query or "").strip()
48
+ if not q:
49
+ return []
50
+ try:
51
+ data = _get_json("https://query2.finance.yahoo.com/v1/finance/search?"
52
+ + up.urlencode({"q": q, "quotesCount": 10, "newsCount": 0}), to)
53
+ except Exception:
54
+ return []
55
+ quotes = [x for x in (data.get("quotes") or [])
56
+ if isinstance(x, dict) and x.get("symbol")]
57
+ # 只留 EQUITY/ETF:指数/货币/期货对「查行情」意图无用,留着只会
58
+ # 挤占结果位(2026-10-06 单测抓到:INDEX 排第 3 位混进输出)。
59
+ # EQUITY 优先于 ETF,组内按 Yahoo 相关度分(score)降序。
60
+ quotes = [x for x in quotes if x.get("quoteType") in ("EQUITY", "ETF")]
61
+ quotes.sort(key=lambda x: (0 if x.get("quoteType") == "EQUITY" else 1,
62
+ -float(x.get("score") or 0)))
63
+ out: list[dict[str, Any]] = []
64
+ for item in quotes[:max(1, min(n, 5))]:
65
+ symbol = str(item.get("symbol", ""))
66
+ name = (item.get("longname") or item.get("shortname") or symbol).strip()
67
+ exch = (item.get("exchDisp") or item.get("fullExchange") or "").strip()
68
+ sector = (item.get("sectorDisp") or "").strip()
69
+ qtype = (item.get("typeDisp") or item.get("quoteType") or "").strip()
70
+ price = item.get("regularMarketPrice")
71
+ if price in (None, ""):
72
+ # search 响应不带价时补一次 chart(crumb-free)
73
+ try:
74
+ ch = _get_json(f"https://query2.finance.yahoo.com/v8/finance/chart/"
75
+ f"{up.quote(symbol)}?range=1d&interval=1d", to)
76
+ meta = ((ch.get("chart") or {}).get("result") or [{}])[0].get("meta") or {}
77
+ price = meta.get("regularMarketPrice")
78
+ if price not in (None, "") and not exch:
79
+ exch = (meta.get("fullExchangeName") or "").strip()
80
+ except Exception:
81
+ price = None
82
+ snippet_bits = []
83
+ if price not in (None, ""):
84
+ snippet_bits.append(f"最新价 {price}")
85
+ if exch:
86
+ snippet_bits.append(exch)
87
+ if sector:
88
+ snippet_bits.append(sector)
89
+ if qtype:
90
+ snippet_bits.append(qtype)
91
+ out.append({
92
+ "title": f"{name} ({symbol})",
93
+ "url": f"https://finance.yahoo.com/quote/{symbol}/",
94
+ "snippet": " | ".join(snippet_bits)[:300],
95
+ "source": "yahoo_finance",
96
+ "facts": {"ticker": symbol, "price": price, "exchange": exch,
97
+ "sector": sector},
98
+ })
99
+ return out
100
+ return _engine
101
+
102
+
103
+ # ── SEC EDGAR 结构化财务(XBRL companyfacts,免认证)───────────────────────────
104
+
105
+ # ticker→CIK 映射表缓存(进程内 + 磁盘 24h):company_tickers.json ~1MB,
106
+ # 每次调用重取是纯浪费;SEC 该文件一天一更,24h TTL 足够。
107
+ _SEC_CF_MEM: dict[str, Any] = {"ts": 0.0, "map": {}}
108
+ _SEC_CF_TTL_S = 86400.0
109
+
110
+ # 关键指标 → XBRL 概念名(按优先级;不同公司用的概念不同,逐个回退)
111
+ _SEC_CF_METRICS: tuple[tuple[str, tuple[str, ...]], ...] = (
112
+ ("营收", ("RevenueFromContractWithCustomerExcludingAssessedTax",
113
+ "Revenues", "RevenueFromContractWithCustomerIncludingAssessedTax",
114
+ "SalesRevenueNet")),
115
+ ("净利润", ("NetIncomeLoss", "ProfitLoss")),
116
+ ("经营利润", ("OperatingIncomeLoss",)),
117
+ ("总资产", ("Assets",)),
118
+ ("总负债", ("Liabilities", "LiabilitiesAndStockholdersEquity")),
119
+ ("摊薄EPS", ("EarningsPerShareDiluted",)),
120
+ ("现金及等价物", ("CashAndCashEquivalentsAtCarryingValue",)),
121
+ )
122
+
123
+
124
+ def _sec_cf_ticker_map(to: float, state_dir: str = "") -> dict[str, str]:
125
+ """加载 ticker→CIK 映射(内存 → 磁盘 → 网络)。失败返回空 dict。"""
126
+ import os as _os
127
+ now = time.time()
128
+ if _SEC_CF_MEM["map"] and now - _SEC_CF_MEM["ts"] < _SEC_CF_TTL_S:
129
+ return _SEC_CF_MEM["map"]
130
+ disk = _os.path.join(state_dir or "/tmp", "sec_company_tickers.json")
131
+ try:
132
+ if _os.path.exists(disk) and now - _os.path.getmtime(disk) < _SEC_CF_TTL_S:
133
+ raw = json.loads(open(disk, encoding="utf-8").read())
134
+ m = {str(v.get("ticker", "")).upper(): str(v.get("cik_str", ""))
135
+ for v in raw.values() if isinstance(v, dict)}
136
+ if m:
137
+ _SEC_CF_MEM.update({"ts": now, "map": m})
138
+ return m
139
+ except Exception:
140
+ pass
141
+ try:
142
+ req = urllib.request.Request("https://www.sec.gov/files/company_tickers.json",
143
+ headers={"User-Agent": "argo-search/2.9 (research contact@example.com)",
144
+ "Accept": "application/json"})
145
+ with http_open(req, timeout=max(to, 10), engine="sec_companyfacts") as resp:
146
+ raw = json.loads(resp.read().decode("utf-8", "replace"))
147
+ m = {str(v.get("ticker", "")).upper(): str(v.get("cik_str", ""))
148
+ for v in raw.values() if isinstance(v, dict)}
149
+ if m:
150
+ _SEC_CF_MEM.update({"ts": now, "map": m})
151
+ try:
152
+ _os.makedirs(_os.path.dirname(disk), exist_ok=True)
153
+ with open(disk, "w", encoding="utf-8") as f:
154
+ json.dump(raw, f)
155
+ except Exception:
156
+ pass
157
+ return m
158
+ except Exception:
159
+ return {}
160
+
161
+
162
+ def _sec_cf_latest_annual(entries: list[dict[str, Any]], unit_key: str) -> tuple[Any, int]:
163
+ """取最近一个 10-K 年度值(fy 优先,其次按 end 日期)。"""
164
+ if not isinstance(entries, list):
165
+ return None, 0
166
+ annual = [e for e in entries
167
+ if isinstance(e, dict) and e.get("form") == "10-K" and e.get("val") is not None]
168
+ if not annual:
169
+ annual = [e for e in entries
170
+ if isinstance(e, dict) and e.get("val") is not None]
171
+ if not annual:
172
+ return None, 0
173
+ def _fy(e: dict[str, Any]) -> int:
174
+ try:
175
+ return int(e.get("fy") or 0)
176
+ except (TypeError, ValueError):
177
+ return 0
178
+ best = max(annual, key=lambda e: (_fy(e), str(e.get("end") or "")))
179
+ return best.get("val"), _fy(best)
180
+
181
+
182
+ def _build_sec_companyfacts_engine(spec: dict[str, Any]) -> Any:
183
+ """SEC XBRL companyfacts:ticker/公司名 → 年度结构化财务(营收/净利/资产/EPS)。
184
+
185
+ 2026-10-06 接入。data.sec.gov 免认证。与 sec_edgar(全文检索)互补:
186
+ 全文回答「哪些文件提到 X」,本引擎回答「这家公司历年财务数字」。
187
+ """
188
+ timeout = spec.get("timeout", 15)
189
+ ua = "argo-search/2.9 (research contact@example.com)"
190
+
191
+ def _ticker_candidates(query: str) -> list[str]:
192
+ q = (query or "").strip()
193
+ # 修饰语剥离子(营收/利润类词不是 ticker)
194
+ q2 = re.sub(r"(?i)\b(us|stock|ticker|financials?|fundamentals?|annual|"
195
+ r"revenue|earnings|income|eps|balance|cash\s*flow|"
196
+ r"营收|净利润|利润|财务|财务数据|年报|美股|股票|公司)\b", " ", q)
197
+ cands: list[str] = []
198
+ for raw in re.findall(r"[A-Za-z][A-Za-z.]{0,9}", q2):
199
+ t = raw.upper()
200
+ if 1 <= len(t) <= 6 and t not in cands:
201
+ cands.append(t)
202
+ for raw in re.findall(r"[A-Za-z][A-Za-z.]{0,9}", q):
203
+ t = raw.upper()
204
+ if 1 <= len(t) <= 6 and t not in cands:
205
+ cands.append(t)
206
+ return cands[:8]
207
+
208
+ @safe_search
209
+ def _engine(query: str, n: int = 5, _timeout: float | None = None, **kwargs) -> list[dict[str, Any]]:
210
+ to = _timeout or timeout
211
+ cands = _ticker_candidates(query)
212
+ if not cands:
213
+ return [] # 纯中文公司名无 ASCII 线索:诚实空,fallback 接手
214
+ tmap = _sec_cf_ticker_map(to)
215
+ cik = next((tmap[t] for t in cands if t in tmap), "")
216
+ if not cik:
217
+ return []
218
+ try:
219
+ req = urllib.request.Request(
220
+ f"https://data.sec.gov/api/xbrl/companyfacts/CIK{int(cik):010d}.json",
221
+ headers={"User-Agent": ua, "Accept": "application/json"})
222
+ with http_open(req, timeout=to, engine=spec.get("_name", "")) as resp:
223
+ data = json.loads(resp.read().decode("utf-8", "replace"))
224
+ except Exception:
225
+ return []
226
+ name = str(data.get("entityName") or cands[0])
227
+ us_gaap = (data.get("facts") or {}).get("us-gaap") or {}
228
+ parts: list[str] = []
229
+ facts: dict[str, Any] = {}
230
+ fy_seen = 0
231
+ for label, concepts in _SEC_CF_METRICS:
232
+ for concept in concepts:
233
+ node = us_gaap.get(concept)
234
+ if not isinstance(node, dict):
235
+ continue
236
+ units = node.get("units") or {}
237
+ unit_key = "USD" if "USD" in units else ("USD/shares" if "USD/shares" in units
238
+ else next(iter(units), ""))
239
+ if not unit_key:
240
+ continue
241
+ val, fy = _sec_cf_latest_annual(units.get(unit_key) or [], unit_key)
242
+ if val is None:
243
+ continue
244
+ fy_seen = fy_seen or fy
245
+ unit_label = "美元" if unit_key == "USD" else ("美元/股" if unit_key == "USD/shares" else unit_key)
246
+ human = f"{val/1e8:.1f}亿" if unit_key == "USD" and abs(val) >= 1e8 else str(val)
247
+ parts.append(f"{label} {human}{unit_label if unit_key != 'USD' else ''}")
248
+ facts[label] = val
249
+ break
250
+ if not parts:
251
+ return []
252
+ ticker = next((t for t in cands if t in tmap), cands[0])
253
+ url = (f"https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany"
254
+ f"&CIK={int(cik):010d}&type=10-K&dateb=&owner=include&count=40")
255
+ return [{
256
+ "title": f"{name} ({ticker}) 年度财务 FY{fy_seen}" if fy_seen else f"{name} ({ticker}) 年度财务",
257
+ "url": url,
258
+ "snippet": (" | ".join(parts))[:300],
259
+ "source": "sec_companyfacts",
260
+ "facts": {"ticker": ticker, "cik": cik, "fy": fy_seen, **facts},
261
+ }]
262
+ return _engine
263
+
264
+
265
+ # ── Frankfurter(ECB 官方参考汇率,免认证)────────────────────────────────────
266
+
267
+ # 查询词 → ISO 代码(覆盖主要币种中英文)
268
+ _FRANKFURTER_CURRENCIES: dict[str, str] = {
269
+ "美元": "USD", "美金": "USD", "usd": "USD", "dollar": "USD",
270
+ "人民币": "CNY", "rmb": "CNY", "cny": "CNY", "yuan": "CNY", "元": "CNY",
271
+ "欧元": "EUR", "eur": "EUR", "euro": "EUR",
272
+ "日元": "JPY", "日圆": "JPY", "jpy": "JPY", "yen": "JPY",
273
+ "英镑": "GBP", "gbp": "GBP", "pound": "GBP",
274
+ "港币": "HKD", "港元": "HKD", "hkd": "HKD",
275
+ "韩元": "KRW", "krw": "KRW", "won": "KRW",
276
+ "加元": "CAD", "cad": "CAD", "澳元": "AUD", "aud": "AUD",
277
+ "瑞士法郎": "CHF", "瑞郎": "CHF", "chf": "CHF",
278
+ "新加坡元": "SGD", "sgd": "SGD", "新币": "SGD",
279
+ "泰铢": "THB", "thb": "THB", "卢布": "RUB", "rub": "RUB",
280
+ "印度卢比": "INR", "inr": "INR", "台币": "TWD", "新台币": "TWD", "twd": "TWD",
281
+ "巴西雷亚尔": "BRL", "brl": "BRL", "墨西哥比索": "MXN", "mxn": "MXN",
282
+ }
283
+
284
+
285
+ def _build_frankfurter_engine(spec: dict[str, Any]) -> Any:
286
+ """Frankfurter:查询内币种解析 → ECB 官方参考汇率(api.frankfurter.dev)。
287
+
288
+ 2026-10-06 接入。与 fx_rate(open.er-api.com 聚合源)互补:ECB 官方
289
+ 参考价,用于宏观/汇率类查询的权威口径。查询里没有两个币种时诚实空
290
+ (不猜 base),由 fx_rate/anysearch 接手。
291
+ """
292
+ timeout = spec.get("timeout", 8)
293
+
294
+ def _currencies(query: str) -> list[str]:
295
+ q = (query or "").lower()
296
+ found: list[str] = []
297
+ for key, code in _FRANKFURTER_CURRENCIES.items():
298
+ if key in q and code not in found:
299
+ found.append(code)
300
+ for code in re.findall(r"\b([A-Z]{3})\b", query or ""):
301
+ if code in _FRANKFURTER_CURRENCIES.values() and code not in found:
302
+ found.append(code)
303
+ return found
304
+
305
+ @safe_search
306
+ def _engine(query: str, n: int = 5, _timeout: float | None = None, **kwargs) -> list[dict[str, Any]]:
307
+ import urllib.parse as up
308
+ to = _timeout or timeout
309
+ codes = _currencies(query)
310
+ if len(codes) < 2:
311
+ return []
312
+ base, symbols = codes[0], codes[1:1 + max(1, min(n, 5))]
313
+ url = ("https://api.frankfurter.dev/v1/latest?"
314
+ + up.urlencode({"base": base, "symbols": ",".join(symbols)}))
315
+ try:
316
+ req = urllib.request.Request(url, headers={
317
+ "User-Agent": "argo-search/2.9 (unified-search@local)",
318
+ "Accept": "application/json"})
319
+ with http_open(req, timeout=to, engine=spec.get("_name", "")) as resp:
320
+ data = json.loads(resp.read().decode("utf-8", "replace"))
321
+ except Exception:
322
+ return []
323
+ rates = data.get("rates") or {}
324
+ date = str(data.get("date") or "")
325
+ out: list[dict[str, Any]] = []
326
+ for sym, val in list(rates.items())[:max(1, n)]:
327
+ try:
328
+ rate = float(val)
329
+ except (TypeError, ValueError):
330
+ continue
331
+ out.append({
332
+ "title": f"1 {base} = {rate:g} {sym}(ECB 参考价 {date})",
333
+ "url": "https://www.ecb.europa.eu/stats/policy_and_exchange_rates/"
334
+ "euro_reference_exchange_rates/html/index.en.html",
335
+ "snippet": f"欧洲央行参考汇率 | 基准 {base} | 报价 {sym} | 日期 {date}",
336
+ "source": "frankfurter",
337
+ "facts": {"base": base, "symbol": sym, "rate": rate, "date": date},
338
+ })
339
+ return out
340
+ return _engine