@jackwener/opencli 1.6.1 → 1.6.2

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 (384) hide show
  1. package/CONTRIBUTING.md +1 -1
  2. package/README.md +27 -45
  3. package/README.zh-CN.md +32 -34
  4. package/autoresearch/browse-tasks.json +18 -20
  5. package/autoresearch/commands/debug.ts +163 -0
  6. package/autoresearch/commands/fix.ts +145 -0
  7. package/autoresearch/commands/plan.ts +88 -0
  8. package/autoresearch/commands/run.ts +138 -0
  9. package/autoresearch/config.ts +82 -0
  10. package/autoresearch/engine.ts +359 -0
  11. package/autoresearch/eval-all.ts +127 -0
  12. package/autoresearch/eval-browse.ts +1 -1
  13. package/autoresearch/eval-publish.ts +238 -0
  14. package/autoresearch/eval-save.ts +249 -0
  15. package/autoresearch/eval-skill.ts +14 -8
  16. package/autoresearch/eval-v2ex.ts +220 -0
  17. package/autoresearch/eval-zhihu.ts +230 -0
  18. package/autoresearch/logger.ts +69 -0
  19. package/autoresearch/presets/combined-reliability.ts +27 -0
  20. package/autoresearch/presets/index.ts +23 -0
  21. package/autoresearch/presets/operate-reliability.ts +24 -0
  22. package/autoresearch/presets/save-reliability.ts +26 -0
  23. package/autoresearch/presets/skill-quality.ts +20 -0
  24. package/autoresearch/presets/v2ex-reliability.ts +24 -0
  25. package/autoresearch/presets/zhihu-reliability.ts +25 -0
  26. package/autoresearch/publish-tasks.json +345 -0
  27. package/autoresearch/run-save.sh +11 -0
  28. package/autoresearch/save-adapters/xhs-explore-deep.ts +64 -0
  29. package/autoresearch/save-adapters/xhs-note-comments.ts +61 -0
  30. package/autoresearch/save-adapters/xhs-search-full.ts +62 -0
  31. package/autoresearch/save-adapters/zhihu-hot-detail.ts +52 -0
  32. package/autoresearch/save-adapters/zhihu-question-full.ts +57 -0
  33. package/autoresearch/save-adapters/zhihu-search-detail.ts +53 -0
  34. package/autoresearch/save-tasks.json +281 -0
  35. package/autoresearch/v2ex-tasks.json +899 -0
  36. package/autoresearch/zhihu-tasks.json +848 -0
  37. package/dist/browser/base-page.d.ts +4 -2
  38. package/dist/browser/base-page.js +37 -4
  39. package/dist/browser/bridge.js +10 -8
  40. package/dist/browser/cdp.js +2 -6
  41. package/dist/browser/daemon-client.d.ts +11 -1
  42. package/dist/browser/daemon-client.js +3 -0
  43. package/dist/browser/dom-helpers.d.ts +4 -2
  44. package/dist/browser/dom-helpers.js +42 -31
  45. package/dist/browser/dom-snapshot.js +23 -1
  46. package/dist/browser/page.d.ts +7 -2
  47. package/dist/browser/page.js +112 -30
  48. package/dist/browser.test.js +1 -1
  49. package/dist/build-manifest.d.ts +1 -0
  50. package/dist/build-manifest.js +1 -0
  51. package/dist/cli-manifest.json +1135 -184
  52. package/dist/cli.d.ts +2 -0
  53. package/dist/cli.js +48 -7
  54. package/dist/cli.test.d.ts +1 -0
  55. package/dist/cli.test.js +88 -0
  56. package/dist/clis/1688/item.d.ts +70 -0
  57. package/dist/clis/1688/item.js +187 -0
  58. package/dist/clis/1688/item.test.d.ts +1 -0
  59. package/dist/clis/1688/item.test.js +67 -0
  60. package/dist/clis/1688/search.d.ts +56 -0
  61. package/dist/clis/1688/search.js +309 -0
  62. package/dist/clis/1688/search.test.d.ts +1 -0
  63. package/dist/clis/1688/search.test.js +75 -0
  64. package/dist/clis/1688/shared.d.ts +112 -0
  65. package/dist/clis/1688/shared.js +514 -0
  66. package/dist/clis/1688/shared.test.d.ts +1 -0
  67. package/dist/clis/1688/shared.test.js +57 -0
  68. package/dist/clis/1688/store.d.ts +45 -0
  69. package/dist/clis/1688/store.js +226 -0
  70. package/dist/clis/1688/store.test.d.ts +1 -0
  71. package/dist/clis/1688/store.test.js +62 -0
  72. package/dist/clis/amazon/bestsellers.d.ts +0 -20
  73. package/dist/clis/amazon/bestsellers.js +6 -129
  74. package/dist/clis/amazon/bestsellers.test.js +12 -3
  75. package/dist/clis/amazon/movers-shakers.d.ts +1 -0
  76. package/dist/clis/amazon/movers-shakers.js +7 -0
  77. package/dist/clis/amazon/new-releases.d.ts +1 -0
  78. package/dist/clis/amazon/new-releases.js +7 -0
  79. package/dist/clis/amazon/rankings.d.ts +59 -0
  80. package/dist/clis/amazon/rankings.js +226 -0
  81. package/dist/clis/amazon/rankings.test.d.ts +1 -0
  82. package/dist/clis/amazon/rankings.test.js +41 -0
  83. package/dist/clis/amazon/shared.d.ts +11 -0
  84. package/dist/clis/amazon/shared.js +121 -11
  85. package/dist/clis/amazon/shared.test.js +11 -0
  86. package/dist/clis/bilibili/comments.js +2 -2
  87. package/dist/clis/bilibili/comments.test.js +3 -2
  88. package/dist/clis/bilibili/download.js +2 -1
  89. package/dist/clis/bilibili/subtitle.js +4 -3
  90. package/dist/clis/bilibili/subtitle.test.js +2 -1
  91. package/dist/clis/bilibili/utils.d.ts +5 -0
  92. package/dist/clis/bilibili/utils.js +30 -0
  93. package/dist/clis/bilibili/utils.test.d.ts +1 -0
  94. package/dist/clis/bilibili/utils.test.js +17 -0
  95. package/dist/clis/douban/marks.js +1 -1
  96. package/dist/clis/douban/subject.yaml +50 -19
  97. package/dist/clis/doubao/utils.js +32 -12
  98. package/dist/clis/douyin/_shared/browser-fetch.test.js +0 -1
  99. package/dist/clis/douyin/_shared/transcode.test.js +0 -2
  100. package/dist/clis/douyin/draft.test.js +0 -2
  101. package/dist/clis/facebook/search.test.js +0 -2
  102. package/dist/clis/gemini/ask.js +9 -3
  103. package/dist/clis/gemini/ask.test.d.ts +1 -0
  104. package/dist/clis/gemini/ask.test.js +100 -0
  105. package/dist/clis/gemini/reply-state.test.d.ts +1 -0
  106. package/dist/clis/gemini/reply-state.test.js +641 -0
  107. package/dist/clis/gemini/utils.d.ts +44 -1
  108. package/dist/clis/gemini/utils.js +528 -61
  109. package/dist/clis/gemini/utils.test.js +149 -2
  110. package/dist/clis/hupu/detail.d.ts +1 -0
  111. package/dist/clis/hupu/detail.js +72 -0
  112. package/dist/clis/hupu/hot.yaml +43 -0
  113. package/dist/clis/hupu/like.d.ts +1 -0
  114. package/dist/clis/hupu/like.js +75 -0
  115. package/dist/clis/hupu/reply.d.ts +1 -0
  116. package/dist/clis/hupu/reply.js +71 -0
  117. package/dist/clis/hupu/search.d.ts +1 -0
  118. package/dist/clis/hupu/search.js +59 -0
  119. package/dist/clis/hupu/unlike.d.ts +1 -0
  120. package/dist/clis/hupu/unlike.js +75 -0
  121. package/dist/clis/hupu/utils.d.ts +20 -0
  122. package/dist/clis/hupu/utils.js +319 -0
  123. package/dist/clis/instagram/_shared/private-publish.d.ts +138 -0
  124. package/dist/clis/instagram/_shared/private-publish.js +1030 -0
  125. package/dist/clis/instagram/_shared/private-publish.test.d.ts +1 -0
  126. package/dist/clis/instagram/_shared/private-publish.test.js +705 -0
  127. package/dist/clis/instagram/_shared/protocol-capture.d.ts +26 -0
  128. package/dist/clis/instagram/_shared/protocol-capture.js +282 -0
  129. package/dist/clis/instagram/_shared/protocol-capture.test.d.ts +1 -0
  130. package/dist/clis/instagram/_shared/protocol-capture.test.js +114 -0
  131. package/dist/clis/instagram/_shared/runtime-info.d.ts +9 -0
  132. package/dist/clis/instagram/_shared/runtime-info.js +81 -0
  133. package/dist/clis/instagram/note.d.ts +1 -0
  134. package/dist/clis/instagram/note.js +222 -0
  135. package/dist/clis/instagram/note.test.d.ts +1 -0
  136. package/dist/clis/instagram/note.test.js +81 -0
  137. package/dist/clis/instagram/post.d.ts +4 -0
  138. package/dist/clis/instagram/post.js +1496 -0
  139. package/dist/clis/instagram/post.test.d.ts +1 -0
  140. package/dist/clis/instagram/post.test.js +1647 -0
  141. package/dist/clis/instagram/reel.d.ts +1 -0
  142. package/dist/clis/instagram/reel.js +826 -0
  143. package/dist/clis/instagram/reel.test.d.ts +1 -0
  144. package/dist/clis/instagram/reel.test.js +167 -0
  145. package/dist/clis/instagram/story.d.ts +1 -0
  146. package/dist/clis/instagram/story.js +115 -0
  147. package/dist/clis/instagram/story.test.d.ts +1 -0
  148. package/dist/clis/instagram/story.test.js +167 -0
  149. package/dist/clis/sinafinance/stock-rank.d.ts +4 -0
  150. package/dist/clis/sinafinance/stock-rank.js +65 -0
  151. package/dist/clis/substack/utils.test.js +0 -2
  152. package/dist/clis/twitter/post.js +72 -45
  153. package/dist/clis/twitter/post.test.d.ts +1 -0
  154. package/dist/clis/twitter/post.test.js +116 -0
  155. package/dist/clis/twitter/reply.d.ts +12 -0
  156. package/dist/clis/twitter/reply.js +257 -35
  157. package/dist/clis/twitter/reply.test.d.ts +1 -0
  158. package/dist/clis/twitter/reply.test.js +151 -0
  159. package/dist/clis/xianyu/chat.d.ts +7 -0
  160. package/dist/clis/xianyu/chat.js +146 -0
  161. package/dist/clis/xianyu/chat.test.d.ts +1 -0
  162. package/dist/clis/xianyu/chat.test.js +15 -0
  163. package/dist/clis/xianyu/item.d.ts +7 -0
  164. package/dist/clis/xianyu/item.js +152 -0
  165. package/dist/clis/xianyu/item.test.d.ts +1 -0
  166. package/dist/clis/xianyu/item.test.js +56 -0
  167. package/dist/clis/xianyu/search.d.ts +10 -0
  168. package/dist/clis/xianyu/search.js +134 -0
  169. package/dist/clis/xianyu/search.test.d.ts +1 -0
  170. package/dist/clis/xianyu/search.test.js +17 -0
  171. package/dist/clis/xianyu/utils.d.ts +1 -0
  172. package/dist/clis/xianyu/utils.js +8 -0
  173. package/dist/clis/xiaoe/catalog.yaml +129 -0
  174. package/dist/clis/xiaoe/content.yaml +43 -0
  175. package/dist/clis/xiaoe/courses.yaml +73 -0
  176. package/dist/clis/xiaoe/detail.yaml +39 -0
  177. package/dist/clis/xiaoe/play-url.yaml +124 -0
  178. package/dist/clis/xiaohongshu/comments.test.js +0 -2
  179. package/dist/clis/xiaohongshu/creator-note-detail.test.js +0 -2
  180. package/dist/clis/xiaohongshu/creator-notes.test.js +0 -2
  181. package/dist/clis/xiaohongshu/download.test.js +0 -2
  182. package/dist/clis/xiaohongshu/note.test.js +0 -2
  183. package/dist/clis/xiaohongshu/publish.test.js +0 -2
  184. package/dist/clis/xiaohongshu/search.js +29 -20
  185. package/dist/clis/xiaohongshu/search.test.js +56 -48
  186. package/dist/clis/yuanbao/ask.d.ts +21 -0
  187. package/dist/clis/yuanbao/ask.js +427 -0
  188. package/dist/clis/yuanbao/ask.test.d.ts +1 -0
  189. package/dist/clis/yuanbao/ask.test.js +124 -0
  190. package/dist/clis/yuanbao/new.d.ts +1 -0
  191. package/dist/clis/yuanbao/new.js +70 -0
  192. package/dist/clis/yuanbao/new.test.d.ts +1 -0
  193. package/dist/clis/yuanbao/new.test.js +30 -0
  194. package/dist/clis/yuanbao/shared.d.ts +13 -0
  195. package/dist/clis/yuanbao/shared.js +49 -0
  196. package/dist/clis/zhihu/question.js +30 -19
  197. package/dist/clis/zhihu/question.test.js +34 -16
  198. package/dist/commanderAdapter.js +8 -4
  199. package/dist/commanderAdapter.test.js +42 -0
  200. package/dist/completion.js +3 -1
  201. package/dist/completion.test.d.ts +1 -0
  202. package/dist/completion.test.js +23 -0
  203. package/dist/doctor.js +1 -1
  204. package/dist/electron-apps.d.ts +2 -0
  205. package/dist/electron-apps.js +7 -1
  206. package/dist/errors.js +1 -1
  207. package/dist/execution.js +25 -35
  208. package/dist/explore.js +1 -1
  209. package/dist/launcher.d.ts +4 -0
  210. package/dist/launcher.js +64 -8
  211. package/dist/launcher.test.js +88 -7
  212. package/dist/output.d.ts +2 -0
  213. package/dist/output.js +10 -1
  214. package/dist/output.test.d.ts +0 -3
  215. package/dist/output.test.js +59 -92
  216. package/dist/pipeline/executor.test.js +0 -2
  217. package/dist/pipeline/steps/download.test.js +0 -2
  218. package/dist/registry.d.ts +2 -0
  219. package/dist/serialization.d.ts +1 -0
  220. package/dist/serialization.js +1 -0
  221. package/dist/types.d.ts +9 -2
  222. package/docs/.vitepress/config.mts +4 -0
  223. package/docs/adapters/browser/1688.md +52 -0
  224. package/docs/adapters/browser/36kr.md +2 -1
  225. package/docs/adapters/browser/doubao.md +5 -1
  226. package/docs/adapters/browser/hupu.md +53 -0
  227. package/docs/adapters/browser/sinafinance.md +32 -2
  228. package/docs/adapters/browser/weibo.md +6 -1
  229. package/docs/adapters/browser/wikipedia.md +2 -0
  230. package/docs/adapters/browser/xianyu.md +42 -0
  231. package/docs/adapters/browser/xiaoe.md +44 -0
  232. package/docs/adapters/browser/yuanbao.md +64 -0
  233. package/docs/adapters/index.md +14 -5
  234. package/docs/comparison.md +1 -1
  235. package/docs/developer/ai-workflow.md +2 -2
  236. package/docs/developer/contributing.md +1 -1
  237. package/docs/developer/testing.md +2 -0
  238. package/docs/guide/plugins.md +1 -0
  239. package/docs/guide/troubleshooting.md +11 -0
  240. package/docs/superpowers/specs/2026-04-03-v2ex-autoresearch-design.md +41 -0
  241. package/docs/zh/guide/plugins.md +1 -0
  242. package/extension/dist/background.js +1127 -0
  243. package/extension/src/background.test.ts +39 -0
  244. package/extension/src/background.ts +223 -34
  245. package/extension/src/cdp.ts +194 -4
  246. package/extension/src/protocol.ts +22 -1
  247. package/package.json +3 -2
  248. package/scripts/postinstall.js +1 -1
  249. package/skills/opencli-explorer/SKILL.md +1 -1
  250. package/skills/opencli-oneshot/SKILL.md +2 -2
  251. package/skills/opencli-operate/SKILL.md +120 -27
  252. package/skills/opencli-usage/SKILL.md +31 -20
  253. package/skills/opencli-usage/browser.md +114 -16
  254. package/skills/opencli-usage/public-api.md +32 -3
  255. package/skills/smart-search/SKILL.md +156 -0
  256. package/skills/smart-search/references/sources-ai.md +74 -0
  257. package/skills/smart-search/references/sources-info.md +43 -0
  258. package/skills/smart-search/references/sources-media.md +50 -0
  259. package/skills/smart-search/references/sources-other.md +42 -0
  260. package/skills/smart-search/references/sources-shopping.md +31 -0
  261. package/skills/smart-search/references/sources-social.md +51 -0
  262. package/skills/smart-search/references/sources-tech.md +42 -0
  263. package/skills/smart-search/references/sources-travel.md +20 -0
  264. package/src/browser/base-page.ts +41 -6
  265. package/src/browser/bridge.ts +11 -8
  266. package/src/browser/cdp.ts +1 -8
  267. package/src/browser/daemon-client.ts +11 -1
  268. package/src/browser/dom-helpers.ts +43 -31
  269. package/src/browser/dom-snapshot.ts +23 -1
  270. package/src/browser/page.ts +115 -31
  271. package/src/browser.test.ts +1 -1
  272. package/src/build-manifest.ts +2 -0
  273. package/src/cli.test.ts +133 -0
  274. package/src/cli.ts +73 -11
  275. package/src/clis/1688/item.test.ts +69 -0
  276. package/src/clis/1688/item.ts +282 -0
  277. package/src/clis/1688/search.test.ts +81 -0
  278. package/src/clis/1688/search.ts +402 -0
  279. package/src/clis/1688/shared.test.ts +75 -0
  280. package/src/clis/1688/shared.ts +623 -0
  281. package/src/clis/1688/store.test.ts +69 -0
  282. package/src/clis/1688/store.ts +300 -0
  283. package/src/clis/amazon/bestsellers.test.ts +12 -3
  284. package/src/clis/amazon/bestsellers.ts +6 -178
  285. package/src/clis/amazon/movers-shakers.ts +8 -0
  286. package/src/clis/amazon/new-releases.ts +8 -0
  287. package/src/clis/amazon/rankings.test.ts +47 -0
  288. package/src/clis/amazon/rankings.ts +312 -0
  289. package/src/clis/amazon/shared.test.ts +16 -0
  290. package/src/clis/amazon/shared.ts +134 -12
  291. package/src/clis/bilibili/comments.test.ts +4 -3
  292. package/src/clis/bilibili/comments.ts +2 -2
  293. package/src/clis/bilibili/download.ts +2 -1
  294. package/src/clis/bilibili/subtitle.test.ts +2 -1
  295. package/src/clis/bilibili/subtitle.ts +4 -3
  296. package/src/clis/bilibili/utils.test.ts +21 -0
  297. package/src/clis/bilibili/utils.ts +27 -0
  298. package/src/clis/douban/marks.ts +1 -1
  299. package/src/clis/douban/subject.yaml +50 -19
  300. package/src/clis/doubao/utils.ts +32 -12
  301. package/src/clis/douyin/_shared/browser-fetch.test.ts +0 -1
  302. package/src/clis/douyin/_shared/transcode.test.ts +0 -2
  303. package/src/clis/douyin/draft.test.ts +0 -2
  304. package/src/clis/facebook/search.test.ts +0 -2
  305. package/src/clis/gemini/ask.test.ts +116 -0
  306. package/src/clis/gemini/ask.ts +10 -3
  307. package/src/clis/gemini/reply-state.test.ts +708 -0
  308. package/src/clis/gemini/utils.test.ts +184 -2
  309. package/src/clis/gemini/utils.ts +588 -60
  310. package/src/clis/hupu/detail.ts +126 -0
  311. package/src/clis/hupu/hot.yaml +43 -0
  312. package/src/clis/hupu/like.ts +76 -0
  313. package/src/clis/hupu/reply.ts +76 -0
  314. package/src/clis/hupu/search.ts +95 -0
  315. package/src/clis/hupu/unlike.ts +76 -0
  316. package/src/clis/hupu/utils.ts +381 -0
  317. package/src/clis/instagram/_shared/private-publish.test.ts +827 -0
  318. package/src/clis/instagram/_shared/private-publish.ts +1303 -0
  319. package/src/clis/instagram/_shared/protocol-capture.test.ts +148 -0
  320. package/src/clis/instagram/_shared/protocol-capture.ts +321 -0
  321. package/src/clis/instagram/_shared/runtime-info.ts +91 -0
  322. package/src/clis/instagram/note.test.ts +96 -0
  323. package/src/clis/instagram/note.ts +254 -0
  324. package/src/clis/instagram/post.test.ts +1716 -0
  325. package/src/clis/instagram/post.ts +1620 -0
  326. package/src/clis/instagram/reel.test.ts +191 -0
  327. package/src/clis/instagram/reel.ts +886 -0
  328. package/src/clis/instagram/story.test.ts +191 -0
  329. package/src/clis/instagram/story.ts +151 -0
  330. package/src/clis/sinafinance/stock-rank.ts +68 -0
  331. package/src/clis/substack/utils.test.ts +0 -2
  332. package/src/clis/twitter/post.test.ts +157 -0
  333. package/src/clis/twitter/post.ts +82 -48
  334. package/src/clis/twitter/reply.test.ts +177 -0
  335. package/src/clis/twitter/reply.ts +285 -39
  336. package/src/clis/xianyu/chat.test.ts +20 -0
  337. package/src/clis/xianyu/chat.ts +175 -0
  338. package/src/clis/xianyu/item.test.ts +67 -0
  339. package/src/clis/xianyu/item.ts +172 -0
  340. package/src/clis/xianyu/search.test.ts +22 -0
  341. package/src/clis/xianyu/search.ts +151 -0
  342. package/src/clis/xianyu/utils.ts +9 -0
  343. package/src/clis/xiaoe/catalog.yaml +129 -0
  344. package/src/clis/xiaoe/content.yaml +43 -0
  345. package/src/clis/xiaoe/courses.yaml +73 -0
  346. package/src/clis/xiaoe/detail.yaml +39 -0
  347. package/src/clis/xiaoe/play-url.yaml +124 -0
  348. package/src/clis/xiaohongshu/comments.test.ts +0 -2
  349. package/src/clis/xiaohongshu/creator-note-detail.test.ts +0 -2
  350. package/src/clis/xiaohongshu/creator-notes.test.ts +0 -2
  351. package/src/clis/xiaohongshu/download.test.ts +0 -2
  352. package/src/clis/xiaohongshu/note.test.ts +0 -2
  353. package/src/clis/xiaohongshu/publish.test.ts +0 -2
  354. package/src/clis/xiaohongshu/search.test.ts +59 -48
  355. package/src/clis/xiaohongshu/search.ts +31 -21
  356. package/src/clis/yuanbao/ask.test.ts +156 -0
  357. package/src/clis/yuanbao/ask.ts +522 -0
  358. package/src/clis/yuanbao/new.test.ts +36 -0
  359. package/src/clis/yuanbao/new.ts +81 -0
  360. package/src/clis/yuanbao/shared.ts +57 -0
  361. package/src/clis/zhihu/question.test.ts +42 -17
  362. package/src/clis/zhihu/question.ts +31 -26
  363. package/src/commanderAdapter.test.ts +51 -0
  364. package/src/commanderAdapter.ts +8 -4
  365. package/src/completion.test.ts +30 -0
  366. package/src/completion.ts +3 -1
  367. package/src/doctor.ts +1 -1
  368. package/src/electron-apps.ts +9 -1
  369. package/src/errors.ts +1 -1
  370. package/src/execution.ts +26 -30
  371. package/src/explore.ts +1 -1
  372. package/src/launcher.test.ts +121 -7
  373. package/src/launcher.ts +87 -9
  374. package/src/output.test.ts +50 -90
  375. package/src/output.ts +10 -1
  376. package/src/pipeline/executor.test.ts +0 -2
  377. package/src/pipeline/steps/download.test.ts +0 -2
  378. package/src/registry.ts +2 -0
  379. package/src/serialization.ts +2 -0
  380. package/src/types.ts +9 -2
  381. package/tests/e2e/browser-auth.test.ts +9 -0
  382. package/CLI-EXPLORER.md +0 -724
  383. package/CLI-ONESHOT.md +0 -216
  384. package/SKILL.md +0 -59
