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,323 @@
1
+ #!/usr/bin/env python3
2
+ """browser_auth.py — argo 自持浏览器登录态(持久 profile,A2 车道)
3
+
4
+ 落地方案:docs-local/连接器能力接入_方案调研_2026-09-26.md §4 方案 A 的 A2。
5
+
6
+ 模型(与既有凭据纪律对齐):
7
+ - profile 是 argo 自持的独立 Chromium profile(不是用户主 Chrome),
8
+ 落在状态根 browser-profiles/<site>/。
9
+ - cookie 只存在于 Chrome 自己的存储里(macOS Keychain / Windows DPAPI
10
+ 加密),argo 全程不读取、不导出 cookie 值——比导出 state 文件再加密
11
+ (agent-browser --session 的做法)少一类落盘凭据。
12
+ - 抓取走 chrome_cdp 的「页面内带凭证 fetch」(fetch_json):cookie 由
13
+ 浏览器附加,argo 只收响应体。
14
+ - 登录态结果标 login_state_used=True / cache_eligible=False,
15
+ cache.assert_cacheable 据此拒绝进公共缓存。
16
+
17
+ CLI(bin/argo 的 auth 子命令):
18
+ argo auth login <url> 开可见窗口登录/扫码一次(Ctrl-C 放弃)
19
+ argo auth status 列出已登录站点
20
+ argo auth logout <site> 删除该站点 profile
21
+
22
+ 开关:ARGO_FETCH_AUTH=0 关闭抓取侧登录态车道;auth login/status/logout
23
+ 不受它管(用户显式管理自己的 profile 永远可用)。
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ import os
30
+ import re
31
+ import shutil
32
+ import sys
33
+ import time
34
+ from pathlib import Path
35
+
36
+ ENV_AUTH_FETCH = "ARGO_AUTH_FETCH"
37
+
38
+ # 备名:与抓取降级链分级开关家族(ARGO_FETCH_*)保持同族命名
39
+ ENV_AUTH_FETCH_ALIAS = "ARGO_FETCH_AUTH"
40
+
41
+ _HOST_RE = re.compile(r"[^a-z0-9.-]")
42
+
43
+
44
+ def auth_fetch_enabled() -> bool:
45
+ """抓取侧登录态车道开关:ARGO_AUTH_FETCH / ARGO_FETCH_AUTH=0 关闭,默认开启。"""
46
+ for name in (ENV_AUTH_FETCH, ENV_AUTH_FETCH_ALIAS):
47
+ try:
48
+ from engine_env import get_env
49
+ val = get_env(name)
50
+ except ImportError:
51
+ val = os.environ.get(name, "")
52
+ if str(val).strip().lower() in ("0", "false", "no", "off"):
53
+ return False
54
+ if str(val).strip():
55
+ return True
56
+ return True
57
+
58
+
59
+ def profile_root() -> Path:
60
+ """持久 profile 根目录:状态根/browser-profiles(ARGO_STATE_DIR 可整体隔离)。"""
61
+ from argo_paths import state_root
62
+ return state_root() / "browser-profiles"
63
+
64
+
65
+ def site_host(url_or_host: str) -> str:
66
+ """URL 或主机名 → 站点身份(去 scheme/port/path,去 www.,小写)。
67
+
68
+ 返回值直接用作 profile 目录名,所以顺带做文件名安全审查:
69
+ 出现 [^a-z0-9.-](路径穿越、空格、怪字符)或没有点(排除裸词)
70
+ 一律拒绝。localhost 显式放行(本地开发站登录是真实场景)。
71
+ """
72
+ s = (url_or_host or "").strip()
73
+ if "://" in s:
74
+ s = s.split("://", 1)[1]
75
+ s = s.split("/", 1)[0].split("?", 1)[0].split("#", 1)[0]
76
+ if s.endswith(":80") or s.endswith(":443"):
77
+ s = s.rsplit(":", 1)[0]
78
+ if ":" in s: # 带端口(或 IPv6 字面量)→ 取端口前段;IPv6 不支持,报错更诚实
79
+ head = s.split(":", 1)[0]
80
+ if not head:
81
+ raise ValueError(f"unsupported site: {url_or_host!r}")
82
+ s = head
83
+ s = s.lower()
84
+ if s.startswith("www."):
85
+ s = s[4:]
86
+ if not s or _HOST_RE.search(s):
87
+ raise ValueError(f"invalid site: {url_or_host!r}")
88
+ if s.startswith("."): # "." / ".." / ".com":点号打头做目录名就是穿越
89
+ raise ValueError(f"invalid site: {url_or_host!r}")
90
+ if "." not in s and s != "localhost":
91
+ raise ValueError(f"invalid site (no dot): {url_or_host!r}")
92
+ return s
93
+
94
+
95
+ def _profile_dir(host: str) -> Path:
96
+ return profile_root() / host
97
+
98
+
99
+ def _read_meta(d: Path) -> dict:
100
+ try:
101
+ return json.loads((d / "meta.json").read_text(encoding="utf-8"))
102
+ except Exception:
103
+ return {}
104
+
105
+
106
+ def _write_meta(d: Path, host: str, created: str | None = None) -> None:
107
+ """原子写 meta.json:profile 的 argo 登记标记(Chrome 不认识也不在乎它)。"""
108
+ prev = _read_meta(d)
109
+ meta = {
110
+ "site": host,
111
+ "created": created or prev.get("created") or time.strftime("%Y-%m-%dT%H:%M:%S"),
112
+ "last_used": time.strftime("%Y-%m-%dT%H:%M:%S"),
113
+ "argo": "browser-profiles/v1",
114
+ }
115
+ tmp = d / f"meta.json.tmp.{os.getpid()}"
116
+ tmp.write_text(json.dumps(meta, ensure_ascii=False), encoding="utf-8")
117
+ os.replace(tmp, d / "meta.json")
118
+
119
+
120
+ def profile_for(url_or_host: str) -> str | None:
121
+ """该站点的持久 profile 路径;没登录过(或站点名不合法)返回 None。
122
+
123
+ 以 meta.json 存在为准:裸目录可能是上次登录失败的残骸,不能当登录态。
124
+ """
125
+ try:
126
+ host = site_host(url_or_host)
127
+ except ValueError:
128
+ return None
129
+ d = _profile_dir(host)
130
+ if d.is_dir() and (d / "meta.json").is_file():
131
+ return str(d)
132
+ return None
133
+
134
+
135
+ def auth_profile_for_fetch(url_or_host: str,
136
+ allow_browser_lane: bool = True) -> str | None:
137
+ """抓取侧入口:车道让路(开关关 / 调用方禁浏览器 / 未登录)→ None。
138
+
139
+ allow_browser_lane=False 对应 use_browser_fallback=False 的调用方
140
+ (批量爬取、evidence 核验等刻意无浏览器的路径)——登录态车道同样让路,
141
+ 未认证不破默认路径的另一半。
142
+ """
143
+ if not allow_browser_lane or not auth_fetch_enabled():
144
+ return None
145
+ return profile_for(url_or_host)
146
+
147
+
148
+ def login(url_or_host: str, chrome_path: str | None = None,
149
+ confirm=None) -> dict:
150
+ """开可见 Chrome 窗口让用户登录一次,profile 持久保存。
151
+
152
+ confirm:等待登录完成的钩子(None → 终端 input())。可注入以便测试。
153
+ """
154
+ host = site_host(url_or_host)
155
+ d = _profile_dir(host)
156
+ fresh = not d.is_dir()
157
+ d.mkdir(parents=True, exist_ok=True)
158
+
159
+ target = url_or_host.strip()
160
+ if "://" not in target:
161
+ target = "https://" + target
162
+
163
+ from chrome_cdp import _ChromeProcess
164
+ proc = _ChromeProcess(chrome_path=chrome_path, headless=False,
165
+ user_data_dir=str(d), start_url=target)
166
+ try:
167
+ proc.start()
168
+ except Exception as e:
169
+ # 新建 profile 时启动即败(占位 SingletonLock / 磁盘满等)→ 别留裸目录
170
+ if fresh:
171
+ shutil.rmtree(d, ignore_errors=True)
172
+ msg = str(e)
173
+ if "CDP failed to start" in msg or "Singleton" in msg:
174
+ msg += "(该 profile 可能已有 Chrome 在跑:关掉它或先 argo auth logout)"
175
+ return {"ok": False, "site": host, "error": msg[:200]}
176
+
177
+ try:
178
+ if confirm is None:
179
+ print(f"\n[{host}] 已在打开的 Chrome 窗口中登录/扫码。", file=sys.stderr)
180
+ print("完成后回到终端按 Enter 保存登录态(Ctrl-C 放弃):", file=sys.stderr)
181
+ try:
182
+ input()
183
+ except EOFError:
184
+ return {"ok": False, "site": host,
185
+ "error": "非交互终端无法确认登录完成(需要 tty)"}
186
+ else:
187
+ confirm()
188
+ _write_meta(d, host)
189
+ return {"ok": True, "site": host, "profile": str(d)}
190
+ finally:
191
+ try:
192
+ proc.stop()
193
+ except Exception:
194
+ pass
195
+
196
+
197
+ def status() -> list[dict]:
198
+ """已登录站点清单(只读 meta,绝不触碰 profile 内的浏览器存储)。"""
199
+ root = profile_root()
200
+ if not root.is_dir():
201
+ return []
202
+ out = []
203
+ for d in sorted(root.iterdir()):
204
+ if not (d.is_dir() and (d / "meta.json").is_file()):
205
+ continue
206
+ m = _read_meta(d)
207
+ out.append({
208
+ "site": m.get("site") or d.name,
209
+ "created": m.get("created"),
210
+ "last_used": m.get("last_used"),
211
+ "path": str(d),
212
+ })
213
+ return out
214
+
215
+
216
+ def logout(url_or_host: str) -> dict:
217
+ """删除站点 profile。Chrome 还开着该 profile 时拒绝(SingletonLock 探活)。"""
218
+ host = site_host(url_or_host)
219
+ d = _profile_dir(host)
220
+ if not d.is_dir():
221
+ return {"ok": False, "site": host, "error": f"no profile for {host}"}
222
+ lock = d / "SingletonLock"
223
+ if lock.is_symlink():
224
+ # Chrome 的 SingletonLock 是 hostname-PID 软链;PID 活着 → 别删活人脚下的地毯
225
+ pid = os.readlink(lock).rsplit("-", 1)[-1]
226
+ if pid.isdigit():
227
+ try:
228
+ os.kill(int(pid), 0)
229
+ except OSError:
230
+ pass # 进程已死:锁是残骸,放行删除
231
+ else:
232
+ return {"ok": False, "site": host,
233
+ "error": f"Chrome(pid {pid}) 正在使用该 profile,先关闭再 logout"}
234
+ shutil.rmtree(d)
235
+ return {"ok": True, "site": host}
236
+
237
+
238
+ def touch(url_or_host: str) -> None:
239
+ """抓取命中登录态车道时更新 last_used(失败静默——只是观测字段)。"""
240
+ try:
241
+ host = site_host(url_or_host)
242
+ d = _profile_dir(host)
243
+ if d.is_dir() and (d / "meta.json").is_file():
244
+ _write_meta(d, host)
245
+ except Exception:
246
+ pass
247
+
248
+
249
+ # ─── CLI(bin/argo 的 auth 子命令入口)──────────────────────────────────────
250
+
251
+ def main(argv: list[str] | None = None) -> None:
252
+ argv = list(sys.argv[1:] if argv is None else argv)
253
+ use_json = "--json" in argv
254
+ argv = [a for a in argv if a != "--json"]
255
+
256
+ if not argv or argv[0] in ("-h", "--help"):
257
+ print(__doc__.strip() or "argo auth — 浏览器登录态")
258
+ sys.exit(0 if argv else 1)
259
+
260
+ cmd, rest = argv[0], argv[1:]
261
+
262
+ def _emit_json(payload: dict) -> None:
263
+ # stdout 一律紧凑 JSON(context-budget 门禁:禁缩进 JSON / dumps_pretty)
264
+ try:
265
+ from cli_io import dumps
266
+ print(dumps(payload))
267
+ except ImportError:
268
+ print(json.dumps(payload, ensure_ascii=False))
269
+
270
+ if cmd == "login":
271
+ if not rest:
272
+ print("用法:argo auth login <url>(如 argo auth login https://example.com)",
273
+ file=sys.stderr)
274
+ sys.exit(1)
275
+ chrome_path = None
276
+ if "--chrome-path" in rest:
277
+ i = rest.index("--chrome-path")
278
+ if i + 1 < len(rest):
279
+ chrome_path = rest[i + 1]
280
+ del rest[i:i + 2]
281
+ out = login(rest[0], chrome_path=chrome_path)
282
+ if use_json:
283
+ _emit_json(out)
284
+ elif out.get("ok"):
285
+ print(f"已保存 {out['site']} 的登录态({out['profile']})\n"
286
+ f"之后 argo fetch 该站点自动带会话;argo auth status 可查看。")
287
+ else:
288
+ print(f"登录失败:{out.get('error')}", file=sys.stderr)
289
+ sys.exit(0 if out.get("ok") else 1)
290
+
291
+ if cmd == "status":
292
+ profiles = status()
293
+ if use_json:
294
+ _emit_json({"profiles": profiles})
295
+ elif not profiles:
296
+ print("(尚无已登录站点——argo auth login <url> 添加)")
297
+ else:
298
+ for p in profiles:
299
+ print(f"{p['site']} 建于 {p.get('created') or '?'} "
300
+ f"最近使用 {p.get('last_used') or '?'}")
301
+ sys.exit(0)
302
+
303
+ if cmd == "logout":
304
+ if not rest:
305
+ print("用法:argo auth logout <site>(站点见 argo auth status)",
306
+ file=sys.stderr)
307
+ sys.exit(1)
308
+ out = logout(rest[0])
309
+ if use_json:
310
+ _emit_json(out)
311
+ elif out.get("ok"):
312
+ print(f"已删除 {out['site']} 的登录态 profile。")
313
+ else:
314
+ print(f"删除失败:{out.get('error')}", file=sys.stderr)
315
+ sys.exit(0 if out.get("ok") else 1)
316
+
317
+ print(f"Unknown auth subcommand: {cmd}(可用:login / status / logout)",
318
+ file=sys.stderr)
319
+ sys.exit(1)
320
+
321
+
322
+ if __name__ == "__main__":
323
+ main()
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env python3
2
+ """budget.py — 按引擎的月度预算(原生币种)。
3
+
4
+ 为什么需要(2026-10-21):`quota.py` 的额度是**次数**(limit/used),而付费引擎
5
+ (DeepSeek、Tavily 等)的真实约束是**钱**。按次数换算会在单价变化时失真,也会
6
+ 把「失败调用」算成花费(DeepSeek 只对成功请求计费)。本模块只做三件事:
7
+
8
+ 1. 读取预算(默认 → 共享文件 → env 覆盖),不碰配额状态;
9
+ 2. 把预算挂到 `quota.py` 的 profile 上(budget_amount / budget_currency / period);
10
+ 3. 提供 `argo budget set|list|unset` 的存储原语。
11
+
12
+ 存储位置:`~/.config/argo/budgets.json`(POSIX)或 `%APPDATA%\\argo\\budgets.json`
13
+ (Windows),与 `~/.config/argo/env` 同根;`ARGO_BUDGET_FILE` 可覆盖(测试/多环境)。
14
+ 格式:
15
+
16
+ {"deepseek_search": {"amount": 5.0, "currency": "CNY", "period": "month"}}
17
+
18
+ env 覆盖(优先级高于文件,便于插件/临时实验):
19
+ - `ARGO_BUDGETS_JSON`:整份 JSON(最高)
20
+ - `ARGO_BUDGET_<ENGINE>`:单引擎金额(币种按引擎 cost_unit,缺省 CNY;周期 month)
21
+
22
+ 默认只给 DeepSeek 一个月度体验额度(¥5 ≈ 1000 次成功调用);其他引擎默认不设,
23
+ 沿用各自 quota_profiles.json 里的既有额度,符合「其他默认不改」。
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ import os
30
+ from pathlib import Path
31
+ from typing import Any
32
+
33
+ __all__ = [
34
+ "DEFAULT_BUDGETS",
35
+ "budget_file",
36
+ "budget_signature",
37
+ "load_budgets",
38
+ "budget_for",
39
+ "apply_to_profiles",
40
+ "set_budget",
41
+ "unset_budget",
42
+ ]
43
+
44
+ # 默认预算:只覆盖 DeepSeek。金额按人民币;折算依据是官方 5 元/千次成功请求。
45
+ DEFAULT_BUDGETS: dict[str, dict[str, Any]] = {
46
+ "deepseek_search": {"amount": 5.0, "currency": "CNY", "period": "month"},
47
+ }
48
+
49
+ _ENV_JSON = "ARGO_BUDGETS_JSON"
50
+ _ENV_FILE = "ARGO_BUDGET_FILE"
51
+ _ENV_PREFIX = "ARGO_" + "BUDGET_"
52
+
53
+ # 单引擎 env 覆盖的默认币种(金额单位必须与引擎 cost_unit 对齐,否则预算
54
+ # 与 cost_used 是两个币种在比大小)。未列出的引擎按 CNY,与历史行为一致。
55
+ _ENV_CURRENCY = {
56
+ "deepseek_search": "CNY",
57
+ "exa": "USD",
58
+ "tavily": "USD",
59
+ }
60
+
61
+
62
+ def budget_file() -> Path:
63
+ """预算文件路径(不保证存在)。"""
64
+ override = (os.environ.get(_ENV_FILE) or "").strip()
65
+ if override:
66
+ return Path(os.path.expanduser(override))
67
+ if os.name == "nt":
68
+ base = (os.environ.get("APPDATA") or "").strip() or os.path.expanduser("~")
69
+ else:
70
+ base = (os.environ.get("XDG_CONFIG_HOME") or "").strip() or os.path.expanduser("~/.config")
71
+ return Path(base) / "argo" / "budgets.json"
72
+
73
+
74
+ def _valid(entry: Any) -> dict[str, Any] | None:
75
+ """校验并归一一条预算;坏条目返回 None(不阻断其他引擎)。"""
76
+ if not isinstance(entry, dict):
77
+ return None
78
+ try:
79
+ amount = float(entry.get("amount"))
80
+ except (TypeError, ValueError):
81
+ return None
82
+ if amount < 0:
83
+ return None
84
+ currency = str(entry.get("currency") or "CNY").strip().upper() or "CNY"
85
+ period = str(entry.get("period") or "month").strip().lower()
86
+ if period not in ("day", "month"):
87
+ period = "month"
88
+ return {"amount": amount, "currency": currency, "period": period}
89
+
90
+
91
+ def _read_file() -> dict[str, dict[str, Any]]:
92
+ try:
93
+ raw = json.loads(budget_file().read_text(encoding="utf-8"))
94
+ except (OSError, ValueError):
95
+ return {}
96
+ if not isinstance(raw, dict):
97
+ return {}
98
+ out: dict[str, dict[str, Any]] = {}
99
+ for engine, entry in raw.items():
100
+ if str(engine).startswith("_"):
101
+ continue
102
+ norm = _valid(entry)
103
+ if norm is not None:
104
+ out[str(engine)] = norm
105
+ return out
106
+
107
+
108
+ def _read_env() -> dict[str, dict[str, Any]]:
109
+ out: dict[str, dict[str, Any]] = {}
110
+ raw = (os.environ.get(_ENV_JSON) or "").strip()
111
+ if raw:
112
+ try:
113
+ parsed = json.loads(raw)
114
+ except ValueError:
115
+ parsed = None
116
+ if isinstance(parsed, dict):
117
+ for engine, entry in parsed.items():
118
+ norm = _valid(entry)
119
+ if norm is not None:
120
+ out[str(engine)] = norm
121
+ # 单引擎便捷覆盖:"ARGO_BUDGET_DEEPSEEK_SEARCH"=5 → deepseek_search 5 CNY/月。
122
+ # 同族命名 ARGO_BUDGET_<ENGINE> 由下面按前缀解析;字面量保留给插件门禁对拍。
123
+ # 只认「大写化后能在默认/文件里找到的引擎」太窄;这里按环境变量名反推引擎名,
124
+ # 由调用方(quota)决定是否真的存在该引擎,避免拼错静默生效。
125
+ for key, value in os.environ.items():
126
+ if not key.startswith(_ENV_PREFIX) or key in (_ENV_JSON, _ENV_FILE):
127
+ continue
128
+ engine = key[len(_ENV_PREFIX):].strip().lower()
129
+ if not engine:
130
+ continue
131
+ try:
132
+ amount = float(str(value).strip())
133
+ except (TypeError, ValueError):
134
+ continue
135
+ if amount < 0:
136
+ continue
137
+ currency = (out.get(engine) or {}).get("currency") \
138
+ or _ENV_CURRENCY.get(engine, "CNY")
139
+ entry = {"amount": amount, "currency": currency, "period": "month"}
140
+ out[engine] = entry
141
+ return out
142
+
143
+
144
+ def budget_signature() -> tuple[Any, ...]:
145
+ """预算配置源的轻量签名(文件 mtime/size + 环境变量覆盖),供热读与路由缓存换键。"""
146
+ p = budget_file()
147
+ try:
148
+ st = p.stat()
149
+ f_sig: tuple[Any, ...] = (st.st_mtime_ns, st.st_size)
150
+ except OSError:
151
+ f_sig = (0, 0)
152
+ return (
153
+ f_sig,
154
+ os.environ.get(_ENV_JSON, ""),
155
+ os.environ.get("ARGO_BUDGET_DEEPSEEK_SEARCH", ""),
156
+ os.environ.get("ARGO_BUDGET_EXA", ""),
157
+ )
158
+
159
+
160
+ def load_budgets() -> dict[str, dict[str, Any]]:
161
+ """默认 → 文件 → env,逐层覆盖。"""
162
+ merged: dict[str, dict[str, Any]] = {k: dict(v) for k, v in DEFAULT_BUDGETS.items()}
163
+ merged.update(_read_file())
164
+ merged.update(_read_env())
165
+ return merged
166
+
167
+
168
+ def budget_for(engine: str) -> dict[str, Any] | None:
169
+ return load_budgets().get(str(engine))
170
+
171
+
172
+ def apply_to_profiles(profiles: dict[str, dict[str, Any]]) -> None:
173
+ """把预算挂到 quota profile(就地)。只覆盖存在的引擎,拼错不生效。"""
174
+ active = load_budgets()
175
+ for engine, spec in profiles.items():
176
+ if not isinstance(spec, dict) or str(engine).startswith("_"):
177
+ continue
178
+ entry = active.get(engine)
179
+ if entry is not None:
180
+ spec["budget_amount"] = entry["amount"]
181
+ spec["budget_currency"] = entry["currency"]
182
+ # 预算周期即重置周期;与 limit 共用同一条 `period`,避免两套时钟。
183
+ spec["period"] = entry["period"]
184
+ else:
185
+ spec.pop("budget_amount", None)
186
+ spec.pop("budget_currency", None)
187
+
188
+
189
+ def _write(data: dict[str, Any]) -> None:
190
+ path = budget_file()
191
+ path.parent.mkdir(parents=True, exist_ok=True)
192
+ tmp = path.with_suffix(path.suffix + ".tmp")
193
+ tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
194
+ encoding="utf-8")
195
+ os.replace(tmp, path)
196
+
197
+
198
+ def over_budget_error(engine: str) -> str | None:
199
+ """预算耗尽时返回可行动错误文本;未耗尽/未配预算/读失败返回 None。
200
+
201
+ 放在 budget.py 而不是 dispatch:engine_dispatch 有行数门禁,预算检查是
202
+ 「读状态 + 拼一句话」的纯策略,搬出来也让显式/自动路径共用同一文案。
203
+ """
204
+ try:
205
+ from engine_env import env_flag
206
+ if env_flag("ARGO_PAID_BUDGET_BYPASS", default=False):
207
+ return None
208
+ except Exception:
209
+ if (os.environ.get("ARGO_PAID_BUDGET_BYPASS", "").strip().lower()
210
+ in ("1", "true", "yes", "on")):
211
+ return None
212
+ try:
213
+ from quota import get_quota_manager
214
+ qm = get_quota_manager()
215
+ # 只认真正的 True:测试常把 quota manager 打成 MagicMock,属性调用
216
+ # 会返回真值 mock——不能把「mock 存在」当成预算耗尽。
217
+ if qm.is_over_budget(engine) is not True:
218
+ return None
219
+ st = (qm.budget_status(engine) if hasattr(qm, "budget_status")
220
+ else (qm.get_stats().get(engine) or {}))
221
+ return (f"本月预算已用完({st.get('cost_used', 0)}/{st.get('budget')} "
222
+ f"{st.get('budget_currency') or ''});"
223
+ f"可用 ARGO_PAID_BUDGET_BYPASS=1 单次放行")
224
+ except Exception:
225
+ return None
226
+
227
+
228
+ def set_budget(engine: str, amount: float, *, currency: str = "CNY",
229
+ period: str = "month") -> dict[str, Any]:
230
+ """写入一条预算(保留文件里其他引擎)。返回归一后的条目。"""
231
+ norm = _valid({"amount": amount, "currency": currency, "period": period})
232
+ if norm is None:
233
+ raise ValueError("预算金额必须是非负数")
234
+ data = _read_file()
235
+ data[str(engine)] = norm
236
+ _write(data)
237
+ return norm
238
+
239
+
240
+ def unset_budget(engine: str) -> bool:
241
+ """删除一条预算;返回是否真的删了。"""
242
+ data = _read_file()
243
+ if engine not in data:
244
+ return False
245
+ data.pop(engine, None)
246
+ _write(data)
247
+ return True
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env python3
2
+ """budget_cli.py — `argo budget`:按引擎的月度预算读写口。
3
+
4
+ 用法:
5
+ argo budget list [--json]
6
+ argo budget set <engine> <amount> [--currency CNY|USD] [--period month|day]
7
+ argo budget unset <engine>
8
+
9
+ 预算存在 ~/.config/argo/budgets.json(`ARGO_BUDGET_FILE` 可覆盖);`quota.py`
10
+ 启动时读取,按引擎原生币种与调用成本比较。默认只有 DeepSeek ¥5/月。
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import sys
17
+
18
+ import budget
19
+ from cli_io import dumps
20
+
21
+
22
+ def _cmd_list(args: argparse.Namespace) -> int:
23
+ items = budget.load_budgets()
24
+ try:
25
+ from quota import get_quota_manager
26
+ stats = get_quota_manager().get_stats()
27
+ except Exception:
28
+ stats = {}
29
+ rows = []
30
+ for engine, entry in sorted(items.items()):
31
+ st = stats.get(engine) or {}
32
+ rows.append({
33
+ "engine": engine,
34
+ "amount": entry["amount"],
35
+ "currency": entry["currency"],
36
+ "period": entry["period"],
37
+ "cost_used": st.get("cost_used"),
38
+ "cost_per_call": st.get("cost_per_call"),
39
+ "available": st.get("available"),
40
+ })
41
+ if args.json:
42
+ print(dumps({"file": str(budget.budget_file()), "budgets": rows}))
43
+ else:
44
+ print(f"预算文件:{budget.budget_file()}")
45
+ if not rows:
46
+ print("(无预算;引擎沿用 quota_profiles.json 的次数额度)")
47
+ for r in rows:
48
+ print(f" {r['engine']:<20} {r['amount']:>8.2f} {r['currency']}/"
49
+ f"{r['period']} 已用 {r['cost_used']} 可用={r['available']}")
50
+ return 0
51
+
52
+
53
+ def _cmd_set(args: argparse.Namespace) -> int:
54
+ try:
55
+ entry = budget.set_budget(args.engine, args.amount,
56
+ currency=args.currency,
57
+ period=args.period)
58
+ except ValueError as exc:
59
+ print(f"设置失败:{exc}", file=sys.stderr)
60
+ return 2
61
+ print(f"已设置 {args.engine}: {entry['amount']} {entry['currency']}/{entry['period']}"
62
+ f"({budget.budget_file()})")
63
+ return 0
64
+
65
+
66
+ def _cmd_unset(args: argparse.Namespace) -> int:
67
+ ok = budget.unset_budget(args.engine)
68
+ print(("已删除 " if ok else "未找到 ") + args.engine)
69
+ return 0 if ok else 1
70
+
71
+
72
+ def main(argv: list[str] | None = None) -> int:
73
+ ap = argparse.ArgumentParser(prog="argo budget",
74
+ description="按引擎的月度预算(原生币种)")
75
+ sub = ap.add_subparsers(dest="cmd", required=True)
76
+ p_list = sub.add_parser("list", help="列出预算与本月已用")
77
+ p_list.add_argument("--json", action="store_true")
78
+ p_list.set_defaults(func=_cmd_list)
79
+ p_set = sub.add_parser("set", help="设置某引擎预算")
80
+ p_set.add_argument("engine")
81
+ p_set.add_argument("amount", type=float)
82
+ p_set.add_argument("--currency", default="CNY")
83
+ p_set.add_argument("--period", default="month", choices=["day", "month"])
84
+ p_set.set_defaults(func=_cmd_set)
85
+ p_unset = sub.add_parser("unset", help="删除某引擎预算")
86
+ p_unset.add_argument("engine")
87
+ p_unset.set_defaults(func=_cmd_unset)
88
+ args = ap.parse_args(argv)
89
+ return args.func(args)
90
+
91
+
92
+ if __name__ == "__main__":
93
+ raise SystemExit(main())