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,800 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """ego-search — argo 子技能:登录态专业搜索(双运行时完全态)。
4
+
5
+ 模式:
6
+ enable / disable / status
7
+ search / fetch / act / api [--runtime auto|ego|webbridge]
8
+
9
+ 双运行时(互补,保留两者):
10
+ - ego lite(ego-browser):Agent 浏览器 + 任务空间隔离
11
+ - WebBridge:用户 Chrome/Edge + 扩展桥
12
+ 任一可用即可做登录态搜索;--runtime auto 优先 ego,否则 webbridge。
13
+
14
+ 与常规 argo 检索:缓存/分区隔离(cache_eligible=false);
15
+ 汇总分析阶段可把 public + login 两路结果一起喂 evidence。
16
+
17
+ 专业搜索模式默认关闭;开启后 search/fetch/act/api 才可用。
18
+ 输出强制 login provenance。见 SKILL.md「完全态」。
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import json
24
+ import os
25
+ import subprocess
26
+ import sys
27
+ import urllib.parse
28
+ from datetime import datetime
29
+ from pathlib import Path
30
+ from typing import Any
31
+
32
+ # 同目录模块(runtime / webbridge_adapter)
33
+ _SCRIPT_DIR = Path(__file__).resolve().parent
34
+ if str(_SCRIPT_DIR) not in sys.path:
35
+ sys.path.insert(0, str(_SCRIPT_DIR))
36
+
37
+ import merge as merge_mod # noqa: E402
38
+ import quality as quality # noqa: E402
39
+ import runtime as rt # noqa: E402
40
+ import safety as safety # noqa: E402
41
+ import webbridge_adapter as wb # noqa: E402
42
+
43
+ # argo 核心 scripts:query_enhance 归一化(词形规范化,保留平台语法);核心不可用时降级
44
+ # 注意:parents[0]=scripts、parents[1]=ego-search、parents[2]=sub-skills、
45
+ # parents[3]=argo 根。勿用 parents[2](会指向 sub-skills/scripts,不存在 → 静默失效)。
46
+ _CORE_SCRIPTS = Path(__file__).resolve().parents[3] / "scripts"
47
+ if str(_CORE_SCRIPTS) not in sys.path and _CORE_SCRIPTS.exists():
48
+ sys.path.insert(0, str(_CORE_SCRIPTS))
49
+ try:
50
+ from query_enhance import normalize_query # noqa: E402
51
+ except Exception: # 核心包缺失时保持原 query
52
+ normalize_query = None
53
+
54
+
55
+ def _normalized_query(q: str) -> str:
56
+ """词形规范化(全角→半角、拆斜杠、压空格);保留 from:/site: 等平台语法;失败回退原 query。"""
57
+ if normalize_query is None or not isinstance(q, str):
58
+ return q
59
+ try:
60
+ return normalize_query(q)
61
+ except Exception:
62
+ return q
63
+
64
+ # ── 常量 ──────────────────────────────────────────────────────────────
65
+ EGO_BIN = rt.EGO_BIN
66
+ RESULT_MARKER = "EGO_RESULT|"
67
+ SOURCE = "ego-browser"
68
+
69
+ # SEARCH_URLS / SERP 与正文提取选择器唯一来源 = serp_spec(ego 与 webbridge 共用)
70
+ from serp_spec import (
71
+ SEARCH_URLS,
72
+ BODY_EXTRACT_TEMPLATE as BODY_EXTRACT_IIFE,
73
+ build_serp_js as _build_serp_js,
74
+ build_body_js as _build_body_js,
75
+ )
76
+
77
+ DEFAULT_TIMEOUT = 120 # 秒
78
+ API_DATA_LIMIT = 100_000 # 字符;超限截断并标记
79
+
80
+ # 专业搜索模式状态文件(默认 $XDG_STATE_HOME/ego-search/pro-mode.json,
81
+ # XDG 未设时回落 ~/.local/state/;可用环境变量 EGO_SEARCH_STATE 覆盖,测试用)
82
+ _STATE_HOME = os.environ.get("XDG_STATE_HOME", "").strip() or os.path.join(
83
+ os.path.expanduser("~"), ".local", "state"
84
+ )
85
+ DEFAULT_STATE_PATH = os.path.join(_STATE_HOME, "ego-search", "pro-mode.json")
86
+ STATE_PATH = os.environ.get("EGO_SEARCH_STATE") or DEFAULT_STATE_PATH
87
+
88
+ # 登录态 provenance 字段(公共 SearchCache 必须拒绝此类载荷)
89
+ LOGIN_PROVENANCE = {
90
+ "login_state_used": True,
91
+ "auth_partition": "login",
92
+ "cache_eligible": False,
93
+ }
94
+
95
+
96
+ # ── 通用工具 ──────────────────────────────────────────────────────────
97
+ def log(msg: str) -> None:
98
+ sys.stderr.write(msg + "\n")
99
+ sys.stderr.flush()
100
+
101
+
102
+ def stamp_login_provenance(payload: dict[str, Any], *, runtime: str | None = None) -> dict[str, Any]:
103
+ """打上登录态 provenance;不覆盖调用方已显式设置的同名字段。"""
104
+ if not isinstance(payload, dict):
105
+ return payload
106
+ for k, v in LOGIN_PROVENANCE.items():
107
+ payload.setdefault(k, v)
108
+ if runtime:
109
+ payload.setdefault("runtime", runtime)
110
+ if runtime == "webbridge":
111
+ payload.setdefault("source", "webbridge")
112
+ else:
113
+ payload.setdefault("source", SOURCE)
114
+ else:
115
+ payload.setdefault("source", payload.get("source") or SOURCE)
116
+ # 融合提示:可与常规 public 检索在分析层合并,不可进公共缓存
117
+ payload.setdefault("search_partition", "login")
118
+ payload.setdefault("merge_with_public_ok", True)
119
+ return payload
120
+
121
+
122
+ def emit_json(payload: dict[str, Any], *, runtime: str | None = None) -> None:
123
+ """stdout 输出已打 provenance + 质量信号的 JSON。"""
124
+ p = stamp_login_provenance(payload, runtime=runtime)
125
+ # fetch / act 正文挂 quality(登录墙/空页)
126
+ if p.get("content") is not None or p.get("detail") is not None:
127
+ p = quality.assess_body(p)
128
+ print(json.dumps(p, ensure_ascii=False, indent=2))
129
+
130
+
131
+ def apply_site_space(args: argparse.Namespace) -> None:
132
+ """--site example.com → 粘性 task-space,利于登录态保温。"""
133
+ site = getattr(args, "site", None)
134
+ if not site:
135
+ return
136
+ host = site.strip().lower()
137
+ host = host.removeprefix("https://").removeprefix("http://").split("/")[0]
138
+ if host:
139
+ # 用户未改默认 task-space 时才覆盖
140
+ if getattr(args, "task_space", None) in (None, "ego-search"):
141
+ args.task_space = f"site:{host}"
142
+ if not getattr(args, "keep_space", False):
143
+ # 站点粘性默认保温;用户可仍显式不加 --keep-space 若想关
144
+ # 仅当 --site 时默认 keep(登录稳定)
145
+ args.keep_space = True
146
+
147
+
148
+ def _pick_runtime(args: argparse.Namespace) -> str:
149
+ explicit = getattr(args, "runtime", None) or "auto"
150
+ name = rt.resolve_runtime(explicit)
151
+ if name == "none":
152
+ info = rt.detect_runtimes(try_start_webbridge=True)
153
+ hints = "\n".join(f" - {h}" for h in info.get("install_hints") or [])
154
+ sys.exit(
155
+ "错误: 无可用登录态运行时(ego lite 与 WebBridge 均不可用)。\n"
156
+ f"{hints}"
157
+ )
158
+ return name
159
+
160
+ def build_js(template: str, **kwargs: Any) -> str:
161
+ """把 JS 模板里的 %%KEY%% 占位符替换为实际值(避免与 JS 大括号冲突)。"""
162
+ for k, v in kwargs.items():
163
+ template = template.replace("%%" + k + "%%", str(v))
164
+ return template
165
+
166
+
167
+ def run_ego(js_script: str, timeout: int = DEFAULT_TIMEOUT) -> dict:
168
+ """执行 ego-browser nodejs -e 脚本,返回 {ok, payload, error}。"""
169
+ cmd = [EGO_BIN, "nodejs", "-e", js_script]
170
+ try:
171
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout,
172
+ encoding="utf-8", errors="replace")
173
+ except FileNotFoundError:
174
+ return {
175
+ "ok": False,
176
+ "error": "未找到 ego-browser 命令,请先安装浏览器运行时(见 references/install.md)",
177
+ }
178
+ except subprocess.TimeoutExpired:
179
+ return {"ok": False, "error": f"ego-browser 执行超时(>{timeout}s)"}
180
+
181
+ if proc.returncode != 0:
182
+ return {
183
+ "ok": False,
184
+ "error": f"ego-browser 退出码 {proc.returncode}: {proc.stderr[-800:]}",
185
+ }
186
+
187
+ # cliLog 输出在 stderr,取最后一个 EGO_RESULT| 标记行
188
+ payload = None
189
+ for line in reversed(proc.stderr.splitlines()):
190
+ line = line.strip()
191
+ if line.startswith(RESULT_MARKER):
192
+ raw = line[len(RESULT_MARKER):]
193
+ try:
194
+ payload = json.loads(raw)
195
+ except json.JSONDecodeError:
196
+ payload = {"raw": raw}
197
+ break
198
+ if payload is None:
199
+ return {
200
+ "ok": False,
201
+ "error": f"未解析到结果标记,stderr={proc.stderr[-800:]}",
202
+ }
203
+ return {"ok": True, "payload": payload}
204
+
205
+
206
+ # ── JS 模板(单源:SERP / BODY 唯一来源见 serp_spec,search·fetch·act 组合)──
207
+ # 转义纪律:页面内选择器写死在模板;Node 层只注入纯安全占位符。
208
+
209
+ FOCUS_FILTER = r"""
210
+ const kw = '%%FOCUS%%'.toLowerCase();
211
+ if (kw) {
212
+ const paras = d.content.split(/\n\n+/);
213
+ const hit = paras.filter(p => p.toLowerCase().includes(kw));
214
+ body = hit.length ? hit.join('\n\n') : d.content.slice(0, 8000);
215
+ if (body.length < 200) body = d.content.slice(0, 8000);
216
+ }
217
+ """
218
+
219
+ # 收尾:默认关掉任务空间,避免标签堆积导致登录态漂移;%%KEEP_SPACE%% 为 true 时保留
220
+ JS_FINISH = r"""
221
+ try {
222
+ if (%%KEEP_SPACE%%) {
223
+ /* keep space warm for multi-round login stability */
224
+ } else {
225
+ await completeTaskSpace('%%TASK_SPACE%%', { keep: false })
226
+ }
227
+ } catch (e) { /* space already gone */ }
228
+ """
229
+
230
+ JS_SEARCH = r"""
231
+ const task = await useOrCreateTaskSpace('%%TASK_SPACE%%')
232
+ await openOrReuseTab('%%URL%%', { wait: true, timeout: 25 })
233
+ const info = await pageInfo()
234
+ const serp = await js(String.raw`%%SERP_IIFE%%`)
235
+ const parsed = JSON.parse(serp)
236
+ cliLog('EGO_RESULT|' + JSON.stringify({
237
+ url: info.url, title: info.title, results: parsed,
238
+ task_space: '%%TASK_SPACE%%', task_id: task && task.id,
239
+ }))
240
+ """ + JS_FINISH
241
+
242
+ # SERP 空结果重抽(不重新导航,任务空间里页面已打开):
243
+ # SERP 是 JS 注入的,load 事件早于渲染,首抽为空≠真没结果。
244
+ JS_SERP_REEXTRACT = r"""
245
+ const task = await useOrCreateTaskSpace('%%TASK_SPACE%%')
246
+ const info = await pageInfo()
247
+ const serp = await js(String.raw`%%SERP_IIFE%%`)
248
+ const parsed = JSON.parse(serp)
249
+ cliLog('EGO_RESULT|' + JSON.stringify({
250
+ url: info.url, title: info.title, results: parsed,
251
+ task_space: '%%TASK_SPACE%%', task_id: task && task.id,
252
+ }))
253
+ """ + JS_FINISH
254
+
255
+ JS_EXTRACT = r"""
256
+ const task = await useOrCreateTaskSpace('%%TASK_SPACE%%')
257
+ await openOrReuseTab('%%URL%%', { wait: true, timeout: 25 })
258
+ const info = await pageInfo()
259
+ const raw = await js(`%%BODY_IIFE%%`)
260
+ const d = JSON.parse(raw)
261
+ let body = d.content.replace(/\n{3,}/g, '\n\n').trim()
262
+ %%FOCUS_FILTER%%
263
+ cliLog('EGO_RESULT|' + JSON.stringify({
264
+ url: info.url, title: d.title,
265
+ content: body, word_count: body.split(/\s+/).length,
266
+ fetch_method: 'browser',
267
+ task_space: '%%TASK_SPACE%%', task_id: task && task.id,
268
+ }))
269
+ """ + JS_FINISH
270
+
271
+ # act:SERP 取 1 条 → 打开首条 → 正文(CONTENT_MAX 略小于 fetch)
272
+ JS_ACT = r"""
273
+ const task = await useOrCreateTaskSpace('%%TASK_SPACE%%')
274
+ await openOrReuseTab('%%URL%%', { wait: true, timeout: 25 })
275
+ const info = await pageInfo()
276
+ const serp = await js(String.raw`%%SERP_IIFE%%`)
277
+ const parsed = JSON.parse(serp)
278
+ let detail = null
279
+ if (parsed.length && parsed[0].url) {
280
+ await openOrReuseTab(parsed[0].url, { wait: true, timeout: 25 })
281
+ const rawd = await js(`%%BODY_IIFE%%`)
282
+ detail = JSON.parse(rawd)
283
+ detail.content = (detail.content || '').replace(/\n{3,}/g, '\n\n').trim()
284
+ }
285
+ cliLog('EGO_RESULT|' + JSON.stringify({
286
+ query: '%%QUERY%%',
287
+ url: info.url,
288
+ results: parsed,
289
+ detail: detail,
290
+ task_space: '%%TASK_SPACE%%', task_id: task && task.id,
291
+ }))
292
+ """ + JS_FINISH
293
+
294
+ # api:主动同源请求(非被动 network 旁路)
295
+ JS_API = r"""
296
+ const task = await useOrCreateTaskSpace('%%TASK_SPACE%%')
297
+ await openOrReuseTab('%%ORIGIN%%', { wait: true, timeout: 25 })
298
+ const info = await pageInfo()
299
+ let data = null, dataType = 'unknown'
300
+ try {
301
+ const r = await browserFetch('%%API_URL%%', {})
302
+ if (typeof r === 'string') {
303
+ data = r; dataType = 'text'
304
+ try { data = JSON.parse(r); dataType = 'json' } catch (e) {}
305
+ } else {
306
+ data = r; dataType = 'json'
307
+ }
308
+ } catch (e) {
309
+ dataType = 'error'
310
+ data = String(e).slice(0, 300)
311
+ }
312
+ cliLog('EGO_RESULT|' + JSON.stringify({
313
+ api_url: '%%API_URL%%',
314
+ page_url: info.url,
315
+ page_title: info.title,
316
+ data: data,
317
+ data_type: dataType,
318
+ fetch_method: 'browser_api',
319
+ task_space: '%%TASK_SPACE%%', task_id: task && task.id,
320
+ }))
321
+ """ + JS_FINISH
322
+
323
+ def _serp_iife(engine: str, n: int) -> str:
324
+ return _build_serp_js(engine, n)
325
+
326
+
327
+ def _body_iife(content_max: int) -> str:
328
+ return _build_body_js(content_max)
329
+
330
+
331
+ # ── 模式实现 ──────────────────────────────────────────────────────────
332
+ def _keep_js(args: argparse.Namespace) -> str:
333
+ """JS 布尔:是否保留任务空间(登录态保温)。"""
334
+ return "true" if getattr(args, "keep_space", False) else "false"
335
+
336
+
337
+ def _require_url(url: str, context: str) -> str:
338
+ v = safety.validate_browser_url(url, context=context)
339
+ if not v.get("ok"):
340
+ sys.exit(f"错误: {v.get('error')}")
341
+ return v["url"]
342
+
343
+
344
+ def run_with_fallback(
345
+ args: argparse.Namespace,
346
+ *,
347
+ ego_fn,
348
+ wb_fn,
349
+ label: str,
350
+ ) -> tuple[dict[str, Any], str]:
351
+ """统一双运行时:ego 优先 + auto 降级 WebBridge(删掉各 cmd 重复分支)。"""
352
+ runtime = _pick_runtime(args)
353
+ if runtime == "webbridge":
354
+ return wb_fn(), "webbridge"
355
+
356
+ r = ego_fn()
357
+ auto = (getattr(args, "runtime", "auto") or "auto") == "auto"
358
+ if not r.get("ok") and auto and rt.webbridge_available(try_start=True).get("available"):
359
+ log(f"ego 失败,降级 WebBridge ({label}): {r.get('error')}")
360
+ return wb_fn(), "webbridge"
361
+ return r, "ego"
362
+
363
+
364
+ def _search_ego(args: argparse.Namespace) -> dict[str, Any]:
365
+ q = urllib.parse.quote(_normalized_query(args.query))
366
+ url = SEARCH_URLS[args.engine].format(q=q)
367
+ # 时间窗:URL 参数下推(google cdr / bing age-lt / baidu gpc)+ 解析后过滤保底
368
+ tparams = wb.time_url_params(args.engine, getattr(args, "since", None), getattr(args, "until", None))
369
+ if tparams:
370
+ sep = "&" if "?" in url else "?"
371
+ url += sep + urllib.parse.urlencode(tparams)
372
+ js = build_js(
373
+ JS_SEARCH,
374
+ TASK_SPACE=args.task_space,
375
+ URL=url,
376
+ SERP_IIFE=_serp_iife(args.engine, args.n),
377
+ KEEP_SPACE=_keep_js(args),
378
+ )
379
+ r = run_ego(js, args.timeout)
380
+ if not r["ok"]:
381
+ return r
382
+ p = r["payload"]
383
+ results = p.get("results", [])
384
+ if not results:
385
+ # 空结果退避重抽(1s/2s + jitter):SERP 渲染晚于 load,页面上
386
+ # 等一等再抽。只重跑提取不重新导航;仍为空则标 selector_stale——
387
+ # 调用方要能区分「真没结果」与「选择器/渲染失效」。
388
+ import random
389
+ import time as _t
390
+ for _delay in (1.0, 2.0):
391
+ _t.sleep(_delay + random.uniform(0, 0.3))
392
+ re_r = run_ego(
393
+ build_js(
394
+ JS_SERP_REEXTRACT,
395
+ TASK_SPACE=args.task_space,
396
+ SERP_IIFE=_serp_iife(args.engine, args.n),
397
+ ),
398
+ args.timeout,
399
+ )
400
+ if re_r.get("ok") and re_r["payload"].get("results"):
401
+ p = re_r["payload"]
402
+ results = p.get("results", [])
403
+ break
404
+ if not results:
405
+ p["selector_stale"] = True
406
+ since = getattr(args, "since", None)
407
+ until = getattr(args, "until", None)
408
+ if since or until:
409
+ results = wb.filter_window(results, since, until)
410
+ return {
411
+ "ok": True,
412
+ "payload": {
413
+ "query": args.query,
414
+ "engine": f"ego_browser_{args.engine}",
415
+ "source": SOURCE,
416
+ "runtime": "ego",
417
+ "url": p.get("url"),
418
+ "page_title": p.get("title"),
419
+ "results": results,
420
+ "count": len(results),
421
+ "fetch_method": "browser",
422
+ "task_space": p.get("task_space") or args.task_space,
423
+ "task_id": p.get("task_id"),
424
+ "space_kept": bool(getattr(args, "keep_space", False)),
425
+ },
426
+ }
427
+
428
+
429
+ def cmd_search(args: argparse.Namespace) -> None:
430
+ apply_site_space(args)
431
+
432
+ def wb_fn():
433
+ return wb.search(
434
+ _normalized_query(args.query), engine=args.engine, n=args.n,
435
+ session=args.task_space, timeout=args.timeout,
436
+ since=getattr(args, "since", None), until=getattr(args, "until", None),
437
+ )
438
+
439
+ r, runtime = run_with_fallback(args, ego_fn=lambda: _search_ego(args), wb_fn=wb_fn, label="search")
440
+ if not r.get("ok"):
441
+ sys.exit(f"错误: {r.get('error')}")
442
+ emit_json(r["payload"], runtime=runtime)
443
+
444
+
445
+ def cmd_fetch(args: argparse.Namespace) -> None:
446
+ apply_site_space(args)
447
+ args.url = _require_url(args.url, "fetch")
448
+
449
+ def ego_fn():
450
+ focus = (
451
+ build_js(FOCUS_FILTER, FOCUS=args.focus or "") if args.focus else ""
452
+ )
453
+ js = build_js(
454
+ JS_EXTRACT,
455
+ TASK_SPACE=args.task_space,
456
+ URL=args.url,
457
+ BODY_IIFE=_body_iife(60_000),
458
+ FOCUS_FILTER=focus,
459
+ KEEP_SPACE=_keep_js(args),
460
+ )
461
+ r = run_ego(js, args.timeout)
462
+ if not r.get("ok"):
463
+ return r
464
+ p = r["payload"]
465
+ p.setdefault("runtime", "ego")
466
+ p["space_kept"] = bool(getattr(args, "keep_space", False))
467
+ return {"ok": True, "payload": p}
468
+
469
+ def wb_fn():
470
+ return wb.fetch(
471
+ args.url, focus=args.focus,
472
+ session=args.task_space, timeout=args.timeout,
473
+ )
474
+
475
+ r, runtime = run_with_fallback(args, ego_fn=ego_fn, wb_fn=wb_fn, label="fetch")
476
+ if not r.get("ok"):
477
+ sys.exit(f"错误: {r.get('error')}")
478
+ emit_json(r["payload"], runtime=runtime)
479
+
480
+
481
+ def cmd_act(args: argparse.Namespace) -> None:
482
+ """act:优先 ego 链式;WebBridge 路径 = search + 打开首条 fetch。"""
483
+ apply_site_space(args)
484
+
485
+ def ego_fn():
486
+ q = urllib.parse.quote(_normalized_query(args.query))
487
+ url = SEARCH_URLS[args.engine].format(q=q)
488
+ # 时间窗:URL 参数下推 + 解析后过滤(与 search 一致)
489
+ tparams = wb.time_url_params(args.engine, getattr(args, "since", None), getattr(args, "until", None))
490
+ if tparams:
491
+ sep = "&" if "?" in url else "?"
492
+ url += sep + urllib.parse.urlencode(tparams)
493
+ js = build_js(
494
+ JS_ACT,
495
+ TASK_SPACE=args.task_space,
496
+ URL=url,
497
+ QUERY=args.query,
498
+ # 时间窗时多取候选(首条可能无日期/窗口外),过滤后取第一条打开
499
+ SERP_IIFE=_serp_iife(args.engine, 5 if (getattr(args, "since", None) or getattr(args, "until", None)) else 1),
500
+ BODY_IIFE=_body_iife(30_000),
501
+ KEEP_SPACE=_keep_js(args),
502
+ )
503
+ r = run_ego(js, args.timeout)
504
+ if not r.get("ok"):
505
+ return r
506
+ p = r["payload"]
507
+ since = getattr(args, "since", None)
508
+ until = getattr(args, "until", None)
509
+ if since or until:
510
+ results = wb.filter_window(p.get("results") or [], since, until)
511
+ # detail 若对应被过滤掉的首条则丢弃,避免结果与详情不一致
512
+ detail = p.get("detail")
513
+ if detail:
514
+ d_url = (detail.get("url") or "") if isinstance(detail, dict) else ""
515
+ if not results or not d_url or d_url != results[0].get("url"):
516
+ p["detail"] = None
517
+ p["results"] = results
518
+ p.setdefault("engine", f"ego_browser_{args.engine}")
519
+ p.setdefault("runtime", "ego")
520
+ p["space_kept"] = bool(getattr(args, "keep_space", False))
521
+ return {"ok": True, "payload": p}
522
+
523
+ def wb_fn():
524
+ sr = wb.search(
525
+ _normalized_query(args.query), engine=args.engine, n=1,
526
+ session=args.task_space, timeout=args.timeout,
527
+ since=getattr(args, "since", None), until=getattr(args, "until", None),
528
+ )
529
+ if not sr.get("ok"):
530
+ return sr
531
+ results = (sr.get("payload") or {}).get("results") or []
532
+ detail = None
533
+ if results and results[0].get("url"):
534
+ first = results[0]["url"]
535
+ v = safety.validate_browser_url(first, context="act_result")
536
+ if v.get("ok"):
537
+ fr = wb.fetch(
538
+ v["url"], session=args.task_space, timeout=args.timeout,
539
+ )
540
+ if fr.get("ok"):
541
+ detail = fr["payload"]
542
+ else:
543
+ detail = {"error": v.get("error"), "url": first}
544
+ return {
545
+ "ok": True,
546
+ "payload": {
547
+ "query": args.query,
548
+ "engine": f"webbridge_{args.engine}",
549
+ "results": results,
550
+ "detail": detail,
551
+ "runtime": "webbridge",
552
+ "source": "webbridge",
553
+ },
554
+ }
555
+
556
+ r, runtime = run_with_fallback(args, ego_fn=ego_fn, wb_fn=wb_fn, label="act")
557
+ if not r.get("ok"):
558
+ sys.exit(f"错误: {r.get('error')}")
559
+ emit_json(r["payload"], runtime=runtime)
560
+
561
+
562
+ def _origin_from_url(url: str) -> str:
563
+ """从 API URL 提取默认 origin(scheme://host),跨子域场景须显式 --origin。"""
564
+ p = urllib.parse.urlparse(url)
565
+ return f"{p.scheme}://{p.netloc}"
566
+
567
+
568
+ def cmd_api(args: argparse.Namespace) -> None:
569
+ apply_site_space(args)
570
+ args.api_url = _require_url(args.api_url, "api")
571
+ origin = args.origin or _origin_from_url(args.api_url)
572
+ origin = _require_url(origin, "api_origin")
573
+
574
+ def ego_fn():
575
+ js = build_js(
576
+ JS_API,
577
+ TASK_SPACE=args.task_space,
578
+ ORIGIN=origin,
579
+ API_URL=args.api_url,
580
+ KEEP_SPACE=_keep_js(args),
581
+ )
582
+ r = run_ego(js, args.timeout)
583
+ if not r.get("ok"):
584
+ return r
585
+ p = r["payload"]
586
+ p.setdefault("runtime", "ego")
587
+ p["space_kept"] = bool(getattr(args, "keep_space", False))
588
+ return {"ok": True, "payload": p}
589
+
590
+ def wb_fn():
591
+ return wb.api(
592
+ args.api_url, origin=origin,
593
+ session=args.task_space, timeout=args.timeout,
594
+ )
595
+
596
+ r, runtime = run_with_fallback(args, ego_fn=ego_fn, wb_fn=wb_fn, label="api")
597
+ if not r.get("ok"):
598
+ sys.exit(f"错误: {r.get('error')}")
599
+ p = r["payload"]
600
+ data = p.get("data")
601
+ if data is not None:
602
+ s = json.dumps(data, ensure_ascii=False, separators=(",", ":"))
603
+ if len(s) > API_DATA_LIMIT:
604
+ p["data"] = s[:API_DATA_LIMIT] + "…[truncated]"
605
+ p["data_type"] = p.get("data_type", "json") + "_truncated"
606
+ p["truncated"] = True
607
+ emit_json(p, runtime=runtime)
608
+
609
+
610
+ # ── 专业搜索模式(默认关闭)──────────────────────────────────────────
611
+ def _load_state() -> dict:
612
+ """读取模式状态;文件缺失/损坏一律视为关闭。"""
613
+ try:
614
+ with open(STATE_PATH, encoding="utf-8") as f:
615
+ state = json.load(f)
616
+ return state if isinstance(state, dict) else {}
617
+ except (OSError, json.JSONDecodeError):
618
+ return {}
619
+
620
+
621
+ def _save_state(enabled: bool) -> None:
622
+ """持久化模式状态。"""
623
+ os.makedirs(os.path.dirname(STATE_PATH), exist_ok=True)
624
+ state: dict[str, Any] = {"enabled": enabled}
625
+ if enabled:
626
+ state["enabled_at"] = datetime.now().isoformat(timespec="seconds")
627
+ with open(STATE_PATH, "w", encoding="utf-8") as f:
628
+ json.dump(state, f, ensure_ascii=False, indent=2)
629
+
630
+
631
+ def mode_enabled() -> bool:
632
+ """专业搜索模式是否开启(默认关闭)。"""
633
+ return bool(_load_state().get("enabled", False))
634
+
635
+
636
+ def mode_gate_blocked(mode: str) -> str | None:
637
+ """闸门:搜索类模式未开启时返回拒绝提示,其余模式返回 None。"""
638
+ if mode in ("search", "fetch", "act", "api") and not mode_enabled():
639
+ return (
640
+ "专业搜索模式未开启(默认关闭,保障登录态安全)。"
641
+ "请先运行: python3 sub-skills/ego-search/scripts/ego_search.py enable"
642
+ )
643
+ return None
644
+
645
+
646
+ def cmd_enable(args: argparse.Namespace) -> None:
647
+ if mode_enabled():
648
+ print("专业搜索模式已开启。")
649
+ return
650
+ _save_state(True)
651
+ print(
652
+ "专业搜索模式已开启:登录态专业搜索可用(ego lite 与/或 WebBridge,"
653
+ "auto 择优),状态已持久化。"
654
+ )
655
+ log("提示:仅用于用户已授权的查询目标;登录态结果不进公共缓存。")
656
+
657
+
658
+ def cmd_disable(args: argparse.Namespace) -> None:
659
+ if not mode_enabled():
660
+ print("专业搜索模式当前未开启。")
661
+ return
662
+ _save_state(False)
663
+ print("专业搜索模式已关闭:登录态专业搜索已禁用。")
664
+
665
+
666
+ def cmd_status(args: argparse.Namespace) -> None:
667
+ try_start = bool(getattr(args, "fix", False))
668
+ runtimes = rt.detect_runtimes(try_start_webbridge=try_start)
669
+ print(json.dumps(
670
+ {
671
+ "pro_mode": mode_enabled(),
672
+ "state_file": STATE_PATH,
673
+ "architecture": "dual_runtime_complete",
674
+ "version": "1.6.0",
675
+ "runtimes": runtimes,
676
+ "isolation": {
677
+ "public_search_cache": "~/.cache/unified-search/cache.db",
678
+ "login_partition": "cache_eligible=false / SearchCache 硬拒绝",
679
+ "merge_at_analysis": True,
680
+ "merge_cli": "merge --public a.json --login b.json",
681
+ },
682
+ "login_stability": {
683
+ "default_close_space": True,
684
+ "keep_space_flag": "--keep-space",
685
+ "site_sticky": "--site example.com → task-space site:example.com + keep",
686
+ },
687
+ },
688
+ ensure_ascii=False, indent=2,
689
+ ))
690
+
691
+
692
+ def cmd_merge(args: argparse.Namespace) -> None:
693
+ """分析层融合:不经过专业模式闸门(只读本地 JSON)。"""
694
+ public = merge_mod.load_json_file(args.public)
695
+ login = merge_mod.load_json_file(args.login)
696
+ out = merge_mod.merge_payloads(public, login, query=args.query)
697
+ print(json.dumps(out, ensure_ascii=False, indent=2))
698
+
699
+
700
+ # ── CLI ───────────────────────────────────────────────────────────────
701
+ def main() -> None:
702
+ ap = argparse.ArgumentParser(
703
+ prog="ego_search.py",
704
+ description=(
705
+ "argo 子技能:登录态专业搜索(ego lite + WebBridge 双运行时;"
706
+ "专业搜索模式默认关闭)。"
707
+ ),
708
+ )
709
+ sub = ap.add_subparsers(dest="mode", required=True)
710
+
711
+ p_enable = sub.add_parser("enable", help="开启专业搜索模式(需用户明确要求)")
712
+ p_enable.set_defaults(fn=cmd_enable)
713
+ p_disable = sub.add_parser("disable", help="关闭专业搜索模式")
714
+ p_disable.set_defaults(fn=cmd_disable)
715
+ p_status = sub.add_parser("status", help="专业模式 + 双运行时探测")
716
+ p_status.add_argument(
717
+ "--fix", action="store_true",
718
+ help="探测前幂等尝试启动 WebBridge 本地桥(永不 stop)",
719
+ )
720
+ p_status.set_defaults(fn=cmd_status)
721
+
722
+ p_merge = sub.add_parser(
723
+ "merge",
724
+ help="融合 public 常规检索 JSON + login 专业检索 JSON(分析层,不写缓存)",
725
+ )
726
+ p_merge.add_argument("--public", required=True, help="常规检索结果 JSON 文件")
727
+ p_merge.add_argument("--login", required=True, help="ego-search 登录态结果 JSON 文件")
728
+ p_merge.add_argument("--query", default=None, help="可选查询词覆盖")
729
+ p_merge.set_defaults(fn=cmd_merge)
730
+
731
+ p_search = sub.add_parser("search", help="浏览器态搜索,输出 argo JSON schema")
732
+ p_search.add_argument("query", help="查询词")
733
+ p_search.add_argument(
734
+ "--engine", choices=list(SEARCH_URLS), default="bing",
735
+ help="搜索引擎(默认 bing;baidu 中文,google 可能弹验证)",
736
+ )
737
+ p_search.add_argument("--n", type=int, default=8, help="结果条数(默认 8)")
738
+ p_search.add_argument("--since", default=None,
739
+ help="发布时间下限(7d / 2026-08-01):SERP URL 时间筛选 + 解析后过滤")
740
+ p_search.add_argument("--until", default=None,
741
+ help="发布时间上限(7d / 2026-08-01):SERP URL 时间筛选 + 解析后过滤")
742
+ p_search.set_defaults(fn=cmd_search)
743
+
744
+ p_fetch = sub.add_parser("fetch", help="浏览器态正文提取(JS 渲染/反爬/登录墙页面)")
745
+ p_fetch.add_argument("url", help="目标 URL")
746
+ p_fetch.add_argument("--focus", help="只返回包含该关键词的段落")
747
+ p_fetch.set_defaults(fn=cmd_fetch)
748
+
749
+ p_act = sub.add_parser("act", help="搜索→点开第一个结果→抓正文(一键链式)")
750
+ p_act.add_argument("query", help="查询词")
751
+ p_act.add_argument(
752
+ "--engine", choices=list(SEARCH_URLS), default="bing",
753
+ help="搜索引擎(默认 bing;与 search 共用)",
754
+ )
755
+ p_act.add_argument("--since", default=None,
756
+ help="发布时间下限(7d / 2026-08-01):SERP URL 时间筛选 + 解析后过滤")
757
+ p_act.add_argument("--until", default=None,
758
+ help="发布时间上限(7d / 2026-08-01):SERP URL 时间筛选 + 解析后过滤")
759
+ p_act.set_defaults(fn=cmd_act)
760
+
761
+ p_api = sub.add_parser("api", help="浏览器上下文数据直取(同源 API,继承登录态)")
762
+ p_api.add_argument("api_url", help="API 完整 URL(须与 --origin 同源)")
763
+ p_api.add_argument("--origin", help="登录态上下文页面 URL(默认取 API URL 同源)")
764
+ p_api.set_defaults(fn=cmd_api)
765
+
766
+ for p in (p_search, p_fetch, p_act, p_api):
767
+ p.add_argument(
768
+ "--task-space", default="ego-search",
769
+ help="任务空间名 / WebBridge session(默认 ego-search;同站复用可稳登录态)",
770
+ )
771
+ p.add_argument(
772
+ "--timeout", type=int, default=DEFAULT_TIMEOUT,
773
+ help=f"超时秒数(默认 {DEFAULT_TIMEOUT})",
774
+ )
775
+ p.add_argument(
776
+ "--runtime", choices=["auto", "ego", "webbridge"], default="auto",
777
+ help="运行时:auto=有 ego 用 ego 否则 WebBridge(默认 auto)",
778
+ )
779
+ p.add_argument(
780
+ "--keep-space", action="store_true",
781
+ help="保留任务空间不关闭(多轮保温登录态;默认关闭空间防标签堆积)",
782
+ )
783
+ p.add_argument(
784
+ "--site", default=None,
785
+ help="粘性站点主机名(如 zhihu.com)→ task-space=site:host 并默认 keep-space",
786
+ )
787
+
788
+ args = ap.parse_args()
789
+
790
+ # merge 只读本地文件,不涉及浏览器登录态闸门
791
+ if args.mode != "merge":
792
+ blocked = mode_gate_blocked(args.mode)
793
+ if blocked:
794
+ sys.exit(blocked)
795
+
796
+ args.fn(args)
797
+
798
+
799
+ if __name__ == "__main__":
800
+ main()