@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
@@ -1,3 +1,4 @@
1
+ import { CommandExecutionError } from '../../errors.js';
1
2
  export const GEMINI_DOMAIN = 'gemini.google.com';
2
3
  export const GEMINI_APP_URL = 'https://gemini.google.com/app';
3
4
  const GEMINI_RESPONSE_NOISE_PATTERNS = [
@@ -6,6 +7,57 @@ const GEMINI_RESPONSE_NOISE_PATTERNS = [
6
7
  /Google Privacy Policy/gi,
7
8
  /Opens in a new window/gi,
8
9
  ];
10
+ const GEMINI_TRANSCRIPT_CHROME_MARKERS = ['gemini', '我的内容', '对话', 'google terms', 'google privacy policy'];
11
+ const GEMINI_COMPOSER_SELECTORS = [
12
+ '.ql-editor[contenteditable="true"]',
13
+ '.ql-editor[role="textbox"]',
14
+ '.ql-editor[aria-label*="Gemini"]',
15
+ '[contenteditable="true"][aria-label*="Gemini"]',
16
+ '[aria-label="Enter a prompt for Gemini"]',
17
+ '[aria-label*="prompt for Gemini"]',
18
+ ];
19
+ const GEMINI_COMPOSER_MARKER_ATTR = 'data-opencli-gemini-composer';
20
+ const GEMINI_COMPOSER_PREPARE_ATTEMPTS = 4;
21
+ const GEMINI_COMPOSER_PREPARE_WAIT_SECONDS = 1;
22
+ function buildGeminiComposerLocatorScript() {
23
+ const selectorsJson = JSON.stringify(GEMINI_COMPOSER_SELECTORS);
24
+ const markerAttrJson = JSON.stringify(GEMINI_COMPOSER_MARKER_ATTR);
25
+ return `
26
+ const isVisible = (el) => {
27
+ if (!(el instanceof HTMLElement)) return false;
28
+ const style = window.getComputedStyle(el);
29
+ if (style.display === 'none' || style.visibility === 'hidden') return false;
30
+ const rect = el.getBoundingClientRect();
31
+ return rect.width > 0 && rect.height > 0;
32
+ };
33
+
34
+ const markerAttr = ${markerAttrJson};
35
+ const clearComposerMarkers = (active) => {
36
+ document.querySelectorAll('[' + markerAttr + ']').forEach((node) => {
37
+ if (node !== active) node.removeAttribute(markerAttr);
38
+ });
39
+ };
40
+
41
+ const markComposer = (node) => {
42
+ if (!(node instanceof HTMLElement)) return null;
43
+ clearComposerMarkers(node);
44
+ node.setAttribute(markerAttr, '1');
45
+ return node;
46
+ };
47
+
48
+ const findComposer = () => {
49
+ const marked = document.querySelector('[' + markerAttr + '="1"]');
50
+ if (marked instanceof HTMLElement && isVisible(marked)) return marked;
51
+
52
+ const selectors = ${selectorsJson};
53
+ for (const selector of selectors) {
54
+ const node = Array.from(document.querySelectorAll(selector)).find((candidate) => candidate instanceof HTMLElement && isVisible(candidate));
55
+ if (node instanceof HTMLElement) return markComposer(node);
56
+ }
57
+ return null;
58
+ };
59
+ `;
60
+ }
9
61
  export function sanitizeGeminiResponseText(value, promptText) {
10
62
  let sanitized = value;
11
63
  for (const pattern of GEMINI_RESPONSE_NOISE_PATTERNS) {
@@ -29,31 +81,146 @@ export function collectGeminiTranscriptAdditions(beforeLines, currentLines, prom
29
81
  const beforeSet = new Set(beforeLines);
30
82
  const additions = currentLines
31
83
  .filter((line) => !beforeSet.has(line))
32
- .map((line) => sanitizeGeminiResponseText(line, promptText))
84
+ .map((line) => extractGeminiTranscriptLineCandidate(line, promptText))
33
85
  .filter((line) => line && line !== promptText);
34
86
  return additions.join('\n').trim();
35
87
  }
88
+ export function collapseAdjacentGeminiTurns(turns) {
89
+ const collapsed = [];
90
+ for (const turn of turns) {
91
+ if (!turn || typeof turn.Role !== 'string' || typeof turn.Text !== 'string')
92
+ continue;
93
+ const previous = collapsed.at(-1);
94
+ if (previous?.Role === turn.Role && previous.Text === turn.Text)
95
+ continue;
96
+ collapsed.push(turn);
97
+ }
98
+ return collapsed;
99
+ }
100
+ function hasGeminiTurnPrefix(before, current) {
101
+ if (before.length > current.length)
102
+ return false;
103
+ return before.every((turn, index) => (turn.Role === current[index]?.Role
104
+ && turn.Text === current[index]?.Text));
105
+ }
106
+ function findLastMatchingGeminiTurnIndex(turns, target) {
107
+ if (!target)
108
+ return null;
109
+ for (let index = turns.length - 1; index >= 0; index -= 1) {
110
+ const turn = turns[index];
111
+ if (turn?.Role === target.Role && turn.Text === target.Text) {
112
+ return index;
113
+ }
114
+ }
115
+ return null;
116
+ }
117
+ function diffTrustedStructuredTurns(before, current) {
118
+ if (!before.structuredTurnsTrusted || !current.structuredTurnsTrusted) {
119
+ return {
120
+ appendedTurns: [],
121
+ hasTrustedAppend: false,
122
+ hasNewUserTurn: false,
123
+ hasNewAssistantTurn: false,
124
+ };
125
+ }
126
+ if (!hasGeminiTurnPrefix(before.turns, current.turns)) {
127
+ return {
128
+ appendedTurns: [],
129
+ hasTrustedAppend: false,
130
+ hasNewUserTurn: false,
131
+ hasNewAssistantTurn: false,
132
+ };
133
+ }
134
+ const appendedTurns = current.turns.slice(before.turns.length);
135
+ return {
136
+ appendedTurns,
137
+ hasTrustedAppend: appendedTurns.length > 0,
138
+ hasNewUserTurn: appendedTurns.some((turn) => turn.Role === 'User'),
139
+ hasNewAssistantTurn: appendedTurns.some((turn) => turn.Role === 'Assistant'),
140
+ };
141
+ }
142
+ function diffTranscriptLines(before, current) {
143
+ const beforeLines = new Set(before.transcriptLines);
144
+ return current.transcriptLines.filter((line) => !beforeLines.has(line));
145
+ }
146
+ function isLikelyGeminiTranscriptChrome(line) {
147
+ const lower = line.toLowerCase();
148
+ const markerHits = GEMINI_TRANSCRIPT_CHROME_MARKERS.filter((marker) => lower.includes(marker)).length;
149
+ return markerHits >= 2;
150
+ }
151
+ function extractGeminiTranscriptLineCandidate(transcriptLine, promptText) {
152
+ const candidate = transcriptLine.trim();
153
+ if (!candidate)
154
+ return '';
155
+ const prompt = promptText.trim();
156
+ const sanitized = sanitizeGeminiResponseText(candidate, promptText);
157
+ if (!prompt)
158
+ return sanitized;
159
+ if (!candidate.includes(prompt))
160
+ return sanitized;
161
+ if (sanitized && sanitized !== prompt && sanitized !== candidate)
162
+ return sanitized;
163
+ if (isLikelyGeminiTranscriptChrome(candidate))
164
+ return '';
165
+ // Some transcript snapshots flatten "prompt + answer" into a single line.
166
+ // Recover the answer only when the line starts with the current prompt.
167
+ if (candidate.startsWith(prompt)) {
168
+ const tail = candidate.slice(prompt.length).replace(/^[\s::,,-]+/, '').trim();
169
+ return tail ? sanitizeGeminiResponseText(tail, '') : '';
170
+ }
171
+ return sanitized;
172
+ }
36
173
  function getStateScript() {
37
174
  return `
38
175
  (() => {
176
+ ${buildGeminiComposerLocatorScript()}
177
+
39
178
  const signInNode = Array.from(document.querySelectorAll('a, button')).find((node) => {
40
179
  const text = (node.textContent || '').trim().toLowerCase();
41
180
  const aria = (node.getAttribute('aria-label') || '').trim().toLowerCase();
42
181
  const href = node.getAttribute('href') || '';
43
182
  return text === 'sign in'
44
183
  || aria === 'sign in'
184
+ || text === '登录'
185
+ || aria === '登录'
45
186
  || href.includes('accounts.google.com/ServiceLogin');
46
187
  });
47
188
 
48
- const composer = document.querySelector('[aria-label="Enter a prompt for Gemini"], [aria-label*="prompt for Gemini"], .ql-editor[aria-label*="Gemini"], [contenteditable="true"][aria-label*="Gemini"]');
49
- const sendButton = document.querySelector('button[aria-label="Send message"]');
189
+ const composer = findComposer();
50
190
 
51
191
  return {
52
192
  url: window.location.href,
53
193
  title: document.title || '',
54
194
  isSignedIn: signInNode ? false : (composer ? true : null),
55
195
  composerLabel: composer?.getAttribute('aria-label') || '',
56
- canSend: !!(sendButton && !sendButton.disabled),
196
+ canSend: !!composer,
197
+ };
198
+ })()
199
+ `;
200
+ }
201
+ function readGeminiSnapshotScript() {
202
+ return `
203
+ (() => {
204
+ ${buildGeminiComposerLocatorScript()}
205
+ const composer = findComposer();
206
+ const composerText = composer?.textContent?.replace(/\\u00a0/g, ' ').trim() || '';
207
+ const isGenerating = !!Array.from(document.querySelectorAll('button, [role="button"]')).find((node) => {
208
+ const text = (node.textContent || '').trim().toLowerCase();
209
+ const aria = (node.getAttribute('aria-label') || '').trim().toLowerCase();
210
+ return text === 'stop response'
211
+ || aria === 'stop response'
212
+ || text === '停止回答'
213
+ || aria === '停止回答';
214
+ });
215
+ const turns = ${getTurnsScript().trim()};
216
+ const transcriptLines = ${getTranscriptLinesScript().trim()};
217
+
218
+ return {
219
+ turns,
220
+ transcriptLines,
221
+ composerHasText: composerText.length > 0,
222
+ isGenerating,
223
+ structuredTurnsTrusted: turns.length > 0 || transcriptLines.length === 0,
57
224
  };
58
225
  })()
59
226
  `;
@@ -159,7 +326,16 @@ function getTurnsScript() {
159
326
  ];
160
327
 
161
328
  const roots = selectors.flatMap((selector) => Array.from(document.querySelectorAll(selector)));
162
- const unique = roots.filter((el, index, all) => all.indexOf(el) === index).filter(isVisible);
329
+ const unique = roots
330
+ .filter((el, index, all) => all.indexOf(el) === index)
331
+ .filter(isVisible)
332
+ .sort((left, right) => {
333
+ if (left === right) return 0;
334
+ const relation = left.compareDocumentPosition(right);
335
+ if (relation & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
336
+ if (relation & Node.DOCUMENT_POSITION_PRECEDING) return 1;
337
+ return 0;
338
+ });
163
339
 
164
340
  const turns = unique.map((el) => {
165
341
  const text = clean(el.innerText || el.textContent || '');
@@ -179,52 +355,184 @@ function getTurnsScript() {
179
355
  return role ? { Role: role, Text: text } : null;
180
356
  }).filter(Boolean);
181
357
 
182
- const deduped = [];
183
- const seen = new Set();
184
- for (const turn of turns) {
185
- const key = turn.Role + '::' + turn.Text;
186
- if (seen.has(key)) continue;
187
- seen.add(key);
188
- deduped.push(turn);
189
- }
190
- return deduped;
358
+ return turns;
191
359
  })()
192
360
  `;
193
361
  }
194
- function fillAndSubmitComposerScript(text) {
362
+ function prepareComposerScript() {
195
363
  return `
196
- ((inputText) => {
197
- const cleanInsert = (el) => {
198
- if (!(el instanceof HTMLElement)) throw new Error('Composer is not editable');
199
- el.focus();
364
+ (() => {
365
+ ${buildGeminiComposerLocatorScript()}
366
+ const composer = findComposer();
367
+
368
+ if (!(composer instanceof HTMLElement)) {
369
+ return { ok: false, reason: 'Could not find Gemini composer' };
370
+ }
371
+
372
+ try {
373
+ composer.focus();
200
374
  const selection = window.getSelection();
201
375
  const range = document.createRange();
202
- range.selectNodeContents(el);
376
+ range.selectNodeContents(composer);
203
377
  range.collapse(false);
204
378
  selection?.removeAllRanges();
205
379
  selection?.addRange(range);
206
- el.textContent = '';
207
- document.execCommand('insertText', false, inputText);
208
- el.dispatchEvent(new InputEvent('input', { bubbles: true, data: inputText, inputType: 'insertText' }));
380
+ composer.textContent = '';
381
+ composer.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteContentBackward' }));
382
+ } catch (error) {
383
+ return {
384
+ ok: false,
385
+ reason: error instanceof Error ? error.message : String(error),
386
+ };
387
+ }
388
+
389
+ return {
390
+ ok: true,
391
+ label: composer.getAttribute('aria-label') || '',
209
392
  };
393
+ })()
394
+ `;
395
+ }
396
+ function composerHasTextScript() {
397
+ return `
398
+ (() => {
399
+ ${buildGeminiComposerLocatorScript()}
400
+ const composer = findComposer();
401
+
402
+ return {
403
+ hasText: !!(composer && ((composer.textContent || '').trim() || (composer.innerText || '').trim())),
404
+ };
405
+ })()
406
+ `;
407
+ }
408
+ function insertComposerTextFallbackScript(text) {
409
+ return `
410
+ ((inputText) => {
411
+ ${buildGeminiComposerLocatorScript()}
412
+ const composer = findComposer();
413
+
414
+ if (!(composer instanceof HTMLElement)) {
415
+ return { hasText: false, reason: 'Could not find Gemini composer' };
416
+ }
417
+
418
+ const selection = window.getSelection();
419
+ const range = document.createRange();
420
+ range.selectNodeContents(composer);
421
+ range.collapse(false);
422
+ selection?.removeAllRanges();
423
+ selection?.addRange(range);
424
+
425
+ composer.focus();
426
+ composer.textContent = '';
427
+ const execResult = typeof document.execCommand === 'function'
428
+ ? document.execCommand('insertText', false, inputText)
429
+ : false;
430
+
431
+ if (!execResult) {
432
+ const paragraph = document.createElement('p');
433
+ const lines = String(inputText).split(/\\n/);
434
+ for (const [index, line] of lines.entries()) {
435
+ if (index > 0) paragraph.appendChild(document.createElement('br'));
436
+ paragraph.appendChild(document.createTextNode(line));
437
+ }
438
+ composer.replaceChildren(paragraph);
439
+ }
440
+
441
+ composer.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, data: inputText, inputType: 'insertText' }));
442
+ composer.dispatchEvent(new InputEvent('input', { bubbles: true, data: inputText, inputType: 'insertText' }));
443
+ composer.dispatchEvent(new Event('change', { bubbles: true }));
444
+
445
+ return {
446
+ hasText: !!((composer.textContent || '').trim() || (composer.innerText || '').trim()),
447
+ };
448
+ })(${JSON.stringify(text)})
449
+ `;
450
+ }
451
+ function submitComposerScript() {
452
+ return `
453
+ (() => {
454
+ ${buildGeminiComposerLocatorScript()}
455
+ const composer = findComposer();
210
456
 
211
- const composer = document.querySelector('[aria-label="Enter a prompt for Gemini"], [aria-label*="prompt for Gemini"], .ql-editor[aria-label*="Gemini"], [contenteditable="true"][aria-label*="Gemini"]');
212
457
  if (!(composer instanceof HTMLElement)) {
213
458
  throw new Error('Could not find Gemini composer');
214
459
  }
215
460
 
216
- cleanInsert(composer);
461
+ const composerRect = composer.getBoundingClientRect();
462
+ const rootCandidates = [
463
+ composer.closest('form'),
464
+ composer.closest('[role="form"]'),
465
+ composer.closest('.input-area-container'),
466
+ composer.closest('.textbox-container'),
467
+ composer.closest('.input-wrapper'),
468
+ composer.parentElement,
469
+ composer.parentElement?.parentElement,
470
+ ].filter(Boolean);
471
+
472
+ const seen = new Set();
473
+ const buttons = [];
474
+ for (const root of rootCandidates) {
475
+ root.querySelectorAll('button, [role="button"]').forEach((node) => {
476
+ if (!(node instanceof HTMLElement)) return;
477
+ if (seen.has(node)) return;
478
+ seen.add(node);
479
+ buttons.push(node);
480
+ });
481
+ }
482
+
483
+ const excludedPattern = /main menu|主菜单|microphone|麦克风|upload|上传|mode|模式|tools|工具|settings|临时对话|new chat|新对话/i;
484
+ const submitPattern = /send|发送|submit|提交/i;
485
+ let bestButton = null;
486
+ let bestScore = -1;
487
+
488
+ for (const button of buttons) {
489
+ if (!isVisible(button)) continue;
490
+ if (button instanceof HTMLButtonElement && button.disabled) continue;
491
+ if (button.getAttribute('aria-disabled') === 'true') continue;
492
+
493
+ const label = ((button.getAttribute('aria-label') || '') + ' ' + ((button.textContent || '').trim())).trim();
494
+ if (excludedPattern.test(label)) continue;
495
+
496
+ const rect = button.getBoundingClientRect();
497
+ const verticalDistance = Math.abs((rect.top + rect.bottom) / 2 - (composerRect.top + composerRect.bottom) / 2);
498
+ if (verticalDistance > 160) continue;
499
+
500
+ let score = 0;
501
+ if (submitPattern.test(label)) score += 10;
502
+ if (rect.left >= composerRect.right - 160) score += 3;
503
+ if (rect.left >= composerRect.left) score += 1;
504
+ if (rect.width <= 96 && rect.height <= 96) score += 1;
217
505
 
218
- const sendButton = document.querySelector('button[aria-label="Send message"]');
219
- if (sendButton instanceof HTMLButtonElement && !sendButton.disabled) {
220
- sendButton.click();
506
+ if (score > bestScore) {
507
+ bestScore = score;
508
+ bestButton = button;
509
+ }
510
+ }
511
+
512
+ if (bestButton instanceof HTMLElement && bestScore >= 3) {
513
+ bestButton.click();
221
514
  return 'button';
222
515
  }
223
516
 
224
- composer.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
225
- composer.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
226
517
  return 'enter';
227
- })(${JSON.stringify(text)})
518
+ })()
519
+ `;
520
+ }
521
+ function dispatchComposerEnterScript() {
522
+ return `
523
+ (() => {
524
+ ${buildGeminiComposerLocatorScript()}
525
+ const composer = findComposer();
526
+
527
+ if (!(composer instanceof HTMLElement)) {
528
+ throw new Error('Could not find Gemini composer');
529
+ }
530
+
531
+ composer.focus();
532
+ composer.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true }));
533
+ composer.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true }));
534
+ return 'enter';
535
+ })()
228
536
  `;
229
537
  }
230
538
  function clickNewChatScript() {
@@ -241,7 +549,14 @@ function clickNewChatScript() {
241
549
  const candidates = Array.from(document.querySelectorAll('button, a')).filter((node) => {
242
550
  const text = (node.textContent || '').trim().toLowerCase();
243
551
  const aria = (node.getAttribute('aria-label') || '').trim().toLowerCase();
244
- return isVisible(node) && (text === 'new chat' || aria === 'new chat');
552
+ return isVisible(node) && (
553
+ text === 'new chat'
554
+ || aria === 'new chat'
555
+ || text === '发起新对话'
556
+ || aria === '发起新对话'
557
+ || text === '新对话'
558
+ || aria === '新对话'
559
+ );
245
560
  });
246
561
 
247
562
  const target = candidates.find((node) => !node.hasAttribute('disabled')) || candidates[0];
@@ -288,23 +603,133 @@ export async function startNewGeminiChat(page) {
288
603
  return action;
289
604
  }
290
605
  export async function getGeminiVisibleTurns(page) {
291
- await ensureGeminiPage(page);
292
- const turns = await page.evaluate(getTurnsScript());
606
+ const turns = await getGeminiStructuredTurns(page);
293
607
  if (Array.isArray(turns) && turns.length > 0)
294
608
  return turns;
295
609
  const lines = await getGeminiTranscriptLines(page);
296
610
  return lines.map((line) => ({ Role: 'System', Text: line }));
297
611
  }
612
+ async function getGeminiStructuredTurns(page) {
613
+ await ensureGeminiPage(page);
614
+ const turns = collapseAdjacentGeminiTurns(await page.evaluate(getTurnsScript()));
615
+ return Array.isArray(turns) ? turns : [];
616
+ }
298
617
  export async function getGeminiTranscriptLines(page) {
299
618
  await ensureGeminiPage(page);
300
619
  return await page.evaluate(getTranscriptLinesScript());
301
620
  }
621
+ export async function readGeminiSnapshot(page) {
622
+ await ensureGeminiPage(page);
623
+ return await page.evaluate(readGeminiSnapshotScript());
624
+ }
625
+ function findLastUserTurnIndex(turns) {
626
+ for (let index = turns.length - 1; index >= 0; index -= 1) {
627
+ if (turns[index]?.Role === 'User')
628
+ return index;
629
+ }
630
+ return null;
631
+ }
632
+ function findLastUserTurn(turns) {
633
+ const index = findLastUserTurnIndex(turns);
634
+ return index === null ? null : turns[index] ?? null;
635
+ }
636
+ export async function waitForGeminiSubmission(page, before, timeoutSeconds) {
637
+ const preSendAssistantCount = before.turns.filter((turn) => turn.Role === 'Assistant').length;
638
+ const maxPolls = Math.max(1, Math.ceil(timeoutSeconds));
639
+ for (let index = 0; index < maxPolls; index += 1) {
640
+ await page.wait(index === 0 ? 0.5 : 1);
641
+ const current = await readGeminiSnapshot(page);
642
+ const structuredAppend = diffTrustedStructuredTurns(before, current);
643
+ const transcriptDelta = diffTranscriptLines(before, current);
644
+ if (structuredAppend.hasTrustedAppend && structuredAppend.hasNewUserTurn) {
645
+ return {
646
+ snapshot: current,
647
+ preSendAssistantCount,
648
+ userAnchorTurn: findLastUserTurn(current.turns),
649
+ reason: 'user_turn',
650
+ };
651
+ }
652
+ if (!current.composerHasText && current.isGenerating) {
653
+ return {
654
+ snapshot: current,
655
+ preSendAssistantCount,
656
+ userAnchorTurn: findLastUserTurn(current.turns),
657
+ reason: 'composer_generating',
658
+ };
659
+ }
660
+ if (!current.composerHasText && transcriptDelta.length > 0) {
661
+ return {
662
+ snapshot: current,
663
+ preSendAssistantCount,
664
+ userAnchorTurn: findLastUserTurn(current.turns),
665
+ reason: 'composer_transcript',
666
+ };
667
+ }
668
+ }
669
+ return null;
670
+ }
302
671
  export async function sendGeminiMessage(page, text) {
303
672
  await ensureGeminiPage(page);
304
- const submittedBy = await page.evaluate(fillAndSubmitComposerScript(text));
673
+ let prepared;
674
+ for (let attempt = 0; attempt < GEMINI_COMPOSER_PREPARE_ATTEMPTS; attempt += 1) {
675
+ prepared = await page.evaluate(prepareComposerScript());
676
+ if (prepared?.ok)
677
+ break;
678
+ if (attempt < GEMINI_COMPOSER_PREPARE_ATTEMPTS - 1)
679
+ await page.wait(GEMINI_COMPOSER_PREPARE_WAIT_SECONDS);
680
+ }
681
+ if (!prepared?.ok) {
682
+ throw new CommandExecutionError(prepared?.reason || 'Could not find Gemini composer');
683
+ }
684
+ let hasText = false;
685
+ if (page.nativeType) {
686
+ try {
687
+ await page.nativeType(text);
688
+ await page.wait(0.2);
689
+ const nativeState = await page.evaluate(composerHasTextScript());
690
+ hasText = !!nativeState?.hasText;
691
+ }
692
+ catch { }
693
+ }
694
+ if (!hasText) {
695
+ const fallbackState = await page.evaluate(insertComposerTextFallbackScript(text));
696
+ hasText = !!fallbackState?.hasText;
697
+ }
698
+ if (!hasText) {
699
+ throw new CommandExecutionError('Failed to insert text into Gemini composer');
700
+ }
701
+ const submitAction = await page.evaluate(submitComposerScript());
702
+ if (submitAction === 'button') {
703
+ await page.wait(1);
704
+ return 'button';
705
+ }
706
+ if (page.nativeKeyPress) {
707
+ try {
708
+ await page.nativeKeyPress('Enter');
709
+ }
710
+ catch {
711
+ await page.evaluate(dispatchComposerEnterScript());
712
+ }
713
+ }
714
+ else {
715
+ await page.evaluate(dispatchComposerEnterScript());
716
+ }
305
717
  await page.wait(1);
306
- return submittedBy;
718
+ return 'enter';
307
719
  }
720
+ export const __test__ = {
721
+ GEMINI_COMPOSER_SELECTORS,
722
+ GEMINI_COMPOSER_MARKER_ATTR,
723
+ collapseAdjacentGeminiTurns,
724
+ clickNewChatScript,
725
+ diffTranscriptLines,
726
+ diffTrustedStructuredTurns,
727
+ hasGeminiTurnPrefix,
728
+ readGeminiSnapshot,
729
+ readGeminiSnapshotScript,
730
+ submitComposerScript,
731
+ insertComposerTextFallbackScript,
732
+ };
308
733
  export async function getGeminiVisibleImageUrls(page) {
309
734
  await ensureGeminiPage(page);
310
735
  return await page.evaluate(`
