@jackwener/opencli 1.6.0 → 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 (390) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +27 -45
  4. package/README.zh-CN.md +32 -34
  5. package/autoresearch/browse-tasks.json +18 -20
  6. package/autoresearch/commands/debug.ts +163 -0
  7. package/autoresearch/commands/fix.ts +145 -0
  8. package/autoresearch/commands/plan.ts +88 -0
  9. package/autoresearch/commands/run.ts +138 -0
  10. package/autoresearch/config.ts +82 -0
  11. package/autoresearch/engine.ts +359 -0
  12. package/autoresearch/eval-all.ts +127 -0
  13. package/autoresearch/eval-browse.ts +1 -1
  14. package/autoresearch/eval-publish.ts +238 -0
  15. package/autoresearch/eval-save.ts +249 -0
  16. package/autoresearch/eval-skill.ts +14 -8
  17. package/autoresearch/eval-v2ex.ts +220 -0
  18. package/autoresearch/eval-zhihu.ts +230 -0
  19. package/autoresearch/logger.ts +69 -0
  20. package/autoresearch/presets/combined-reliability.ts +27 -0
  21. package/autoresearch/presets/index.ts +23 -0
  22. package/autoresearch/presets/operate-reliability.ts +24 -0
  23. package/autoresearch/presets/save-reliability.ts +26 -0
  24. package/autoresearch/presets/skill-quality.ts +20 -0
  25. package/autoresearch/presets/v2ex-reliability.ts +24 -0
  26. package/autoresearch/presets/zhihu-reliability.ts +25 -0
  27. package/autoresearch/publish-tasks.json +345 -0
  28. package/autoresearch/run-save.sh +11 -0
  29. package/autoresearch/save-adapters/xhs-explore-deep.ts +64 -0
  30. package/autoresearch/save-adapters/xhs-note-comments.ts +61 -0
  31. package/autoresearch/save-adapters/xhs-search-full.ts +62 -0
  32. package/autoresearch/save-adapters/zhihu-hot-detail.ts +52 -0
  33. package/autoresearch/save-adapters/zhihu-question-full.ts +57 -0
  34. package/autoresearch/save-adapters/zhihu-search-detail.ts +53 -0
  35. package/autoresearch/save-tasks.json +281 -0
  36. package/autoresearch/v2ex-tasks.json +899 -0
  37. package/autoresearch/zhihu-tasks.json +848 -0
  38. package/bun.lock +615 -0
  39. package/dist/browser/base-page.d.ts +4 -2
  40. package/dist/browser/base-page.js +37 -4
  41. package/dist/browser/bridge.js +10 -8
  42. package/dist/browser/cdp.js +2 -6
  43. package/dist/browser/daemon-client.d.ts +11 -1
  44. package/dist/browser/daemon-client.js +3 -0
  45. package/dist/browser/dom-helpers.d.ts +4 -2
  46. package/dist/browser/dom-helpers.js +42 -31
  47. package/dist/browser/dom-snapshot.js +23 -1
  48. package/dist/browser/page.d.ts +7 -2
  49. package/dist/browser/page.js +112 -30
  50. package/dist/browser.test.js +1 -1
  51. package/dist/build-manifest.d.ts +1 -0
  52. package/dist/build-manifest.js +1 -0
  53. package/dist/cli-manifest.json +1133 -182
  54. package/dist/cli.d.ts +2 -0
  55. package/dist/cli.js +48 -7
  56. package/dist/cli.test.d.ts +1 -0
  57. package/dist/cli.test.js +88 -0
  58. package/dist/clis/1688/item.d.ts +70 -0
  59. package/dist/clis/1688/item.js +187 -0
  60. package/dist/clis/1688/item.test.d.ts +1 -0
  61. package/dist/clis/1688/item.test.js +67 -0
  62. package/dist/clis/1688/search.d.ts +56 -0
  63. package/dist/clis/1688/search.js +309 -0
  64. package/dist/clis/1688/search.test.d.ts +1 -0
  65. package/dist/clis/1688/search.test.js +75 -0
  66. package/dist/clis/1688/shared.d.ts +112 -0
  67. package/dist/clis/1688/shared.js +514 -0
  68. package/dist/clis/1688/shared.test.d.ts +1 -0
  69. package/dist/clis/1688/shared.test.js +57 -0
  70. package/dist/clis/1688/store.d.ts +45 -0
  71. package/dist/clis/1688/store.js +226 -0
  72. package/dist/clis/1688/store.test.d.ts +1 -0
  73. package/dist/clis/1688/store.test.js +62 -0
  74. package/dist/clis/amazon/bestsellers.d.ts +0 -20
  75. package/dist/clis/amazon/bestsellers.js +6 -129
  76. package/dist/clis/amazon/bestsellers.test.js +12 -3
  77. package/dist/clis/amazon/movers-shakers.d.ts +1 -0
  78. package/dist/clis/amazon/movers-shakers.js +7 -0
  79. package/dist/clis/amazon/new-releases.d.ts +1 -0
  80. package/dist/clis/amazon/new-releases.js +7 -0
  81. package/dist/clis/amazon/rankings.d.ts +59 -0
  82. package/dist/clis/amazon/rankings.js +226 -0
  83. package/dist/clis/amazon/rankings.test.d.ts +1 -0
  84. package/dist/clis/amazon/rankings.test.js +41 -0
  85. package/dist/clis/amazon/shared.d.ts +11 -0
  86. package/dist/clis/amazon/shared.js +121 -11
  87. package/dist/clis/amazon/shared.test.js +11 -0
  88. package/dist/clis/bilibili/comments.js +2 -2
  89. package/dist/clis/bilibili/comments.test.js +3 -2
  90. package/dist/clis/bilibili/download.js +2 -1
  91. package/dist/clis/bilibili/subtitle.js +4 -3
  92. package/dist/clis/bilibili/subtitle.test.js +2 -1
  93. package/dist/clis/bilibili/utils.d.ts +5 -0
  94. package/dist/clis/bilibili/utils.js +30 -0
  95. package/dist/clis/bilibili/utils.test.d.ts +1 -0
  96. package/dist/clis/bilibili/utils.test.js +17 -0
  97. package/dist/clis/douban/marks.js +1 -1
  98. package/dist/clis/douban/subject.yaml +50 -19
  99. package/dist/clis/doubao/utils.js +32 -12
  100. package/dist/clis/douyin/_shared/browser-fetch.test.js +0 -1
  101. package/dist/clis/douyin/_shared/transcode.test.js +0 -2
  102. package/dist/clis/douyin/draft.test.js +0 -2
  103. package/dist/clis/facebook/search.test.js +0 -2
  104. package/dist/clis/gemini/ask.js +9 -3
  105. package/dist/clis/gemini/ask.test.d.ts +1 -0
  106. package/dist/clis/gemini/ask.test.js +100 -0
  107. package/dist/clis/gemini/reply-state.test.d.ts +1 -0
  108. package/dist/clis/gemini/reply-state.test.js +641 -0
  109. package/dist/clis/gemini/utils.d.ts +44 -1
  110. package/dist/clis/gemini/utils.js +528 -61
  111. package/dist/clis/gemini/utils.test.js +149 -2
  112. package/dist/clis/hupu/detail.d.ts +1 -0
  113. package/dist/clis/hupu/detail.js +72 -0
  114. package/dist/clis/hupu/hot.yaml +43 -0
  115. package/dist/clis/hupu/like.d.ts +1 -0
  116. package/dist/clis/hupu/like.js +75 -0
  117. package/dist/clis/hupu/reply.d.ts +1 -0
  118. package/dist/clis/hupu/reply.js +71 -0
  119. package/dist/clis/hupu/search.d.ts +1 -0
  120. package/dist/clis/hupu/search.js +59 -0
  121. package/dist/clis/hupu/unlike.d.ts +1 -0
  122. package/dist/clis/hupu/unlike.js +75 -0
  123. package/dist/clis/hupu/utils.d.ts +20 -0
  124. package/dist/clis/hupu/utils.js +319 -0
  125. package/dist/clis/instagram/_shared/private-publish.d.ts +138 -0
  126. package/dist/clis/instagram/_shared/private-publish.js +1030 -0
  127. package/dist/clis/instagram/_shared/private-publish.test.d.ts +1 -0
  128. package/dist/clis/instagram/_shared/private-publish.test.js +705 -0
  129. package/dist/clis/instagram/_shared/protocol-capture.d.ts +26 -0
  130. package/dist/clis/instagram/_shared/protocol-capture.js +282 -0
  131. package/dist/clis/instagram/_shared/protocol-capture.test.d.ts +1 -0
  132. package/dist/clis/instagram/_shared/protocol-capture.test.js +114 -0
  133. package/dist/clis/instagram/_shared/runtime-info.d.ts +9 -0
  134. package/dist/clis/instagram/_shared/runtime-info.js +81 -0
  135. package/dist/clis/instagram/note.d.ts +1 -0
  136. package/dist/clis/instagram/note.js +222 -0
  137. package/dist/clis/instagram/note.test.d.ts +1 -0
  138. package/dist/clis/instagram/note.test.js +81 -0
  139. package/dist/clis/instagram/post.d.ts +4 -0
  140. package/dist/clis/instagram/post.js +1496 -0
  141. package/dist/clis/instagram/post.test.d.ts +1 -0
  142. package/dist/clis/instagram/post.test.js +1647 -0
  143. package/dist/clis/instagram/reel.d.ts +1 -0
  144. package/dist/clis/instagram/reel.js +826 -0
  145. package/dist/clis/instagram/reel.test.d.ts +1 -0
  146. package/dist/clis/instagram/reel.test.js +167 -0
  147. package/dist/clis/instagram/story.d.ts +1 -0
  148. package/dist/clis/instagram/story.js +115 -0
  149. package/dist/clis/instagram/story.test.d.ts +1 -0
  150. package/dist/clis/instagram/story.test.js +167 -0
  151. package/dist/clis/sinafinance/stock-rank.d.ts +4 -0
  152. package/dist/clis/sinafinance/stock-rank.js +65 -0
  153. package/dist/clis/substack/utils.test.js +0 -2
  154. package/dist/clis/twitter/post.js +72 -45
  155. package/dist/clis/twitter/post.test.d.ts +1 -0
  156. package/dist/clis/twitter/post.test.js +116 -0
  157. package/dist/clis/twitter/reply.d.ts +12 -0
  158. package/dist/clis/twitter/reply.js +257 -35
  159. package/dist/clis/twitter/reply.test.d.ts +1 -0
  160. package/dist/clis/twitter/reply.test.js +151 -0
  161. package/dist/clis/twitter/search.js +67 -5
  162. package/dist/clis/twitter/search.test.js +83 -5
  163. package/dist/clis/xianyu/chat.d.ts +7 -0
  164. package/dist/clis/xianyu/chat.js +146 -0
  165. package/dist/clis/xianyu/chat.test.d.ts +1 -0
  166. package/dist/clis/xianyu/chat.test.js +15 -0
  167. package/dist/clis/xianyu/item.d.ts +7 -0
  168. package/dist/clis/xianyu/item.js +152 -0
  169. package/dist/clis/xianyu/item.test.d.ts +1 -0
  170. package/dist/clis/xianyu/item.test.js +56 -0
  171. package/dist/clis/xianyu/search.d.ts +10 -0
  172. package/dist/clis/xianyu/search.js +134 -0
  173. package/dist/clis/xianyu/search.test.d.ts +1 -0
  174. package/dist/clis/xianyu/search.test.js +17 -0
  175. package/dist/clis/xianyu/utils.d.ts +1 -0
  176. package/dist/clis/xianyu/utils.js +8 -0
  177. package/dist/clis/xiaoe/catalog.yaml +129 -0
  178. package/dist/clis/xiaoe/content.yaml +43 -0
  179. package/dist/clis/xiaoe/courses.yaml +73 -0
  180. package/dist/clis/xiaoe/detail.yaml +39 -0
  181. package/dist/clis/xiaoe/play-url.yaml +124 -0
  182. package/dist/clis/xiaohongshu/comments.test.js +0 -2
  183. package/dist/clis/xiaohongshu/creator-note-detail.test.js +0 -2
  184. package/dist/clis/xiaohongshu/creator-notes.test.js +0 -2
  185. package/dist/clis/xiaohongshu/download.test.js +0 -2
  186. package/dist/clis/xiaohongshu/note.test.js +0 -2
  187. package/dist/clis/xiaohongshu/publish.test.js +0 -2
  188. package/dist/clis/xiaohongshu/search.js +29 -20
  189. package/dist/clis/xiaohongshu/search.test.js +56 -48
  190. package/dist/clis/yuanbao/ask.d.ts +21 -0
  191. package/dist/clis/yuanbao/ask.js +427 -0
  192. package/dist/clis/yuanbao/ask.test.d.ts +1 -0
  193. package/dist/clis/yuanbao/ask.test.js +124 -0
  194. package/dist/clis/yuanbao/new.d.ts +1 -0
  195. package/dist/clis/yuanbao/new.js +70 -0
  196. package/dist/clis/yuanbao/new.test.d.ts +1 -0
  197. package/dist/clis/yuanbao/new.test.js +30 -0
  198. package/dist/clis/yuanbao/shared.d.ts +13 -0
  199. package/dist/clis/yuanbao/shared.js +49 -0
  200. package/dist/clis/zhihu/question.js +30 -19
  201. package/dist/clis/zhihu/question.test.js +34 -16
  202. package/dist/commanderAdapter.js +8 -4
  203. package/dist/commanderAdapter.test.js +42 -0
  204. package/dist/completion.js +3 -1
  205. package/dist/completion.test.d.ts +1 -0
  206. package/dist/completion.test.js +23 -0
  207. package/dist/doctor.js +1 -1
  208. package/dist/electron-apps.d.ts +2 -0
  209. package/dist/electron-apps.js +7 -1
  210. package/dist/errors.js +1 -1
  211. package/dist/execution.js +25 -35
  212. package/dist/explore.js +1 -1
  213. package/dist/launcher.d.ts +4 -0
  214. package/dist/launcher.js +64 -8
  215. package/dist/launcher.test.js +88 -7
  216. package/dist/output.d.ts +2 -0
  217. package/dist/output.js +10 -1
  218. package/dist/output.test.d.ts +0 -3
  219. package/dist/output.test.js +59 -92
  220. package/dist/pipeline/executor.test.js +0 -2
  221. package/dist/pipeline/steps/download.test.js +0 -2
  222. package/dist/registry.d.ts +2 -0
  223. package/dist/serialization.d.ts +1 -0
  224. package/dist/serialization.js +1 -0
  225. package/dist/types.d.ts +9 -2
  226. package/docs/.vitepress/config.mts +4 -0
  227. package/docs/adapters/browser/1688.md +52 -0
  228. package/docs/adapters/browser/36kr.md +2 -1
  229. package/docs/adapters/browser/doubao.md +5 -1
  230. package/docs/adapters/browser/hupu.md +53 -0
  231. package/docs/adapters/browser/sinafinance.md +32 -2
  232. package/docs/adapters/browser/weibo.md +6 -1
  233. package/docs/adapters/browser/wikipedia.md +2 -0
  234. package/docs/adapters/browser/xianyu.md +42 -0
  235. package/docs/adapters/browser/xiaoe.md +44 -0
  236. package/docs/adapters/browser/yuanbao.md +64 -0
  237. package/docs/adapters/index.md +14 -5
  238. package/docs/comparison.md +1 -1
  239. package/docs/developer/ai-workflow.md +2 -2
  240. package/docs/developer/contributing.md +1 -1
  241. package/docs/developer/testing.md +2 -0
  242. package/docs/guide/plugins.md +1 -0
  243. package/docs/guide/troubleshooting.md +11 -0
  244. package/docs/superpowers/specs/2026-04-03-v2ex-autoresearch-design.md +41 -0
  245. package/docs/zh/guide/plugins.md +1 -0
  246. package/extension/dist/background.js +1127 -0
  247. package/extension/src/background.test.ts +39 -0
  248. package/extension/src/background.ts +223 -34
  249. package/extension/src/cdp.ts +194 -4
  250. package/extension/src/protocol.ts +22 -1
  251. package/package.json +3 -2
  252. package/scripts/postinstall.js +1 -1
  253. package/skills/opencli-explorer/SKILL.md +1 -1
  254. package/skills/opencli-oneshot/SKILL.md +2 -2
  255. package/skills/opencli-operate/SKILL.md +120 -27
  256. package/skills/opencli-usage/SKILL.md +31 -20
  257. package/skills/opencli-usage/browser.md +114 -16
  258. package/skills/opencli-usage/public-api.md +32 -3
  259. package/skills/smart-search/SKILL.md +156 -0
  260. package/skills/smart-search/references/sources-ai.md +74 -0
  261. package/skills/smart-search/references/sources-info.md +43 -0
  262. package/skills/smart-search/references/sources-media.md +50 -0
  263. package/skills/smart-search/references/sources-other.md +42 -0
  264. package/skills/smart-search/references/sources-shopping.md +31 -0
  265. package/skills/smart-search/references/sources-social.md +51 -0
  266. package/skills/smart-search/references/sources-tech.md +42 -0
  267. package/skills/smart-search/references/sources-travel.md +20 -0
  268. package/src/browser/base-page.ts +41 -6
  269. package/src/browser/bridge.ts +11 -8
  270. package/src/browser/cdp.ts +1 -8
  271. package/src/browser/daemon-client.ts +11 -1
  272. package/src/browser/dom-helpers.ts +43 -31
  273. package/src/browser/dom-snapshot.ts +23 -1
  274. package/src/browser/page.ts +115 -31
  275. package/src/browser.test.ts +1 -1
  276. package/src/build-manifest.ts +2 -0
  277. package/src/cli.test.ts +133 -0
  278. package/src/cli.ts +73 -11
  279. package/src/clis/1688/item.test.ts +69 -0
  280. package/src/clis/1688/item.ts +282 -0
  281. package/src/clis/1688/search.test.ts +81 -0
  282. package/src/clis/1688/search.ts +402 -0
  283. package/src/clis/1688/shared.test.ts +75 -0
  284. package/src/clis/1688/shared.ts +623 -0
  285. package/src/clis/1688/store.test.ts +69 -0
  286. package/src/clis/1688/store.ts +300 -0
  287. package/src/clis/amazon/bestsellers.test.ts +12 -3
  288. package/src/clis/amazon/bestsellers.ts +6 -178
  289. package/src/clis/amazon/movers-shakers.ts +8 -0
  290. package/src/clis/amazon/new-releases.ts +8 -0
  291. package/src/clis/amazon/rankings.test.ts +47 -0
  292. package/src/clis/amazon/rankings.ts +312 -0
  293. package/src/clis/amazon/shared.test.ts +16 -0
  294. package/src/clis/amazon/shared.ts +134 -12
  295. package/src/clis/bilibili/comments.test.ts +4 -3
  296. package/src/clis/bilibili/comments.ts +2 -2
  297. package/src/clis/bilibili/download.ts +2 -1
  298. package/src/clis/bilibili/subtitle.test.ts +2 -1
  299. package/src/clis/bilibili/subtitle.ts +4 -3
  300. package/src/clis/bilibili/utils.test.ts +21 -0
  301. package/src/clis/bilibili/utils.ts +27 -0
  302. package/src/clis/douban/marks.ts +1 -1
  303. package/src/clis/douban/subject.yaml +50 -19
  304. package/src/clis/doubao/utils.ts +32 -12
  305. package/src/clis/douyin/_shared/browser-fetch.test.ts +0 -1
  306. package/src/clis/douyin/_shared/transcode.test.ts +0 -2
  307. package/src/clis/douyin/draft.test.ts +0 -2
  308. package/src/clis/facebook/search.test.ts +0 -2
  309. package/src/clis/gemini/ask.test.ts +116 -0
  310. package/src/clis/gemini/ask.ts +10 -3
  311. package/src/clis/gemini/reply-state.test.ts +708 -0
  312. package/src/clis/gemini/utils.test.ts +184 -2
  313. package/src/clis/gemini/utils.ts +588 -60
  314. package/src/clis/hupu/detail.ts +126 -0
  315. package/src/clis/hupu/hot.yaml +43 -0
  316. package/src/clis/hupu/like.ts +76 -0
  317. package/src/clis/hupu/reply.ts +76 -0
  318. package/src/clis/hupu/search.ts +95 -0
  319. package/src/clis/hupu/unlike.ts +76 -0
  320. package/src/clis/hupu/utils.ts +381 -0
  321. package/src/clis/instagram/_shared/private-publish.test.ts +827 -0
  322. package/src/clis/instagram/_shared/private-publish.ts +1303 -0
  323. package/src/clis/instagram/_shared/protocol-capture.test.ts +148 -0
  324. package/src/clis/instagram/_shared/protocol-capture.ts +321 -0
  325. package/src/clis/instagram/_shared/runtime-info.ts +91 -0
  326. package/src/clis/instagram/note.test.ts +96 -0
  327. package/src/clis/instagram/note.ts +254 -0
  328. package/src/clis/instagram/post.test.ts +1716 -0
  329. package/src/clis/instagram/post.ts +1620 -0
  330. package/src/clis/instagram/reel.test.ts +191 -0
  331. package/src/clis/instagram/reel.ts +886 -0
  332. package/src/clis/instagram/story.test.ts +191 -0
  333. package/src/clis/instagram/story.ts +151 -0
  334. package/src/clis/sinafinance/stock-rank.ts +68 -0
  335. package/src/clis/substack/utils.test.ts +0 -2
  336. package/src/clis/twitter/post.test.ts +157 -0
  337. package/src/clis/twitter/post.ts +82 -48
  338. package/src/clis/twitter/reply.test.ts +177 -0
  339. package/src/clis/twitter/reply.ts +285 -39
  340. package/src/clis/twitter/search.test.ts +88 -5
  341. package/src/clis/twitter/search.ts +68 -5
  342. package/src/clis/xianyu/chat.test.ts +20 -0
  343. package/src/clis/xianyu/chat.ts +175 -0
  344. package/src/clis/xianyu/item.test.ts +67 -0
  345. package/src/clis/xianyu/item.ts +172 -0
  346. package/src/clis/xianyu/search.test.ts +22 -0
  347. package/src/clis/xianyu/search.ts +151 -0
  348. package/src/clis/xianyu/utils.ts +9 -0
  349. package/src/clis/xiaoe/catalog.yaml +129 -0
  350. package/src/clis/xiaoe/content.yaml +43 -0
  351. package/src/clis/xiaoe/courses.yaml +73 -0
  352. package/src/clis/xiaoe/detail.yaml +39 -0
  353. package/src/clis/xiaoe/play-url.yaml +124 -0
  354. package/src/clis/xiaohongshu/comments.test.ts +0 -2
  355. package/src/clis/xiaohongshu/creator-note-detail.test.ts +0 -2
  356. package/src/clis/xiaohongshu/creator-notes.test.ts +0 -2
  357. package/src/clis/xiaohongshu/download.test.ts +0 -2
  358. package/src/clis/xiaohongshu/note.test.ts +0 -2
  359. package/src/clis/xiaohongshu/publish.test.ts +0 -2
  360. package/src/clis/xiaohongshu/search.test.ts +59 -48
  361. package/src/clis/xiaohongshu/search.ts +31 -21
  362. package/src/clis/yuanbao/ask.test.ts +156 -0
  363. package/src/clis/yuanbao/ask.ts +522 -0
  364. package/src/clis/yuanbao/new.test.ts +36 -0
  365. package/src/clis/yuanbao/new.ts +81 -0
  366. package/src/clis/yuanbao/shared.ts +57 -0
  367. package/src/clis/zhihu/question.test.ts +42 -17
  368. package/src/clis/zhihu/question.ts +31 -26
  369. package/src/commanderAdapter.test.ts +51 -0
  370. package/src/commanderAdapter.ts +8 -4
  371. package/src/completion.test.ts +30 -0
  372. package/src/completion.ts +3 -1
  373. package/src/doctor.ts +1 -1
  374. package/src/electron-apps.ts +9 -1
  375. package/src/errors.ts +1 -1
  376. package/src/execution.ts +26 -30
  377. package/src/explore.ts +1 -1
  378. package/src/launcher.test.ts +121 -7
  379. package/src/launcher.ts +87 -9
  380. package/src/output.test.ts +50 -90
  381. package/src/output.ts +10 -1
  382. package/src/pipeline/executor.test.ts +0 -2
  383. package/src/pipeline/steps/download.test.ts +0 -2
  384. package/src/registry.ts +2 -0
  385. package/src/serialization.ts +2 -0
  386. package/src/types.ts +9 -2
  387. package/tests/e2e/browser-auth.test.ts +9 -0
  388. package/CLI-EXPLORER.md +0 -724
  389. package/CLI-ONESHOT.md +0 -216
  390. package/SKILL.md +0 -59
