@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
@@ -1,3 +1,4 @@
1
+ import { CommandExecutionError } from '../../errors.js';
1
2
  import type { IPage } from '../../types.js';
2
3
 
3
4
  export const GEMINI_DOMAIN = 'gemini.google.com';
@@ -16,12 +17,88 @@ export interface GeminiTurn {
16
17
  Text: string;
17
18
  }
18
19
 
20
+ export interface GeminiSnapshot {
21
+ turns: GeminiTurn[];
22
+ transcriptLines: string[];
23
+ composerHasText: boolean;
24
+ isGenerating: boolean;
25
+ structuredTurnsTrusted: boolean;
26
+ }
27
+
28
+ export interface GeminiStructuredAppend {
29
+ appendedTurns: GeminiTurn[];
30
+ hasTrustedAppend: boolean;
31
+ hasNewUserTurn: boolean;
32
+ hasNewAssistantTurn: boolean;
33
+ }
34
+
35
+ export interface GeminiSubmissionBaseline {
36
+ snapshot: GeminiSnapshot;
37
+ preSendAssistantCount: number;
38
+ userAnchorTurn: GeminiTurn | null;
39
+ reason: 'user_turn' | 'composer_generating' | 'composer_transcript';
40
+ }
41
+
19
42
  const GEMINI_RESPONSE_NOISE_PATTERNS = [
20
43
  /Gemini can make mistakes\.?/gi,
21
44
  /Google Terms/gi,
22
45
  /Google Privacy Policy/gi,
23
46
  /Opens in a new window/gi,
24
47
  ];
48
+ const GEMINI_TRANSCRIPT_CHROME_MARKERS = ['gemini', '我的内容', '对话', 'google terms', 'google privacy policy'];
49
+
50
+ const GEMINI_COMPOSER_SELECTORS = [
51
+ '.ql-editor[contenteditable="true"]',
52
+ '.ql-editor[role="textbox"]',
53
+ '.ql-editor[aria-label*="Gemini"]',
54
+ '[contenteditable="true"][aria-label*="Gemini"]',
55
+ '[aria-label="Enter a prompt for Gemini"]',
56
+ '[aria-label*="prompt for Gemini"]',
57
+ ];
58
+
59
+ const GEMINI_COMPOSER_MARKER_ATTR = 'data-opencli-gemini-composer';
60
+ const GEMINI_COMPOSER_PREPARE_ATTEMPTS = 4;
61
+ const GEMINI_COMPOSER_PREPARE_WAIT_SECONDS = 1;
62
+
63
+ function buildGeminiComposerLocatorScript(): string {
64
+ const selectorsJson = JSON.stringify(GEMINI_COMPOSER_SELECTORS);
65
+ const markerAttrJson = JSON.stringify(GEMINI_COMPOSER_MARKER_ATTR);
66
+ return `
67
+ const isVisible = (el) => {
68
+ if (!(el instanceof HTMLElement)) return false;
69
+ const style = window.getComputedStyle(el);
70
+ if (style.display === 'none' || style.visibility === 'hidden') return false;
71
+ const rect = el.getBoundingClientRect();
72
+ return rect.width > 0 && rect.height > 0;
73
+ };
74
+
75
+ const markerAttr = ${markerAttrJson};
76
+ const clearComposerMarkers = (active) => {
77
+ document.querySelectorAll('[' + markerAttr + ']').forEach((node) => {
78
+ if (node !== active) node.removeAttribute(markerAttr);
79
+ });
80
+ };
81
+
82
+ const markComposer = (node) => {
83
+ if (!(node instanceof HTMLElement)) return null;
84
+ clearComposerMarkers(node);
85
+ node.setAttribute(markerAttr, '1');
86
+ return node;
87
+ };
88
+
89
+ const findComposer = () => {
90
+ const marked = document.querySelector('[' + markerAttr + '="1"]');
91
+ if (marked instanceof HTMLElement && isVisible(marked)) return marked;
92
+
93
+ const selectors = ${selectorsJson};
94
+ for (const selector of selectors) {
95
+ const node = Array.from(document.querySelectorAll(selector)).find((candidate) => candidate instanceof HTMLElement && isVisible(candidate));
96
+ if (node instanceof HTMLElement) return markComposer(node);
97
+ }
98
+ return null;
99
+ };
100
+ `;
101
+ }
25
102
 