@@ -7,6 +7,28 @@
7
7
  */
8
8
  import { cli, Strategy } from '../../registry.js';
9
9
  import { AuthRequiredError } from '../../errors.js';
10
+ /**
11
+ * Wait for search results or login wall using MutationObserver (max 5s).
12
+ * Returns 'content' if note items appeared, 'login_wall' if login gate
13
+ * detected, or 'timeout' if neither appeared within the deadline.
14
+ */
15
+ const WAIT_FOR_CONTENT_JS = `
16
+ new Promise((resolve) => {
17
+ const detect = () => {
18
+ if (document.querySelector('section.note-item')) return 'content';
19
+ if (/登录后查看搜索结果/.test(document.body?.innerText || '')) return 'login_wall';
20
+ return null;
21
+ };
22
+ const found = detect();
23
+ if (found) return resolve(found);
24
+ const observer = new MutationObserver(() => {
25
+ const result = detect();
26
+ if (result) { observer.disconnect(); resolve(result); }
27
+ });
28
+ observer.observe(document.body, { childList: true, subtree: true });
29
+ setTimeout(() => { observer.disconnect(); resolve('timeout'); }, 5000);
30
+ })
31
+ `;
10
32
  /**
11
33
  * Extract approximate publish date from a Xiaohongshu note URL.
12
34
  * XHS note IDs follow MongoDB ObjectID format where the first 8 hex
@@ -39,22 +61,17 @@ cli({
39
61
  func: async (page, kwargs) => {
40
62
  const keyword = encodeURIComponent(kwargs.query);
41
63
  await page.goto(`https://www.xiaohongshu.com/search_result?keyword=${keyword}&source=web_search_result_notes`);
42
- await page.wait(3);
43
- // Early login-wall detection: XHS may show a login gate instead of
44
- // results. Check *before* autoScroll to avoid crashing on a page
45
- // that has no meaningful content to scroll through.
46
- const loginCheck = await page.evaluate(`
47
- (() => /登录后查看搜索结果/.test(document.body?.innerText || ''))()
48
- `);
49
- if (loginCheck) {
64
+ // Wait for search results to render (or login wall to appear).
65
+ // Uses MutationObserver to resolve as soon as content appears,
66
+ // instead of a fixed delay + blind retry.
67
+ const waitResult = await page.evaluate(WAIT_FOR_CONTENT_JS);
68
+ if (waitResult === 'login_wall') {
50
69
  throw new AuthRequiredError('www.xiaohongshu.com', 'Xiaohongshu search results are blocked behind a login wall');
51
70
  }
52
71
  // Scroll a couple of times to load more results
53
72
  await page.autoScroll({ times: 2 });
54
73
  const payload = await page.evaluate(`
55
74
  (() => {
56
- const loginWall = /登录后查看搜索结果/.test(document.body.innerText || '');
57
-
58
75
  const normalizeUrl = (href) => {
59
76
  if (!href) return '';
60
77
  if (href.startsWith('http://') || href.startsWith('https://')) return href;
@@ -98,18 +115,10 @@ cli({
98
115
  });
99
116
  });
100
117
 
101
- return {
102
- loginWall,
103
- results,
104
- };
118
+ return results;
105
119
  })()
106
120
  `);
107
- if (!payload || typeof payload !== 'object')
108
- return [];
109
- if (payload.loginWall) {
110
- throw new AuthRequiredError('www.xiaohongshu.com', 'Xiaohongshu search results are blocked behind a login wall');
111
- }
112
- const data = Array.isArray(payload.results) ? payload.results : [];
121
+ const data = Array.isArray(payload) ? payload : [];
113
122
  return data
114
123
  .filter((item) => item.title)
115
124
  .slice(0, kwargs.limit)
@@ -17,8 +17,6 @@ function createPageMock(evaluateResults) {
17
17
  getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
18
18
  wait: vi.fn().mockResolvedValue(undefined),
19
19
  tabs: vi.fn().mockResolvedValue([]),
20
- closeTab: vi.fn().mockResolvedValue(undefined),
21
- newTab: vi.fn().mockResolvedValue(undefined),
22
20
  selectTab: vi.fn().mockResolvedValue(undefined),
23
21
  networkRequests: vi.fn().mockResolvedValue([]),
24
22
  consoleMessages: vi.fn().mockResolvedValue([]),
@@ -36,8 +34,8 @@ describe('xiaohongshu search', () => {
36
34
  const cmd = getRegistry().get('xiaohongshu/search');
37
35
  expect(cmd?.func).toBeTypeOf('function');
38
36
  const page = createPageMock([
39
- // First evaluate: early login-wall check (returns true)
40
- true,
37
+ // First evaluate: MutationObserver wait (login wall detected)
38
+ 'login_wall',
41
39
  ]);
42
40
  await expect(cmd.func(page, { query: '特斯拉', limit: 5 })).rejects.toThrow('Xiaohongshu search results are blocked behind a login wall');
43
41
  // autoScroll must NOT be called when a login wall is detected early
@@ -49,21 +47,18 @@ describe('xiaohongshu search', () => {
49
47
  const detailUrl = 'https://www.xiaohongshu.com/search_result/68e90be80000000004022e66?xsec_token=test-token&xsec_source=';
50
48
  const authorUrl = 'https://www.xiaohongshu.com/user/profile/635a9c720000000018028b40?xsec_token=user-token&xsec_source=pc_search';
51
49
  const page = createPageMock([
52
- // First evaluate: early login-wall check (returns false → no wall)
53
- false,
54
- // Second evaluate: main DOM extraction
55
- {
56
- loginWall: false,
57
- results: [
58
- {
59
- title: '某鱼买FSD被坑了4万',
60
- author: '随风',
61
- likes: '261',
62
- url: detailUrl,
63
- author_url: authorUrl,
64
- },
65
- ],
66
- },
50
+ // First evaluate: MutationObserver wait (content appeared)
51
+ 'content',
52
+ // Second evaluate: main DOM extraction (returns array directly)
53
+ [
54
+ {
55
+ title: '某鱼买FSD被坑了4万',
56
+ author: '随风',
57
+ likes: '261',
58
+ url: detailUrl,
59
+ author_url: authorUrl,
60
+ },
61
+ ],
67
62
  ]);
68
63
  const result = await cmd.func(page, { query: '特斯拉', limit: 1 });
69
64
  // Should only do one goto (the search page itself), no per-note detail navigation
@@ -84,41 +79,54 @@ describe('xiaohongshu search', () => {
84
79
  const cmd = getRegistry().get('xiaohongshu/search');
85
80
  expect(cmd?.func).toBeTypeOf('function');
86
81
  const page = createPageMock([
87
- // First evaluate: early login-wall check (returns false → no wall)
88
- false,
89
- // Second evaluate: main DOM extraction
90
- {
91
- loginWall: false,
92
- results: [
93
- {
94
- title: 'Result A',
95
- author: 'UserA',
96
- likes: '10',
97
- url: 'https://www.xiaohongshu.com/search_result/aaa',
98
- author_url: '',
99
- },
100
- {
101
- title: '',
102
- author: 'UserB',
103
- likes: '5',
104
- url: 'https://www.xiaohongshu.com/search_result/bbb',
105
- author_url: '',
106
- },
107
- {
108
- title: 'Result C',
109
- author: 'UserC',
110
- likes: '3',
111
- url: 'https://www.xiaohongshu.com/search_result/ccc',
112
- author_url: '',
113
- },
114
- ],
115
- },
82
+ // First evaluate: MutationObserver wait (content appeared)
83
+ 'content',
84
+ // Second evaluate: main DOM extraction (returns array directly)
85
+ [
86
+ {
87
+ title: 'Result A',
88
+ author: 'UserA',
89
+ likes: '10',
90
+ url: 'https://www.xiaohongshu.com/search_result/aaa',
91
+ author_url: '',
92
+ },
93
+ {
94
+ title: '',
95
+ author: 'UserB',
96
+ likes: '5',
97
+ url: 'https://www.xiaohongshu.com/search_result/bbb',
98
+ author_url: '',
99
+ },
100
+ {
101
+ title: 'Result C',
102
+ author: 'UserC',
103
+ likes: '3',
104
+ url: 'https://www.xiaohongshu.com/search_result/ccc',
105
+ author_url: '',
106
+ },
107
+ ],
116
108
  ]);
117
109
  const result = (await cmd.func(page, { query: '测试', limit: 1 }));
118
110
  // limit=1 should return only the first valid-titled result
119
111
  expect(result).toHaveLength(1);
120
112
  expect(result[0]).toMatchObject({ rank: 1, title: 'Result A' });
121
113
  });
114
+ it('waits for content via MutationObserver before extracting', async () => {
115
+ const cmd = getRegistry().get('xiaohongshu/search');
116
+ expect(cmd?.func).toBeTypeOf('function');
117
+ const page = createPageMock([
118
+ // First evaluate: MutationObserver wait (content appeared)
119
+ 'content',
120
+ // Second evaluate: extraction (returns empty array)
121
+ [],
122
+ ]);
123
+ const result = (await cmd.func(page, { query: '测试等待', limit: 5 }));
124
+ expect(result).toHaveLength(0);
125
+ // Only one navigation, no retry
126
+ expect(page.goto).toHaveBeenCalledTimes(1);
127
+ // Two evaluate calls: wait + extraction
128
+ expect(page.evaluate).toHaveBeenCalledTimes(2);
129
+ });
122
130
  });
123
131
  describe('noteIdToDate (ObjectID timestamp parsing)', () => {
124
132
  it('parses a known note ID to the correct China-timezone date', () => {
@@ -0,0 +1,21 @@
1
+ import { isOnYuanbao } from './shared.js';
2
+ declare function normalizeBooleanFlag(value: unknown, fallback: boolean): boolean;
3
+ export declare function convertYuanbaoHtmlToMarkdown(value: string): string;
4
+ export declare function sanitizeYuanbaoResponseText(value: string, promptText: string): string;
5
+ export declare function collectYuanbaoTranscriptAdditions(beforeLines: string[], currentLines: string[], promptText: string): string;
6
+ export declare function pickLatestYuanbaoAssistantCandidate(messages: string[], baselineCount: number, promptText: string): string;
7
+ export declare function updateStableState(previousText: string, stableCount: number, nextText: string): {
8
+ previousText: string;
9
+ stableCount: number;
10
+ };
11
+ export declare const askCommand: import("../../registry.js").CliCommand;
12
+ export declare const __test__: {
13
+ collectYuanbaoTranscriptAdditions: typeof collectYuanbaoTranscriptAdditions;
14
+ convertYuanbaoHtmlToMarkdown: typeof convertYuanbaoHtmlToMarkdown;
15
+ isOnYuanbao: typeof isOnYuanbao;
16
+ normalizeBooleanFlag: typeof normalizeBooleanFlag;
17
+ pickLatestYuanbaoAssistantCandidate: typeof pickLatestYuanbaoAssistantCandidate;
18
+ sanitizeYuanbaoResponseText: typeof sanitizeYuanbaoResponseText;
19
+ updateStableState: typeof updateStableState;
20
+ };
21
+ export {};
@@ -0,0 +1,427 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import TurndownService from 'turndown';
3
+ import { CommandExecutionError, TimeoutError } from '../../errors.js';
4
+ import { YUANBAO_DOMAIN, IS_VISIBLE_JS, authRequired, isOnYuanbao, ensureYuanbaoPage, hasLoginGate } from './shared.js';
5
+ const YUANBAO_RESPONSE_POLL_INTERVAL_SECONDS = 2;
6
+ const YUANBAO_MIN_WAIT_MS = 8_000;
7
+ const YUANBAO_STABLE_POLLS_REQUIRED = 3;
8
+ function sendFailure(reason, detail) {
9
+ const suffix = detail ? ` Detail: ${detail}` : '';
10
+ return new CommandExecutionError(`${reason || 'Unknown Yuanbao send failure.'}${suffix}`, 'Make sure the Yuanbao chat composer is visible and ready before retrying.');
11
+ }
12
+ function normalizeText(value) {
13
+ return typeof value === 'string' ? value.trim() : '';
14
+ }
15
+ function normalizeBooleanFlag(value, fallback) {
16
+ if (typeof value === 'boolean')
17
+ return value;
18
+ if (value == null || value === '')
19
+ return fallback;
20
+ const normalized = String(value).trim().toLowerCase();
21
+ return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
22
+ }
23
+ function createYuanbaoTurndown() {
24
+ const td = new TurndownService({
25
+ headingStyle: 'atx',
26
+ codeBlockStyle: 'fenced',
27
+ bulletListMarker: '-',
28
+ });
29
+ td.addRule('linebreak', {
30
+ filter: 'br',
31
+ replacement: () => '\n',
32
+ });
33
+ td.addRule('table', {
34
+ filter: 'table',
35
+ replacement: (content) => `\n\n${content}\n\n`,
36
+ });
37
+ td.addRule('tableSection', {
38
+ filter: ['thead', 'tbody', 'tfoot'],
39
+ replacement: (content) => content,
40
+ });
41
+ td.addRule('tableRow', {
42
+ filter: 'tr',
43
+ replacement: (content, node) => {
44
+ const element = node;
45
+ const cells = Array.from(element.children);
46
+ const isHeaderRow = element.parentElement?.tagName === 'THEAD'
47
+ || (cells.length > 0 && cells.every((cell) => cell.tagName === 'TH'));
48
+ const row = `${content}\n`;
49
+ if (!isHeaderRow)
50
+ return row;
51
+ const separator = `| ${cells.map(() => '---').join(' | ')} |\n`;
52
+ return `${row}${separator}`;
53
+ },
54
+ });
55
+ td.addRule('tableCell', {
56
+ filter: ['th', 'td'],
57
+ replacement: (content, node) => {
58
+ const element = node;
59
+ const index = element.parentElement ? Array.from(element.parentElement.children).indexOf(element) : 0;
60
+ const prefix = index === 0 ? '| ' : ' ';
61
+ return `${prefix}${content.trim()} |`;
62
+ },
63
+ });
64
+ return td;
65
+ }
66
+ const yuanbaoTurndown = createYuanbaoTurndown();
67
+ export function convertYuanbaoHtmlToMarkdown(value) {
68
+ const markdown = yuanbaoTurndown.turndown(value || '');
69
+ return markdown
70
+ .replace(/\u00a0/g, ' ')
71
+ .replace(/\n{4,}/g, '\n\n\n')
72
+ .replace(/[ \t]+$/gm, '')
73
+ .trim();
74
+ }
75
+ export function sanitizeYuanbaoResponseText(value, promptText) {
76
+ let sanitized = value
77
+ .replace(/内容由AI生成,仅供参考/gi, '')
78
+ .replace(/重新回答/gi, '')
79
+ .trim();
80
+ if (/^(正在搜索资料|搜索资料中|正在思考|思考中)[.。…]*$/u.test(sanitized)) {
81
+ return '';
82
+ }
83
+ const prompt = promptText.trim();
84
+ if (!prompt)
85
+ return sanitized;
86
+ if (sanitized === prompt)
87
+ return '';
88
+ for (const separator of ['\n\n', '\n', '\r\n\r\n', '\r\n', ' ']) {
89
+ const prefix = `${prompt}${separator}`;
90
+ if (sanitized.startsWith(prefix)) {
91
+ sanitized = sanitized.slice(prefix.length).trim();
92
+ break;
93
+ }
94
+ }
95
+ return sanitized;
96
+ }
97
+ export function collectYuanbaoTranscriptAdditions(beforeLines, currentLines, promptText) {
98
+ const beforeSet = new Set(beforeLines);
99
+ const additions = currentLines
100
+ .filter((line) => !beforeSet.has(line))
101
+ .map((line) => sanitizeYuanbaoResponseText(line, promptText))
102
+ .filter((line) => line && line !== promptText);
103
+ return additions.join('\n').trim();
104
+ }
105
+ export function pickLatestYuanbaoAssistantCandidate(messages, baselineCount, promptText) {
106
+ const freshMessages = messages
107
+ .slice(Math.max(0, baselineCount))
108
+ .map((message) => sanitizeYuanbaoResponseText(message, promptText))
109
+ .filter(Boolean);
110
+ for (let i = freshMessages.length - 1; i >= 0; i -= 1) {
111
+ if (freshMessages[i] !== promptText.trim())
112
+ return freshMessages[i];
113
+ }
114
+ return '';
115
+ }
116
+ export function updateStableState(previousText, stableCount, nextText) {
117
+ if (!nextText)
118
+ return { previousText: '', stableCount: 0 };
119
+ if (nextText === previousText)
120
+ return { previousText, stableCount: stableCount + 1 };
121
+ return { previousText: nextText, stableCount: 0 };
122
+ }
123
+ function getTranscriptLinesScript() {
124
+ return `
125
+ (() => {
126
+ const clean = (value) => (value || '')
127
+ .replace(/\\u00a0/g, ' ')
128
+ .replace(/\\n{3,}/g, '\\n\\n')
129
+ .trim();
130
+
131
+ const root = (
132
+ document.querySelector('.agent-dialogue__content--common')
133
+ || document.querySelector('.agent-dialogue__content')
134
+ || document.querySelector('.agent-dialogue')
135
+ || document.body
136
+ ).cloneNode(true);
137
+
138
+ const removableSelectors = [
139
+ '.agent-dialogue__content--common__input',
140
+ '.agent-dialogue__tool',
141
+ '.agent-dialogue__content-copyright',
142
+ '.index_chatLandingBox__G7hAT',
143
+ '.index_chatLandingBoxMobile__J8i8v',
144
+ '.index_chatLandingHintList__M69Lr',
145
+ '.yb-nav',
146
+ '.agent-dialogue__content--common__input .ql-toolbar',
147
+ '.agent-dialogue__content--common__input .ql-container',
148
+ '.agent-dialogue__content--common__input .ql-editor',
149
+ '[role="dialog"]',
150
+ 'iframe',
151
+ 'button',
152
+ 'script',
153
+ 'style',
154
+ 'noscript',
155
+ ];
156
+
157
+ for (const selector of removableSelectors) {
158
+ root.querySelectorAll(selector).forEach((node) => node.remove());
159
+ }
160
+
161
+ const stopLines = new Set([
162
+ '元宝',
163
+ 'DeepSeek',
164
+ '深度思考',
165
+ '联网搜索',
166
+ '工具',
167
+ '登录',
168
+ '安装电脑版',
169
+ '内容由AI生成,仅供参考',
170
+ '有问题,尽管问,shift+enter换行',
171
+ '立即创建团队',
172
+ '微信',
173
+ '手机',
174
+ 'QQ',
175
+ '微信扫码登录',
176
+ '扫码默认已阅读并同意',
177
+ '用户服务协议',
178
+ '隐私协议',
179
+ ]);
180
+
181
+ const noisyPatterns = [
182
+ /^支持文件格式[::]/,
183
+ /^文件拖动到此处即可上传/,
184
+ /^下载元宝电脑版/,
185
+ ];
186
+
187
+ return clean(root.innerText || root.textContent || '')
188
+ .split('\\n')
189
+ .map((line) => clean(line))
190
+ .filter((line) => line
191
+ && line.length <= 4000
192
+ && !stopLines.has(line)
193
+ && !noisyPatterns.some((pattern) => pattern.test(line)));
194
+ })()
195
+ `;
196
+ }
197
+ async function getYuanbaoTranscriptLines(page) {
198
+ const result = await page.evaluate(getTranscriptLinesScript());
199
+ return Array.isArray(result) ? result.map(normalizeText).filter(Boolean) : [];
200
+ }
201
+ async function getYuanbaoAssistantMessages(page) {
202
+ const result = await page.evaluate(`(() => {
203
+ ${IS_VISIBLE_JS}
204
+
205
+ const roots = Array.from(document.querySelectorAll('.agent-chat__list__item--ai'))
206
+ .filter((node) => isVisible(node));
207
+
208
+ return roots.map((root) => {
209
+ const doneContent = root.querySelector('.hyc-content-md-done');
210
+ const markdownContent = doneContent || root.querySelector('.hyc-content-md');
211
+ const speechContent = root.querySelector('.agent-chat__speech-text');
212
+ const bubbleContent = root.querySelector('.agent-chat__bubble__content');
213
+ const content = markdownContent || speechContent || bubbleContent;
214
+
215
+ if (content instanceof HTMLElement) {
216
+ return content.innerHTML || content.textContent || '';
217
+ }
218
+
219
+ return root instanceof HTMLElement ? (root.innerHTML || root.textContent || '') : '';
220
+ }).filter(Boolean);
221
+ })()`);
222
+ return Array.isArray(result)
223
+ ? result
224
+ .map((value) => convertYuanbaoHtmlToMarkdown(typeof value === 'string' ? value : ''))
225
+ .map(normalizeText)
226
+ .filter(Boolean)
227
+ : [];
228
+ }
229
+ async function getYuanbaoInternetSearchState(page) {
230
+ const result = await page.evaluate(`(() => {
231
+ ${IS_VISIBLE_JS}
232
+
233
+ const button = Array.from(document.querySelectorAll('[dt-button-id="internet_search"]'))
234
+ .find((node) => isVisible(node));
235
+
236
+ if (!(button instanceof HTMLElement)) return { found: false, enabled: false };
237
+
238
+ const attr = button.getAttribute('dt-internet-search') || '';
239
+ const className = button.className || '';
240
+ return {
241
+ found: true,
242
+ enabled: attr === 'openInternetSearch' || className.includes('index_v2_active__'),
243
+ };
244
+ })()`);
245
+ return result;
246
+ }
247
+ async function setYuanbaoInternetSearch(page, enabled) {
248
+ const current = await getYuanbaoInternetSearchState(page);
249
+ if (!current.found || current.enabled === enabled)
250
+ return;
251
+ await page.evaluate(`(() => {
252
+ ${IS_VISIBLE_JS}
253
+
254
+ const button = Array.from(document.querySelectorAll('[dt-button-id="internet_search"]'))
255
+ .find((node) => isVisible(node));
256
+
257
+ if (button instanceof HTMLElement) button.click();
258
+ })()`);
259
+ await page.wait(0.5);
260
+ }
261
+ async function getYuanbaoDeepThinkState(page) {
262
+ const result = await page.evaluate(`(() => {
263
+ ${IS_VISIBLE_JS}
264
+
265
+ const button = Array.from(document.querySelectorAll('[dt-button-id="deep_think"]'))
266
+ .find((node) => isVisible(node));
267
+
268
+ if (!(button instanceof HTMLElement)) return { found: false, enabled: false };
269
+
270
+ const className = button.className || '';
271
+ return {
272
+ found: true,
273
+ enabled: className.includes('ThinkSelector_selected__'),
274
+ };
275
+ })()`);
276
+ return result;
277
+ }
278
+ async function setYuanbaoDeepThink(page, enabled) {
279
+ const current = await getYuanbaoDeepThinkState(page);
280
+ if (!current.found || current.enabled === enabled)
281
+ return;
282
+ await page.evaluate(`(() => {
283
+ ${IS_VISIBLE_JS}
284
+
285
+ const button = Array.from(document.querySelectorAll('[dt-button-id="deep_think"]'))
286
+ .find((node) => isVisible(node));
287
+
288
+ if (button instanceof HTMLElement) button.click();
289
+ })()`);
290
+ await page.wait(0.5);
291
+ }
292
+ async function sendYuanbaoMessage(page, prompt) {
293
+ return await page.evaluate(`(async () => {
294
+ const waitFor = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
295
+ ${IS_VISIBLE_JS}
296
+
297
+ const composer = Array.from(document.querySelectorAll('.ql-editor[contenteditable="true"], .ql-editor, [contenteditable="true"]'))
298
+ .find(isVisible);
299
+
300
+ if (!(composer instanceof HTMLElement)) {
301
+ return {
302
+ ok: false,
303
+ reason: 'Yuanbao composer was not found.',
304
+ };
305
+ }
306
+
307
+ try {
308
+ composer.focus();
309
+ const selection = window.getSelection();
310
+ const range = document.createRange();
311
+ range.selectNodeContents(composer);
312
+ range.collapse(false);
313
+ selection?.removeAllRanges();
314
+ selection?.addRange(range);
315
+ composer.textContent = '';
316
+ document.execCommand('insertText', false, ${JSON.stringify(prompt)});
317
+ composer.dispatchEvent(new InputEvent('input', { bubbles: true, data: ${JSON.stringify(prompt)}, inputType: 'insertText' }));
318
+ await waitFor(200);
319
+ } catch (error) {
320
+ return {
321
+ ok: false,
322
+ reason: 'Failed to insert the prompt into the Yuanbao composer.',
323
+ detail: error instanceof Error ? error.message : String(error),
324
+ };
325
+ }
326
+
327
+ const submit = Array.from(document.querySelectorAll('a[class*="send-btn"], button[class*="send-btn"]'))
328
+ .find((node) => {
329
+ if (!(node instanceof HTMLElement) || !isVisible(node)) return false;
330
+ const className = node.className || '';
331
+ if (typeof className === 'string' && className.includes('disabled')) return false;
332
+ return true;
333
+ });
334
+
335
+ if (submit instanceof HTMLElement) {
336
+ submit.click();
337
+ return { ok: true, action: 'click' };
338
+ }
339
+
340
+ composer.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
341
+ composer.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
342
+ return { ok: true, action: 'enter' };
343
+ })()`);
344
+ }
345
+ async function waitForYuanbaoResponse(page, baselineAssistantCount, beforeLines, prompt, timeoutSeconds) {
346
+ const startTime = Date.now();
347
+ let previousText = '';
348
+ let stableCount = 0;
349
+ let latestCandidate = '';
350
+ while (Date.now() - startTime < timeoutSeconds * 1000) {
351
+ await page.wait(YUANBAO_RESPONSE_POLL_INTERVAL_SECONDS);
352
+ if (await hasLoginGate(page))
353
+ return 'blocked';
354
+ const assistantMessages = await getYuanbaoAssistantMessages(page);
355
+ const assistantCandidate = pickLatestYuanbaoAssistantCandidate(assistantMessages, baselineAssistantCount, prompt);
356
+ const candidate = assistantCandidate || collectYuanbaoTranscriptAdditions(beforeLines, await getYuanbaoTranscriptLines(page), prompt);
357
+ if (!candidate)
358
+ continue;
359
+ latestCandidate = candidate;
360
+ const nextState = updateStableState(previousText, stableCount, candidate);
361
+ previousText = nextState.previousText;
362
+ stableCount = nextState.stableCount;
363
+ const waitedLongEnough = Date.now() - startTime >= YUANBAO_MIN_WAIT_MS;
364
+ if (waitedLongEnough && stableCount >= YUANBAO_STABLE_POLLS_REQUIRED)
365
+ return candidate;
366
+ }
367
+ return latestCandidate || null;
368
+ }
369
+ export const askCommand = cli({
370
+ site: 'yuanbao',
371
+ name: 'ask',
372
+ description: 'Send a prompt to Yuanbao web chat and wait for the assistant response',
373
+ domain: YUANBAO_DOMAIN,
374
+ strategy: Strategy.COOKIE,
375
+ browser: true,
376
+ navigateBefore: false,
377
+ defaultFormat: 'plain',
378
+ timeoutSeconds: 180,
379
+ args: [
380
+ { name: 'prompt', required: true, positional: true, help: 'Prompt to send' },
381
+ { name: 'timeout', required: false, help: 'Max seconds to wait (default: 60)', default: '60' },
382
+ { name: 'search', type: 'boolean', required: false, help: 'Enable Yuanbao internet search (default: true)', default: true },
383
+ { name: 'think', type: 'boolean', required: false, help: 'Enable Yuanbao deep thinking (default: false)', default: false },
384
+ ],
385
+ columns: ['Role', 'Text'],
386
+ func: async (page, kwargs) => {
387
+ const prompt = kwargs.prompt;
388
+ const timeout = parseInt(kwargs.timeout, 10) || 60;
389
+ const useSearch = normalizeBooleanFlag(kwargs.search, true);
390
+ const useThink = normalizeBooleanFlag(kwargs.think, false);
391
+ await ensureYuanbaoPage(page);
392
+ if (await hasLoginGate(page)) {
393
+ throw authRequired('Yuanbao opened a login gate before sending the prompt.');
394
+ }
395
+ await setYuanbaoInternetSearch(page, useSearch);
396
+ await setYuanbaoDeepThink(page, useThink);
397
+ const beforeAssistantMessages = await getYuanbaoAssistantMessages(page);
398
+ const beforeLines = await getYuanbaoTranscriptLines(page);
399
+ const sendResult = await sendYuanbaoMessage(page, prompt);
400
+ if (!sendResult?.ok) {
401
+ if (await hasLoginGate(page)) {
402
+ throw authRequired('Yuanbao opened a login gate instead of accepting the prompt.');
403
+ }
404
+ throw sendFailure(sendResult?.reason, sendResult?.detail);
405
+ }
406
+ const response = await waitForYuanbaoResponse(page, beforeAssistantMessages.length, beforeLines, prompt, timeout);
407
+ if (response === 'blocked') {
408
+ throw authRequired('Yuanbao opened a login gate instead of returning a chat response.');
409
+ }
410
+ if (!response) {
411
+ throw new TimeoutError('yuanbao ask', timeout, 'No Yuanbao response was observed before the timeout. Retry with --timeout, and verify the current browser session is still interactive.');
412
+ }
413
+ return [
414
+ { Role: 'User', Text: prompt },
415
+ { Role: 'Assistant', Text: response },
416
+ ];
417
+ },
418
+ });
419
+ export const __test__ = {
420
+ collectYuanbaoTranscriptAdditions,
421
+ convertYuanbaoHtmlToMarkdown,
422
+ isOnYuanbao,
423
+ normalizeBooleanFlag,
424
+ pickLatestYuanbaoAssistantCandidate,
425
+ sanitizeYuanbaoResponseText,
426
+ updateStableState,
427
+ };
@@ -0,0 +1 @@
1
+ export {};