@@ -0,0 +1,151 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+ import { getRegistry } from '../../registry.js';
6
+ import { __test__ } from './reply.js';
7
+ function createPageMock(evaluateResults, overrides = {}) {
8
+ const evaluate = vi.fn();
9
+ for (const result of evaluateResults) {
10
+ evaluate.mockResolvedValueOnce(result);
11
+ }
12
+ return {
13
+ goto: vi.fn().mockResolvedValue(undefined),
14
+ evaluate,
15
+ snapshot: vi.fn().mockResolvedValue(undefined),
16
+ click: vi.fn().mockResolvedValue(undefined),
17
+ typeText: vi.fn().mockResolvedValue(undefined),
18
+ pressKey: vi.fn().mockResolvedValue(undefined),
19
+ scrollTo: vi.fn().mockResolvedValue(undefined),
20
+ getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
21
+ wait: vi.fn().mockResolvedValue(undefined),
22
+ tabs: vi.fn().mockResolvedValue([]),
23
+ selectTab: vi.fn().mockResolvedValue(undefined),
24
+ networkRequests: vi.fn().mockResolvedValue([]),
25
+ consoleMessages: vi.fn().mockResolvedValue([]),
26
+ scroll: vi.fn().mockResolvedValue(undefined),
27
+ autoScroll: vi.fn().mockResolvedValue(undefined),
28
+ installInterceptor: vi.fn().mockResolvedValue(undefined),
29
+ getInterceptedRequests: vi.fn().mockResolvedValue([]),
30
+ getCookies: vi.fn().mockResolvedValue([]),
31
+ screenshot: vi.fn().mockResolvedValue(''),
32
+ waitForCapture: vi.fn().mockResolvedValue(undefined),
33
+ ...overrides,
34
+ };
35
+ }
36
+ describe('twitter reply command', () => {
37
+ it('keeps the text-only reply flow working', async () => {
38
+ const cmd = getRegistry().get('twitter/reply');
39
+ expect(cmd?.func).toBeTypeOf('function');
40
+ const page = createPageMock([
41
+ { ok: true, message: 'Reply posted successfully.' },
42
+ ]);
43
+ const result = await cmd.func(page, {
44
+ url: 'https://x.com/_kop6/status/2040254679301718161?s=20',
45
+ text: 'text-only reply',
46
+ });
47
+ expect(page.goto).toHaveBeenCalledWith('https://x.com/_kop6/status/2040254679301718161?s=20');
48
+ expect(page.wait).toHaveBeenCalledWith({ selector: '[data-testid="primaryColumn"]' });
49
+ expect(result).toEqual([
50
+ {
51
+ status: 'success',
52
+ message: 'Reply posted successfully.',
53
+ text: 'text-only reply',
54
+ },
55
+ ]);
56
+ });
57
+ it('uploads a local image through the dedicated reply composer when --image is provided', async () => {
58
+ const cmd = getRegistry().get('twitter/reply');
59
+ expect(cmd?.func).toBeTypeOf('function');
60
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-twitter-reply-'));
61
+ const imagePath = path.join(tempDir, 'qr.png');
62
+ fs.writeFileSync(imagePath, Buffer.from([0x89, 0x50, 0x4e, 0x47]));
63
+ const setFileInput = vi.fn().mockResolvedValue(undefined);
64
+ const page = createPageMock([
65
+ { ok: true, previewCount: 1 },
66
+ { ok: true, message: 'Reply posted successfully.' },
67
+ ], {
68
+ setFileInput,
69
+ });
70
+ const result = await cmd.func(page, {
71
+ url: 'https://x.com/_kop6/status/2040254679301718161?s=20',
72
+ text: 'reply with image',
73
+ image: imagePath,
74
+ });
75
+ expect(page.goto).toHaveBeenCalledWith('https://x.com/compose/post?in_reply_to=2040254679301718161');
76
+ expect(page.wait).toHaveBeenNthCalledWith(1, { selector: '[data-testid="tweetTextarea_0"]' });
77
+ expect(page.wait).toHaveBeenNthCalledWith(2, { selector: 'input[type="file"][data-testid="fileInput"]', timeout: 20 });
78
+ expect(setFileInput).toHaveBeenCalledWith([imagePath], 'input[type="file"][data-testid="fileInput"]');
79
+ expect(result).toEqual([
80
+ {
81
+ status: 'success',
82
+ message: 'Reply posted successfully.',
83
+ text: 'reply with image',
84
+ image: imagePath,
85
+ },
86
+ ]);
87
+ });
88
+ it('downloads a remote image before uploading when --image-url is provided', async () => {
89
+ const cmd = getRegistry().get('twitter/reply');
90
+ expect(cmd?.func).toBeTypeOf('function');
91
+ const fetchMock = vi.fn().mockResolvedValue({
92
+ ok: true,
93
+ headers: {
94
+ get: vi.fn().mockReturnValue('image/png'),
95
+ },
96
+ arrayBuffer: vi.fn().mockResolvedValue(Uint8Array.from([0x89, 0x50, 0x4e, 0x47]).buffer),
97
+ });
98
+ vi.stubGlobal('fetch', fetchMock);
99
+ const setFileInput = vi.fn().mockResolvedValue(undefined);
100
+ const page = createPageMock([
101
+ { ok: true, previewCount: 1 },
102
+ { ok: true, message: 'Reply posted successfully.' },
103
+ ], {
104
+ setFileInput,
105
+ });
106
+ const result = await cmd.func(page, {
107
+ url: 'https://x.com/_kop6/status/2040254679301718161?s=20',
108
+ text: 'reply with remote image',
109
+ 'image-url': 'https://example.com/qr',
110
+ });
111
+ expect(fetchMock).toHaveBeenCalledWith('https://example.com/qr');
112
+ expect(setFileInput).toHaveBeenCalledTimes(1);
113
+ const uploadedPath = setFileInput.mock.calls[0][0][0];
114
+ expect(uploadedPath).toMatch(/opencli-twitter-reply-.*\/image\.png$/);
115
+ expect(fs.existsSync(uploadedPath)).toBe(false);
116
+ expect(result).toEqual([
117
+ {
118
+ status: 'success',
119
+ message: 'Reply posted successfully.',
120
+ text: 'reply with remote image',
121
+ 'image-url': 'https://example.com/qr',
122
+ },
123
+ ]);
124
+ vi.unstubAllGlobals();
125
+ });
126
+ it('rejects invalid image paths early', async () => {
127
+ await expect(() => __test__.resolveImagePath('/tmp/does-not-exist.png'))
128
+ .toThrow('Image file not found');
129
+ });
130
+ it('rejects using --image and --image-url together', async () => {
131
+ const cmd = getRegistry().get('twitter/reply');
132
+ expect(cmd?.func).toBeTypeOf('function');
133
+ const page = createPageMock([]);
134
+ await expect(cmd.func(page, {
135
+ url: 'https://x.com/_kop6/status/2040254679301718161?s=20',
136
+ text: 'nope',
137
+ image: '/tmp/a.png',
138
+ 'image-url': 'https://example.com/a.png',
139
+ })).rejects.toThrow('Use either --image or --image-url, not both.');
140
+ });
141
+ it('extracts tweet ids from both user and i/status URLs', () => {
142
+ expect(__test__.extractTweetId('https://x.com/_kop6/status/2040254679301718161?s=20')).toBe('2040254679301718161');
143
+ expect(__test__.extractTweetId('https://x.com/i/status/2040318731105313143')).toBe('2040318731105313143');
144
+ expect(__test__.buildReplyComposerUrl('https://x.com/i/status/2040318731105313143'))
145
+ .toBe('https://x.com/compose/post?in_reply_to=2040318731105313143');
146
+ });
147
+ it('prefers content-type when resolving remote image extensions', () => {
148
+ expect(__test__.resolveImageExtension('https://example.com/no-ext', 'image/webp')).toBe('.webp');
149
+ expect(__test__.resolveImageExtension('https://example.com/a.jpeg?x=1', null)).toBe('.jpeg');
150
+ });
151
+ });
@@ -1,15 +1,18 @@
1
1
  import { CommandExecutionError } from '../../errors.js';
