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,136 @@
1
+ #!/usr/bin/env python3
2
+ """article.py — 微信公众号文章全文抓取。
3
+
4
+ 用法: argo article <url> [--json]
5
+ argo article <url> --json > article.json
6
+
7
+ 绕过 robots.txt 限制:公众号 robots 禁爬,但文章页对真实浏览器开放,
8
+ 用手机 UA 直连(curl/urllib 不遵守 robots),解析 js_content 提取正文。
9
+
10
+ 输出: 标题 / 作者 / 发布时间 / 正文纯文本 / 图片 URL 列表
11
+ """
12
+ import argparse, json, re, sys, html as htmllib, urllib.request
13
+ from cli_io import dumps
14
+
15
+ UA = ("Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) "
16
+ "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1")
17
+
18
+ HEADERS = {
19
+ "User-Agent": UA,
20
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
21
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
22
+ "Referer": "https://mp.weixin.qq.com/",
23
+ }
24
+
25
+
26
+ def fetch(url: str, timeout: int = 30) -> str:
27
+ req = urllib.request.Request(url, headers=HEADERS)
28
+ # 起点域名白名单挡不住服务端 302:重定向的每一跳都要过 SSRF 校验
29
+ # (http/https scheme + 非本机/内网目标),被拒即抛错而非跟随
30
+ opener = urllib.request.build_opener(_SafeRedirectHandler)
31
+ with opener.open(req, timeout=timeout) as r:
32
+ return r.read().decode("utf-8", errors="ignore")
33
+
34
+
35
+ class _SafeRedirectHandler(urllib.request.HTTPRedirectHandler):
36
+ """每跳重定向目标过 url_safety.check_url,拦截内网/非 http(s) 目标。"""
37
+
38
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
39
+ from url_safety import check_url
40
+ ok, reason = check_url(newurl)
41
+ if not ok:
42
+ raise ValueError(f"重定向目标被 SSRF 防护拦截({reason}): {newurl}")
43
+ return super().redirect_request(req, fp, code, msg, headers, newurl)
44
+
45
+
46
+ def strip_tags(s: str) -> str:
47
+ s = re.sub(r"<script[\s\S]*?</script>|<style[\s\S]*?</style>", "", s)
48
+ s = re.sub(r"<br\s*/?>", "\n", s)
49
+ s = re.sub(r"</(p|div|section|h\d|li|tr|blockquote)>", "\n", s)
50
+ s = re.sub(r"<[^>]+>", "", s)
51
+ s = htmllib.unescape(s)
52
+ lines = [l.strip() for l in s.split("\n")]
53
+ return "\n".join(l for l in lines if l)
54
+
55
+
56
+ def fetch_article(url: str, timeout: int = 30) -> dict:
57
+ """抓取并解析公众号文章,返回结构化 dict(CLI 与 MCP 共用主链)。
58
+
59
+ URL 非 mp.weixin.qq.com 时抛 ValueError(CLI 走 stderr 退出,
60
+ MCP 转 isError);抓取失败/反爬拦截返回 {"ok": False, "error": ...}。
61
+ """
62
+ if not re.match(r"https?://mp\.weixin\.qq\.com/", url):
63
+ raise ValueError("仅支持 mp.weixin.qq.com 链接")
64
+
65
+ try:
66
+ raw = fetch(url, timeout)
67
+ except Exception as e:
68
+ return {"ok": False, "error": f"抓取失败: {e}"}
69
+
70
+ if "js_content" not in raw and "环境异常" in raw:
71
+ return {"ok": False, "error": "微信反爬拦截(环境异常),请稍后重试"}
72
+
73
+ def grab(pattern, default=""):
74
+ m = re.search(pattern, raw)
75
+ return m.group(1).strip() if m else default
76
+
77
+ title = grab(r'<meta property="og:title" content="([^"]+)"') or \
78
+ grab(r'<h1[^>]*class="rich_media_title"[^>]*>([\s\S]*?)</h1>')
79
+ author = grab(r'<meta property="og:article:author" content="([^"]+)"') or \
80
+ grab(r'id="js_name"[^>]*>([^<]+)<')
81
+ pubtime = grab(r'id="publish_time"[^>]*>([^<]+)<')
82
+
83
+ m = re.search(r'id="js_content"[^>]*>([\s\S]*?)</div>\s*<script', raw)
84
+ if not m:
85
+ m = re.search(r'id="js_content"[^>]*>([\s\S]*?)</div>', raw)
86
+ body_html = m.group(1) if m else ""
87
+
88
+ text = strip_tags(body_html)
89
+ imgs = re.findall(r'data-src="(https://mmbiz\.qpic\.cn/[^"]+)"', body_html)
90
+ if not imgs:
91
+ imgs = re.findall(r'src="(https://mmbiz\.qpic\.cn/[^"]+)"', body_html)
92
+
93
+ return {
94
+ "ok": True,
95
+ "url": url,
96
+ "title": title,
97
+ "author": author,
98
+ "publish_time": pubtime,
99
+ "char_count": len(text),
100
+ "image_count": len(imgs),
101
+ "images": imgs,
102
+ "content": text,
103
+ }
104
+
105
+
106
+ def main():
107
+ ap = argparse.ArgumentParser(description="微信公众号文章全文抓取")
108
+ ap.add_argument("url", help="mp.weixin.qq.com 文章链接")
109
+ ap.add_argument("--json", action="store_true", help="输出 JSON")
110
+ args = ap.parse_args()
111
+
112
+ try:
113
+ out = fetch_article(args.url)
114
+ except ValueError as e:
115
+ print(str(e), file=sys.stderr)
116
+ sys.exit(1)
117
+ if not out["ok"]:
118
+ print(dumps(out))
119
+ sys.exit(1)
120
+
121
+ if args.json:
122
+ print(dumps(out))
123
+ else:
124
+ print(f"标题: {out['title']}")
125
+ print(f"作者: {out['author']} 时间: {out['publish_time']} "
126
+ f"正文 {out['char_count']} 字 / {out['image_count']} 图")
127
+ print("---")
128
+ print(out["content"])
129
+ if out["images"]:
130
+ print("---")
131
+ for i, u in enumerate(out["images"], 1):
132
+ print(f"[图{i}] {u}")
133
+
134
+
135
+ if __name__ == "__main__":
136
+ main()
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env python3
2
+ """article_meta.py — 从已抓取的 HTML 里提取文章级元数据与正文链接。
3
+
4
+ 为什么要单独一层(2026-10-22,对标 open-websearch 的 includeLinks/byline):
5
+ `readability_extract` 只交付正文文本,链接、作者、站点名、摘要这些**结构化
6
+ 线索**在提取时被丢掉。Agent 要顺着文章里的引用继续取证,只能再抓一次页面;
7
+ 研究/报告场景里作者与站点名也是引用格式的必需字段。它们都在同一份 HTML 里,
8
+ 一次解析就能拿到,不该让调用方付第二次网络往返。
9
+
10
+ 边界:
11
+ - 纯标准库 HTMLParser,毫秒级;不做正文判定(那是 readability 的职责)。
12
+ - 链接只取 <article>/<main> 内。没有语义容器时宁可不给,也不把
13
+ nav/header/footer 的导航链接当正文引用;相对链接按页面 URL 解析。
14
+ - 标题/摘要/站点名只读 meta,不从正文猜;猜错比缺失更糟。
15
+ - 全部字段都可能缺失,调用方按 key 是否存在判断,不返回空字符串占位。
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from html.parser import HTMLParser
21
+ from urllib.parse import urljoin, urlparse
22
+
23
+ MAX_LINKS = 30
24
+ MAX_TEXT = 300
25
+ _SKIP_TAGS = {"nav", "header", "footer", "aside", "script", "style", "noscript"}
26
+ _META_KEYS = {
27
+ "author": "byline",
28
+ "article:author": "byline",
29
+ "og:article:author": "byline",
30
+ "og:site_name": "site_name",
31
+ "application-name": "site_name",
32
+ "og:description": "excerpt",
33
+ "description": "excerpt",
34
+ "twitter:description": "excerpt",
35
+ "article:published_time": "published_at",
36
+ }
37
+
38
+
39
+ class _MetaParser(HTMLParser):
40
+ def __init__(self) -> None:
41
+ super().__init__(convert_charrefs=True)
42
+ self.meta: dict[str, str] = {}
43
+ self.article_links: list[dict[str, str]] = []
44
+ self._in_article = 0
45
+ self._skip = 0
46
+ self._href: str | None = None
47
+ self._text: list[str] = []
48
+
49
+ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
50
+ tag = tag.lower()
51
+ if tag == "meta":
52
+ data = {k.lower(): (v or "") for k, v in attrs if k}
53
+ key = (data.get("property") or data.get("name") or "").strip().lower()
54
+ value = data.get("content") or ""
55
+ field = _META_KEYS.get(key)
56
+ if field and value and field not in self.meta:
57
+ self.meta[field] = value.strip()[:MAX_TEXT]
58
+ return
59
+ if tag in _SKIP_TAGS:
60
+ self._skip += 1
61
+ return
62
+ if tag in ("article", "main"):
63
+ self._in_article += 1
64
+ elif tag == "a" and self._skip == 0:
65
+ href = ""
66
+ for k, v in attrs:
67
+ if k.lower() == "href" and v:
68
+ href = v.strip()
69
+ break
70
+ if href:
71
+ self._href = href
72
+ self._text = []
73
+
74
+ def handle_endtag(self, tag: str) -> None:
75
+ tag = tag.lower()
76
+ if tag in _SKIP_TAGS:
77
+ self._skip = max(0, self._skip - 1)
78
+ return
79
+ if tag in ("article", "main"):
80
+ self._in_article = max(0, self._in_article - 1)
81
+ elif tag == "a" and self._href is not None:
82
+ text = " ".join("".join(self._text).split())[:120]
83
+ if self._in_article:
84
+ self.article_links.append({"url": self._href, "text": text})
85
+ self._href = None
86
+ self._text = []
87
+
88
+ def handle_data(self, data: str) -> None:
89
+ if self._href is not None and self._skip == 0:
90
+ self._text.append(data)
91
+
92
+
93
+ def extract_article_meta(html: str, base_url: str,
94
+ max_links: int = MAX_LINKS) -> dict:
95
+ """返回 {byline, excerpt, site_name, published_at, links},缺失的键不出现。
96
+
97
+ links: [{"url": 绝对 URL, "text": 锚文本}],按出现顺序去重,最多 max_links。
98
+ 同页锚点、javascript/mailto、非 http(s)、空文本一律丢弃。
99
+ """
100
+ if not html:
101
+ return {}
102
+ p = _MetaParser()
103
+ try:
104
+ p.feed(html)
105
+ p.close()
106
+ except Exception:
107
+ return {}
108
+ out: dict = dict(p.meta)
109
+ raw_links = p.article_links
110
+ if raw_links:
111
+ links: list[dict[str, str]] = []
112
+ seen: set[str] = set()
113
+ for item in raw_links:
114
+ href = item["url"].strip()
115
+ if not href or href.startswith(("#", "javascript:", "mailto:", "tel:")):
116
+ continue
117
+ url = urljoin(base_url, href)
118
+ if urlparse(url).scheme not in ("http", "https"):
119
+ continue
120
+ if url.split("#", 1)[0] == base_url.split("#", 1)[0]:
121
+ continue
122
+ text = item["text"]
123
+ if len(text) < 2 or url in seen:
124
+ continue
125
+ seen.add(url)
126
+ links.append({"url": url, "text": text})
127
+ if len(links) >= max(1, int(max_links)):
128
+ break
129
+ if links:
130
+ out["links"] = links
131
+ return out
@@ -0,0 +1,201 @@
1
+ """arXiv 源码包(e-print)获取与安全解包 — 论文 LaTeX 精读的取数层。
2
+
3
+ P1 论文深读三能力之一(2026-09-30 P0 实测后立项):外部论文工具的
4
+ get_paper_latex 实测 3.4s 拿到 main tex + 源文件清单,argo 此前无此能力。
5
+
6
+ 安全边界(全部内存态,不落盘解包):
7
+ 1. 只认 arxiv.org 的 https URL(scheme + 域名白名单,拒环回/私有地址)
8
+ 2. 下载总量封顶(默认 64MB),成员数封顶(500),单文件解压封顶(2MB)
9
+ 3. tar 成员名拒绝绝对路径 / 路径穿越 / 盘符——本模块只读内存,永不写文件
10
+ 4. 只收 .tex/.bbl 文本(其余成员只列名不收内容)
11
+
12
+ 缓存:走 fetch 缓存(同 id 1h 内二读零成本),与 pdf 提取缓存同一纪律。
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import gzip
17
+ import io
18
+ import tarfile
19
+ from typing import Any
20
+
21
+ from net_proxy import open_url
22
+ from pdf_extract import CONTENT_WARNING, chunk_text
23
+
24
+ # 域名白名单(e-print 只在 arxiv.org 主站)
25
+ _ALLOWED_HOSTS = {"arxiv.org", "www.arxiv.org", "export.arxiv.org"}
26
+ _MAX_DOWNLOAD = 64 * 1024 * 1024 # 压缩包下载上限
27
+ _MAX_MEMBERS = 500 # 成员数上限
28
+ _MAX_TOTAL_UNCOMPRESSED = 64 * 1024 * 1024 # 解压总量上限
29
+ _MAX_FILE_UNCOMPRESSED = 2 * 1024 * 1024 # 单文件解压上限(.tex/.bbl)
30
+ _MAX_CACHE_TEXTS = 2 * 1024 * 1024 # 进缓存的文本总量上限(超出只存清单)
31
+ _TEX_EXTS = (".tex", ".bbl")
32
+ _DOCUMENTCLASS_RE = None # 惰性编译(见 _is_main_candidate)
33
+
34
+
35
+ class EprintError(Exception):
36
+ """e-print 取数/解包失败(消息可直接进用户输出)。"""
37
+
38
+
39
+ def _is_main_candidate(name: str, text: str) -> bool:
40
+ global _DOCUMENTCLASS_RE
41
+ if _DOCUMENTCLASS_RE is None:
42
+ import re
43
+ _DOCUMENTCLASS_RE = re.compile(r"\\documentclass")
44
+ return _DOCUMENTCLASS_RE.search(text) is not None
45
+
46
+
47
+ def _validate_url(url: str) -> None:
48
+ """仅允许白名单域名的 http(s) URL(拒环回/私有/保留地址)。"""
49
+ from urllib.parse import urlparse
50
+ p = urlparse(url)
51
+ if p.scheme not in ("http", "https"):
52
+ raise EprintError(f"仅允许 http(s) URL:{url[:80]}")
53
+ host = (p.hostname or "").lower()
54
+ if host not in _ALLOWED_HOSTS:
55
+ raise EprintError(f"域名不在白名单({'/'.join(sorted(_ALLOWED_HOSTS))}):{host}")
56
+
57
+
58
+ def _safe_name(name: str) -> bool:
59
+ """tar 成员名安全判定:拒绝对路径/盘符/穿越(只读内存态,防御性双保险)。"""
60
+ if not name or name.startswith(("/", "\\")):
61
+ return False
62
+ if ":" in name.split("/")[0]: # C:\ 盘符形态
63
+ return False
64
+ parts = name.replace("\\", "/").split("/")
65
+ return ".." not in parts
66
+
67
+
68
+ def _pick_main(texts: dict[str, str]) -> str | None:
69
+ """主 tex:含 \\documentclass 的最大文件;没有则最大的 .tex。"""
70
+ candidates = [n for n in texts if n.endswith(".tex")]
71
+ if not candidates:
72
+ return None
73
+ doc_class = [n for n in candidates if _is_main_candidate(n, texts[n])]
74
+ pool = doc_class or candidates
75
+ return max(pool, key=lambda n: len(texts[n]))
76
+
77
+
78
+ def parse_eprint(body: bytes, *, max_members: int = _MAX_MEMBERS,
79
+ max_total: int = _MAX_TOTAL_UNCOMPRESSED,
80
+ max_file: int = _MAX_FILE_UNCOMPRESSED) -> dict[str, Any]:
81
+ """解析 e-print 字节流(tar.gz / 单文件 gzip / 纯 tex),全内存态。
82
+
83
+ 返回 {format, files:[{name,size}], texts:{name:content}, rejected:[...]},
84
+ 非 LaTeX 源包(如误传 PDF)抛 EprintError。
85
+ """
86
+ if body[:5] == b"%PDF-":
87
+ raise EprintError("这是 PDF 不是 LaTeX 源包(e-print 端点才返回源码)")
88
+ tf = None
89
+ try:
90
+ try:
91
+ tf = tarfile.open(fileobj=io.BytesIO(body), mode="r:*")
92
+ members = [(m.name, m.size, tf.extractfile(m)) for m in tf.getmembers()]
93
+ fmt = "tar"
94
+ except tarfile.ReadError:
95
+ if body[:2] == b"\x1f\x8b": # 单文件 gzip(老论文常见:单个 .tex.gz)
96
+ text = gzip.decompress(body).decode("utf-8", errors="replace")
97
+ name = "main.tex"
98
+ return {"format": "gz", "main_file": name,
99
+ "files": [{"name": name, "size": len(text)}],
100
+ "texts": {name: text}, "rejected": []}
101
+ text = body.decode("utf-8", errors="replace")
102
+ return {"format": "plain", "main_file": "main.tex",
103
+ "files": [{"name": "main.tex", "size": len(text)}],
104
+ "texts": {"main.tex": text}, "rejected": []}
105
+ except EprintError:
106
+ raise
107
+ except Exception as e:
108
+ raise EprintError(f"源码包解析失败: {str(e)[:120]}")
109
+
110
+ files: list[dict[str, Any]] = []
111
+ texts: dict[str, str] = {}
112
+ rejected: list[str] = []
113
+ total = 0
114
+ if len(members) > max_members:
115
+ raise EprintError(f"成员数 {len(members)} 超上限 {max_members},疑似恶意包")
116
+ for raw_name, size, fh in members:
117
+ if not _safe_name(raw_name):
118
+ rejected.append(raw_name)
119
+ continue
120
+ if size > max_file:
121
+ rejected.append(f"{raw_name}(单文件 {size} 超上限)")
122
+ continue
123
+ total += size
124
+ if total > max_total:
125
+ raise EprintError(f"解压总量超上限 {max_total // (1024 * 1024)}MB")
126
+ if not raw_name.lower().endswith(_TEX_EXTS) or fh is None:
127
+ files.append({"name": raw_name, "size": size})
128
+ continue
129
+ data = fh.read(max_file + 1)
130
+ if len(data) > max_file:
131
+ rejected.append(f"{raw_name}(解压超限)")
132
+ continue
133
+ text = data.decode("utf-8", errors="replace")
134
+ files.append({"name": raw_name, "size": size})
135
+ texts[raw_name] = text
136
+ main = _pick_main(texts)
137
+ return {"format": fmt, "files": files, "texts": texts,
138
+ "main_file": main, "rejected": rejected}
139
+
140
+
141
+ def fetch_eprint(arxiv_id: str, use_cache: bool = True) -> dict[str, Any]:
142
+ """拉取并解析 arXiv 源码包;带 fetch 缓存(1h 内二读零成本)。
143
+
144
+ 返回 parse_eprint 结果 + {"content_warning": ...};texts 总量超缓存上限时
145
+ 只缓存清单并置 texts_omitted(本次调用仍返回全文)。
146
+ """
147
+ aid = arxiv_id.strip()
148
+ if not aid or any(c.isspace() for c in aid):
149
+ raise EprintError(f"arXiv id 形态不对:{aid[:40]}")
150
+ url = f"https://arxiv.org/e-print/{aid}"
151
+ _validate_url(url)
152
+
153
+ cache = None
154
+ cache_key = f"arxiv-eprint:{aid}"
155
+ if use_cache:
156
+ try:
157
+ from cache import SearchCache
158
+ cache = SearchCache()
159
+ hit = cache.get_fetch(cache_key)
160
+ if isinstance(hit, dict) and hit.get("files"):
161
+ hit["_cached"] = True
162
+ return hit
163
+ except Exception:
164
+ cache = None
165
+
166
+ with open_url(url, timeout=60) as resp:
167
+ body = resp.read(_MAX_DOWNLOAD + 1)
168
+ if len(body) > _MAX_DOWNLOAD:
169
+ raise EprintError(f"源码包超过下载上限 {_MAX_DOWNLOAD // (1024 * 1024)}MB")
170
+
171
+ result = parse_eprint(body)
172
+ result["arxiv_id"] = aid
173
+ result["content_warning"] = CONTENT_WARNING
174
+ if cache is not None:
175
+ try:
176
+ payload = {k: v for k, v in result.items() if k != "_cached"}
177
+ if sum(len(t) for t in payload.get("texts", {}).values()) > _MAX_CACHE_TEXTS:
178
+ payload["texts"] = {}
179
+ payload["texts_omitted"] = True
180
+ cache.set_fetch(cache_key, payload)
181
+ except Exception:
182
+ pass # 缓存写失败不影响本次结果
183
+ return result
184
+
185
+
186
+ def latex_read(source: dict[str, Any], file: str | None = None,
187
+ start: int = 0, max_chars: int | None = None) -> dict[str, Any]:
188
+ """从已解析的源码包按文件分页读取(默认主 tex);chunk 契约与 pdf 相同。"""
189
+ texts = source.get("texts") or {}
190
+ name = file or source.get("main_file")
191
+ if not name:
192
+ raise EprintError("源码包内没有可读的 .tex 文件")
193
+ if name not in texts:
194
+ if source.get("texts_omitted"):
195
+ raise EprintError("该源码包文本未入缓存(体积超限),请重新拉取")
196
+ available = ", ".join(sorted(texts))[:200] or "(无 .tex 成员)"
197
+ raise EprintError(f"文件 {name} 不在源码包内。可用:{available}")
198
+ seg = chunk_text(texts[name], start, max_chars or 12000)
199
+ return {"content_warning": CONTENT_WARNING, "file": name,
200
+ "main_file": source.get("main_file"), "source_format": source.get("format"),
201
+ **seg}