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

QingJ © 2025

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