2
2
  import { cli, Strategy } from '../../registry.js';
3
3
  /**
4
- * Trigger Twitter search SPA navigation and retry once on transient failures.
4
+ * Trigger Twitter search SPA navigation with fallback strategies.
5
5
  *
6
- * Twitter/X sometimes keeps the page on /explore for a short period even after
7
- * pushState + popstate. A second attempt is enough for the intermittent cases
8
- * reported in issue #353 while keeping the flow narrowly scoped.
6
+ * Primary: pushState + popstate (works in most environments).
7
+ * Fallback: Type into the search input and press Enter when pushState fails
8
+ * intermittently (e.g. due to Twitter A/B tests or timing races — see #690).
9
+ *
10
+ * Both strategies preserve the JS context so the fetch interceptor stays alive.
9
11
  */
10
12
  async function navigateToSearch(page, query, filter) {
11
13
  const searchUrl = JSON.stringify(`/search?q=${encodeURIComponent(query)}&f=${filter}`);
12
14
  let lastPath = '';
15
+ // Strategy 1 (primary): pushState + popstate with retry
13
16
  for (let attempt = 1; attempt <= 2; attempt++) {
14
17
  await page.evaluate(`
15
18
  (() => {
@@ -17,7 +20,12 @@ async function navigateToSearch(page, query, filter) {
17
20
  window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));
18
21
  })()
19
22
  `);
20
- await page.wait({ selector: '[data-testid="primaryColumn"]' });
23
+ try {
24
+ await page.wait({ selector: '[data-testid="primaryColumn"]' });
25
+ }
26
+ catch {
27
+ // selector timeout — fall through to path check or next attempt
28
+ }
21
29
  lastPath = String(await page.evaluate('() => window.location.pathname') || '');
22
30
  if (lastPath.startsWith('/search')) {
23
31
  return;
@@ -26,6 +34,60 @@ async function navigateToSearch(page, query, filter) {
26
34
  await page.wait(1);
27
35
  }
28
36
  }
37
+ // Strategy 2 (fallback): Use the search input on /explore.
38
+ // The nativeSetter + Enter approach triggers Twitter's own form handler,
39
+ // performing SPA navigation without a full page reload.
40
+ const queryStr = JSON.stringify(query);
41
+ const navResult = await page.evaluate(`(async () => {
42
+ try {
43
+ const input = document.querySelector('[data-testid="SearchBox_Search_Input"]');
44
+ if (!input) return { ok: false };
45
+
46
+ input.focus();
47
+ await new Promise(r => setTimeout(r, 300));
48
+
49
+ const nativeSetter = Object.getOwnPropertyDescriptor(
50
+ window.HTMLInputElement.prototype, 'value'
51
+ )?.set;
52
+ if (!nativeSetter) return { ok: false };
53
+ nativeSetter.call(input, ${queryStr});
54
+ input.dispatchEvent(new Event('input', { bubbles: true }));
55
+ input.dispatchEvent(new Event('change', { bubbles: true }));
56
+ await new Promise(r => setTimeout(r, 500));
57
+
58
+ input.dispatchEvent(new KeyboardEvent('keydown', {
59
+ key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true
60
+ }));
61
+
62
+ return { ok: true };
63
+ } catch {
64
+ return { ok: false };
65
+ }
66
+ })()`);
67
+ if (navResult?.ok) {
68
+ try {
69
+ await page.wait({ selector: '[data-testid="primaryColumn"]' });
70
+ }
71
+ catch {
72
+ // fall through to path check
73
+ }
74
+ lastPath = String(await page.evaluate('() => window.location.pathname') || '');
75
+ if (lastPath.startsWith('/search')) {
76
+ if (filter === 'live') {
77
+ await page.evaluate(`(() => {
78
+ const tabs = document.querySelectorAll('[role="tab"]');
79
+ for (const tab of tabs) {
80
+ if (tab.textContent.includes('Latest') || tab.textContent.includes('最新')) {
81
+ tab.click();
82
+ return;
83
+ }
84
+ }
85
+ })()`);
86
+ await page.wait(2);
87
+ }
88
+ return;
89
+ }
90
+ }
29
91
  throw new CommandExecutionError(`SPA navigation to /search failed. Final path: ${lastPath || '(empty)'}. Twitter may have changed its routing.`);
30
92
  }
31
93
  cli({
@@ -132,14 +132,92 @@ describe('twitter search command', () => {
132
132
  const pushStateCall = evaluate.mock.calls[0][0];
133
133
  expect(pushStateCall).toContain('f=top');
134
134
  });
135
+ it('falls back to search input when pushState fails twice', async () => {
136
+ const command = getRegistry().get('twitter/search');
137
+ expect(command?.func).toBeTypeOf('function');
138
+ const evaluate = vi.fn()
139
+ .mockResolvedValueOnce(undefined) // pushState attempt 1
140
+ .mockResolvedValueOnce('/explore') // pathname check 1 — not /search
141
+ .mockResolvedValueOnce(undefined) // pushState attempt 2
142
+ .mockResolvedValueOnce('/explore') // pathname check 2 — still not /search
143
+ .mockResolvedValueOnce({ ok: true }) // search input fallback succeeds
144
+ .mockResolvedValueOnce('/search'); // pathname check after fallback
145
+ const page = {
146
+ goto: vi.fn().mockResolvedValue(undefined),
147
+ wait: vi.fn().mockResolvedValue(undefined),
148
+ installInterceptor: vi.fn().mockResolvedValue(undefined),
149
+ evaluate,
150
+ autoScroll: vi.fn().mockResolvedValue(undefined),
151
+ getInterceptedRequests: vi.fn().mockResolvedValue([
152
+ {
153
+ data: {
154
+ search_by_raw_query: {
155
+ search_timeline: {
156
+ timeline: {
157
+ instructions: [
158
+ {
159
+ type: 'TimelineAddEntries',
160
+ entries: [
161
+ {
162
+ entryId: 'tweet-99',
163
+ content: {
164
+ itemContent: {
165
+ tweet_results: {
166
+ result: {
167
+ rest_id: '99',
168
+ legacy: {
169
+ full_text: 'fallback works',
170
+ favorite_count: 3,
171
+ created_at: 'Wed Apr 02 12:00:00 +0000 2026',
172
+ },
173
+ core: {
174
+ user_results: {
175
+ result: {
176
+ core: { screen_name: 'bob' },
177
+ },
178
+ },
179
+ },
180
+ views: { count: '5' },
181
+ },
182
+ },
183
+ },
184
+ },
185
+ },
186
+ ],
187
+ },
188
+ ],
189
+ },
190
+ },
191
+ },
192
+ },
193
+ },
194
+ ]),
195
+ };
196
+ const result = await command.func(page, { query: 'test fallback', filter: 'top', limit: 5 });
197
+ expect(result).toEqual([
198
+ {
199
+ id: '99',
200
+ author: 'bob',
201
+ text: 'fallback works',
202
+ created_at: 'Wed Apr 02 12:00:00 +0000 2026',
203
+ likes: 3,
204
+ views: '5',
205
+ url: 'https://x.com/i/status/99',
206
+ },
207
+ ]);
208
+ // 6 evaluate calls: 2x pushState + 2x pathname check + 1x fallback + 1x pathname check
209
+ expect(evaluate).toHaveBeenCalledTimes(6);
210
+ expect(page.autoScroll).toHaveBeenCalled();
211
+ });
135
212
  it('throws with the final path after both attempts fail', async () => {
136
213
  const command = getRegistry().get('twitter/search');
137
214
  expect(command?.func).toBeTypeOf('function');
138
215
  const evaluate = vi.fn()
139
- .mockResolvedValueOnce(undefined)
140
- .mockResolvedValueOnce('/explore')
141
- .mockResolvedValueOnce(undefined)
142
- .mockResolvedValueOnce('/login');
216
+ .mockResolvedValueOnce(undefined) // pushState attempt 1
217
+ .mockResolvedValueOnce('/explore') // pathname check 1
218
+ .mockResolvedValueOnce(undefined) // pushState attempt 2
219
+ .mockResolvedValueOnce('/login') // pathname check 2
220
+ .mockResolvedValueOnce({ ok: false }); // search input fallback
143
221
  const page = {
144
222
  goto: vi.fn().mockResolvedValue(undefined),
145
223
  wait: vi.fn().mockResolvedValue(undefined),
@@ -153,6 +231,6 @@ describe('twitter search command', () => {
153
231
  .toThrow('Final path: /login');
154
232
  expect(page.autoScroll).not.toHaveBeenCalled();
155
233
  expect(page.getInterceptedRequests).not.toHaveBeenCalled();
156
- expect(evaluate).toHaveBeenCalledTimes(4);
234
+ expect(evaluate).toHaveBeenCalledTimes(5);
157
235
  });
158
236
  });
@@ -0,0 +1,7 @@
1
+ import { normalizeNumericId } from './utils.js';
2
+ declare function buildChatUrl(itemId: string, peerUserId: string): string;
3
+ export declare const __test__: {
4
+ normalizeNumericId: typeof normalizeNumericId;
5
+ buildChatUrl: typeof buildChatUrl;
6
+ };
7
+ export {};
@@ -0,0 +1,146 @@
1
+ import { AuthRequiredError, SelectorError } from '../../errors.js';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ import { normalizeNumericId } from './utils.js';
4
+ function buildChatUrl(itemId, peerUserId) {
5
+ return `https://www.goofish.com/im?itemId=${encodeURIComponent(itemId)}&peerUserId=${encodeURIComponent(peerUserId)}`;
6
+ }
7
+ function buildExtractChatStateEvaluate() {
8
+ return `
9
+ (() => {
10
+ const clean = (value) => (value || '').replace(/\\s+/g, ' ').trim();
11
+ const bodyText = document.body?.innerText || '';
12
+ const requiresAuth = /请先登录|登录后/.test(bodyText);
13
+
14
+ const textarea = document.querySelector('textarea');
15
+ const sendButton = Array.from(document.querySelectorAll('button'))
16
+ .find((btn) => clean(btn.textContent || '') === '发送');
17
+ const topbar = document.querySelector('[class*="message-topbar"]');
18
+ const itemCard = Array.from(document.querySelectorAll('a[href*="/item?id="]'))
19
+ .find((el) => el.closest('main'));
20
+ const itemTitleNode =
21
+ document.querySelector('[class*="container"] [class*="title"]')
22
+ || document.querySelector('[class*="item-main-info"] [class*="desc"]')
23
+ || document.querySelector('[class*="headSkuInfo"]')
24
+ || itemCard?.querySelector('[class*="title"]')
25
+ || itemCard?.previousElementSibling?.querySelector?.('[class*="title"]');
26
+
27
+ const messageRoot = document.querySelector('#message-list-scrollable');
28
+ const visibleMessages = Array.from(
29
+ (messageRoot || document).querySelectorAll('[class*="message"], [class*="msg"], [class*="bubble"]')
30
+ ).map((el) => clean(el.textContent || ''))
31
+ .filter(Boolean)
32
+ .filter((text) => !['发送', '闲鱼号', '立即购买'].includes(text))
33
+ .filter((text) => !/^消息\\d*\\+?$/.test(text))
34
+ .slice(-20);
35
+
36
+ return {
37
+ requiresAuth,
38
+ title: clean(document.title || ''),
39
+ peer_name: clean(topbar?.querySelector('[class*="text1"]')?.textContent || ''),
40
+ peer_masked_id: clean(topbar?.querySelector('[class*="text2"]')?.textContent || '').replace(/^\\(|\\)$/g, ''),
41
+ item_title: clean(itemTitleNode?.textContent || ''),
42
+ item_url: itemCard?.href || '',
43
+ price: clean(itemCard?.querySelector('[class*="money"]')?.textContent || ''),
44
+ location: clean(itemCard?.querySelector('[class*="delivery"] + [class*="delivery"], [class*="delivery"]:last-child')?.textContent || ''),
45
+ can_input: Boolean(textarea && !textarea.disabled),
46
+ can_send: Boolean(sendButton),
47
+ visible_messages: visibleMessages,
48
+ };
49
+ })()
50
+ `;
51
+ }
52
+ function buildSendMessageEvaluate(text) {
53
+ return `
54
+ (() => {
55
+ const clean = (value) => (value || '').replace(/\\s+/g, ' ').trim();
56
+ const textarea = document.querySelector('textarea');
57
+ if (!textarea || textarea.disabled) {
58
+ return { ok: false, reason: 'input-not-found' };
59
+ }
60
+
61
+ const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
62
+ if (!setter) {
63
+ return { ok: false, reason: 'textarea-setter-not-found' };
64
+ }
65
+
66
+ textarea.focus();
67
+ setter.call(textarea, ${JSON.stringify(text)});
68
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
69
+ textarea.dispatchEvent(new Event('change', { bubbles: true }));
70
+
71
+ const sendButton = Array.from(document.querySelectorAll('button'))
72
+ .find((btn) => clean(btn.textContent || '') === '发送');
73
+ if (!sendButton) {
74
+ return { ok: false, reason: 'send-button-not-found' };
75
+ }
76
+
77
+ sendButton.click();
78
+ return { ok: true };
79
+ })()
80
+ `;
81
+ }
82
+ cli({
83
+ site: 'xianyu',
84
+ name: 'chat',
85
+ description: '打开闲鱼聊一聊会话,并可选发送消息',
86
+ domain: 'www.goofish.com',
87
+ strategy: Strategy.COOKIE,
88
+ navigateBefore: false,
89
+ browser: true,
90
+ args: [
91
+ { name: 'item_id', required: true, positional: true, help: '闲鱼商品 item_id' },
92
+ { name: 'user_id', required: true, positional: true, help: '聊一聊对方的 user_id / peerUserId' },
93
+ { name: 'text', help: 'Message to send after opening the chat' },
94
+ ],
95
+ columns: ['status', 'peer_name', 'item_title', 'price', 'location', 'message'],
96
+ func: async (page, kwargs) => {
97
+ const itemId = normalizeNumericId(kwargs.item_id, 'item_id', '1038951278192');
98
+ const userId = normalizeNumericId(kwargs.user_id, 'user_id', '3650092411');
99
+ const url = buildChatUrl(itemId, userId);
100
+ const text = String(kwargs.text || '').trim();
101
+ await page.goto(url);
102
+ await page.wait(2);
103
+ const state = await page.evaluate(buildExtractChatStateEvaluate());
104
+ if (state?.requiresAuth) {
105
+ throw new AuthRequiredError('www.goofish.com', 'Xianyu chat requires a logged-in browser session');
106
+ }
107
+ if (!state?.can_input) {
108
+ throw new SelectorError('闲鱼聊天输入框', '未找到可用的聊天输入框,请确认该会话页已正确加载');
109
+ }
110
+ if (!text) {
111
+ return [{
112
+ status: 'ready',
113
+ peer_name: state.peer_name || '',
114
+ item_title: state.item_title || '',
115
+ price: state.price || '',
116
+ location: state.location || '',
117
+ message: (state.visible_messages || []).slice(-1)[0] || '',
118
+ peer_user_id: userId,
119
+ item_id: itemId,
120
+ url,
121
+ item_url: state.item_url || '',
122
+ }];
123
+ }
124
+ const sent = await page.evaluate(buildSendMessageEvaluate(text));
125
+ if (!sent?.ok) {
126
+ throw new SelectorError('闲鱼发送按钮', `消息发送失败:${sent?.reason || 'unknown-reason'}`);
127
+ }
128
+ await page.wait(1);
129
+ return [{
130
+ status: 'sent',
131
+ peer_name: state.peer_name || '',
132
+ item_title: state.item_title || '',
133
+ price: state.price || '',
134
+ location: state.location || '',
135
+ message: text,
136
+ peer_user_id: userId,
137
+ item_id: itemId,
138
+ url,
139
+ item_url: state.item_url || '',
140
+ }];
141
+ },
142
+ });
143
+ export const __test__ = {
144
+ normalizeNumericId,
145
+ buildChatUrl,
146
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { __test__ } from './chat.js';
3
+ describe('xianyu chat helpers', () => {
4
+ it('builds goofish im urls from ids', () => {
5
+ expect(__test__.buildChatUrl('1038951278192', '3650092411')).toBe('https://www.goofish.com/im?itemId=1038951278192&peerUserId=3650092411');
6
+ });
7
+ it('normalizes numeric ids', () => {
8
+ expect(__test__.normalizeNumericId('1038951278192', 'item_id', '1038951278192')).toBe('1038951278192');
9
+ expect(__test__.normalizeNumericId(3650092411, 'user_id', '3650092411')).toBe('3650092411');
10
+ });
11
+ it('rejects non-numeric ids', () => {
12
+ expect(() => __test__.normalizeNumericId('abc', 'item_id', '1038951278192')).toThrow();
13
+ expect(() => __test__.normalizeNumericId('3650092411x', 'user_id', '3650092411')).toThrow();
14
+ });
15
+ });
@@ -0,0 +1,7 @@
1
+ import { normalizeNumericId } from './utils.js';
2
+ declare function buildItemUrl(itemId: string): string;
3
+ export declare const __test__: {
4
+ normalizeNumericId: typeof normalizeNumericId;
5
+ buildItemUrl: typeof buildItemUrl;
6
+ };
7
+ export {};