26
103
  export function sanitizeGeminiResponseText(value: string, promptText: string): string {
27
104
  let sanitized = value;
@@ -52,33 +129,160 @@ export function collectGeminiTranscriptAdditions(
52
129
  const beforeSet = new Set(beforeLines);
53
130
  const additions = currentLines
54
131
  .filter((line) => !beforeSet.has(line))
55
- .map((line) => sanitizeGeminiResponseText(line, promptText))
132
+ .map((line) => extractGeminiTranscriptLineCandidate(line, promptText))
56
133
  .filter((line) => line && line !== promptText);
57
134
 
58
135
  return additions.join('\n').trim();
59
136
  }
60
137
 
138
+ export function collapseAdjacentGeminiTurns(turns: GeminiTurn[]): GeminiTurn[] {
139
+ const collapsed: GeminiTurn[] = [];
140
+
141
+ for (const turn of turns) {
142
+ if (!turn || typeof turn.Role !== 'string' || typeof turn.Text !== 'string') continue;
143
+ const previous = collapsed.at(-1);
144
+ if (previous?.Role === turn.Role && previous.Text === turn.Text) continue;
145
+ collapsed.push(turn);
146
+ }
147
+
148
+ return collapsed;
149
+ }
150
+
151
+ function hasGeminiTurnPrefix(before: GeminiTurn[], current: GeminiTurn[]): boolean {
152
+ if (before.length > current.length) return false;
153
+ return before.every((turn, index) => (
154
+ turn.Role === current[index]?.Role
155
+ && turn.Text === current[index]?.Text
156
+ ));
157
+ }
158
+
159
+ function findLastMatchingGeminiTurnIndex(turns: GeminiTurn[], target: GeminiTurn | null): number | null {
160
+ if (!target) return null;
161
+ for (let index = turns.length - 1; index >= 0; index -= 1) {
162
+ const turn = turns[index];
163
+ if (turn?.Role === target.Role && turn.Text === target.Text) {
164
+ return index;
165
+ }
166
+ }
167
+ return null;
168
+ }
169
+
170
+ function diffTrustedStructuredTurns(
171
+ before: GeminiSnapshot,
172
+ current: GeminiSnapshot,
173
+ ): GeminiStructuredAppend {
174
+ if (!before.structuredTurnsTrusted || !current.structuredTurnsTrusted) {
175
+ return {
176
+ appendedTurns: [],
177
+ hasTrustedAppend: false,
178
+ hasNewUserTurn: false,
179
+ hasNewAssistantTurn: false,
180
+ };
181
+ }
182
+
183
+ if (!hasGeminiTurnPrefix(before.turns, current.turns)) {
184
+ return {
185
+ appendedTurns: [],
186
+ hasTrustedAppend: false,
187
+ hasNewUserTurn: false,
188
+ hasNewAssistantTurn: false,
189
+ };
190
+ }
191
+
192
+ const appendedTurns = current.turns.slice(before.turns.length);
193
+ return {
194
+ appendedTurns,
195
+ hasTrustedAppend: appendedTurns.length > 0,
196
+ hasNewUserTurn: appendedTurns.some((turn) => turn.Role === 'User'),
197
+ hasNewAssistantTurn: appendedTurns.some((turn) => turn.Role === 'Assistant'),
198
+ };
199
+ }
200
+
201
+ function diffTranscriptLines(before: GeminiSnapshot, current: GeminiSnapshot): string[] {
202
+ const beforeLines = new Set(before.transcriptLines);
203
+ return current.transcriptLines.filter((line) => !beforeLines.has(line));
204
+ }
205
+
206
+ function isLikelyGeminiTranscriptChrome(line: string): boolean {
207
+ const lower = line.toLowerCase();
208
+ const markerHits = GEMINI_TRANSCRIPT_CHROME_MARKERS.filter((marker) => lower.includes(marker)).length;
209
+ return markerHits >= 2;
210
+ }
211
+
212
+ function extractGeminiTranscriptLineCandidate(transcriptLine: string, promptText: string): string {
213
+ const candidate = transcriptLine.trim();
214
+ if (!candidate) return '';
215
+
216
+ const prompt = promptText.trim();
217
+ const sanitized = sanitizeGeminiResponseText(candidate, promptText);
218
+
219
+ if (!prompt) return sanitized;
220
+ if (!candidate.includes(prompt)) return sanitized;
221
+ if (sanitized && sanitized !== prompt && sanitized !== candidate) return sanitized;
222
+ if (isLikelyGeminiTranscriptChrome(candidate)) return '';
223
+
224
+ // Some transcript snapshots flatten "prompt + answer" into a single line.
225
+ // Recover the answer only when the line starts with the current prompt.
226
+ if (candidate.startsWith(prompt)) {
227
+ const tail = candidate.slice(prompt.length).replace(/^[\s::,,-]+/, '').trim();
228
+ return tail ? sanitizeGeminiResponseText(tail, '') : '';
229
+ }
230
+
231
+ return sanitized;
232
+ }
233
+
61
234
  function getStateScript(): string {
62
235
  return `
63
236
  (() => {
237
+ ${buildGeminiComposerLocatorScript()}
238
+
64
239
  const signInNode = Array.from(document.querySelectorAll('a, button')).find((node) => {
65
240
  const text = (node.textContent || '').trim().toLowerCase();
66
241
  const aria = (node.getAttribute('aria-label') || '').trim().toLowerCase();
67
242
  const href = node.getAttribute('href') || '';
68
243
  return text === 'sign in'
69
244
  || aria === 'sign in'
245
+ || text === '登录'
246
+ || aria === '登录'
70
247
  || href.includes('accounts.google.com/ServiceLogin');
71
248
  });
72
249
 
73
- 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"]');
74
- const sendButton = document.querySelector('button[aria-label="Send message"]');
250
+ const composer = findComposer();
75
251
 
76
252
  return {
77
253
  url: window.location.href,
78
254
  title: document.title || '',
79
255
  isSignedIn: signInNode ? false : (composer ? true : null),
80
256
  composerLabel: composer?.getAttribute('aria-label') || '',
81
- canSend: !!(sendButton && !sendButton.disabled),
257
+ canSend: !!composer,
258
+ };
259
+ })()
260
+ `;
261
+ }
262
+
263
+ function readGeminiSnapshotScript(): string {
264
+ return `
265
+ (() => {
266
+ ${buildGeminiComposerLocatorScript()}
267
+ const composer = findComposer();
268
+ const composerText = composer?.textContent?.replace(/\\u00a0/g, ' ').trim() || '';
269
+ const isGenerating = !!Array.from(document.querySelectorAll('button, [role="button"]')).find((node) => {
270
+ const text = (node.textContent || '').trim().toLowerCase();
271
+ const aria = (node.getAttribute('aria-label') || '').trim().toLowerCase();
272
+ return text === 'stop response'
273
+ || aria === 'stop response'
274
+ || text === '停止回答'
275
+ || aria === '停止回答';
276
+ });
277
+ const turns = ${getTurnsScript().trim()};
278
+ const transcriptLines = ${getTranscriptLinesScript().trim()};
279
+
280
+ return {
281
+ turns,
282
+ transcriptLines,
283
+ composerHasText: composerText.length > 0,
284
+ isGenerating,
285
+ structuredTurnsTrusted: turns.length > 0 || transcriptLines.length === 0,
82
286
  };
83
287
  })()
84
288
  `;
@@ -186,7 +390,16 @@ function getTurnsScript(): string {
186
390
  ];
187
391
 
188
392
  const roots = selectors.flatMap((selector) => Array.from(document.querySelectorAll(selector)));
189
- const unique = roots.filter((el, index, all) => all.indexOf(el) === index).filter(isVisible);
393
+ const unique = roots
394
+ .filter((el, index, all) => all.indexOf(el) === index)
395
+ .filter(isVisible)
396
+ .sort((left, right) => {
397
+ if (left === right) return 0;
398
+ const relation = left.compareDocumentPosition(right);
399
+ if (relation & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
400
+ if (relation & Node.DOCUMENT_POSITION_PRECEDING) return 1;
401
+ return 0;
402
+ });
190
403
 
191
404
  const turns = unique.map((el) => {
192
405
  const text = clean(el.innerText || el.textContent || '');
@@ -206,53 +419,189 @@ function getTurnsScript(): string {
206
419
  return role ? { Role: role, Text: text } : null;
207
420
  }).filter(Boolean);
208
421
 
209
- const deduped = [];
210
- const seen = new Set();
211
- for (const turn of turns) {
212
- const key = turn.Role + '::' + turn.Text;
213
- if (seen.has(key)) continue;
214
- seen.add(key);
215
- deduped.push(turn);
216
- }
217
- return deduped;
422
+ return turns;
218
423
  })()
219
424
  `;
220
425
  }
221
426
 
222
- function fillAndSubmitComposerScript(text: string): string {
427
+ function prepareComposerScript(): string {
223
428
  return `
224
- ((inputText) => {
225
- const cleanInsert = (el) => {
226
- if (!(el instanceof HTMLElement)) throw new Error('Composer is not editable');
227
- el.focus();
429
+ (() => {
430
+ ${buildGeminiComposerLocatorScript()}
431
+ const composer = findComposer();
432
+
433
+ if (!(composer instanceof HTMLElement)) {
434
+ return { ok: false, reason: 'Could not find Gemini composer' };
435
+ }
436
+
437
+ try {
438
+ composer.focus();
228
439
  const selection = window.getSelection();
229
440
  const range = document.createRange();
230
- range.selectNodeContents(el);
441
+ range.selectNodeContents(composer);
231
442
  range.collapse(false);
232
443
  selection?.removeAllRanges();
233
444
  selection?.addRange(range);
234
- el.textContent = '';
235
- document.execCommand('insertText', false, inputText);
236
- el.dispatchEvent(new InputEvent('input', { bubbles: true, data: inputText, inputType: 'insertText' }));
445
+ composer.textContent = '';
446
+ composer.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteContentBackward' }));
447
+ } catch (error) {
448
+ return {
449
+ ok: false,
450
+ reason: error instanceof Error ? error.message : String(error),
451
+ };
452
+ }
453
+
454
+ return {
455
+ ok: true,
456
+ label: composer.getAttribute('aria-label') || '',
457
+ };
458
+ })()
459
+ `;
460
+ }
461
+
462
+ function composerHasTextScript(): string {
463
+ return `
464
+ (() => {
465
+ ${buildGeminiComposerLocatorScript()}
466
+ const composer = findComposer();
467
+
468
+ return {
469
+ hasText: !!(composer && ((composer.textContent || '').trim() || (composer.innerText || '').trim())),
470
+ };
471
+ })()
472
+ `;
473
+ }
474
+
475
+ function insertComposerTextFallbackScript(text: string): string {
476
+ return `
477
+ ((inputText) => {
478
+ ${buildGeminiComposerLocatorScript()}
479
+ const composer = findComposer();
480
+
481
+ if (!(composer instanceof HTMLElement)) {
482
+ return { hasText: false, reason: 'Could not find Gemini composer' };
483
+ }
484
+
485
+ const selection = window.getSelection();
486
+ const range = document.createRange();
487
+ range.selectNodeContents(composer);
488
+ range.collapse(false);
489
+ selection?.removeAllRanges();
490
+ selection?.addRange(range);
491
+
492
+ composer.focus();
493
+ composer.textContent = '';
494
+ const execResult = typeof document.execCommand === 'function'
495
+ ? document.execCommand('insertText', false, inputText)
496
+ : false;
497
+
498
+ if (!execResult) {
499
+ const paragraph = document.createElement('p');
500
+ const lines = String(inputText).split(/\\n/);
501
+ for (const [index, line] of lines.entries()) {
502
+ if (index > 0) paragraph.appendChild(document.createElement('br'));
503
+ paragraph.appendChild(document.createTextNode(line));
504
+ }
505
+ composer.replaceChildren(paragraph);
506
+ }
507
+
508
+ composer.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, data: inputText, inputType: 'insertText' }));
509
+ composer.dispatchEvent(new InputEvent('input', { bubbles: true, data: inputText, inputType: 'insertText' }));
510
+ composer.dispatchEvent(new Event('change', { bubbles: true }));
511
+
512
+ return {
513
+ hasText: !!((composer.textContent || '').trim() || (composer.innerText || '').trim()),
237
514
  };
515
+ })(${JSON.stringify(text)})
516
+ `;
517
+ }
518
+
519
+ function submitComposerScript(): string {
520
+ return `
521
+ (() => {
522
+ ${buildGeminiComposerLocatorScript()}
523
+ const composer = findComposer();
238
524
 
239
- 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"]');
240
525
  if (!(composer instanceof HTMLElement)) {
241
526
  throw new Error('Could not find Gemini composer');
242
527
  }
243
528
 
244
- cleanInsert(composer);
529
+ const composerRect = composer.getBoundingClientRect();
530
+ const rootCandidates = [
531
+ composer.closest('form'),
532
+ composer.closest('[role="form"]'),
533
+ composer.closest('.input-area-container'),
534
+ composer.closest('.textbox-container'),
535
+ composer.closest('.input-wrapper'),
536
+ composer.parentElement,
537
+ composer.parentElement?.parentElement,
538
+ ].filter(Boolean);
539
+
540
+ const seen = new Set();
541
+ const buttons = [];
542
+ for (const root of rootCandidates) {
543
+ root.querySelectorAll('button, [role="button"]').forEach((node) => {
544
+ if (!(node instanceof HTMLElement)) return;
545
+ if (seen.has(node)) return;
546
+ seen.add(node);
547
+ buttons.push(node);
548
+ });
549
+ }
550
+
551
+ const excludedPattern = /main menu|主菜单|microphone|麦克风|upload|上传|mode|模式|tools|工具|settings|临时对话|new chat|新对话/i;
552
+ const submitPattern = /send|发送|submit|提交/i;
553
+ let bestButton = null;
554
+ let bestScore = -1;
555
+
556
+ for (const button of buttons) {
557
+ if (!isVisible(button)) continue;
558
+ if (button instanceof HTMLButtonElement && button.disabled) continue;
559
+ if (button.getAttribute('aria-disabled') === 'true') continue;
560
+
561
+ const label = ((button.getAttribute('aria-label') || '') + ' ' + ((button.textContent || '').trim())).trim();
562
+ if (excludedPattern.test(label)) continue;
245
563
 
246
- const sendButton = document.querySelector('button[aria-label="Send message"]');
247
- if (sendButton instanceof HTMLButtonElement && !sendButton.disabled) {
248
- sendButton.click();
564
+ const rect = button.getBoundingClientRect();
565
+ const verticalDistance = Math.abs((rect.top + rect.bottom) / 2 - (composerRect.top + composerRect.bottom) / 2);
566
+ if (verticalDistance > 160) continue;
567
+
568
+ let score = 0;
569
+ if (submitPattern.test(label)) score += 10;
570
+ if (rect.left >= composerRect.right - 160) score += 3;
571
+ if (rect.left >= composerRect.left) score += 1;
572
+ if (rect.width <= 96 && rect.height <= 96) score += 1;
573
+
574
+ if (score > bestScore) {
575
+ bestScore = score;
576
+ bestButton = button;
577
+ }
578
+ }
579
+
580
+ if (bestButton instanceof HTMLElement && bestScore >= 3) {
581
+ bestButton.click();
249
582
  return 'button';
250
583
  }
251
584
 
252
- composer.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
253
- composer.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
254
585
  return 'enter';
255
- })(${JSON.stringify(text)})
586
+ })()
587
+ `;
588
+ }
589
+
590
+ function dispatchComposerEnterScript(): string {
591
+ return `
592
+ (() => {
593
+ ${buildGeminiComposerLocatorScript()}
594
+ const composer = findComposer();
595
+
596
+ if (!(composer instanceof HTMLElement)) {
597
+ throw new Error('Could not find Gemini composer');
598
+ }
599
+
600
+ composer.focus();
601
+ composer.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true }));
602
+ composer.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true }));
603
+ return 'enter';
604
+ })()
256
605
  `;
257
606
  }
258
607
 
@@ -270,7 +619,14 @@ function clickNewChatScript(): string {
270
619
  const candidates = Array.from(document.querySelectorAll('button, a')).filter((node) => {
271
620
  const text = (node.textContent || '').trim().toLowerCase();
272
621
  const aria = (node.getAttribute('aria-label') || '').trim().toLowerCase();
273
- return isVisible(node) && (text === 'new chat' || aria === 'new chat');
622
+ return isVisible(node) && (
623
+ text === 'new chat'
624
+ || aria === 'new chat'
625
+ || text === '发起新对话'
626
+ || aria === '发起新对话'
627
+ || text === '新对话'
628
+ || aria === '新对话'
629
+ );
274
630
  });
275
631
 
276
632
  const target = candidates.find((node) => !node.hasAttribute('disabled')) || candidates[0];
@@ -321,27 +677,150 @@ export async function startNewGeminiChat(page: IPage): Promise<'clicked' | 'navi
321
677
  }
322
678
 
323
679
  export async function getGeminiVisibleTurns(page: IPage): Promise<GeminiTurn[]> {
324
- await ensureGeminiPage(page);
325
- const turns = await page.evaluate(getTurnsScript()) as GeminiTurn[];
680
+ const turns = await getGeminiStructuredTurns(page);
326
681
  if (Array.isArray(turns) && turns.length > 0) return turns;
327
682
 
328
683
  const lines = await getGeminiTranscriptLines(page);
329
684
  return lines.map((line) => ({ Role: 'System', Text: line }));
330
685
  }
331
686
 
687
+ async function getGeminiStructuredTurns(page: IPage): Promise<GeminiTurn[]> {
688
+ await ensureGeminiPage(page);
689
+ const turns = collapseAdjacentGeminiTurns(await page.evaluate(getTurnsScript()) as GeminiTurn[]);
690
+ return Array.isArray(turns) ? turns : [];
691
+ }
692
+
332
693
  export async function getGeminiTranscriptLines(page: IPage): Promise<string[]> {
333
694
  await ensureGeminiPage(page);
334
695
  return await page.evaluate(getTranscriptLinesScript()) as string[];
335
696
  }
336
697
 
698
+ export async function readGeminiSnapshot(page: IPage): Promise<GeminiSnapshot> {
699
+ await ensureGeminiPage(page);
700
+ return await page.evaluate(readGeminiSnapshotScript()) as GeminiSnapshot;
701
+ }
702
+
703
+ function findLastUserTurnIndex(turns: GeminiTurn[]): number | null {
704
+ for (let index = turns.length - 1; index >= 0; index -= 1) {
705
+ if (turns[index]?.Role === 'User') return index;
706
+ }
707
+ return null;
708
+ }
709
+
710
+ function findLastUserTurn(turns: GeminiTurn[]): GeminiTurn | null {
711
+ const index = findLastUserTurnIndex(turns);
712
+ return index === null ? null : turns[index] ?? null;
713
+ }
714
+
715
+ export async function waitForGeminiSubmission(
716
+ page: IPage,
717
+ before: GeminiSnapshot,
718
+ timeoutSeconds: number,
719
+ ): Promise<GeminiSubmissionBaseline | null> {
720
+ const preSendAssistantCount = before.turns.filter((turn) => turn.Role === 'Assistant').length;
721
+ const maxPolls = Math.max(1, Math.ceil(timeoutSeconds));
722
+
723
+ for (let index = 0; index < maxPolls; index += 1) {
724
+ await page.wait(index === 0 ? 0.5 : 1);
725
+ const current = await readGeminiSnapshot(page);
726
+ const structuredAppend = diffTrustedStructuredTurns(before, current);
727
+ const transcriptDelta = diffTranscriptLines(before, current);
728
+
729
+ if (structuredAppend.hasTrustedAppend && structuredAppend.hasNewUserTurn) {
730
+ return {
731
+ snapshot: current,
732
+ preSendAssistantCount,
733
+ userAnchorTurn: findLastUserTurn(current.turns),
734
+ reason: 'user_turn',
735
+ };
736
+ }
737
+
738
+ if (!current.composerHasText && current.isGenerating) {
739
+ return {
740
+ snapshot: current,
741
+ preSendAssistantCount,
742
+ userAnchorTurn: findLastUserTurn(current.turns),
743
+ reason: 'composer_generating',
744
+ };
745
+ }
746
+
747
+ if (!current.composerHasText && transcriptDelta.length > 0) {
748
+ return {
749
+ snapshot: current,
750
+ preSendAssistantCount,
751
+ userAnchorTurn: findLastUserTurn(current.turns),
752
+ reason: 'composer_transcript',
753
+ };
754
+ }
755
+ }
756
+
757
+ return null;
758
+ }
759
+
337
760
  export async function sendGeminiMessage(page: IPage, text: string): Promise<'button' | 'enter'> {
338
761
  await ensureGeminiPage(page);
339
- const submittedBy = await page.evaluate(fillAndSubmitComposerScript(text)) as 'button' | 'enter';
762
+ let prepared: { ok?: boolean; reason?: string } | undefined;
763
+ for (let attempt = 0; attempt < GEMINI_COMPOSER_PREPARE_ATTEMPTS; attempt += 1) {
764
+ prepared = await page.evaluate(prepareComposerScript()) as { ok?: boolean; reason?: string };
765
+ if (prepared?.ok) break;
766
+ if (attempt < GEMINI_COMPOSER_PREPARE_ATTEMPTS - 1) await page.wait(GEMINI_COMPOSER_PREPARE_WAIT_SECONDS);
767
+ }
768
+ if (!prepared?.ok) {
769
+ throw new CommandExecutionError(prepared?.reason || 'Could not find Gemini composer');
770
+ }
771
+
772
+ let hasText = false;
773
+ if (page.nativeType) {
774
+ try {
775
+ await page.nativeType(text);
776
+ await page.wait(0.2);
777
+ const nativeState = await page.evaluate(composerHasTextScript()) as { hasText?: boolean };
778
+ hasText = !!nativeState?.hasText;
779
+ } catch {}
780
+ }
781
+
782
+ if (!hasText) {
783
+ const fallbackState = await page.evaluate(insertComposerTextFallbackScript(text)) as { hasText?: boolean };
784
+ hasText = !!fallbackState?.hasText;
785
+ }
786
+
787
+ if (!hasText) {
788
+ throw new CommandExecutionError('Failed to insert text into Gemini composer');
789
+ }
790
+
791
+ const submitAction = await page.evaluate(submitComposerScript()) as 'button' | 'enter';
792
+ if (submitAction === 'button') {
793
+ await page.wait(1);
794
+ return 'button';
795
+ }
796
+
797
+ if (page.nativeKeyPress) {
798
+ try {
799
+ await page.nativeKeyPress('Enter');
800
+ } catch {
801
+ await page.evaluate(dispatchComposerEnterScript());
802
+ }
803
+ } else {
804
+ await page.evaluate(dispatchComposerEnterScript());
805
+ }
806
+
340
807
  await page.wait(1);
341
- return submittedBy;
808
+ return 'enter';
342
809
  }
343
810
 
344
-
811
+ export const __test__ = {
812
+ GEMINI_COMPOSER_SELECTORS,
813
+ GEMINI_COMPOSER_MARKER_ATTR,
814
+ collapseAdjacentGeminiTurns,
815
+ clickNewChatScript,
816
+ diffTranscriptLines,
817
+ diffTrustedStructuredTurns,
818
+ hasGeminiTurnPrefix,
819
+ readGeminiSnapshot,
820
+ readGeminiSnapshotScript,
821
+ submitComposerScript,
822
+ insertComposerTextFallbackScript,
823
+ };
345
824
 
346
825
  export async function getGeminiVisibleImageUrls(page: IPage): Promise<string[]> {
347
826
  await ensureGeminiPage(page);
@@ -484,40 +963,89 @@ export async function exportGeminiImages(page: IPage, urls: string[]): Promise<G
484
963
  }
485
964
  export async function waitForGeminiResponse(
486
965
  page: IPage,
487
- beforeLines: string[],
966
+ baseline: GeminiSubmissionBaseline,
488
967
  promptText: string,
489
968
  timeoutSeconds: number,
490
969
  ): Promise<string> {
491
- const getCandidate = async (): Promise<string> => {
492
- const turns = await getGeminiVisibleTurns(page);
493
- const assistantCandidate = [...turns].reverse().find((turn) => turn.Role === 'Assistant');
494
- const visibleCandidate = assistantCandidate
495
- ? sanitizeGeminiResponseText(assistantCandidate.Text, promptText)
496
- : '';
497
- if (visibleCandidate && visibleCandidate !== promptText) return visibleCandidate;
498
-
499
- const lines = await getGeminiTranscriptLines(page);
500
- return collectGeminiTranscriptAdditions(beforeLines, lines, promptText);
970
+ if (timeoutSeconds <= 0) return '';
971
+
972
+ // Reply ownership must survive Gemini prepending older history later.
973
+ // Re-anchor on the submitted user turn when possible, and otherwise only
974
+ // accept assistants that are appended to the exact submission snapshot.
975
+ const pickStructuredReplyCandidate = (current: GeminiSnapshot): string => {
976
+ if (!current.structuredTurnsTrusted) return '';
977
+
978
+ const userAnchorTurnIndex = findLastMatchingGeminiTurnIndex(current.turns, baseline.userAnchorTurn);
979
+ if (userAnchorTurnIndex !== null) {
980
+ const candidate = current.turns
981
+ .slice(userAnchorTurnIndex + 1)
982
+ .filter((turn) => turn.Role === 'Assistant')
983
+ .at(-1);
984
+ return candidate ? sanitizeGeminiResponseText(candidate.Text, promptText) : '';
985
+ }
986
+
987
+ if (hasGeminiTurnPrefix(baseline.snapshot.turns, current.turns)) {
988
+ const appendedAssistant = current.turns
989
+ .slice(baseline.snapshot.turns.length)
990
+ .filter((turn) => turn.Role === 'Assistant')
991
+ .at(-1);
992
+ if (appendedAssistant) {
993
+ return sanitizeGeminiResponseText(appendedAssistant.Text, promptText);
994
+ }
995
+ }
996
+
997
+ return '';
501
998
  };
502
999
 
503
- const pollIntervalSeconds = 2;
504
- const maxPolls = Math.max(1, Math.ceil(timeoutSeconds / pollIntervalSeconds));
505
- let lastCandidate = '';
506
- let stableCount = 0;
1000
+ const pickFallbackGeminiTranscriptReply = (current: GeminiSnapshot): string => current.transcriptLines
1001
+ .filter((line) => !baseline.snapshot.transcriptLines.includes(line))
1002
+ .map((line) => extractGeminiTranscriptLineCandidate(line, promptText))
1003
+ .filter(Boolean)
1004
+ .join('\n')
1005
+ .trim();
1006
+
1007
+ const maxPolls = Math.max(1, Math.ceil(timeoutSeconds / 2));
1008
+ let lastStructured = '';
1009
+ let structuredStableCount = 0;
1010
+ let lastTranscript = '';
1011
+ let transcriptStableCount = 0;
1012
+ let transcriptMissCount = 0;
507
1013
 
508
1014
  for (let index = 0; index < maxPolls; index += 1) {
509
- await page.wait(index === 0 ? 1.5 : pollIntervalSeconds);
510
- const candidate = await getCandidate();
511
- if (!candidate) continue;
1015
+ await page.wait(index === 0 ? 1 : 2);
1016
+ const current = await readGeminiSnapshot(page);
1017
+ const structuredCandidate = pickStructuredReplyCandidate(current);
1018
+
1019
+ if (structuredCandidate) {
1020
+ if (structuredCandidate === lastStructured) structuredStableCount += 1;
1021
+ else {
1022
+ lastStructured = structuredCandidate;
1023
+ structuredStableCount = 1;
1024
+ }
1025
+
1026
+ if (!current.isGenerating && structuredStableCount >= 2) {
1027
+ return structuredCandidate;
1028
+ }
512
1029
 
513
- if (candidate === lastCandidate) stableCount += 1;
1030
+ continue;
1031
+ }
1032
+
1033
+ transcriptMissCount += 1;
1034
+ if (transcriptMissCount < 2) continue;
1035
+
1036
+ const transcriptCandidate = pickFallbackGeminiTranscriptReply(current);
1037
+ if (!transcriptCandidate) continue;
1038
+
1039
+ if (transcriptCandidate === lastTranscript) transcriptStableCount += 1;
514
1040
  else {
515
- lastCandidate = candidate;
516
- stableCount = 1;
1041
+ lastTranscript = transcriptCandidate;
1042
+ transcriptStableCount = 1;
517
1043
  }
518
1044
 
519
- if (stableCount >= 2 || index === maxPolls - 1) return candidate;
1045
+ if (!current.isGenerating && transcriptStableCount >= 2) {
1046
+ return transcriptCandidate;
1047
+ }
520
1048
  }
521
1049
 
522
- return lastCandidate;
1050
+ return '';
523
1051
  }