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,656 @@
1
+ #!/usr/bin/env python3
2
+ """批次十四构建器:港股披露 / 商品期货 / 人民币中间价 / 链上 TVL(2026-10-21)。
3
+
4
+ hkex 港交所披露易(HKEXnews)公告检索:代码/公司名解析 + 标题关键词
5
+ shfe 上海期货交易所日行情(铜/铝/黄金/螺纹钢等,按品种过滤)
6
+ cfets 中国外汇交易中心人民币中间价(25 个币种,官方口径)
7
+ defillama DeFiLlama 协议 TVL(链上资金规模/24h 与 7d 变化)
8
+
9
+ 四个源都是免 key 的官方/一手接口,补的是既有覆盖的结构性空白:
10
+ 港股公告(A 股有巨潮、美股有 SEC,港股此前没有)、商品期货(此前零源)、
11
+ 人民币中间价(此前只有 ECB/聚合汇率,没有在岸官方价)、链上 TVL(此前只有
12
+ CoinGecko 币价,没有协议资金规模)。
13
+
14
+ 共同约束:
15
+ - 上游都不是「关键词全文检索」,而是「按标的枚举」——查询先做标的解析
16
+ (代码/品种/币种/协议名),解析不出来就诚实返回空,不猜、不拿全量
17
+ 数据冒充结果。
18
+ - 全部走 http_open(失败现场归因),异常由 safe_search 兜底成空结果。
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import re
25
+ import time
26
+ import urllib.parse
27
+ import urllib.request
28
+ from datetime import datetime, timedelta, timezone
29
+ from typing import Any
30
+
31
+ from engines_base import http_open, safe_search
32
+
33
+ _UA = "argo-search/2.9 (unified-search@local)"
34
+ _CST = timezone(timedelta(hours=8)) # 三个中国市场源都按北京时间取「今天」
35
+
36
+
37
+ def _today_cst() -> datetime:
38
+ return datetime.now(_CST)
39
+
40
+
41
+ def _json_open(url: str, timeout: float, engine: str,
42
+ headers: dict[str, str] | None = None) -> Any:
43
+ hdr = {"User-Agent": _UA, "Accept": "application/json"}
44
+ hdr.update(headers or {})
45
+ req = urllib.request.Request(url, headers=hdr)
46
+ with http_open(req, timeout=timeout, engine=engine) as resp:
47
+ return json.loads(resp.read().decode("utf-8", "replace"))
48
+
49
+
50
+ # ── 港交所披露易(HKEXnews)──────────────────────────────────────────────────
51
+
52
+ # 披露易文档标题是繁体中文, mainland 用户习惯输入简体;不做整表转换
53
+ # (引入 OpenCC 太重),只覆盖港股公告里的高频词。未命中的简体词仍可通过
54
+ # 代码/公司名解析路径命中,不会整源失效。
55
+ _HKEX_S2T_CHARS = str.maketrans({
56
+ "腾": "騰", "讯": "訊", "汇": "匯", "丰": "豐", "团": "團", "亚": "亞",
57
+ "银": "銀", "证": "證", "产": "產", "业": "業", "务": "務", "国": "國",
58
+ "际": "際", "华": "華", "东": "東", "电": "電", "车": "車", "险": "險",
59
+ "药": "藥", "医": "醫", "疗": "療", "网": "網", "络": "絡", "软": "軟",
60
+ "数": "數", "据": "據", "显": "顯", "传": "傳", "铁": "鐵", "钢": "鋼",
61
+ "铝": "鋁", "铜": "銅", "矿": "礦", "黄": "黃", "锂": "鋰", "风": "風",
62
+ "环": "環", "气": "氣", "农": "農", "养": "養", "饲": "飼", "纺": "紡",
63
+ "织": "織", "装": "裝", "递": "遞", "线": "線", "导": "導", "体": "體",
64
+ "制": "製", "筑": "築", "饰": "飾", "轮": "輪", "纸": "紙", "贸": "貿",
65
+ "资": "資", "权": "權", "兴": "興", "发": "發", "长": "長", "龙": "龍",
66
+ "马": "馬", "湾": "灣", "门": "門", "开": "開", "关": "關", "报": "報",
67
+ "总": "總", "结": "結", "构": "構", "变": "變", "调": "調", "库": "庫",
68
+ "检": "檢", "测": "測", "试": "試", "验": "驗", "认": "認", "书": "書",
69
+ "许": "許", "营": "營", "销": "銷", "价": "價", "额": "額", "页": "頁",
70
+ "码": "碼", "视": "視", "频": "頻", "图": "圖", "买": "買", "卖": "賣",
71
+ "货": "貨", "币": "幣", "债": "債", "万": "萬", "亿": "億", "广": "廣",
72
+ "宁": "寧", "辽": "遼", "苏": "蘇", "无": "無", "与": "與", "为": "為",
73
+ "会": "會", "员": "員", "计": "計", "划": "劃", "实": "實", "现": "現",
74
+ "场": "場", "区": "區", "儿": "兒", "妇": "婦", "学": "學", "术": "術",
75
+ "艺": "藝", "乐": "樂", "闻": "聞", "题": "題", "问": "問", "见": "見",
76
+ "观": "觀", "识": "識", "说": "說", "请": "請", "谢": "謝", "让": "讓",
77
+ "对": "對", "应": "應", "该": "該", "个": "個", "这": "這", "时": "時",
78
+ "间": "間", "户": "戶", "决": "決", "议": "議", "注": "註",
79
+ })
80
+
81
+ # 常见公告主题词(简体→繁体)。字表之外的整词映射,覆盖「回购/业绩」这类
82
+ # 高频查询;未覆盖的简体生僻词仍可用代码查询命中。
83
+ _HKEX_TERMS = {
84
+ "回购": "購回", "回購": "購回", "回购注销": "購回註銷",
85
+ "业绩": "業績", "年报": "年報", "月报表": "月報表", "中期报告": "中期報告",
86
+ "分红": "分紅", "股东": "股東", "股东大会": "股東大會",
87
+ "董事会": "董事會", "收购": "收購", "合并": "合併",
88
+ "私有化": "私有化", "供股": "供股", "配股": "配股",
89
+ "停牌": "停牌", "复牌": "復牌", "招股": "招股",
90
+ "股权": "股權", "债券": "債券", "增持": "增持", "减持": "減持",
91
+ }
92
+
93
+ # 查询里描述「要什么」而非「哪家公司」的词,解析公司名前先剥掉。
94
+ # 主题词(回购/业绩/停牌…)不在此列——它们是过滤条件,剥掉就退化成
95
+ # 「该公司最新公告」,用户要的特定公告会被淹掉。
96
+ _HKEX_NOISE = re.compile(
97
+ r"(?i)(公告|通告|披露|文件|最新|查询|查詢|全部|港股|港交所|披露易|hkex|H股|红筹|"
98
+ r"恒生指数|announcements?|filing|filings?)"
99
+ )
100
+
101
+
102
+ def _hkex_s2t(text: str) -> str:
103
+ out = str(text or "")
104
+ # 最长词优先替换:防止「回购」先把「回购注销」前半截换掉而漏掉「注销→註銷」
105
+ for s, t in sorted(_HKEX_TERMS.items(), key=lambda kv: len(kv[0]), reverse=True):
106
+ out = out.replace(s, t)
107
+ return out.translate(_HKEX_S2T_CHARS)
108
+
109
+
110
+ _HKEX_PREFIX_CACHE: dict[str, list[dict[str, Any]]] = {}
111
+
112
+
113
+ def _hkex_prefix(probe: str) -> list[dict[str, Any]]:
114
+ hit = _HKEX_PREFIX_CACHE.get(probe)
115
+ if hit is not None:
116
+ return hit
117
+ url = ("https://www1.hkexnews.hk/search/prefix.do?"
118
+ + urllib.parse.urlencode({"callback": "callback", "lang": "ZH",
119
+ "type": "A", "name": probe,
120
+ "market": "SEHK"}))
121
+ req = urllib.request.Request(url, headers={
122
+ "User-Agent": _UA,
123
+ "Referer": "https://www1.hkexnews.hk/search/titlesearch.xhtml",
124
+ })
125
+ with http_open(req, timeout=10, engine="hkex") as resp:
126
+ body = resp.read().decode("utf-8", "replace")
127
+ m = re.search(r"callback\((.*)\)\s*;?\s*$", body, re.S)
128
+ info: list[dict[str, Any]] = []
129
+ if m:
130
+ try:
131
+ info = (json.loads(m.group(1)) or {}).get("stockInfo") or []
132
+ except (json.JSONDecodeError, ValueError):
133
+ info = []
134
+ if len(_HKEX_PREFIX_CACHE) > 200:
135
+ _HKEX_PREFIX_CACHE.clear()
136
+ _HKEX_PREFIX_CACHE[probe] = info
137
+ return info
138
+
139
+
140
+ _HKEX_NON_EQUITY = re.compile(r"(購|沽|牛|熊|證|ETF|基金|債券|票據|優先|票據)")
141
+
142
+
143
+ from concurrent.futures import ThreadPoolExecutor
144
+
145
+ _TOPIC_TERMS = tuple(sorted(
146
+ frozenset(_HKEX_TERMS) | frozenset(_HKEX_TERMS.values()),
147
+ key=len, reverse=True,
148
+ ))
149
+
150
+
151
+ def _hkex_resolve(query: str) -> tuple[list[tuple[str, str, str]], str] | None:
152
+ """查询 → ([(stockId, code, name)…], core)。
153
+
154
+ code 优先;公司名先剥主题词(「腾讯 回购」→「騰訊」)再做简→繁,然后按
155
+ 「原名 → 加控股/股份/集團 → 截短」探测,前 6 个候选并发发(单次 2~3s,
156
+ 串行会把整次查询顶到 10s 预算外)。
157
+
158
+ 加后缀是必需的:前缀接口对两字简称只回窝轮(实测「騰訊」前 6 条全是
159
+ 「騰訊中銀…購」),「騰訊控股」才回正股 00700。返回的 core 是命中的
160
+ 那截,调用方据此把主题词从查询里扣出来。
161
+ """
162
+ q = (query or "").strip()
163
+ if not q:
164
+ return None
165
+ m = re.search(r"(?<!\d)(\d{5})(?!\d)", q)
166
+ if not m:
167
+ m4 = re.search(r"(?<!\d)(\d{4})(?!\d)", q)
168
+ code = m4.group(1).zfill(5) if m4 else ""
169
+ else:
170
+ code = m.group(1)
171
+ if code:
172
+ rows = [(str(i.get("stockId") or "").strip(), str(i.get("code") or ""),
173
+ str(i.get("name") or "")) for i in _hkex_prefix(code)]
174
+ rows = [r for r in rows if r[0]]
175
+ return (rows[:2], code) if rows else None
176
+
177
+ raw = _HKEX_NOISE.sub(" ", q)
178
+ raw = re.sub(r"\s+", " ", raw).strip()
179
+ full = _hkex_s2t(raw)
180
+ if not full:
181
+ return None
182
+ name_only = full
183
+ for term in _TOPIC_TERMS:
184
+ if term and term in name_only:
185
+ name_only = name_only.replace(term, " ")
186
+ name_only = re.sub(r"\s+", " ", name_only).strip() or full
187
+
188
+ candidates: list[tuple[str, str]] = [] # (probe, core)
189
+
190
+ def _add(probe: str, core: str) -> None:
191
+ probe = probe.strip()
192
+ if probe and probe not in [c[0] for c in candidates]:
193
+ candidates.append((probe, core))
194
+
195
+ for base in (name_only, full):
196
+ for suffix in ("控股", "股份", "集團", "集团", ""):
197
+ _add(base + suffix, base)
198
+ parts = name_only.split()
199
+ for i in range(len(parts) - 1, 0, -1):
200
+ _add(" ".join(parts[:i]), " ".join(parts[:i]))
201
+ for size in (4, 3, 2):
202
+ if len(name_only) > size:
203
+ _add(name_only[:size], name_only[:size])
204
+ candidates = candidates[:4]
205
+ if not candidates:
206
+ return None
207
+
208
+ def _probe(item: tuple[str, str]) -> list[tuple[str, str, str]]:
209
+ probe, _core = item
210
+ info = _hkex_prefix(probe)
211
+ rows = [(str(i.get("stockId") or "").strip(), str(i.get("code") or ""),
212
+ str(i.get("name") or "")) for i in info
213
+ if not _HKEX_NON_EQUITY.search(str(i.get("name") or ""))]
214
+ return [r for r in rows if r[0]]
215
+
216
+ with ThreadPoolExecutor(max_workers=4) as pool:
217
+ results = list(pool.map(_probe, candidates))
218
+ for (probe, core), rows in zip(candidates, results):
219
+ if rows:
220
+ rows.sort(key=lambda r: abs(len(r[2]) - len(core)))
221
+ return rows[:2], core
222
+ return None
223
+
224
+
225
+ def _hkex_query(params: dict[str, Any], kw_tokens: list[str], filt: bool,
226
+ to: float, n: int, seen: list[dict[str, Any]]) -> list[dict[str, Any]]:
227
+ """按一组参数拉公告并转成统一结果;filt=True 时按主题词过滤标题。"""
228
+ url = ("https://www1.hkexnews.hk/search/titleSearchServlet.do?"
229
+ + urllib.parse.urlencode(params))
230
+ req = urllib.request.Request(url, headers={
231
+ "User-Agent": _UA,
232
+ "Referer": "https://www1.hkexnews.hk/search/titlesearch.xhtml",
233
+ })
234
+ with http_open(req, timeout=to, engine="hkex") as resp:
235
+ payload = json.loads(resp.read().decode("utf-8", "replace"))
236
+ raw = payload.get("result") or "[]"
237
+ rows = json.loads(raw) if isinstance(raw, str) else raw
238
+ if not isinstance(rows, list):
239
+ return []
240
+ have = {str(r.get("url") or "") for r in seen}
241
+ out: list[dict[str, Any]] = []
242
+ for row in rows:
243
+ if not isinstance(row, dict):
244
+ continue
245
+ title = re.sub(r"<[^>]+>", "", str(row.get("TITLE") or "")).strip()
246
+ long_text = re.sub(r"<[^>]+>", "", str(row.get("LONG_TEXT") or "")).strip()
247
+ body = long_text or title
248
+ if filt and kw_tokens and not all(t in body for t in kw_tokens):
249
+ continue
250
+ code = re.sub(r"<[^>]+>", " ", str(row.get("STOCK_CODE") or "")).split()
251
+ name = re.sub(r"<[^>]+>", " ", str(row.get("STOCK_NAME") or "")).split()
252
+ link = str(row.get("FILE_LINK") or "").strip()
253
+ if not link:
254
+ continue
255
+ full = "https://www1.hkexnews.hk" + link
256
+ if full in have:
257
+ continue
258
+ have.add(full)
259
+ code_s = code[0] if code else ""
260
+ name_s = name[0] if name else ""
261
+ when = str(row.get("DATE_TIME") or "").strip()
262
+ published = ""
263
+ m = re.match(r"(\d{2})/(\d{2})/(\d{4})\s+(\d{2}):(\d{2})", when)
264
+ if m:
265
+ d, mo, y, hh, mm = m.groups()
266
+ published = f"{y}-{mo}-{d}T{hh}:{mm}:00+08:00"
267
+ out.append({
268
+ "title": f"{code_s} {name_s}|{body}"[:160],
269
+ "url": full,
270
+ "snippet": (f"{body} · {when} · {row.get('FILE_TYPE') or ''} "
271
+ f"{row.get('FILE_INFO') or ''}").strip(" ·"),
272
+ "source": "hkex",
273
+ "published_at": published,
274
+ "facts": {"stock_code": code_s, "stock_name": name_s,
275
+ "news_id": str(row.get("NEWS_ID") or "")},
276
+ })
277
+ if len(seen) + len(out) >= max(1, n):
278
+ break
279
+ return out
280
+
281
+
282
+ def _build_hkex_engine(spec: dict[str, Any]) -> Any:
283
+ timeout = spec.get("timeout", 20)
284
+
285
+ @safe_search
286
+ def _engine(query: str, n: int = 5, _timeout: float | None = None,
287
+ **kwargs) -> list[dict[str, Any]]:
288
+ to = _timeout or timeout
289
+ q = (query or "").strip()
290
+ if not q:
291
+ return []
292
+
293
+ hit = None
294
+ try:
295
+ hit = _hkex_resolve(q)
296
+ except Exception:
297
+ hit = None
298
+
299
+ # 剩余主题词:公司解析命中时扣掉 probe,否则用整条查询
300
+ base = _hkex_s2t(_HKEX_NOISE.sub(" ", q))
301
+ base = re.sub(r"(?<!\d)\d{4,5}(?!\d)", " ", base)
302
+ base = re.sub(r"\s+", " ", base).strip()
303
+ topic = base
304
+ if hit and hit[1] and hit[1] in base:
305
+ topic = base.replace(hit[1], " ", 1).strip()
306
+ topic = re.sub(r"\s+", " ", topic).strip()
307
+
308
+ has_cjk = bool(re.search(r"[\u4e00-\u9fff]", topic))
309
+ targets: list[tuple[str, str, str]] = []
310
+ if hit:
311
+ targets = hit[0]
312
+ elif topic and has_cjk:
313
+ targets = [("-1", "", "")]
314
+ else:
315
+ # 既没有可解析的标的、也没有中文主题词:不猜,交回上游路由
316
+ return []
317
+
318
+ kw_tokens = [t for t in re.split(r"\s+", topic) if t] if (has_cjk and hit) else []
319
+ # 公司+主题时只查最匹配的一家(前缀接口对简称会返回多家关联主体,
320
+ # 逐家拉 60 条会把单次查询推到 10s 预算之上);纯代码/纯公司名查 2 家。
321
+ targets = targets[:1] if kw_tokens else targets[:2]
322
+ out: list[dict[str, Any]] = []
323
+ for sid, _code, _name in targets:
324
+ if len(out) >= max(1, n):
325
+ break
326
+ params: dict[str, Any] = {
327
+ "sortDir": "0", "sortByOptions": "DateTime", "category": "0",
328
+ "market": "SEHK", "documentType": "-1",
329
+ # 公司+主题词查的是「历史上哪份公告讲了这个」,45 天窗口
330
+ # 会漏掉半年报/年报(业绩类公告的间隔本来就长),放宽到 180 天。
331
+ "fromDate": (_today_cst() - timedelta(
332
+ days=180 if kw_tokens else 45)).strftime("%Y%m%d"),
333
+ "toDate": _today_cst().strftime("%Y%m%d"),
334
+ "title": "", "searchType": "0",
335
+ "t1code": "-2", "t2Gcode": "-2", "t2code": "-2",
336
+ "rowRange": str(max(5, min(int(n) * 3, 30))), "lang": "ZH",
337
+ }
338
+ if sid == "-1":
339
+ params["stockId"] = "-1"
340
+ params["title"] = topic
341
+ params["searchType"] = "1"
342
+ else:
343
+ params["stockId"] = sid
344
+ if kw_tokens:
345
+ params["rowRange"] = "60" # 公司+主题:多拉再本地筛
346
+ out.extend(_hkex_query(params, kw_tokens, filt=bool(hit), to=to,
347
+ n=n, seen=out))
348
+ return out[:max(1, n)]
349
+
350
+ return _engine
351
+
352
+
353
+ # ── 上海期货交易所日行情 ─────────────────────────────────────────────────────
354
+
355
+ _SHFE_CACHE: dict[str, tuple[float, dict[str, Any]]] = {}
356
+ _SHFE_TTL = 600.0
357
+
358
+
359
+ def _shfe_daily(engine: str, timeout: float) -> tuple[str, dict[str, Any]] | None:
360
+ """取最近一个可用交易日的全量日行情(周末/节假日自动往前找)。"""
361
+ today = _today_cst()
362
+ for back in range(0, 12):
363
+ day = today - timedelta(days=back)
364
+ if day.weekday() >= 5:
365
+ continue
366
+ key = day.strftime("%Y%m%d")
367
+ hit = _SHFE_CACHE.get(key)
368
+ if hit and time.time() - hit[0] < _SHFE_TTL:
369
+ return key, hit[1]
370
+ url = ("https://www.shfe.com.cn/data/tradedata/future/dailydata/"
371
+ f"kx{key}.dat")
372
+ try:
373
+ data = _json_open(url, timeout, engine)
374
+ except Exception:
375
+ continue
376
+ rows = (data or {}).get("o_curinstrument") if isinstance(data, dict) else None
377
+ # 当日收盘前文件已生成但价格为空的快照(实测 2026-10-21 14:00 的
378
+ # kx20261021.dat:331 行、CLOSEPRICE 全空)不能当结果用,继续往前找。
379
+ if not rows or not any(str(r.get("CLOSEPRICE") or "").strip() for r in rows
380
+ if isinstance(r, dict)):
381
+ continue
382
+ _SHFE_CACHE[key] = (time.time(), data)
383
+ if len(_SHFE_CACHE) > 8:
384
+ oldest = min(_SHFE_CACHE, key=lambda k: _SHFE_CACHE[k][0])
385
+ _SHFE_CACHE.pop(oldest, None)
386
+ return key, data
387
+ return None
388
+
389
+
390
+ def _fmt_num(v: Any) -> str:
391
+ try:
392
+ f = float(v)
393
+ except (TypeError, ValueError):
394
+ return "-"
395
+ if f == int(f):
396
+ return f"{int(f):,}"
397
+ return f"{f:,.2f}"
398
+
399
+
400
+ def _build_shfe_engine(spec: dict[str, Any]) -> Any:
401
+ timeout = spec.get("timeout", 20)
402
+
403
+ @safe_search
404
+ def _engine(query: str, n: int = 5, _timeout: float | None = None,
405
+ **kwargs) -> list[dict[str, Any]]:
406
+ to = _timeout or timeout
407
+ q = (query or "").strip()
408
+ if not q:
409
+ return []
410
+ # 「沪铜主力」「螺纹钢 2610」这类查询:剥掉交易所/意图噪声,留品种名
411
+ core = re.sub(
412
+ r"(?i)(上期所|上海期货交易所|期货|主力|连续|合约|行情|价格|报价|今日|最新|"
413
+ r"收盘|结算价|shfe|futures?)", " ", q)
414
+ core = core.replace("沪", " ")
415
+ core = re.sub(r"(?<!\d)\d{4}(?!\d)", " ", core)
416
+ core = re.sub(r"\s+", " ", core).strip().lower()
417
+ if not core:
418
+ return []
419
+ got = _shfe_daily("shfe", to)
420
+ if not got:
421
+ return []
422
+ day, data = got
423
+ rows = data.get("o_curinstrument") or []
424
+ out: list[dict[str, Any]] = []
425
+ for row in rows:
426
+ if not isinstance(row, dict):
427
+ continue
428
+ pname = str(row.get("PRODUCTNAME") or "").strip()
429
+ pgroup = str(row.get("PRODUCTGROUPID") or "").strip().lower()
430
+ pid = str(row.get("PRODUCTID") or "").strip().lower()
431
+ if not pname:
432
+ continue
433
+ hay = f"{pname} {pgroup} {pid}".lower()
434
+ if core not in hay and not any(tok in hay for tok in core.split()):
435
+ continue
436
+ close = _fmt_num(row.get("CLOSEPRICE"))
437
+ settle = _fmt_num(row.get("SETTLEMENTPRICE"))
438
+ chg = _fmt_num(row.get("ZD1_CHG"))
439
+ month = str(row.get("DELIVERYMONTH") or "").strip()
440
+ out.append({
441
+ "title": f"{pname} {month} 收 {close} 结算 {settle}({day})",
442
+ "url": f"https://www.shfe.com.cn/products/future/{pgroup}/",
443
+ "snippet": (f"上海期货交易所日行情 · {pname} {month} · "
444
+ f"收盘 {close} · 结算 {settle} · 涨跌 {chg} · "
445
+ f"成交 {_fmt_num(row.get('VOLUME'))} 手 · "
446
+ f"持仓 {_fmt_num(row.get('OPENINTEREST'))} 手"),
447
+ "source": "shfe",
448
+ "published_at": f"{day[:4]}-{day[4:6]}-{day[6:]}T15:00:00+08:00",
449
+ "facts": {"product": pname, "month": month, "date": day,
450
+ "close": close, "settlement": settle},
451
+ })
452
+ if len(out) >= max(1, n):
453
+ break
454
+ return out
455
+
456
+ return _engine
457
+
458
+
459
+ # ── 中国外汇交易中心人民币中间价 ─────────────────────────────────────────────
460
+
461
+ # 币种词 → 披露易口径的货币对(100JPY 表示每 100 日元报价,是 CFETS 惯例)
462
+ _CFETS_CCY = {
463
+ "美元": "USD/CNY", "美金": "USD/CNY", "usd": "USD/CNY",
464
+ "欧元": "EUR/CNY", "歐元": "EUR/CNY", "eur": "EUR/CNY",
465
+ "日元": "100JPY/CNY", "日币": "100JPY/CNY", "jpy": "100JPY/CNY",
466
+ "港元": "HKD/CNY", "港币": "HKD/CNY", "港幣": "HKD/CNY", "hkd": "HKD/CNY",
467
+ "英镑": "GBP/CNY", "英鎊": "GBP/CNY", "gbp": "GBP/CNY",
468
+ "澳元": "AUD/CNY", "aud": "AUD/CNY",
469
+ "新西兰元": "NZD/CNY", "nzd": "NZD/CNY",
470
+ "新加坡元": "SGD/CNY", "sgd": "SGD/CNY",
471
+ "瑞士法郎": "CHF/CNY", "chf": "CHF/CNY",
472
+ "加元": "CAD/CNY", "加拿大元": "CAD/CNY", "cad": "CAD/CNY",
473
+ "澳门元": "CNY/MOP", "mop": "CNY/MOP",
474
+ "林吉特": "CNY/MYR", "myr": "CNY/MYR",
475
+ "卢布": "CNY/RUB", "rub": "CNY/RUB",
476
+ "兰特": "CNY/ZAR", "zar": "CNY/ZAR",
477
+ "韩元": "CNY/KRW", "韩币": "CNY/KRW", "krw": "CNY/KRW",
478
+ "迪拉姆": "CNY/AED", "aed": "CNY/AED",
479
+ "里亚尔": "CNY/SAR", "sar": "CNY/SAR",
480
+ "福林": "CNY/HUF", "huf": "CNY/HUF",
481
+ "兹罗提": "CNY/PLN", "pln": "CNY/PLN",
482
+ "丹麦克朗": "CNY/DKK", "dkk": "CNY/DKK",
483
+ "瑞典克朗": "CNY/SEK", "sek": "CNY/SEK",
484
+ "挪威克朗": "CNY/NOK", "nok": "CNY/NOK",
485
+ "里拉": "CNY/TRY", "try": "CNY/TRY",
486
+ "比索": "CNY/MXN", "mxn": "CNY/MXN",
487
+ "泰铢": "CNY/THB", "thb": "CNY/THB",
488
+ }
489
+
490
+
491
+ def _cfets_pair(query: str) -> str:
492
+ q = (query or "").lower()
493
+ for word, pair in _CFETS_CCY.items():
494
+ if word in q:
495
+ return pair
496
+ m = re.search(r"\b([a-z]{3})\s*[/\-]\s*(cny|rmb)\b", q)
497
+ if m:
498
+ code = m.group(1).upper()
499
+ for pair in _CFETS_CCY.values():
500
+ if pair.startswith(code + "/"):
501
+ return pair
502
+ m = re.search(r"\b(cny|rmb)\s*[/\-]\s*([a-z]{3})\b", q)
503
+ if m:
504
+ code = m.group(2).upper()
505
+ for pair in _CFETS_CCY.values():
506
+ if pair.endswith("/" + code):
507
+ return pair
508
+ return "USD/CNY" # 只说「人民币中间价」时按最常用的美元对
509
+
510
+
511
+ def _build_cfets_engine(spec: dict[str, Any]) -> Any:
512
+ timeout = spec.get("timeout", 15)
513
+
514
+ @safe_search
515
+ def _engine(query: str, n: int = 5, _timeout: float | None = None,
516
+ **kwargs) -> list[dict[str, Any]]:
517
+ to = _timeout or timeout
518
+ if not (query or "").strip():
519
+ return []
520
+ pair = _cfets_pair(query)
521
+ end = _today_cst()
522
+ start = end - timedelta(days=14)
523
+ url = ("https://www.chinamoney.com.cn/ags/ms/cm-u-bk-ccpr/CcprHisNew?"
524
+ + urllib.parse.urlencode({
525
+ "startDate": start.strftime("%Y-%m-%d"),
526
+ "endDate": end.strftime("%Y-%m-%d"),
527
+ "currency": pair,
528
+ "pageNum": 1,
529
+ "pageSize": max(1, min(int(n), 20)),
530
+ }))
531
+ data = _json_open(url, to, "cfets", headers={
532
+ "Referer": "https://www.chinamoney.com.cn/",
533
+ })
534
+ head = ((data or {}).get("data") or {})
535
+ searchlist = head.get("searchlist") or [pair]
536
+ out: list[dict[str, Any]] = []
537
+ for rec in (data or {}).get("records") or []:
538
+ if not isinstance(rec, dict):
539
+ continue
540
+ vals = rec.get("values") or []
541
+ if not vals:
542
+ continue
543
+ day = str(rec.get("date") or "")
544
+ value = str(vals[0]).strip()
545
+ label = str(searchlist[0]) if searchlist else pair
546
+ out.append({
547
+ "title": f"{label} 人民币中间价 {value}({day})",
548
+ "url": "https://www.chinamoney.com.cn/chinese/bkccpr/",
549
+ "snippet": (f"中国外汇交易中心受权公布 · {label} · {day} · "
550
+ f"中间价 {value}"),
551
+ "source": "cfets",
552
+ "published_at": f"{day}T09:15:00+08:00" if day else "",
553
+ "facts": {"pair": label, "date": day, "mid_price": value},
554
+ })
555
+ return out
556
+
557
+ return _engine
558
+
559
+
560
+ # ── DeFiLlama 协议 TVL ─────────────────────────────────────────────────────
561
+
562
+ _DLLAMA_CACHE: dict[str, Any] = {"ts": 0.0, "rows": []}
563
+ _DLLAMA_TTL = 300.0
564
+
565
+
566
+ def _dllama_rows(timeout: float) -> list[dict[str, Any]]:
567
+ now = time.time()
568
+ if _DLLAMA_CACHE["rows"] and now - float(_DLLAMA_CACHE["ts"]) < _DLLAMA_TTL:
569
+ return _DLLAMA_CACHE["rows"]
570
+ data = _json_open("https://api.llama.fi/protocols", max(timeout, 20), "defillama")
571
+ rows = data if isinstance(data, list) else []
572
+ _DLLAMA_CACHE["rows"] = rows
573
+ _DLLAMA_CACHE["ts"] = now
574
+ return rows
575
+
576
+
577
+ def _usd(v: Any) -> str:
578
+ try:
579
+ f = float(v)
580
+ except (TypeError, ValueError):
581
+ return "-"
582
+ for unit, scale in (("B", 1e9), ("M", 1e6), ("K", 1e3)):
583
+ if abs(f) >= scale:
584
+ return f"${f / scale:.2f}{unit}"
585
+ return f"${f:.0f}"
586
+
587
+
588
+ def _build_defillama_engine(spec: dict[str, Any]) -> Any:
589
+ timeout = spec.get("timeout", 30)
590
+
591
+ @safe_search
592
+ def _engine(query: str, n: int = 5, _timeout: float | None = None,
593
+ **kwargs) -> list[dict[str, Any]]:
594
+ to = _timeout or timeout
595
+ q = (query or "").strip().lower()
596
+ if not q:
597
+ return []
598
+ noise = re.compile(
599
+ r"(?i)(defillama|tvl|总锁仓|liquidity|protocol|协议|链上|资金|规模|排名|"
600
+ r"排名前|top|多少|查询|最新)")
601
+ core = noise.sub(" ", q)
602
+ tokens = [t for t in re.split(r"\s+", core) if len(t) >= 2]
603
+ if not tokens:
604
+ return []
605
+ rows = _dllama_rows(to)
606
+ if not rows:
607
+ return []
608
+ scored: list[tuple[int, float, dict[str, Any]]] = []
609
+ for p in rows:
610
+ if not isinstance(p, dict):
611
+ continue
612
+ name = str(p.get("name") or "")
613
+ symbol = str(p.get("symbol") or "")
614
+ category = str(p.get("category") or "")
615
+ chains = p.get("chains") or []
616
+ hay = f"{name} {symbol} {category} {' '.join(map(str, chains[:8]))}".lower()
617
+ score = sum(1 for t in tokens if t in hay)
618
+ if score == 0:
619
+ continue
620
+ try:
621
+ tvl = float(p.get("tvl") or 0)
622
+ except (TypeError, ValueError):
623
+ tvl = 0.0
624
+ scored.append((score, tvl, p))
625
+ scored.sort(key=lambda x: (x[0], x[1]), reverse=True)
626
+ out: list[dict[str, Any]] = []
627
+ for score, tvl, p in scored[:max(1, n)]:
628
+ name = str(p.get("name") or "")
629
+ symbol = str(p.get("symbol") or "")
630
+ slug = str(p.get("slug") or "")
631
+ cat = str(p.get("category") or "")
632
+ chg1 = p.get("change_1d")
633
+ chg7 = p.get("change_7d")
634
+ chg_txt = ""
635
+ try:
636
+ chg_txt = f"24h {float(chg1):+.2f}%"
637
+ except (TypeError, ValueError):
638
+ pass
639
+ try:
640
+ chg_txt += f" · 7d {float(chg7):+.2f}%"
641
+ except (TypeError, ValueError):
642
+ pass
643
+ desc = re.sub(r"\s+", " ", str(p.get("description") or "")).strip()
644
+ out.append({
645
+ "title": f"{name}({symbol or '-'})TVL {_usd(tvl)}"
646
+ + (f" · {cat}" if cat else ""),
647
+ "url": (f"https://defillama.com/protocol/{slug}" if slug
648
+ else str(p.get("url") or "https://defillama.com/protocols")),
649
+ "snippet": " · ".join(x for x in (chg_txt, desc[:180]) if x),
650
+ "source": "defillama",
651
+ "facts": {"protocol": name, "tvl_usd": tvl,
652
+ "change_1d": chg1, "change_7d": chg7},
653
+ })
654
+ return out
655
+
656
+ return _engine