AI Enter 换行

让 AI 聊天输入区的 Enter 键可换行,使用 Cmd+Enter(Mac)或 Ctrl+Enter(Windows)发送消息。

  1. // ==UserScript==
  2. // @name AI Enter as Newline
  3. // @name:zh-TW AI Enter 換行
  4. // @name:zh-CN AI Enter 换行
  5. // @namespace http://tampermonkey.net/
  6. // @version 1.2.1
  7. // @description Enable Enter key for newline in AI chat input, use Cmd+Enter (Mac) or Ctrl+Enter (Windows) to send message.
  8. // @description:zh-TW 讓 AI 聊天輸入區的 Enter 鍵可換行,使用 Cmd+Enter(Mac)或 Ctrl+Enter(Windows)送出訊息。
  9. // @description:zh-CN 让 AI 聊天输入区的 Enter 键可换行,使用 Cmd+Enter(Mac)或 Ctrl+Enter(Windows)发送消息。
  10. // @author windofage
  11. // @license MIT
  12. // @match https://chatgpt.com/*
  13. // @match https://claude.ai/*
  14. // @match https://gemini.google.com/*
  15. // @match https://www.perplexity.ai/*
  16. // @match https://felo.ai/*
  17. // @match https://chat.deepseek.com/*
  18. // @match https://grok.com/*
  19. // @match https://duckduckgo.com/*
  20. // @include http://192.168.*.*:*/*
  21. // @icon data:image/png;base64,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
  22.  
  23. // @grant GM_getValue
  24. // @grant GM_setValue
  25. // @grant GM_registerMenuCommand
  26. // ==/UserScript==
  27.  
  28. (() => {
  29. "use strict";
  30.  
  31. // ----- 設定管理 -----
  32.  
  33. // 預設設定
  34. const defaultConfig = {
  35. shortcuts: {
  36. send: {
  37. ctrl: true, // Ctrl + Enter
  38. alt: false, // Alt/Option + Enter
  39. meta: true, // Win/Cmd/Super + Enter
  40. },
  41. },
  42. };
  43.  
  44. // 多語系翻譯字典
  45. const translations = {
  46. en: {
  47. settings: "Settings",
  48. close: "✕",
  49. sendShortcut: "Send Message Shortcut (+ Enter):",
  50. save: "Save",
  51. reset: "Reset",
  52. saveSuccess: "Settings saved!",
  53. saveFailed: "Failed to save settings!",
  54. resetConfirm: "Are you sure you want to reset to default settings?",
  55. resetSuccess: "Settings reset to default!",
  56. ctrlEnter: "Ctrl + Enter",
  57. altEnter: "Alt + Enter",
  58. cmdEnter: "Cmd + Enter",
  59. winEnter: "Win + Enter",
  60. superEnter: "Super + Enter",
  61. },
  62.  
  63. "zh-tw": {
  64. settings: "設定",
  65. close: "✕",
  66. sendShortcut: "傳送訊息快捷鍵(+ Enter):",
  67. save: "儲存",
  68. reset: "重設",
  69. saveSuccess: "設定已儲存!",
  70. saveFailed: "儲存設定失敗!",
  71. resetConfirm: "確定要重設為預設設定嗎?",
  72. resetSuccess: "設定已重設為預設值!",
  73. ctrlEnter: "Ctrl + Enter",
  74. altEnter: "Alt + Enter",
  75. cmdEnter: "Cmd + Enter",
  76. winEnter: "Win + Enter",
  77. superEnter: "Super + Enter",
  78. },
  79.  
  80. "zh-cn": {
  81. settings: "设置",
  82. close: "✕",
  83. sendShortcut: "发送消息快捷键(+ Enter):",
  84. save: "保存",
  85. reset: "重置",
  86. saveSuccess: "设置已保存!",
  87. saveFailed: "保存设置失败!",
  88. resetConfirm: "确定要重置为默认设置吗?",
  89. resetSuccess: "设置已重置为默认值!",
  90. ctrlEnter: "Ctrl + Enter",
  91. altEnter: "Alt + Enter",
  92. cmdEnter: "Cmd + Enter",
  93. winEnter: "Win + Enter",
  94. superEnter: "Super + Enter",
  95. },
  96. };
  97.  
  98. // 偵測瀏覽器語言偏好
  99. function detectBrowserLanguage() {
  100. const lang = navigator.language || navigator.userLanguage;
  101. if (lang.startsWith("zh")) {
  102. if (lang.includes("TW") || lang.includes("HK") || lang.includes("MO")) {
  103. return "zh-tw";
  104. } else {
  105. return "zh-cn";
  106. }
  107. } else {
  108. return "en";
  109. }
  110. }
  111.  
  112. // 取得目前使用的語言
  113. function getCurrentLanguage() {
  114. return detectBrowserLanguage();
  115. }
  116.  
  117. // 取得翻譯文字
  118. function t(key) {
  119. const lang = getCurrentLanguage();
  120. return translations[lang]?.[key] || translations.en[key] || key;
  121. }
  122.  
  123. // 載入使用者設定
  124. function loadConfig() {
  125. try {
  126. const savedConfig = GM_getValue("aiEnterConfig");
  127. if (savedConfig) {
  128. const config = JSON.parse(savedConfig);
  129. return {
  130. shortcuts: {
  131. send: {
  132. ctrl:
  133. config.shortcuts?.send?.ctrl !== undefined
  134. ? config.shortcuts.send.ctrl
  135. : defaultConfig.shortcuts.send.ctrl,
  136. alt:
  137. config.shortcuts?.send?.alt !== undefined
  138. ? config.shortcuts.send.alt
  139. : defaultConfig.shortcuts.send.alt,
  140. meta:
  141. config.shortcuts?.send?.meta !== undefined
  142. ? config.shortcuts.send.meta
  143. : defaultConfig.shortcuts.send.meta,
  144. },
  145. },
  146. };
  147. }
  148. } catch (error) {
  149. console.error("載入設定時發生錯誤:", error);
  150. }
  151. return defaultConfig;
  152. }
  153.  
  154. // 儲存設定
  155. function saveConfig(config) {
  156. try {
  157. GM_setValue("aiEnterConfig", JSON.stringify(config));
  158. return true;
  159. } catch (error) {
  160. console.error("儲存設定時發生錯誤:", error);
  161. return false;
  162. }
  163. }
  164.  
  165. // 建立設定介面
  166. function createConfigInterface() {
  167. // 如果已經有設定視窗開啟,則關閉它
  168. const existingDialog = document.getElementById("ai-enter-config");
  169. if (existingDialog) {
  170. existingDialog.remove();
  171. return;
  172. }
  173.  
  174. // 偵測使用者的作業系統
  175. function detectOS() {
  176. const userAgent = navigator.userAgent.toLowerCase();
  177. const platform = navigator.platform.toLowerCase();
  178.  
  179. if (platform.includes("mac") || userAgent.includes("mac")) {
  180. return "mac";
  181. } else if (platform.includes("win") || userAgent.includes("win")) {
  182. return "windows";
  183. } else if (platform.includes("linux") || userAgent.includes("linux")) {
  184. return "linux";
  185. } else {
  186. return "other";
  187. }
  188. }
  189.  
  190. const currentOS = detectOS();
  191.  
  192. // 載入目前設定
  193. const config = loadConfig();
  194.  
  195. // 偵測深色模式
  196. const isDarkMode =
  197. window.matchMedia &&
  198. window.matchMedia("(prefers-color-scheme: dark)").matches;
  199.  
  200. // 根據深色/淺色模式設定配色
  201. const colors = {
  202. background: isDarkMode ? "#2d2d2d" : "#ffffff",
  203. text: isDarkMode ? "#e0e0e0" : "#333333",
  204. border: isDarkMode ? "#555555" : "#dddddd",
  205. inputBg: isDarkMode ? "#3d3d3d" : "#ffffff",
  206. inputBorder: isDarkMode ? "#666666" : "#dddddd",
  207. buttonBg: isDarkMode ? "#3d3d3d" : "#f5f5f5",
  208. buttonText: isDarkMode ? "#e0e0e0" : "#333333",
  209. primary: "#4caf50", // 綠色按鈕,保持不變
  210. shadow: isDarkMode ? "rgba(0,0,0,0.3)" : "rgba(0,0,0,0.15)",
  211. };
  212.  
  213. // 建立設定對話框
  214. const dialogDiv = document.createElement("div");
  215. dialogDiv.id = "ai-enter-config";
  216. dialogDiv.style.position = "fixed";
  217. dialogDiv.style.top = "50%";
  218. dialogDiv.style.left = "50%";
  219. dialogDiv.style.transform = "translate(-50%, -50%)";
  220. dialogDiv.style.backgroundColor = colors.background;
  221. dialogDiv.style.color = colors.text;
  222. dialogDiv.style.border = `1px solid ${colors.border}`;
  223. dialogDiv.style.borderRadius = "8px";
  224. dialogDiv.style.padding = "20px";
  225. dialogDiv.style.width = "350px";
  226. dialogDiv.style.maxWidth = "90vw";
  227. dialogDiv.style.maxHeight = "90vh";
  228. dialogDiv.style.overflowY = "auto";
  229. dialogDiv.style.zIndex = "10000";
  230. dialogDiv.style.boxShadow = `0 4px 12px ${colors.shadow}`;
  231. dialogDiv.style.fontFamily =
  232. "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
  233.  
  234. // 設定標題
  235. const titleDiv = document.createElement("div");
  236. titleDiv.style.display = "flex";
  237. titleDiv.style.justifyContent = "space-between";
  238. titleDiv.style.alignItems = "center";
  239. titleDiv.style.marginBottom = "16px";
  240.  
  241. const title = document.createElement("h2");
  242. title.textContent = t("settings");
  243. title.style.margin = "0";
  244. title.style.fontSize = "18px";
  245. title.style.color = colors.text;
  246.  
  247. const closeButton = document.createElement("button");
  248. closeButton.textContent = t("close");
  249. closeButton.style.background = "none";
  250. closeButton.style.border = "none";
  251. closeButton.style.color = colors.text;
  252. closeButton.style.cursor = "pointer";
  253. closeButton.style.fontSize = "18px";
  254. closeButton.onclick = () => dialogDiv.remove();
  255.  
  256. titleDiv.appendChild(title);
  257. titleDiv.appendChild(closeButton);
  258. dialogDiv.appendChild(titleDiv);
  259.  
  260. // 快捷鍵設定
  261. const shortcutsLabel = document.createElement("label");
  262. shortcutsLabel.textContent = t("sendShortcut");
  263. shortcutsLabel.style.display = "block";
  264. shortcutsLabel.style.marginBottom = "12px";
  265. shortcutsLabel.style.color = colors.text;
  266. shortcutsLabel.style.fontWeight = "bold";
  267. dialogDiv.appendChild(shortcutsLabel);
  268.  
  269. // 快捷鍵選項容器
  270. const shortcutsContainer = document.createElement("div");
  271. shortcutsContainer.style.marginBottom = "16px";
  272. shortcutsContainer.style.padding = "12px";
  273. shortcutsContainer.style.backgroundColor = isDarkMode
  274. ? "#3a3a3a"
  275. : "#f8f9fa";
  276. shortcutsContainer.style.border = `1px solid ${colors.border}`;
  277. shortcutsContainer.style.borderRadius = "6px";
  278.  
  279. // 根據作業系統顯示適當的快捷鍵標籤
  280. const shortcuts = [
  281. {
  282. key: "ctrl",
  283. label: currentOS === "mac" ? `⌃ ${t("ctrlEnter")}` : t("ctrlEnter"),
  284. },
  285. {
  286. key: "alt",
  287. label: currentOS === "mac" ? `⌥ ${t("altEnter")}` : t("altEnter"),
  288. },
  289. {
  290. key: "meta",
  291. label:
  292. currentOS === "mac"
  293. ? `⌘ ${t("cmdEnter")}`
  294. : currentOS === "windows"
  295. ? `⊞ ${t("winEnter")}`
  296. : currentOS === "linux"
  297. ? t("superEnter")
  298. : t("winEnter"),
  299. },
  300. ];
  301.  
  302. shortcuts.forEach((shortcut) => {
  303. const optionDiv = document.createElement("div");
  304. optionDiv.style.display = "flex";
  305. optionDiv.style.alignItems = "center";
  306. optionDiv.style.marginBottom = "8px";
  307.  
  308. const checkbox = document.createElement("input");
  309. checkbox.type = "checkbox";
  310. checkbox.id = `shortcut-${shortcut.key}`;
  311. checkbox.checked =
  312. config.shortcuts?.send?.[shortcut.key] !== undefined
  313. ? config.shortcuts.send[shortcut.key]
  314. : defaultConfig.shortcuts.send[shortcut.key];
  315. if (isDarkMode) {
  316. checkbox.style.accentColor = colors.primary;
  317. }
  318.  
  319. const labelElement = document.createElement("label");
  320. labelElement.htmlFor = `shortcut-${shortcut.key}`;
  321. labelElement.style.marginLeft = "8px";
  322. labelElement.style.color = colors.text;
  323. labelElement.style.cursor = "pointer";
  324. labelElement.style.flexGrow = "1";
  325. labelElement.textContent = shortcut.label;
  326.  
  327. optionDiv.appendChild(checkbox);
  328. optionDiv.appendChild(labelElement);
  329. shortcutsContainer.appendChild(optionDiv);
  330. });
  331.  
  332. dialogDiv.appendChild(shortcutsContainer);
  333.  
  334. // 按鈕區域
  335. const buttonDiv = document.createElement("div");
  336. buttonDiv.style.display = "flex";
  337. buttonDiv.style.justifyContent = "flex-end";
  338. buttonDiv.style.marginTop = "16px";
  339.  
  340. const saveButton = document.createElement("button");
  341. saveButton.textContent = t("save");
  342. saveButton.style.padding = "8px 16px";
  343. saveButton.style.backgroundColor = colors.primary;
  344. saveButton.style.color = "white";
  345. saveButton.style.border = "none";
  346. saveButton.style.borderRadius = "4px";
  347. saveButton.style.cursor = "pointer";
  348. saveButton.style.marginLeft = "8px";
  349.  
  350. saveButton.onclick = () => {
  351. // 取得勾選的快捷鍵設定
  352. const sendShortcuts = {
  353. ctrl: document.getElementById("shortcut-ctrl").checked,
  354. alt: document.getElementById("shortcut-alt").checked,
  355. meta: document.getElementById("shortcut-meta").checked,
  356. };
  357.  
  358. const newConfig = {
  359. shortcuts: {
  360. send: sendShortcuts,
  361. },
  362. };
  363.  
  364. if (saveConfig(newConfig)) {
  365. alert(t("saveSuccess"));
  366. dialogDiv.remove();
  367. // 重新載入設定
  368. currentConfig = loadConfig();
  369. } else {
  370. alert(t("saveFailed"));
  371. }
  372. };
  373.  
  374. const resetButton = document.createElement("button");
  375. resetButton.textContent = t("reset");
  376. resetButton.style.padding = "8px 16px";
  377. resetButton.style.backgroundColor = colors.buttonBg;
  378. resetButton.style.color = colors.buttonText;
  379. resetButton.style.border = `1px solid ${colors.border}`;
  380. resetButton.style.borderRadius = "4px";
  381. resetButton.style.cursor = "pointer";
  382.  
  383. resetButton.onclick = () => {
  384. if (confirm(t("resetConfirm"))) {
  385. saveConfig(defaultConfig);
  386. alert(t("resetSuccess"));
  387. dialogDiv.remove();
  388. // 重新載入設定
  389. currentConfig = loadConfig();
  390. // 移除背景遮罩
  391. const overlay = document.querySelector('div[style*="z-index: 9999"]');
  392. if (overlay) overlay.remove();
  393. // 重新開啟設定介面以顯示重設後的設定
  394. createConfigInterface();
  395. }
  396. };
  397.  
  398. buttonDiv.appendChild(resetButton);
  399. buttonDiv.appendChild(saveButton);
  400. dialogDiv.appendChild(buttonDiv);
  401.  
  402. // 新增設定對話框到頁面
  403. document.body.appendChild(dialogDiv);
  404.  
  405. // 新增背景遮罩
  406. const overlay = document.createElement("div");
  407. overlay.style.position = "fixed";
  408. overlay.style.top = "0";
  409. overlay.style.left = "0";
  410. overlay.style.width = "100%";
  411. overlay.style.height = "100%";
  412. overlay.style.backgroundColor = isDarkMode
  413. ? "rgba(0,0,0,0.7)"
  414. : "rgba(0,0,0,0.5)";
  415. overlay.style.zIndex = "9999";
  416. overlay.onclick = () => {
  417. overlay.remove();
  418. dialogDiv.remove();
  419. };
  420.  
  421. document.body.insertBefore(overlay, dialogDiv);
  422. }
  423.  
  424. // 載入設定
  425. let currentConfig = loadConfig();
  426.  
  427. // 註冊設定選單
  428. GM_registerMenuCommand("⚙️ Settings", createConfigInterface);
  429.  
  430. // 輸出啟動資訊至 console
  431. console.log(
  432. "AI Enter Newline UserScript loaded. Current config:",
  433. currentConfig
  434. );
  435.  
  436. // 輔助函數:取得事件目標元素
  437. function getEventTarget(e) {
  438. return e.composedPath ? e.composedPath()[0] || e.target : e.target;
  439. }
  440.  
  441. // 輔助函數:檢查是否正在進行中文輸入
  442. function isChineseInputMode(e) {
  443. return e.isComposing || e.keyCode === 229;
  444. }
  445.  
  446. // 輔助函數:檢查是否在 ChatGPT 輸入框內
  447. function isInChatGPTTextarea(target) {
  448. return (
  449. target.id === "prompt-textarea" ||
  450. target.closest("#prompt-textarea") ||
  451. (target.getAttribute && target.getAttribute("contenteditable") === "true")
  452. );
  453. }
  454.  
  455. /**
  456. * 檢查按鍵組合是否為任何可能的發送快捷鍵(不論是否啟用)
  457. * @param {KeyboardEvent} e - 鍵盤事件
  458. * @returns {boolean} 是否為潛在的發送快捷鍵組合
  459. */
  460. function isPotentialSendShortcut(e) {
  461. if (e.key !== "Enter") return false;
  462.  
  463. // 檢查是否為任何可能的發送快捷鍵組合:Ctrl+Enter、Alt+Enter 或 Cmd+Enter
  464. const isCtrlOnly = e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey;
  465. const isAltOnly = e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey;
  466. const isMetaOnly = e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey;
  467.  
  468. return isCtrlOnly || isAltOnly || isMetaOnly;
  469. }
  470.  
  471. // 檢查是否為發送快捷鍵
  472. function isSendShortcut(e) {
  473. // 必須按下 Enter 鍵
  474. if (e.key !== "Enter") return false;
  475.  
  476. const shortcuts =
  477. currentConfig.shortcuts?.send || defaultConfig.shortcuts.send;
  478.  
  479. // 檢查是否有任何一個勾選的快捷鍵符合目前按鍵組合
  480. return (
  481. (shortcuts.ctrl && e.ctrlKey && !e.altKey && !e.metaKey) ||
  482. (shortcuts.alt && e.altKey && !e.ctrlKey && !e.metaKey) ||
  483. (shortcuts.meta && e.metaKey && !e.ctrlKey && !e.altKey)
  484. );
  485. }
  486.  
  487. // ChatGPT 特殊處理:尋找送出按鈕
  488. let findChatGPTSubmitButton = () => {
  489. return document.querySelector('button[data-testid="send-button"]');
  490. };
  491.  
  492. // 監聽 keydown 事件,攔截非預期的 Enter 按下事件,避免在輸入元件內誤觸送出
  493. window.addEventListener(
  494. "keydown",
  495. (e) => {
  496. // ChatGPT 網站特殊處理
  497. if (window.location.href.includes("chatgpt.com")) {
  498. // 如果正在進行中文輸入法選字,不干擾原生行為
  499. if (isChineseInputMode(e)) {
  500. return;
  501. }
  502.  
  503. // 如果是 Enter 鍵且沒有按下其他修飾鍵
  504. if (
  505. e.key === "Enter" &&
  506. !e.ctrlKey &&
  507. !e.shiftKey &&
  508. !e.metaKey &&
  509. !e.altKey
  510. ) {
  511. const target = getEventTarget(e);
  512. // 檢查是否在 prompt-textarea 或其他輸入區域
  513. if (isInChatGPTTextarea(target)) {
  514. e.stopPropagation();
  515. e.preventDefault();
  516.  
  517. // 更可靠的換行方法:模擬 Shift+Enter 按鍵事件
  518. const shiftEnterEvent = new KeyboardEvent("keydown", {
  519. key: "Enter",
  520. code: "Enter",
  521. shiftKey: true,
  522. bubbles: true,
  523. cancelable: true,
  524. });
  525. target.dispatchEvent(shiftEnterEvent);
  526.  
  527. // 如果上述方法無效,嘗試使用 insertParagraph 命令
  528. if (!shiftEnterEvent.defaultPrevented) {
  529. document.execCommand("insertParagraph");
  530. }
  531.  
  532. return;
  533. }
  534. }
  535.  
  536. // 使用自訂快捷鍵觸發送出
  537. if (isSendShortcut(e)) {
  538. // 同樣,如果正在中文輸入,不處理
  539. if (isChineseInputMode(e)) {
  540. return;
  541. }
  542.  
  543. const target = getEventTarget(e);
  544. if (isInChatGPTTextarea(target)) {
  545. const submitButton = findChatGPTSubmitButton();
  546. if (submitButton && !submitButton.disabled) {
  547. e.preventDefault();
  548. e.stopPropagation();
  549. submitButton.click();
  550. }
  551. }
  552. }
  553.  
  554. // 智慧型事件冒泡防止:如果是潛在的快捷鍵但未被使用者啟用,
  555. // 阻止事件傳播,避免觸發 ChatGPT 的原生快捷鍵行為
  556. if (isPotentialSendShortcut(e)) {
  557. const target = getEventTarget(e);
  558. if (isInChatGPTTextarea(target)) {
  559. e.preventDefault();
  560. e.stopPropagation();
  561. }
  562. }
  563. } else {
  564. // 其他網站的處理邏輯
  565. // 如果正在進行中文輸入法選字,不干擾原生行為
  566. if (isChineseInputMode(e)) {
  567. return;
  568. }
  569.  
  570. // 如果是 Enter 鍵且沒有按下其他修飾鍵(純 Enter)
  571. if (
  572. e.key === "Enter" &&
  573. !e.ctrlKey &&
  574. !e.shiftKey &&
  575. !e.metaKey &&
  576. !e.altKey
  577. ) {
  578. const target = getEventTarget(e);
  579. if (
  580. /INPUT|TEXTAREA|SELECT|LABEL/.test(target.tagName) ||
  581. (target.getAttribute &&
  582. target.getAttribute("contenteditable") === "true")
  583. ) {
  584. // 阻止事件向上冒泡,避免觸發不必要的送出行為
  585. e.stopPropagation();
  586. }
  587. }
  588.  
  589. // 如果是自訂快捷鍵組合,讓原生行為執行(不阻止)
  590. // 這樣使用者可以在其他網站使用相同的快捷鍵設定
  591. if (isSendShortcut(e)) {
  592. // 不做任何處理,讓網站的原生快捷鍵邏輯執行
  593. return;
  594. }
  595.  
  596. // 智慧型事件冒泡防止:如果是潛在的快捷鍵但未被使用者啟用,
  597. // 也要阻止冒泡,避免觸發網站的原生快捷鍵行為
  598. // 但對於 felo.ai,允許 ctrl+enter 正常冒泡, 因為 felo.ai 的 ctrl+enter 是用來搜尋網頁的
  599. if (isPotentialSendShortcut(e)) {
  600. // 如果是 felo.ai 且是 ctrl+enter,不阻止冒泡
  601. if (
  602. window.location.href.includes("felo.ai") &&
  603. e.ctrlKey &&
  604. e.key === "Enter" &&
  605. !e.altKey &&
  606. !e.metaKey
  607. ) {
  608. return;
  609. }
  610.  
  611. const target = getEventTarget(e);
  612. if (
  613. /INPUT|TEXTAREA|SELECT|LABEL/.test(target.tagName) ||
  614. (target.getAttribute &&
  615. target.getAttribute("contenteditable") === "true")
  616. ) {
  617. e.stopPropagation();
  618. }
  619. }
  620. }
  621. },
  622. true
  623. );
  624.  
  625. // 監聽 keypress 事件,防止在輸入元件內誤觸送出
  626. window.addEventListener(
  627. "keypress",
  628. (e) => {
  629. // ChatGPT 網站使用 keydown 處理就足夠,這裡保持原樣
  630. if (window.location.href.includes("chatgpt.com")) return;
  631.  
  632. // 如果正在進行中文輸入法選字,不干擾原生行為
  633. if (isChineseInputMode(e)) return; // 如果是 Enter 鍵且沒有按下其他修飾鍵(純 Enter)
  634. if (
  635. e.key === "Enter" &&
  636. !e.ctrlKey &&
  637. !e.shiftKey &&
  638. !e.metaKey &&
  639. !e.altKey
  640. ) {
  641. const target = getEventTarget(e);
  642. if (
  643. /INPUT|TEXTAREA|SELECT|LABEL/.test(target.tagName) ||
  644. (target.getAttribute &&
  645. target.getAttribute("contenteditable") === "true")
  646. ) {
  647. // 同樣阻止事件冒泡
  648. e.stopPropagation();
  649. }
  650. }
  651.  
  652. // 如果是自訂快捷鍵組合,讓原生行為執行(不阻止)
  653. if (isSendShortcut(e)) {
  654. return;
  655. }
  656.  
  657. // 智慧型事件冒泡防止:如果是潛在的快捷鍵但未被使用者啟用,
  658. // 也要阻止冒泡,避免觸發網站的原生快捷鍵行為
  659. // 但對於 felo.ai,允許 ctrl+enter 正常冒泡
  660. if (isPotentialSendShortcut(e)) {
  661. // 如果是 felo.ai 且是 ctrl+enter,不阻止冒泡
  662. if (
  663. window.location.href.includes("felo.ai") &&
  664. e.ctrlKey &&
  665. e.key === "Enter" &&
  666. !e.altKey &&
  667. !e.metaKey
  668. ) {
  669. return;
  670. }
  671.  
  672. const target = getEventTarget(e);
  673. if (
  674. /INPUT|TEXTAREA|SELECT|LABEL/.test(target.tagName) ||
  675. (target.getAttribute &&
  676. target.getAttribute("contenteditable") === "true")
  677. ) {
  678. e.stopPropagation();
  679. }
  680. }
  681. },
  682. true
  683. );
  684. })();