@zenalexa/unicli 0.220.1 → 0.221.0

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 (471) hide show
  1. package/AGENTS.md +28 -6
  2. package/README.md +8 -8
  3. package/README.zh-CN.md +8 -8
  4. package/dist/adapters/bilibili/comments.js +66 -4
  5. package/dist/adapters/bilibili/comments.js.map +1 -1
  6. package/dist/adapters/bilibili/compat.js +2 -2
  7. package/dist/adapters/bilibili/compat.js.map +1 -1
  8. package/dist/adapters/bilibili/download.js +4 -4
  9. package/dist/adapters/bilibili/download.js.map +1 -1
  10. package/dist/adapters/bilibili/wbi.d.ts.map +1 -1
  11. package/dist/adapters/bilibili/wbi.js +3 -3
  12. package/dist/adapters/bilibili/wbi.js.map +1 -1
  13. package/dist/adapters/cipo/_shared.d.ts +21 -0
  14. package/dist/adapters/cipo/_shared.d.ts.map +1 -0
  15. package/dist/adapters/cipo/_shared.js +67 -0
  16. package/dist/adapters/cipo/_shared.js.map +1 -0
  17. package/dist/adapters/cipo/get.d.ts +19 -0
  18. package/dist/adapters/cipo/get.d.ts.map +1 -0
  19. package/dist/adapters/cipo/get.js +140 -0
  20. package/dist/adapters/cipo/get.js.map +1 -0
  21. package/dist/adapters/cipo/legal-status.d.ts +19 -0
  22. package/dist/adapters/cipo/legal-status.d.ts.map +1 -0
  23. package/dist/adapters/cipo/legal-status.js +111 -0
  24. package/dist/adapters/cipo/legal-status.js.map +1 -0
  25. package/dist/adapters/cipo/search.d.ts +20 -0
  26. package/dist/adapters/cipo/search.d.ts.map +1 -0
  27. package/dist/adapters/cipo/search.js +148 -0
  28. package/dist/adapters/cipo/search.js.map +1 -0
  29. package/dist/adapters/cnipa/_shared.d.ts +47 -0
  30. package/dist/adapters/cnipa/_shared.d.ts.map +1 -0
  31. package/dist/adapters/cnipa/_shared.js +97 -0
  32. package/dist/adapters/cnipa/_shared.js.map +1 -0
  33. package/dist/adapters/cnipa/get.d.ts +19 -0
  34. package/dist/adapters/cnipa/get.d.ts.map +1 -0
  35. package/dist/adapters/cnipa/get.js +149 -0
  36. package/dist/adapters/cnipa/get.js.map +1 -0
  37. package/dist/adapters/cnipa/legal-status.d.ts +19 -0
  38. package/dist/adapters/cnipa/legal-status.d.ts.map +1 -0
  39. package/dist/adapters/cnipa/legal-status.js +119 -0
  40. package/dist/adapters/cnipa/legal-status.js.map +1 -0
  41. package/dist/adapters/cnipa/search.d.ts +21 -0
  42. package/dist/adapters/cnipa/search.d.ts.map +1 -0
  43. package/dist/adapters/cnipa/search.js +170 -0
  44. package/dist/adapters/cnipa/search.js.map +1 -0
  45. package/dist/adapters/espacenet/_shared.d.ts +21 -0
  46. package/dist/adapters/espacenet/_shared.d.ts.map +1 -0
  47. package/dist/adapters/espacenet/_shared.js +67 -0
  48. package/dist/adapters/espacenet/_shared.js.map +1 -0
  49. package/dist/adapters/espacenet/family.d.ts +19 -0
  50. package/dist/adapters/espacenet/family.d.ts.map +1 -0
  51. package/dist/adapters/espacenet/family.js +118 -0
  52. package/dist/adapters/espacenet/family.js.map +1 -0
  53. package/dist/adapters/espacenet/get.d.ts +19 -0
  54. package/dist/adapters/espacenet/get.d.ts.map +1 -0
  55. package/dist/adapters/espacenet/get.js +130 -0
  56. package/dist/adapters/espacenet/get.js.map +1 -0
  57. package/dist/adapters/espacenet/legal-status.d.ts +19 -0
  58. package/dist/adapters/espacenet/legal-status.d.ts.map +1 -0
  59. package/dist/adapters/espacenet/legal-status.js +110 -0
  60. package/dist/adapters/espacenet/legal-status.js.map +1 -0
  61. package/dist/adapters/espacenet/search.d.ts +20 -0
  62. package/dist/adapters/espacenet/search.d.ts.map +1 -0
  63. package/dist/adapters/espacenet/search.js +165 -0
  64. package/dist/adapters/espacenet/search.js.map +1 -0
  65. package/dist/adapters/facebook/subtitles.d.ts +9 -0
  66. package/dist/adapters/facebook/subtitles.d.ts.map +1 -0
  67. package/dist/adapters/facebook/subtitles.js +42 -0
  68. package/dist/adapters/facebook/subtitles.js.map +1 -0
  69. package/dist/adapters/fips/_shared.d.ts +21 -0
  70. package/dist/adapters/fips/_shared.d.ts.map +1 -0
  71. package/dist/adapters/fips/_shared.js +77 -0
  72. package/dist/adapters/fips/_shared.js.map +1 -0
  73. package/dist/adapters/fips/get.d.ts +19 -0
  74. package/dist/adapters/fips/get.d.ts.map +1 -0
  75. package/dist/adapters/fips/get.js +139 -0
  76. package/dist/adapters/fips/get.js.map +1 -0
  77. package/dist/adapters/fips/search.d.ts +20 -0
  78. package/dist/adapters/fips/search.d.ts.map +1 -0
  79. package/dist/adapters/fips/search.js +148 -0
  80. package/dist/adapters/fips/search.js.map +1 -0
  81. package/dist/adapters/freepatentsonline-web/_shared.d.ts +72 -0
  82. package/dist/adapters/freepatentsonline-web/_shared.d.ts.map +1 -0
  83. package/dist/adapters/freepatentsonline-web/_shared.js +216 -0
  84. package/dist/adapters/freepatentsonline-web/_shared.js.map +1 -0
  85. package/dist/adapters/freepatentsonline-web/get.d.ts +21 -0
  86. package/dist/adapters/freepatentsonline-web/get.d.ts.map +1 -0
  87. package/dist/adapters/freepatentsonline-web/get.js +127 -0
  88. package/dist/adapters/freepatentsonline-web/get.js.map +1 -0
  89. package/dist/adapters/freepatentsonline-web/search.d.ts +22 -0
  90. package/dist/adapters/freepatentsonline-web/search.d.ts.map +1 -0
  91. package/dist/adapters/freepatentsonline-web/search.js +149 -0
  92. package/dist/adapters/freepatentsonline-web/search.js.map +1 -0
  93. package/dist/adapters/google-patents-web/_shared.d.ts +110 -0
  94. package/dist/adapters/google-patents-web/_shared.d.ts.map +1 -0
  95. package/dist/adapters/google-patents-web/_shared.js +164 -0
  96. package/dist/adapters/google-patents-web/_shared.js.map +1 -0
  97. package/dist/adapters/google-patents-web/get.d.ts +36 -0
  98. package/dist/adapters/google-patents-web/get.d.ts.map +1 -0
  99. package/dist/adapters/google-patents-web/get.js +187 -0
  100. package/dist/adapters/google-patents-web/get.js.map +1 -0
  101. package/dist/adapters/google-patents-web/search.d.ts +23 -0
  102. package/dist/adapters/google-patents-web/search.d.ts.map +1 -0
  103. package/dist/adapters/google-patents-web/search.js +169 -0
  104. package/dist/adapters/google-patents-web/search.js.map +1 -0
  105. package/dist/adapters/inpi-br/_shared.d.ts +21 -0
  106. package/dist/adapters/inpi-br/_shared.d.ts.map +1 -0
  107. package/dist/adapters/inpi-br/_shared.js +67 -0
  108. package/dist/adapters/inpi-br/_shared.js.map +1 -0
  109. package/dist/adapters/inpi-br/get.d.ts +19 -0
  110. package/dist/adapters/inpi-br/get.d.ts.map +1 -0
  111. package/dist/adapters/inpi-br/get.js +142 -0
  112. package/dist/adapters/inpi-br/get.js.map +1 -0
  113. package/dist/adapters/inpi-br/search.d.ts +20 -0
  114. package/dist/adapters/inpi-br/search.d.ts.map +1 -0
  115. package/dist/adapters/inpi-br/search.js +154 -0
  116. package/dist/adapters/inpi-br/search.js.map +1 -0
  117. package/dist/adapters/instagram/subtitles.d.ts +9 -0
  118. package/dist/adapters/instagram/subtitles.d.ts.map +1 -0
  119. package/dist/adapters/instagram/subtitles.js +42 -0
  120. package/dist/adapters/instagram/subtitles.js.map +1 -0
  121. package/dist/adapters/mastodon/statuses.d.ts +40 -0
  122. package/dist/adapters/mastodon/statuses.d.ts.map +1 -0
  123. package/dist/adapters/mastodon/statuses.js +153 -0
  124. package/dist/adapters/mastodon/statuses.js.map +1 -0
  125. package/dist/adapters/reddit/comments.d.ts +9 -0
  126. package/dist/adapters/reddit/comments.d.ts.map +1 -0
  127. package/dist/adapters/reddit/comments.js +124 -0
  128. package/dist/adapters/reddit/comments.js.map +1 -0
  129. package/dist/adapters/threads/post.d.ts +32 -0
  130. package/dist/adapters/threads/post.d.ts.map +1 -0
  131. package/dist/adapters/threads/post.js +287 -0
  132. package/dist/adapters/threads/post.js.map +1 -0
  133. package/dist/adapters/tiktok/subtitles.d.ts +9 -0
  134. package/dist/adapters/tiktok/subtitles.d.ts.map +1 -0
  135. package/dist/adapters/tiktok/subtitles.js +42 -0
  136. package/dist/adapters/tiktok/subtitles.js.map +1 -0
  137. package/dist/adapters/twitter/accept.js +2 -2
  138. package/dist/adapters/twitter/accept.js.map +1 -1
  139. package/dist/adapters/twitter/browser-fallback.d.ts +26 -0
  140. package/dist/adapters/twitter/browser-fallback.d.ts.map +1 -0
  141. package/dist/adapters/twitter/browser-fallback.js +93 -0
  142. package/dist/adapters/twitter/browser-fallback.js.map +1 -0
  143. package/dist/adapters/twitter/browser-state.d.ts +11 -0
  144. package/dist/adapters/twitter/browser-state.d.ts.map +1 -0
  145. package/dist/adapters/twitter/browser-state.js +46 -0
  146. package/dist/adapters/twitter/browser-state.js.map +1 -0
  147. package/dist/adapters/twitter/client.d.ts.map +1 -1
  148. package/dist/adapters/twitter/client.js +36 -13
  149. package/dist/adapters/twitter/client.js.map +1 -1
  150. package/dist/adapters/twitter/reply-dm.js +2 -2
  151. package/dist/adapters/twitter/reply-dm.js.map +1 -1
  152. package/dist/adapters/twitter/reply.js +1 -0
  153. package/dist/adapters/twitter/reply.js.map +1 -1
  154. package/dist/adapters/twitter/search.js +11 -18
  155. package/dist/adapters/twitter/search.js.map +1 -1
  156. package/dist/adapters/twitter/thread.d.ts +14 -0
  157. package/dist/adapters/twitter/thread.d.ts.map +1 -1
  158. package/dist/adapters/twitter/thread.js +28 -2
  159. package/dist/adapters/twitter/thread.js.map +1 -1
  160. package/dist/adapters/twitter/trending.js +13 -59
  161. package/dist/adapters/twitter/trending.js.map +1 -1
  162. package/dist/adapters/xiaohongshu/browser-state.d.ts +19 -0
  163. package/dist/adapters/xiaohongshu/browser-state.d.ts.map +1 -0
  164. package/dist/adapters/xiaohongshu/browser-state.js +67 -0
  165. package/dist/adapters/xiaohongshu/browser-state.js.map +1 -0
  166. package/dist/adapters/xiaohongshu/comments.js +28 -5
  167. package/dist/adapters/xiaohongshu/comments.js.map +1 -1
  168. package/dist/adapters/xiaohongshu/download.js +49 -11
  169. package/dist/adapters/xiaohongshu/download.js.map +1 -1
  170. package/dist/adapters/xiaohongshu/search.d.ts.map +1 -1
  171. package/dist/adapters/xiaohongshu/search.js +11 -5
  172. package/dist/adapters/xiaohongshu/search.js.map +1 -1
  173. package/dist/adapters/xiaohongshu/trending.d.ts +9 -0
  174. package/dist/adapters/xiaohongshu/trending.d.ts.map +1 -0
  175. package/dist/adapters/xiaohongshu/trending.js +94 -0
  176. package/dist/adapters/xiaohongshu/trending.js.map +1 -0
  177. package/dist/adapters/youtube/comments.d.ts +80 -0
  178. package/dist/adapters/youtube/comments.d.ts.map +1 -1
  179. package/dist/adapters/youtube/comments.js +108 -12
  180. package/dist/adapters/youtube/comments.js.map +1 -1
  181. package/dist/adapters/youtube/subtitles.d.ts +9 -0
  182. package/dist/adapters/youtube/subtitles.d.ts.map +1 -0
  183. package/dist/adapters/youtube/subtitles.js +42 -0
  184. package/dist/adapters/youtube/subtitles.js.map +1 -0
  185. package/dist/adapters/yt-dlp/subtitles.d.ts +9 -0
  186. package/dist/adapters/yt-dlp/subtitles.d.ts.map +1 -0
  187. package/dist/adapters/yt-dlp/subtitles.js +41 -0
  188. package/dist/adapters/yt-dlp/subtitles.js.map +1 -0
  189. package/dist/adapters/zhihu/answer-detail.d.ts +39 -0
  190. package/dist/adapters/zhihu/answer-detail.d.ts.map +1 -0
  191. package/dist/adapters/zhihu/answer-detail.js +204 -0
  192. package/dist/adapters/zhihu/answer-detail.js.map +1 -0
  193. package/dist/adapters/zhihu/comment.d.ts +9 -0
  194. package/dist/adapters/zhihu/comment.d.ts.map +1 -0
  195. package/dist/adapters/zhihu/comment.js +149 -0
  196. package/dist/adapters/zhihu/comment.js.map +1 -0
  197. package/dist/adapters/zhihu/recommend.d.ts +36 -0
  198. package/dist/adapters/zhihu/recommend.d.ts.map +1 -0
  199. package/dist/adapters/zhihu/recommend.js +151 -0
  200. package/dist/adapters/zhihu/recommend.js.map +1 -0
  201. package/dist/browser/bridge.d.ts.map +1 -1
  202. package/dist/browser/bridge.js +14 -3
  203. package/dist/browser/bridge.js.map +1 -1
  204. package/dist/browser/daemon-client.d.ts +6 -0
  205. package/dist/browser/daemon-client.d.ts.map +1 -1
  206. package/dist/browser/daemon-client.js +75 -15
  207. package/dist/browser/daemon-client.js.map +1 -1
  208. package/dist/browser/daemon.js +39 -15
  209. package/dist/browser/daemon.js.map +1 -1
  210. package/dist/browser/protocol.d.ts +1 -0
  211. package/dist/browser/protocol.d.ts.map +1 -1
  212. package/dist/browser/protocol.js +1 -0
  213. package/dist/browser/protocol.js.map +1 -1
  214. package/dist/cli.d.ts.map +1 -1
  215. package/dist/cli.js +6 -0
  216. package/dist/cli.js.map +1 -1
  217. package/dist/commands/approvals.d.ts.map +1 -1
  218. package/dist/commands/approvals.js +1 -37
  219. package/dist/commands/approvals.js.map +1 -1
  220. package/dist/commands/browser/index.d.ts.map +1 -1
  221. package/dist/commands/browser/index.js +7 -2
  222. package/dist/commands/browser/index.js.map +1 -1
  223. package/dist/commands/daemon.d.ts.map +1 -1
  224. package/dist/commands/daemon.js +7 -3
  225. package/dist/commands/daemon.js.map +1 -1
  226. package/dist/commands/dispatch.d.ts.map +1 -1
  227. package/dist/commands/dispatch.js +27 -3
  228. package/dist/commands/dispatch.js.map +1 -1
  229. package/dist/commands/patent-doctor.d.ts +48 -0
  230. package/dist/commands/patent-doctor.d.ts.map +1 -0
  231. package/dist/commands/patent-doctor.js +109 -0
  232. package/dist/commands/patent-doctor.js.map +1 -0
  233. package/dist/commands/patent.d.ts +78 -0
  234. package/dist/commands/patent.d.ts.map +1 -0
  235. package/dist/commands/patent.js +919 -0
  236. package/dist/commands/patent.js.map +1 -0
  237. package/dist/commands/social.d.ts +19 -0
  238. package/dist/commands/social.d.ts.map +1 -0
  239. package/dist/commands/social.js +236 -0
  240. package/dist/commands/social.js.map +1 -0
  241. package/dist/core/registry.d.ts +1 -1
  242. package/dist/core/registry.d.ts.map +1 -1
  243. package/dist/core/registry.js +11 -2
  244. package/dist/core/registry.js.map +1 -1
  245. package/dist/discovery/loader.d.ts.map +1 -1
  246. package/dist/discovery/loader.js +4 -0
  247. package/dist/discovery/loader.js.map +1 -1
  248. package/dist/engine/approval-presenter.d.ts +10 -0
  249. package/dist/engine/approval-presenter.d.ts.map +1 -0
  250. package/dist/engine/approval-presenter.js +45 -0
  251. package/dist/engine/approval-presenter.js.map +1 -0
  252. package/dist/engine/approval-store.d.ts +4 -0
  253. package/dist/engine/approval-store.d.ts.map +1 -1
  254. package/dist/engine/approval-store.js +85 -11
  255. package/dist/engine/approval-store.js.map +1 -1
  256. package/dist/engine/auth/oauth2-cc.d.ts +67 -0
  257. package/dist/engine/auth/oauth2-cc.d.ts.map +1 -0
  258. package/dist/engine/auth/oauth2-cc.js +120 -0
  259. package/dist/engine/auth/oauth2-cc.js.map +1 -0
  260. package/dist/engine/cookies.d.ts +10 -0
  261. package/dist/engine/cookies.d.ts.map +1 -1
  262. package/dist/engine/cookies.js +64 -0
  263. package/dist/engine/cookies.js.map +1 -1
  264. package/dist/engine/download.d.ts +5 -0
  265. package/dist/engine/download.d.ts.map +1 -1
  266. package/dist/engine/download.js +11 -4
  267. package/dist/engine/download.js.map +1 -1
  268. package/dist/engine/executor.d.ts +1 -0
  269. package/dist/engine/executor.d.ts.map +1 -1
  270. package/dist/engine/executor.js +25 -0
  271. package/dist/engine/executor.js.map +1 -1
  272. package/dist/engine/framework.d.ts +5 -5
  273. package/dist/engine/framework.js +5 -5
  274. package/dist/engine/harden.d.ts +1 -1
  275. package/dist/engine/harden.js +1 -1
  276. package/dist/engine/kernel/stages.d.ts.map +1 -1
  277. package/dist/engine/kernel/stages.js +2 -1
  278. package/dist/engine/kernel/stages.js.map +1 -1
  279. package/dist/engine/normalizer/patent-envelope.d.ts +61 -0
  280. package/dist/engine/normalizer/patent-envelope.d.ts.map +1 -0
  281. package/dist/engine/normalizer/patent-envelope.js +132 -0
  282. package/dist/engine/normalizer/patent-envelope.js.map +1 -0
  283. package/dist/engine/research.d.ts +5 -7
  284. package/dist/engine/research.d.ts.map +1 -1
  285. package/dist/engine/research.js +6 -9
  286. package/dist/engine/research.js.map +1 -1
  287. package/dist/engine/steps/browser-helpers.d.ts +2 -2
  288. package/dist/engine/steps/browser-helpers.d.ts.map +1 -1
  289. package/dist/engine/steps/browser-helpers.js +39 -16
  290. package/dist/engine/steps/browser-helpers.js.map +1 -1
  291. package/dist/engine/steps/download.d.ts +1 -0
  292. package/dist/engine/steps/download.d.ts.map +1 -1
  293. package/dist/engine/steps/download.js +3 -1
  294. package/dist/engine/steps/download.js.map +1 -1
  295. package/dist/engine/steps/index.d.ts +2 -0
  296. package/dist/engine/steps/index.d.ts.map +1 -1
  297. package/dist/engine/steps/index.js +2 -0
  298. package/dist/engine/steps/index.js.map +1 -1
  299. package/dist/engine/steps/oauth2-token.d.ts +41 -0
  300. package/dist/engine/steps/oauth2-token.d.ts.map +1 -0
  301. package/dist/engine/steps/oauth2-token.js +115 -0
  302. package/dist/engine/steps/oauth2-token.js.map +1 -0
  303. package/dist/engine/steps/select-xml.d.ts +34 -0
  304. package/dist/engine/steps/select-xml.d.ts.map +1 -0
  305. package/dist/engine/steps/select-xml.js +222 -0
  306. package/dist/engine/steps/select-xml.js.map +1 -0
  307. package/dist/engine/template.d.ts.map +1 -1
  308. package/dist/engine/template.js +7 -0
  309. package/dist/engine/template.js.map +1 -1
  310. package/dist/engine/transport/mcp-browser.d.ts +128 -0
  311. package/dist/engine/transport/mcp-browser.d.ts.map +1 -0
  312. package/dist/engine/transport/mcp-browser.js +120 -0
  313. package/dist/engine/transport/mcp-browser.js.map +1 -0
  314. package/dist/fast-path/handlers/approvals.d.ts +11 -0
  315. package/dist/fast-path/handlers/approvals.d.ts.map +1 -0
  316. package/dist/fast-path/handlers/approvals.js +136 -0
  317. package/dist/fast-path/handlers/approvals.js.map +1 -0
  318. package/dist/fast-path/manifest.d.ts +1 -0
  319. package/dist/fast-path/manifest.d.ts.map +1 -1
  320. package/dist/fast-path/manifest.js.map +1 -1
  321. package/dist/fast-path.d.ts.map +1 -1
  322. package/dist/fast-path.js +3 -0
  323. package/dist/fast-path.js.map +1 -1
  324. package/dist/index.d.ts +23 -0
  325. package/dist/index.d.ts.map +1 -0
  326. package/dist/index.js +38 -0
  327. package/dist/index.js.map +1 -0
  328. package/dist/manifest-compact.txt +3 -3
  329. package/dist/manifest-search.json +1 -1
  330. package/dist/manifest.json +2239 -176
  331. package/dist/output/auth-guidance.d.ts +14 -0
  332. package/dist/output/auth-guidance.d.ts.map +1 -0
  333. package/dist/output/auth-guidance.js +50 -0
  334. package/dist/output/auth-guidance.js.map +1 -0
  335. package/dist/output/error-map.d.ts +1 -1
  336. package/dist/output/error-map.d.ts.map +1 -1
  337. package/dist/output/error-map.js +28 -4
  338. package/dist/output/error-map.js.map +1 -1
  339. package/dist/output/next-actions.d.ts.map +1 -1
  340. package/dist/output/next-actions.js +19 -3
  341. package/dist/output/next-actions.js.map +1 -1
  342. package/dist/registry.d.ts +18 -1
  343. package/dist/registry.d.ts.map +1 -1
  344. package/dist/registry.js +5 -0
  345. package/dist/registry.js.map +1 -1
  346. package/dist/social/browser-errors.d.ts +13 -0
  347. package/dist/social/browser-errors.d.ts.map +1 -0
  348. package/dist/social/browser-errors.js +36 -0
  349. package/dist/social/browser-errors.js.map +1 -0
  350. package/dist/social/capabilities.d.ts +29 -0
  351. package/dist/social/capabilities.d.ts.map +1 -0
  352. package/dist/social/capabilities.js +448 -0
  353. package/dist/social/capabilities.js.map +1 -0
  354. package/dist/social/comments.d.ts +26 -0
  355. package/dist/social/comments.d.ts.map +1 -0
  356. package/dist/social/comments.js +97 -0
  357. package/dist/social/comments.js.map +1 -0
  358. package/dist/social/video-text.d.ts +27 -0
  359. package/dist/social/video-text.d.ts.map +1 -0
  360. package/dist/social/video-text.js +140 -0
  361. package/dist/social/video-text.js.map +1 -0
  362. package/dist/types/patent.d.ts +160 -0
  363. package/dist/types/patent.d.ts.map +1 -0
  364. package/dist/types/patent.js +16 -0
  365. package/dist/types/patent.js.map +1 -0
  366. package/dist/types.d.ts +12 -0
  367. package/dist/types.d.ts.map +1 -1
  368. package/dist/types.js.map +1 -1
  369. package/package.json +9 -4
  370. package/server.json +3 -3
  371. package/skills/unicli/SKILL.md +1 -1
  372. package/skills/unicli-claude-code/SKILL.md +1 -1
  373. package/skills/unicli-hermes/SKILL.md +1 -1
  374. package/src/adapters/bilibili/comments-tree.test.ts +41 -0
  375. package/src/adapters/bilibili/comments.ts +78 -4
  376. package/src/adapters/bilibili/compat.ts +5 -2
  377. package/src/adapters/bilibili/download.ts +7 -4
  378. package/src/adapters/bilibili/wbi.ts +6 -3
  379. package/src/adapters/brave/search.yaml +53 -0
  380. package/src/adapters/cipo/_shared.ts +98 -0
  381. package/src/adapters/cipo/get.ts +188 -0
  382. package/src/adapters/cipo/legal-status.ts +148 -0
  383. package/src/adapters/cipo/search.ts +195 -0
  384. package/src/adapters/cnipa/_shared.ts +138 -0
  385. package/src/adapters/cnipa/get.ts +199 -0
  386. package/src/adapters/cnipa/legal-status.ts +162 -0
  387. package/src/adapters/cnipa/search.ts +229 -0
  388. package/src/adapters/dpma/get.yaml +67 -0
  389. package/src/adapters/dpma/search.yaml +77 -0
  390. package/src/adapters/duckduckgo/search.yaml +54 -0
  391. package/src/adapters/duckduckgo/suggest.yaml +52 -0
  392. package/src/adapters/epo/family.yaml +69 -0
  393. package/src/adapters/epo/get.yaml +74 -0
  394. package/src/adapters/epo/legal-status.yaml +63 -0
  395. package/src/adapters/epo/search.yaml +84 -0
  396. package/src/adapters/espacenet/_shared.ts +98 -0
  397. package/src/adapters/espacenet/family.ts +161 -0
  398. package/src/adapters/espacenet/get.ts +185 -0
  399. package/src/adapters/espacenet/legal-status.ts +151 -0
  400. package/src/adapters/espacenet/search.ts +229 -0
  401. package/src/adapters/facebook/subtitles.ts +44 -0
  402. package/src/adapters/fips/_shared.ts +109 -0
  403. package/src/adapters/fips/get.ts +186 -0
  404. package/src/adapters/fips/search.ts +195 -0
  405. package/src/adapters/freepatentsonline-web/_shared.ts +273 -0
  406. package/src/adapters/freepatentsonline-web/get.ts +144 -0
  407. package/src/adapters/freepatentsonline-web/search.ts +170 -0
  408. package/src/adapters/google-patents-bq/prior-art.yaml +80 -0
  409. package/src/adapters/google-patents-bq/search.yaml +97 -0
  410. package/src/adapters/google-patents-web/_shared.ts +242 -0
  411. package/src/adapters/google-patents-web/get.ts +224 -0
  412. package/src/adapters/google-patents-web/search.ts +196 -0
  413. package/src/adapters/inpi-br/_shared.ts +98 -0
  414. package/src/adapters/inpi-br/get.ts +193 -0
  415. package/src/adapters/inpi-br/search.ts +206 -0
  416. package/src/adapters/inpi-fr/get.yaml +62 -0
  417. package/src/adapters/inpi-fr/search.yaml +74 -0
  418. package/src/adapters/instagram/subtitles.ts +44 -0
  419. package/src/adapters/ipaustralia/get.yaml +67 -0
  420. package/src/adapters/ipaustralia/search.yaml +74 -0
  421. package/src/adapters/jpo/get.yaml +63 -0
  422. package/src/adapters/jpo/search.yaml +76 -0
  423. package/src/adapters/kipris/get.yaml +69 -0
  424. package/src/adapters/kipris/legal-status.yaml +58 -0
  425. package/src/adapters/kipris/search.yaml +79 -0
  426. package/src/adapters/lens/get.yaml +64 -0
  427. package/src/adapters/lens/search.yaml +82 -0
  428. package/src/adapters/mastodon/statuses.test.ts +82 -0
  429. package/src/adapters/mastodon/statuses.ts +208 -0
  430. package/src/adapters/patsnap/get.yaml +65 -0
  431. package/src/adapters/patsnap/search.yaml +77 -0
  432. package/src/adapters/pqai/prior-art.yaml +59 -0
  433. package/src/adapters/pqai/search.yaml +60 -0
  434. package/src/adapters/reddit/comments-tree.test.ts +79 -0
  435. package/src/adapters/reddit/comments.ts +159 -0
  436. package/src/adapters/threads/post.test.ts +64 -0
  437. package/src/adapters/threads/post.ts +366 -0
  438. package/src/adapters/threads/user.yaml +73 -0
  439. package/src/adapters/tiktok/subtitles.ts +44 -0
  440. package/src/adapters/twitter/accept.ts +5 -2
  441. package/src/adapters/twitter/browser-fallback.ts +138 -0
  442. package/src/adapters/twitter/browser-state.ts +74 -0
  443. package/src/adapters/twitter/client.ts +51 -21
  444. package/src/adapters/twitter/reply-dm.ts +5 -2
  445. package/src/adapters/twitter/reply.ts +1 -0
  446. package/src/adapters/twitter/search.ts +12 -38
  447. package/src/adapters/twitter/thread.test.ts +43 -0
  448. package/src/adapters/twitter/thread.ts +44 -2
  449. package/src/adapters/twitter/trending.ts +14 -95
  450. package/src/adapters/ukipo/info.yaml +43 -0
  451. package/src/adapters/uspto/get.yaml +67 -0
  452. package/src/adapters/uspto/legal-status.yaml +58 -0
  453. package/src/adapters/uspto/search.yaml +88 -0
  454. package/src/adapters/wipo-patentscope/info.yaml +43 -0
  455. package/src/adapters/xiaohongshu/browser-state.ts +95 -0
  456. package/src/adapters/xiaohongshu/comments.ts +29 -6
  457. package/src/adapters/xiaohongshu/download.ts +60 -11
  458. package/src/adapters/xiaohongshu/search.ts +18 -6
  459. package/src/adapters/xiaohongshu/trending.ts +112 -0
  460. package/src/adapters/yahoo/search.yaml +52 -0
  461. package/src/adapters/youtube/comments-microformat.test.ts +35 -0
  462. package/src/adapters/youtube/comments-tree.test.ts +74 -0
  463. package/src/adapters/youtube/comments.ts +166 -12
  464. package/src/adapters/youtube/subtitles.ts +44 -0
  465. package/src/adapters/yt-dlp/subtitles.ts +43 -0
  466. package/src/adapters/zhihu/answer-detail.test.ts +83 -0
  467. package/src/adapters/zhihu/answer-detail.ts +275 -0
  468. package/src/adapters/zhihu/comment-tree.test.ts +57 -0
  469. package/src/adapters/zhihu/comment.ts +186 -0
  470. package/src/adapters/zhihu/recommend.test.ts +65 -0
  471. package/src/adapters/zhihu/recommend.ts +207 -0
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @owner YouTube subtitle adapter.
3
+ * @does Extracts LLM-readable subtitle text from public YouTube video URLs.
4
+ * @needs `yt-dlp` on PATH and an accessible video subtitle track.
5
+ * @feeds Search-to-transcript social video analysis workflows.
6
+ * @breaks Upstream yt-dlp extractor changes can alter subtitle availability.
7
+ */
8
+
9
+ import { cli, Strategy } from "../../registry.js";
10
+ import { runVideoSubtitleExtraction } from "../../social/video-text.js";
11
+
12
+ cli({
13
+ site: "youtube",
14
+ name: "subtitles",
15
+ description:
16
+ "Extract LLM-readable subtitles from a YouTube video URL with optional browser-cookie reuse",
17
+ domain: "www.youtube.com",
18
+ strategy: Strategy.PUBLIC,
19
+ args: [
20
+ {
21
+ name: "url",
22
+ type: "str",
23
+ required: true,
24
+ positional: true,
25
+ description: "YouTube video URL",
26
+ format: "uri",
27
+ },
28
+ {
29
+ name: "languages",
30
+ type: "str",
31
+ default: "zh-Hans,zh,en",
32
+ description: "Comma-separated subtitle languages",
33
+ },
34
+ {
35
+ name: "cookies-from-browser",
36
+ type: "str",
37
+ description: "Browser name for yt-dlp --cookies-from-browser",
38
+ },
39
+ ],
40
+ columns: ["language", "text", "path"],
41
+ socialCapabilities: ["read", "media", "subtitles"],
42
+ defaultFormat: "json",
43
+ func: async (_page, kwargs) => runVideoSubtitleExtraction(kwargs),
44
+ });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @owner yt-dlp subtitle adapter.
3
+ * @does Extracts subtitle files from any yt-dlp-supported video URL.
4
+ * @needs Local yt-dlp binary and optional browser cookies for private or rate-limited videos.
5
+ * @feeds Unified social video-text workflows across YouTube, Bilibili, TikTok, Douyin, X, Instagram, and other sites.
6
+ * @breaks Returns an error when yt-dlp cannot produce subtitle files for the requested URL and languages.
7
+ */
8
+
9
+ import { cli, Strategy } from "../../registry.js";
10
+ import { runVideoSubtitleExtraction } from "../../social/video-text.js";
11
+
12
+ cli({
13
+ site: "yt-dlp",
14
+ name: "subtitles",
15
+ description:
16
+ "Extract subtitles from any yt-dlp-supported video URL with optional browser-cookie reuse",
17
+ strategy: Strategy.PUBLIC,
18
+ args: [
19
+ {
20
+ name: "url",
21
+ type: "str",
22
+ required: true,
23
+ positional: true,
24
+ description: "Video URL",
25
+ format: "uri",
26
+ },
27
+ {
28
+ name: "languages",
29
+ type: "str",
30
+ default: "zh-Hans,zh,en",
31
+ description: "Comma-separated subtitle languages",
32
+ },
33
+ {
34
+ name: "cookies-from-browser",
35
+ type: "str",
36
+ description: "Browser name for yt-dlp --cookies-from-browser",
37
+ },
38
+ ],
39
+ columns: ["language", "text", "path"],
40
+ socialCapabilities: ["read", "media", "subtitles"],
41
+ defaultFormat: "json",
42
+ func: async (_page, kwargs) => runVideoSubtitleExtraction(kwargs),
43
+ });
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ extractQuestionIdFromZhihuUrl,
4
+ mapZhihuAnswerDetail,
5
+ parseMaxContent,
6
+ parseZhihuAnswerTarget,
7
+ stripZhihuAnswerHtml,
8
+ } from "./answer-detail.js";
9
+
10
+ describe("zhihu answer-detail helpers", () => {
11
+ it("parses numeric, URL, and typed answer targets", () => {
12
+ expect(parseZhihuAnswerTarget("1937205528846655537")).toEqual({
13
+ answerId: "1937205528846655537",
14
+ questionId: "",
15
+ });
16
+ expect(
17
+ parseZhihuAnswerTarget(
18
+ "https://www.zhihu.com/question/123456/answer/789012",
19
+ ),
20
+ ).toEqual({ questionId: "123456", answerId: "789012" });
21
+ expect(parseZhihuAnswerTarget("answer:123456:789012")).toEqual({
22
+ questionId: "123456",
23
+ answerId: "789012",
24
+ });
25
+ });
26
+
27
+ it("rejects non-Zhihu answer targets", () => {
28
+ expect(() =>
29
+ parseZhihuAnswerTarget("https://example.com/question/1/answer/2"),
30
+ ).toThrow("Zhihu answer target");
31
+ expect(() => parseZhihuAnswerTarget("question:1")).toThrow(
32
+ "Zhihu answer target",
33
+ );
34
+ });
35
+
36
+ it("strips answer HTML without collapsing paragraphs", () => {
37
+ expect(
38
+ stripZhihuAnswerHtml(
39
+ "<p>Hello&nbsp;&amp;&nbsp;world</p><blockquote>A<br>B</blockquote>",
40
+ ),
41
+ ).toBe("Hello & world\n\nA\nB");
42
+ });
43
+
44
+ it("maps full answer payloads with explicit truncation only", () => {
45
+ const row = mapZhihuAnswerDetail(
46
+ {
47
+ content: "<p>abcdef</p>",
48
+ voteup_count: 7,
49
+ comment_count: 2,
50
+ created_time: 1,
51
+ updated_time: 2,
52
+ author: { name: "Ada" },
53
+ question: { id: 123, title: "Question" },
54
+ },
55
+ { answerId: "456", questionId: "" },
56
+ "",
57
+ parseMaxContent(3),
58
+ );
59
+ expect(row).toMatchObject({
60
+ id: "456",
61
+ author: "Ada",
62
+ votes: 7,
63
+ comments: 2,
64
+ question_id: "123",
65
+ question_title: "Question",
66
+ content: "abc",
67
+ });
68
+ });
69
+
70
+ it("extracts question ids from supported Zhihu URLs", () => {
71
+ expect(
72
+ extractQuestionIdFromZhihuUrl(
73
+ "https://www.zhihu.com/question/123/answer/456",
74
+ ),
75
+ ).toBe("123");
76
+ expect(
77
+ extractQuestionIdFromZhihuUrl("https://zhihu.com/question/123"),
78
+ ).toBe("123");
79
+ expect(
80
+ extractQuestionIdFromZhihuUrl("https://example.com/question/123"),
81
+ ).toBe("");
82
+ });
83
+ });
@@ -0,0 +1,275 @@
1
+ /**
2
+ * @owner src/adapters/zhihu/answer-detail.ts
3
+ * @does Register full-content Zhihu answer detail reader.
4
+ * @needs Logged-in Zhihu browser session, answer API payload, exact answer target parsing.
5
+ * @feeds surface coverage ledger, long-form answer extraction workflows, reference parity checks.
6
+ * @breaks Zhihu answer URL shape or API content field drift can block answer detail extraction.
7
+ */
8
+
9
+ import { cli, Strategy } from "../../registry.js";
10
+ import type { IPage } from "../../types.js";
11
+
12
+ const ANSWER_ID_RE = /^\d+$/;
13
+ const ANSWER_TYPED_RE = /^answer:(\d+):(\d+)$/;
14
+ const ANSWER_PATH_RE = /^\/question\/(\d+)\/answer\/(\d+)\/?$/;
15
+ const BARE_ANSWER_PATH_RE = /^\/answer\/(\d+)\/?$/;
16
+ const QUESTION_PATH_RE = /^\/question\/(\d+)\/?$/;
17
+ const QUESTION_API_PATH_RE = /^\/api\/v4\/questions\/(\d+)\/?$/;
18
+
19
+ interface ZhihuAnswerTarget {
20
+ answerId: string;
21
+ questionId: string;
22
+ }
23
+
24
+ interface ZhihuAnswerPayload {
25
+ id?: unknown;
26
+ content?: unknown;
27
+ voteup_count?: unknown;
28
+ comment_count?: unknown;
29
+ created_time?: unknown;
30
+ updated_time?: unknown;
31
+ author?: { name?: unknown };
32
+ question?: { id?: unknown; title?: unknown; url?: unknown };
33
+ error?: { message?: unknown };
34
+ error_msg?: unknown;
35
+ message?: unknown;
36
+ }
37
+
38
+ interface BrowserFetchResult {
39
+ ok: boolean;
40
+ status: number;
41
+ text: string;
42
+ }
43
+
44
+ function stringField(value: unknown): string {
45
+ return value === undefined || value === null ? "" : String(value);
46
+ }
47
+
48
+ function countField(value: unknown): number {
49
+ const n = Number(value);
50
+ return Number.isInteger(n) && n >= 0 ? n : 0;
51
+ }
52
+
53
+ function isoFromUnixSeconds(value: unknown): string {
54
+ const n = Number(value);
55
+ return Number.isFinite(n) && n > 0 ? new Date(n * 1000).toISOString() : "";
56
+ }
57
+
58
+ function pageOf(page: unknown): IPage {
59
+ if (!page) throw new Error("Zhihu answer-detail requires a browser page.");
60
+ return page as IPage;
61
+ }
62
+
63
+ export function stripZhihuAnswerHtml(value: unknown): string {
64
+ return stringField(value)
65
+ .replace(/<br\s*\/?\s*>/gi, "\n")
66
+ .replace(/<\/(?:p|div|h[1-6]|li|blockquote)>/gi, "\n\n")
67
+ .replace(/<[^>]+>/g, "")
68
+ .replace(/&nbsp;/g, " ")
69
+ .replace(/&lt;/g, "<")
70
+ .replace(/&gt;/g, ">")
71
+ .replace(/&amp;/g, "&")
72
+ .replace(/&quot;/g, '"')
73
+ .replace(/&#39;/g, "'")
74
+ .replace(/\n{3,}/g, "\n\n")
75
+ .trim();
76
+ }
77
+
78
+ export function parseZhihuAnswerTarget(input: unknown): ZhihuAnswerTarget {
79
+ const value = stringField(input).trim();
80
+ if (!value) {
81
+ throw new Error(
82
+ "Zhihu answer target must be a numeric id, answer URL, or answer:<questionId>:<answerId>.",
83
+ );
84
+ }
85
+ if (ANSWER_ID_RE.test(value)) return { answerId: value, questionId: "" };
86
+ const typed = value.match(ANSWER_TYPED_RE);
87
+ if (typed) return { questionId: typed[1], answerId: typed[2] };
88
+ try {
89
+ const url = new URL(value);
90
+ if (
91
+ url.protocol !== "https:" ||
92
+ url.username ||
93
+ url.password ||
94
+ url.port ||
95
+ (url.hostname !== "www.zhihu.com" && url.hostname !== "zhihu.com")
96
+ ) {
97
+ throw new Error("invalid host");
98
+ }
99
+ let match = url.pathname.match(ANSWER_PATH_RE);
100
+ if (match) return { questionId: match[1], answerId: match[2] };
101
+ match = url.pathname.match(BARE_ANSWER_PATH_RE);
102
+ if (match) return { questionId: "", answerId: match[1] };
103
+ } catch {
104
+ throw new Error(
105
+ "Zhihu answer target must be a numeric id, answer URL, or answer:<questionId>:<answerId>.",
106
+ );
107
+ }
108
+ throw new Error(
109
+ "Zhihu answer target must be a numeric id, answer URL, or answer:<questionId>:<answerId>.",
110
+ );
111
+ }
112
+
113
+ export function extractQuestionIdFromZhihuUrl(input: unknown): string {
114
+ const value = stringField(input).trim();
115
+ if (!value) return "";
116
+ if (!URL.canParse(value)) return "";
117
+ const url = new URL(value);
118
+ if (
119
+ url.protocol !== "https:" ||
120
+ (url.hostname !== "www.zhihu.com" && url.hostname !== "zhihu.com")
121
+ ) {
122
+ return "";
123
+ }
124
+ return (
125
+ url.pathname.match(ANSWER_PATH_RE)?.[1] ??
126
+ url.pathname.match(QUESTION_PATH_RE)?.[1] ??
127
+ url.pathname.match(QUESTION_API_PATH_RE)?.[1] ??
128
+ ""
129
+ );
130
+ }
131
+
132
+ export function parseMaxContent(value: unknown): number {
133
+ const n = value === undefined || value === null ? 0 : Number(value);
134
+ if (!Number.isInteger(n) || n < 0) {
135
+ throw new Error("--max-content must be a non-negative integer.");
136
+ }
137
+ return n;
138
+ }
139
+
140
+ async function browserFetchJson(page: IPage, url: string): Promise<unknown> {
141
+ const raw = await page.evaluate(`(async () => {
142
+ const response = await fetch(${JSON.stringify(url)}, {
143
+ credentials: "include",
144
+ headers: { Accept: "application/json" }
145
+ });
146
+ const text = await response.text();
147
+ return JSON.stringify({ ok: response.ok, status: response.status, text });
148
+ })()`);
149
+ const result = JSON.parse(stringField(raw)) as BrowserFetchResult;
150
+ if (!result.ok) {
151
+ if (result.status === 401 || result.status === 403) {
152
+ throw new Error(
153
+ `Zhihu answer-detail requires Zhihu cookies (HTTP ${result.status}).`,
154
+ );
155
+ }
156
+ if (result.status === 404) {
157
+ throw new Error("No Zhihu answer was found for the requested id.");
158
+ }
159
+ throw new Error(
160
+ `Zhihu answer-detail request failed (HTTP ${result.status}).`,
161
+ );
162
+ }
163
+ try {
164
+ return JSON.parse(result.text);
165
+ } catch (err) {
166
+ throw new Error(
167
+ `Zhihu answer-detail returned malformed JSON: ${
168
+ err instanceof Error ? err.message : String(err)
169
+ }`,
170
+ );
171
+ }
172
+ }
173
+
174
+ export function mapZhihuAnswerDetail(
175
+ payload: ZhihuAnswerPayload,
176
+ target: ZhihuAnswerTarget,
177
+ currentQuestionId: string,
178
+ maxContent: number,
179
+ ): Record<string, unknown> {
180
+ if (payload.error || payload.error_msg || payload.message) {
181
+ throw new Error(
182
+ `Zhihu answer-detail returned an error payload: ${stringField(
183
+ payload.error?.message ?? payload.error_msg ?? payload.message,
184
+ )}`,
185
+ );
186
+ }
187
+ if (!Object.prototype.hasOwnProperty.call(payload, "content")) {
188
+ throw new Error("Zhihu answer-detail payload did not include content.");
189
+ }
190
+ const question = payload.question ?? {};
191
+ const questionId =
192
+ target.questionId ||
193
+ currentQuestionId ||
194
+ extractQuestionIdFromZhihuUrl(question.url) ||
195
+ stringField(question.id);
196
+ const stripped = stripZhihuAnswerHtml(payload.content);
197
+ const content =
198
+ maxContent > 0 && stripped.length > maxContent
199
+ ? stripped.slice(0, maxContent)
200
+ : stripped;
201
+ return {
202
+ id: target.answerId || stringField(payload.id),
203
+ author: stringField(payload.author?.name) || "anonymous",
204
+ votes: countField(payload.voteup_count),
205
+ comments: countField(payload.comment_count),
206
+ question_id: questionId,
207
+ question_title: stringField(question.title),
208
+ url: questionId
209
+ ? `https://www.zhihu.com/question/${questionId}/answer/${target.answerId}`
210
+ : `https://www.zhihu.com/answer/${target.answerId}`,
211
+ created_at: isoFromUnixSeconds(payload.created_time),
212
+ updated_at: isoFromUnixSeconds(payload.updated_time),
213
+ content,
214
+ };
215
+ }
216
+
217
+ cli({
218
+ site: "zhihu",
219
+ name: "answer-detail",
220
+ description: "知乎单个回答完整内容(按 answer ID 获取)",
221
+ domain: "www.zhihu.com",
222
+ strategy: Strategy.COOKIE,
223
+ browser: true,
224
+ args: [
225
+ {
226
+ name: "id",
227
+ type: "str",
228
+ required: true,
229
+ positional: true,
230
+ description: "Answer ID, full Zhihu answer URL, or typed target",
231
+ },
232
+ {
233
+ name: "max-content",
234
+ type: "int",
235
+ default: 0,
236
+ description:
237
+ "Optional cap on stripped content length; 0 returns full content",
238
+ },
239
+ ],
240
+ columns: [
241
+ "id",
242
+ "author",
243
+ "votes",
244
+ "comments",
245
+ "question_id",
246
+ "question_title",
247
+ "url",
248
+ "created_at",
249
+ "updated_at",
250
+ "content",
251
+ ],
252
+ func: async (page, kwargs) => {
253
+ const browser = pageOf(page);
254
+ const target = parseZhihuAnswerTarget(kwargs.id);
255
+ const maxContent = parseMaxContent(kwargs["max-content"]);
256
+ await browser.goto(`https://www.zhihu.com/answer/${target.answerId}`);
257
+ const currentQuestionId = extractQuestionIdFromZhihuUrl(
258
+ await browser.url(),
259
+ );
260
+ const apiUrl = new URL(
261
+ `https://www.zhihu.com/api/v4/answers/${target.answerId}`,
262
+ );
263
+ apiUrl.searchParams.set(
264
+ "include",
265
+ "content,voteup_count,comment_count,author,created_time,updated_time,question",
266
+ );
267
+ const payload = (await browserFetchJson(
268
+ browser,
269
+ apiUrl.toString(),
270
+ )) as ZhihuAnswerPayload;
271
+ return [
272
+ mapZhihuAnswerDetail(payload, target, currentQuestionId, maxContent),
273
+ ];
274
+ },
275
+ });
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { normalizeCommentRows } from "../../social/comments.js";
4
+ import { extractZhihuCommentRows } from "./comment.js";
5
+
6
+ describe("zhihu comment tree extraction", () => {
7
+ it("normalizes root comments and child comments", () => {
8
+ const rows = normalizeCommentRows(
9
+ extractZhihuCommentRows({
10
+ data: [
11
+ {
12
+ id: "root",
13
+ author: { name: "Alice" },
14
+ content: "<p>Root</p>",
15
+ like_count: 4,
16
+ child_comment_count: 1,
17
+ created_time: 1,
18
+ child_comments: [
19
+ {
20
+ id: "child",
21
+ author: { name: "Bob" },
22
+ content: "<p>Child</p>",
23
+ like_count: 2,
24
+ created_time: 2,
25
+ reply_to_author: { name: "Alice" },
26
+ },
27
+ ],
28
+ },
29
+ ],
30
+ }),
31
+ { platform: "zhihu", contentId: "answer:123" },
32
+ );
33
+
34
+ expect(rows).toEqual([
35
+ expect.objectContaining({
36
+ comment_id: "root",
37
+ parent_id: "",
38
+ depth: 0,
39
+ path: "0001",
40
+ author: "Alice",
41
+ text: "Root",
42
+ likes: 4,
43
+ replies: 1,
44
+ }),
45
+ expect.objectContaining({
46
+ comment_id: "child",
47
+ parent_id: "root",
48
+ depth: 1,
49
+ path: "0001.0001",
50
+ author: "Bob",
51
+ text: "Child",
52
+ likes: 2,
53
+ reply_to: "Alice",
54
+ }),
55
+ ]);
56
+ });
57
+ });
@@ -0,0 +1,186 @@
1
+ /**
2
+ * @owner Zhihu comment adapter.
3
+ * @does Reads Zhihu answer/article comments and normalizes nested child comments.
4
+ * @needs Zhihu comment_v5 API and z_c0 cookie for authenticated access.
5
+ * @feeds Social comment tree analysis for Zhihu answers and articles.
6
+ * @breaks Zhihu API shape, auth cookie requirements, or anti-bot policy changes.
7
+ */
8
+
9
+ import { USER_AGENT } from "../../constants.js";
10
+ import {
11
+ loadCookiesWithCDP,
12
+ formatCookieHeader,
13
+ } from "../../engine/cookies.js";
14
+ import { cli, Strategy } from "../../registry.js";
15
+ import { normalizeCommentRows } from "../../social/comments.js";
16
+
17
+ interface ZhihuAuthor {
18
+ name?: string;
19
+ }
20
+
21
+ interface ZhihuComment {
22
+ id?: string | number;
23
+ author?: ZhihuAuthor;
24
+ content?: string;
25
+ like_count?: number;
26
+ child_comment_count?: number;
27
+ created_time?: number;
28
+ child_comments?: ZhihuComment[];
29
+ reply_to_author?: ZhihuAuthor;
30
+ }
31
+
32
+ interface ZhihuCommentResponse {
33
+ data?: ZhihuComment[];
34
+ }
35
+
36
+ function stripHtml(value: string): string {
37
+ return value
38
+ .replace(/<[^>]+>/g, "")
39
+ .replace(/\s+/g, " ")
40
+ .trim();
41
+ }
42
+
43
+ function zhihuCreated(value: number | undefined): string {
44
+ return value ? new Date(value * 1000).toISOString() : "";
45
+ }
46
+
47
+ function zhihuCommentToRow(
48
+ item: ZhihuComment,
49
+ parentId = "",
50
+ ): Record<string, unknown> {
51
+ const id = item.id === undefined ? "" : String(item.id);
52
+ return {
53
+ id,
54
+ parent_id: parentId,
55
+ author: item.author?.name ?? "",
56
+ content: stripHtml(item.content ?? ""),
57
+ text: stripHtml(item.content ?? ""),
58
+ likes: item.like_count ?? 0,
59
+ replies: item.child_comment_count ?? item.child_comments?.length ?? 0,
60
+ created: zhihuCreated(item.created_time),
61
+ reply_to: item.reply_to_author?.name ?? "",
62
+ };
63
+ }
64
+
65
+ export function extractZhihuCommentRows(
66
+ response: unknown,
67
+ ): Array<Record<string, unknown>> {
68
+ const data = (response as ZhihuCommentResponse).data;
69
+ if (!Array.isArray(data)) {
70
+ throw new Error("Unexpected Zhihu comments response");
71
+ }
72
+ const rows: Array<Record<string, unknown>> = [];
73
+ for (const root of data) {
74
+ const rootRow = zhihuCommentToRow(root);
75
+ rows.push(rootRow);
76
+ const rootId = String(rootRow.id ?? "");
77
+ for (const child of root.child_comments ?? []) {
78
+ rows.push(zhihuCommentToRow(child, rootId));
79
+ }
80
+ }
81
+ return rows;
82
+ }
83
+
84
+ async function zhihuHeaders(): Promise<Record<string, string>> {
85
+ const headers: Record<string, string> = {
86
+ "User-Agent": USER_AGENT,
87
+ Accept: "application/json",
88
+ Referer: "https://www.zhihu.com/",
89
+ };
90
+ const cookies = await loadCookiesWithCDP("zhihu", "www.zhihu.com");
91
+ if (cookies) headers.Cookie = formatCookieHeader(cookies);
92
+ return headers;
93
+ }
94
+
95
+ async function fetchZhihuJson(
96
+ url: string,
97
+ headers: Record<string, string>,
98
+ ): Promise<unknown> {
99
+ const response = await fetch(url, { headers });
100
+ if (!response.ok) {
101
+ throw new Error(`Zhihu comments request failed: ${response.status}`);
102
+ }
103
+ return response.json();
104
+ }
105
+
106
+ cli({
107
+ site: "zhihu",
108
+ name: "comment",
109
+ description: "Get Zhihu comments with normalized nested reply hierarchy",
110
+ domain: "www.zhihu.com",
111
+ strategy: Strategy.COOKIE,
112
+ args: [
113
+ {
114
+ name: "type",
115
+ required: true,
116
+ positional: true,
117
+ description: "Content type: answer or article",
118
+ choices: ["answer", "article"],
119
+ },
120
+ {
121
+ name: "id",
122
+ required: true,
123
+ positional: true,
124
+ description: "Answer or article ID",
125
+ "x-unicli-kind": "id",
126
+ },
127
+ {
128
+ name: "limit",
129
+ type: "int",
130
+ default: 20,
131
+ description: "Number of root comments",
132
+ },
133
+ {
134
+ name: "with-replies",
135
+ type: "bool",
136
+ default: false,
137
+ description:
138
+ "Fetch child replies for root comments that omit child_comments",
139
+ },
140
+ ],
141
+ columns: [
142
+ "platform",
143
+ "content_id",
144
+ "comment_id",
145
+ "parent_id",
146
+ "depth",
147
+ "path",
148
+ "author",
149
+ "text",
150
+ "likes",
151
+ "replies",
152
+ "created",
153
+ "content",
154
+ "reply_to",
155
+ ],
156
+ socialCapabilities: ["read", "comments", "comment_replies"],
157
+ func: async (_page, kwargs) => {
158
+ const type = String(kwargs.type);
159
+ const id = String(kwargs.id);
160
+ const limit = Number(kwargs.limit ?? 20);
161
+ const withReplies = Boolean(kwargs["with-replies"]);
162
+ const headers = await zhihuHeaders();
163
+ const rootUrl = `https://www.zhihu.com/api/v4/comment_v5/${encodeURIComponent(type)}s/${encodeURIComponent(id)}/root_comment?limit=${encodeURIComponent(String(limit))}&offset=0&order_by=score`;
164
+ const rootJson = await fetchZhihuJson(rootUrl, headers);
165
+ const rows = extractZhihuCommentRows(rootJson);
166
+
167
+ if (withReplies) {
168
+ const roots = (rootJson as ZhihuCommentResponse).data ?? [];
169
+ for (const root of roots) {
170
+ const rootId = root.id === undefined ? "" : String(root.id);
171
+ if (!rootId || (root.child_comments?.length ?? 0) > 0) continue;
172
+ if ((root.child_comment_count ?? 0) <= 0) continue;
173
+ const childUrl = `https://www.zhihu.com/api/v4/comment_v5/comment/${encodeURIComponent(rootId)}/child_comment?limit=20&offset=0&order_by=score`;
174
+ const childJson = await fetchZhihuJson(childUrl, headers);
175
+ for (const child of (childJson as ZhihuCommentResponse).data ?? []) {
176
+ rows.push(zhihuCommentToRow(child, rootId));
177
+ }
178
+ }
179
+ }
180
+
181
+ return normalizeCommentRows(rows, {
182
+ platform: "zhihu",
183
+ contentId: `${type}:${id}`,
184
+ });
185
+ },
186
+ });