@@ -429,35 +854,77 @@ export async function exportGeminiImages(page, urls) {
429
854
  })(${urlsJson})
430
855
  `);
431
856
  }
432
- export async function waitForGeminiResponse(page, beforeLines, promptText, timeoutSeconds) {
433
- const getCandidate = async () => {
434
- const turns = await getGeminiVisibleTurns(page);
435
- const assistantCandidate = [...turns].reverse().find((turn) => turn.Role === 'Assistant');
436
- const visibleCandidate = assistantCandidate
437
- ? sanitizeGeminiResponseText(assistantCandidate.Text, promptText)
438
- : '';
439
- if (visibleCandidate && visibleCandidate !== promptText)
440
- return visibleCandidate;
441
- const lines = await getGeminiTranscriptLines(page);
442
- return collectGeminiTranscriptAdditions(beforeLines, lines, promptText);
857
+ export async function waitForGeminiResponse(page, baseline, promptText, timeoutSeconds) {
858
+ if (timeoutSeconds <= 0)
859
+ return '';
860
+ // Reply ownership must survive Gemini prepending older history later.
861
+ // Re-anchor on the submitted user turn when possible, and otherwise only
862
+ // accept assistants that are appended to the exact submission snapshot.
863
+ const pickStructuredReplyCandidate = (current) => {
864
+ if (!current.structuredTurnsTrusted)
865
+ return '';
866
+ const userAnchorTurnIndex = findLastMatchingGeminiTurnIndex(current.turns, baseline.userAnchorTurn);
867
+ if (userAnchorTurnIndex !== null) {
868
+ const candidate = current.turns
869
+ .slice(userAnchorTurnIndex + 1)
870
+ .filter((turn) => turn.Role === 'Assistant')
871
+ .at(-1);
872
+ return candidate ? sanitizeGeminiResponseText(candidate.Text, promptText) : '';
873
+ }
874
+ if (hasGeminiTurnPrefix(baseline.snapshot.turns, current.turns)) {
875
+ const appendedAssistant = current.turns
876
+ .slice(baseline.snapshot.turns.length)
877
+ .filter((turn) => turn.Role === 'Assistant')
878
+ .at(-1);
879
+ if (appendedAssistant) {
880
+ return sanitizeGeminiResponseText(appendedAssistant.Text, promptText);
881
+ }
882
+ }
883
+ return '';
443
884
  };
444
- const pollIntervalSeconds = 2;
445
- const maxPolls = Math.max(1, Math.ceil(timeoutSeconds / pollIntervalSeconds));
446
- let lastCandidate = '';
447
- let stableCount = 0;
885
+ const pickFallbackGeminiTranscriptReply = (current) => current.transcriptLines
886
+ .filter((line) => !baseline.snapshot.transcriptLines.includes(line))
887
+ .map((line) => extractGeminiTranscriptLineCandidate(line, promptText))
888
+ .filter(Boolean)
889
+ .join('\n')
890
+ .trim();
891
+ const maxPolls = Math.max(1, Math.ceil(timeoutSeconds / 2));
892
+ let lastStructured = '';
893
+ let structuredStableCount = 0;
894
+ let lastTranscript = '';
895
+ let transcriptStableCount = 0;
896
+ let transcriptMissCount = 0;
448
897
  for (let index = 0; index < maxPolls; index += 1) {
449
- await page.wait(index === 0 ? 1.5 : pollIntervalSeconds);
450
- const candidate = await getCandidate();
451
- if (!candidate)
898
+ await page.wait(index === 0 ? 1 : 2);
899
+ const current = await readGeminiSnapshot(page);
900
+ const structuredCandidate = pickStructuredReplyCandidate(current);
901
+ if (structuredCandidate) {
902
+ if (structuredCandidate === lastStructured)
903
+ structuredStableCount += 1;
904
+ else {
905
+ lastStructured = structuredCandidate;
906
+ structuredStableCount = 1;
907
+ }
908
+ if (!current.isGenerating && structuredStableCount >= 2) {
909
+ return structuredCandidate;
910
+ }
452
911
  continue;
453
- if (candidate === lastCandidate)
454
- stableCount += 1;
912
+ }
913
+ transcriptMissCount += 1;
914
+ if (transcriptMissCount < 2)
915
+ continue;
916
+ const transcriptCandidate = pickFallbackGeminiTranscriptReply(current);
917
+ if (!transcriptCandidate)
918
+ continue;
919
+ if (transcriptCandidate === lastTranscript)
920
+ transcriptStableCount += 1;
455
921
  else {
456
- lastCandidate = candidate;
457
- stableCount = 1;
922
+ lastTranscript = transcriptCandidate;
923
+ transcriptStableCount = 1;
924
+ }
925
+ if (!current.isGenerating && transcriptStableCount >= 2) {
926
+ return transcriptCandidate;
458
927
  }
459
- if (stableCount >= 2 || index === maxPolls - 1)
460
- return candidate;
461
928
  }
462
- return lastCandidate;
929
+ return '';
463
930
  }