ChatGPT File-Batch Sender (v0.6)

Batch-send JSON messages with collapsible, draggable, resizable panel & adjustable rest interval

当前为 2025-05-20 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name ChatGPT File-Batch Sender (v0.6)
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.6
  5. // @description Batch-send JSON messages with collapsible, draggable, resizable panel & adjustable rest interval
  6. // @author liuweiqing
  7. // @match https://chat.openai.com/*
  8. // @match https://chatgpt.com/*
  9. // @grant none
  10. // @license MIT
  11. // @icon https://www.google.com/s2/favicons?sz=64&domain=chatgpt.com
  12.  
  13. // ==/UserScript==
  14.  
  15. (() => {
  16. 'use strict';
  17.  
  18. /* ---------- 工具 ---------- */
  19. const $ = (sel, ctx = document) => ctx.querySelector(sel);
  20. const delay = ms => new Promise(r => setTimeout(r, ms));
  21. async function waitFor(sel, t = 10000) {
  22. const start = performance.now();
  23. while (performance.now() - start < t) {
  24. const n = $(sel); if (n) return n;
  25. await delay(100);
  26. }
  27. throw `timeout: ${sel}`;
  28. }
  29. const untilEnabled = btn =>
  30. new Promise(res => {
  31. if (!btn.disabled) return res();
  32. const mo = new MutationObserver(() => {
  33. if (!btn.disabled) { mo.disconnect(); res(); }
  34. });
  35. mo.observe(btn, { attributes: true, attributeFilter: ['disabled'] });
  36. });
  37. async function setComposer(text) {
  38. const p = await waitFor('div.ProseMirror[data-virtualkeyboard]');
  39. p.focus();
  40. document.execCommand('selectAll', false);
  41. document.execCommand('insertText', false, text);
  42. p.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText' }));
  43. }
  44.  
  45. /* ---------- 主题变化监听 ---------- */
  46. const onTheme = cb => {
  47. cb();
  48. new MutationObserver(cb)
  49. .observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
  50. };
  51.  
  52. /* ---------- 生成操作面板 ---------- */
  53. const panel = document.createElement('div');
  54. panel.id = 'batchPanel';
  55. panel.innerHTML = `
  56. <style>
  57. #batchPanel{
  58. position:fixed;top:12px;right:12px;z-index:2147483647;
  59. width:220px;padding:0;font-family:Arial,sans-serif;
  60. background:var(--bg,#fff);color:var(--fg,#000);
  61. border:1px solid var(--bd,#0003);border-radius:8px;
  62. box-shadow:0 4px 14px #0004;resize:both;overflow:auto;
  63. transition:background .2s,color .2s;
  64. }
  65. #batchPanel.collapsed{width:46px;height:46px;padding:0;overflow:hidden}
  66. #batchHeader{
  67. cursor:move;user-select:none;height:36px;line-height:36px;
  68. padding:0 10px;font-weight:bold;display:flex;justify-content:space-between;align-items:center;
  69. border-bottom:1px solid var(--bd,#0003);background:var(--hdr,#f1f3f5);
  70. }
  71. #batchBody{padding:10px;display:flex;flex-direction:column;gap:6px}
  72. #batchPanel input[type="text"],
  73. #batchPanel input[type="number"]{width:100%;padding:4px;border:1px solid var(--bd,#0003);border-radius:4px}
  74. #batchBody div.flexRow{display:flex;gap:4px}
  75. #run{padding:6px;border:none;border-radius:4px;background:#0b5cff;color:#fff;cursor:pointer}
  76. #run:disabled{opacity:.5;cursor:not-allowed}
  77. .dark #batchPanel{--bg:#1f1f1f;--fg:#f8f8f8;--bd:#555;--hdr:#2a2a2a}
  78. </style>
  79. <div id="batchHeader">
  80. <span>Batch&nbsp;Sender</span>
  81. <span id="toggle">▾</span>
  82. </div>
  83. <div id="batchBody">
  84. <input type="file" id="file">
  85. <span id="fname" style="font-size:12px;color:#888"></span>
  86.  
  87. <label>Prompt 前缀</label>
  88. <input type="text" id="common">
  89.  
  90. <label style="display:flex;align-items:center;gap:4px">
  91. <input type="checkbox" id="restSwitch"> 自动休息
  92. </label>
  93.  
  94. <div class="flexRow" style="align-items:center">
  95. <input type="number" id="restCount" value="25"
  96. placeholder="条数" title="连续发送多少条后休息" style="width:60px">
  97. <span style="font-size:12px;">条</span>
  98. <input type="number" id="restHours" value="3"
  99. placeholder="小时" title="每次休息时长(小时,可填小数)"
  100. step="0.1" min="0" style="width:60px">
  101. <span style="font-size:12px;">小时</span>
  102. </div>
  103.  
  104. <input type="number" id="gap" placeholder="间隔(s)">
  105. <button id="run">开始</button>
  106. <progress id="bar" value="0" max="1" style="width:100%"></progress>
  107. </div>`;
  108. document.body.appendChild(panel);
  109.  
  110. const $header = $('#batchHeader');
  111. const $toggle = $('#toggle');
  112.  
  113. /* ---------- 主题同步 ---------- */
  114. onTheme(() => {
  115. if (document.documentElement.classList.contains('dark'))
  116. panel.classList.add('dark');
  117. else panel.classList.remove('dark');
  118. });
  119.  
  120. /* ---------- 折叠 / 展开 ---------- */
  121. let collapsed = localStorage.getItem('batchCollapsed') === '1';
  122. const applyCollapse = () => {
  123. panel.classList.toggle('collapsed', collapsed);
  124. $toggle.textContent = collapsed ? '▸' : '▾';
  125. localStorage.setItem('batchCollapsed', collapsed ? '1' : '0');
  126. };
  127. $toggle.onclick = e => { collapsed = !collapsed; applyCollapse(); e.stopPropagation(); };
  128. applyCollapse();
  129.  
  130. /* ---------- 拖拽移动 ---------- */
  131. let drag = null;
  132. $header.addEventListener('mousedown', e => {
  133. if (e.button !== 0) return;
  134. drag = { x: e.clientX, y: e.clientY, left: panel.offsetLeft, top: panel.offsetTop };
  135. e.preventDefault();
  136. });
  137. window.addEventListener('mousemove', e => {
  138. if (!drag) return;
  139. panel.style.left = drag.left + (e.clientX - drag.x) + 'px';
  140. panel.style.top = drag.top + (e.clientY - drag.y) + 'px';
  141. });
  142. window.addEventListener('mouseup', () => { drag = null; });
  143.  
  144. /* ---------- DOM 引用 ---------- */
  145. const $file = $('#file'); const $fname = $('#fname');
  146. const $common = $('#common'); const $gap = $('#gap');
  147. const $restSw = $('#restSwitch');
  148. const $restCt = $('#restCount');
  149. const $restHr = $('#restHours');
  150. const $run = $('#run'); const $bar = $('#bar');
  151.  
  152. /* ---------- 恢复设置 ---------- */
  153. [
  154. ['savedFileName', v => $fname.textContent = v],
  155. ['prompt', v => $common.value = v],
  156. ['delay', v => $gap.value = v],
  157. ['restFlag', v => $restSw.checked = v === '1'],
  158. ['restCount', v => $restCt.value = v],
  159. ['restHours', v => $restHr.value = v],
  160. ['panelLeft', v => panel.style.left = v],
  161. ['panelTop', v => panel.style.top = v],
  162. ['panelBottom', v => panel.style.bottom = v]
  163. ].forEach(([k, fn]) => { const v = localStorage.getItem(k); if (v) fn(v); });
  164.  
  165. /* ---------- 保存位置变化 ---------- */
  166. new MutationObserver(() => {
  167. localStorage.setItem('panelLeft', panel.style.left || '');
  168. localStorage.setItem('panelTop', panel.style.top || '');
  169. localStorage.setItem('panelBottom', panel.style.bottom || '');
  170. }).observe(panel, { attributes: true, attributeFilter: ['style'] });
  171.  
  172. /* ---------- 读取文件 ---------- */
  173. $file.onchange = e => {
  174. const f = e.target.files?.[0]; if (!f) return;
  175. const rd = new FileReader();
  176. rd.onload = ev => {
  177. localStorage.setItem('savedFile', ev.target.result);
  178. localStorage.setItem('savedFileName', f.name);
  179. $fname.textContent = f.name;
  180. };
  181. rd.readAsText(f);
  182. };
  183.  
  184. /* ---------- 主要发送逻辑 ---------- */
  185. $run.onclick = async () => {
  186. const raw = localStorage.getItem('savedFile');
  187. if (!raw) return alert('请先选择文件');
  188. let data;
  189. try { data = JSON.parse(raw); } catch { return alert('JSON 解析失败'); }
  190. if (!Array.isArray(data)) return alert('JSON 必须是数组');
  191.  
  192. /* 保存参数 */
  193. localStorage.setItem('prompt', $common.value);
  194. localStorage.setItem('delay', $gap.value);
  195. localStorage.setItem('restFlag', $restSw.checked ? '1' : '0');
  196. localStorage.setItem('restCount', $restCt.value);
  197. localStorage.setItem('restHours', $restHr.value);
  198.  
  199. /* 参数解析 */
  200. const prefix = $common.value || '';
  201. const gapMs = (+$gap.value || 100) * 1000;
  202. const restOn = $restSw.checked;
  203. const restAfter = Math.max(1, +$restCt.value || 25);
  204. const restMs = Math.max(0, +$restHr.value || 3) * 3600 * 1000;
  205.  
  206. $bar.max = data.length;
  207. $run.disabled = true;
  208.  
  209. try {
  210. for (let i = 0; i < data.length; i++) {
  211. await setComposer(`${prefix}${data[i].title ?? data[i]}`);
  212. await delay(1000); // 额外 1 秒
  213. const btn = await waitFor('button[data-testid="send-button"]');
  214. await untilEnabled(btn); btn.click();
  215. $bar.value = i + 1;
  216.  
  217. if (restOn && (i + 1) % restAfter === 0) await delay(restMs);
  218. else await delay(gapMs);
  219. }
  220. alert('全部发送完毕!');
  221. } catch (e) {
  222. console.error(e);
  223. alert(e.message);
  224. } finally {
  225. $run.disabled = false;
  226. }
  227. };
  228. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址