Greasy Fork

来自缓存

Greasy Fork is available in English.

React App to Manus.im Sync (v2.0 - Chatbox + Send Button)

Sync text from React app to manus.im chat box and click send on trigger

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         React App to Manus.im Sync (v2.0 - Chatbox + Send Button)
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  Sync text from React app to manus.im chat box and click send on trigger
// @author       Samuel r Nason Tomaszewski, Yunnuo Zhang (modified)
// @match        https://manus.im/*
// @match        https://www.manus.im/*
// @match        *://localhost:*/*
// @match        *://192.168.30.7:*/*
// @match        *://10.*.*.*:*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        window.focus
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  const MESSAGE_TYPES = {
    RECIPIENT: "recipient", // kept for compatibility with your React app UI
    SUBJECT: "subject",     // kept for compatibility with your React app UI
    BODY: "body",
  };

  function debugLog(message) {
    console.log(`[Manus Sync Debug] ${message}`);
  }

  function getMessageType(buttonText) {
    const baseText = (buttonText || "").split(":")[0].trim();
    if (baseText.startsWith("Recipient")) return MESSAGE_TYPES.RECIPIENT;
    if (baseText.startsWith("Subject")) return MESSAGE_TYPES.SUBJECT;
    if (baseText.startsWith("Body")) return MESSAGE_TYPES.BODY;
    return "";
  }

  function updateSyncedText(messageType, text) {
    if (!messageType) return;

    GM_setValue("syncedText", {
      type: messageType,
      content: text,
      timestamp: Date.now(),
    });

    // Persistent caches for send operations
    if (messageType === MESSAGE_TYPES.SUBJECT) GM_setValue("lastSubject", text);
    if (messageType === MESSAGE_TYPES.BODY) GM_setValue("lastBody", text);

    debugLog(`Stored text type=${messageType}, len=${(text || "").length}`);
  }

  // -----------------------------
  // React app side (unchanged logic)
  // -----------------------------
  function checkReactAppState() {
    const selectedButton = document.querySelector(".context-button.selected");
    const textDisplay = document.querySelector('.text-display[contenteditable="true"]');

    if (selectedButton && textDisplay) {
      const messageType = getMessageType(selectedButton.textContent);
      const currentText = textDisplay.textContent || "";
      const lastSync = GM_getValue("syncedText");

      if (!lastSync || lastSync.content !== currentText) {
        if (messageType === MESSAGE_TYPES.RECIPIENT) {
          GM_setValue("recipientContent", currentText);
        } else {
          updateSyncedText(messageType, currentText);
        }
      }
    }

    // Button trigger logic (send/compose)
    const sentButton = document.querySelector(".keyboard-button.sent");
    if (sentButton) {
      const lastTrigger = GM_getValue("buttonTrigger");

      if (sentButton.textContent === "Sent!") {
        if (!lastTrigger || lastTrigger.status !== "pending" || lastTrigger.action !== "send") {
          debugLog("React: Send button changed to Sent! -> trigger send");
          GM_setValue("buttonTrigger", { timestamp: Date.now(), status: "pending", action: "send" });

          setTimeout(() => {
            const normalButton = document.querySelector(".keyboard-button.sendemail.default");
            if (normalButton && normalButton.textContent === "Send") {
              GM_setValue("buttonTrigger", { timestamp: Date.now(), status: "reset", action: "send" });
            }
          }, 2000);
        }
      } else if (sentButton.textContent === "Start composing") {
        if (!lastTrigger || lastTrigger.status !== "pending" || lastTrigger.action !== "compose") {
          debugLog("React: Compose button changed -> trigger compose");
          GM_setValue("buttonTrigger", { timestamp: Date.now(), status: "pending", action: "compose" });

          setTimeout(() => {
            const normalButton = document.querySelector(".keyboard-button.compose.default");
            if (normalButton && normalButton.textContent === "Compose") {
              GM_setValue("buttonTrigger", { timestamp: Date.now(), status: "reset", action: "compose" });
            }
          }, 2000);
        }
      }
    }
  }

  function handleReactApp() {
    debugLog("Initializing React app polling");
    setInterval(checkReactAppState, 500);
  }

  // -----------------------------
  // Manus.im side
  // -----------------------------

  // Target editor is the contenteditable tiptap ProseMirror:
  // <div contenteditable="true" class="tiptap ProseMirror" ...>
  const manusSelectors = {
    [MESSAGE_TYPES.BODY]: [
      'div.tiptap.ProseMirror[contenteditable="true"]',
      'div.ProseMirror[contenteditable="true"]',
      'div[contenteditable="true"][translate="no"]',
    ],
    // Provided send button has a very distinctive SVG path; we’ll match on that.
    // We'll still keep some fallback heuristics.
    SEND_BUTTON_FALLBACKS: [
      'button[type="submit"]',
      'button[aria-label*="Send"]',
      'button[title*="Send"]',
      'button.rounded-full',
    ],
  };

  function escapeHtml(s) {
    return (s || "")
      .replaceAll("&", "&amp;")
      .replaceAll("<", "&lt;")
      .replaceAll(">", "&gt;")
      .replaceAll('"', "&quot;")
      .replaceAll("'", "&#039;");
  }

  function findManusEditor() {
    for (const sel of manusSelectors[MESSAGE_TYPES.BODY]) {
      const el = document.querySelector(sel);
      if (el) return el;
    }
    return null;
  }

  // Your provided SVG path snippet (unique “up arrow”)
  const SEND_ICON_PATH_SNIPPET = "M7.91699 15.0642";

  function findManusSendButton() {
    // 1) Best: find button that contains that SVG path snippet
    const buttons = Array.from(document.querySelectorAll("button"));
    for (const btn of buttons) {
      const html = btn.innerHTML || "";
      if (html.includes(SEND_ICON_PATH_SNIPPET)) return btn;
    }

    // 2) Fallbacks
    for (const sel of manusSelectors.SEND_BUTTON_FALLBACKS) {
      const el = document.querySelector(sel);
      if (el) return el;
    }

    return null;
  }

  function setManusEditorContent(editorEl, content) {
    // TipTap/ProseMirror usually expects <p> nodes; we’ll replace with a single <p>
    // and fire input/keydown to make the UI enable the send button.
    const safe = escapeHtml(content).replaceAll("\n", "<br/>");

    editorEl.focus();

    // ProseMirror likes actual paragraph nodes
    editorEl.innerHTML = `<p>${safe || "<br/>"}</p>`;

    // Fire events that commonly update framework state
    editorEl.dispatchEvent(new InputEvent("input", { bubbles: true }));
    editorEl.dispatchEvent(new Event("change", { bubbles: true }));
    editorEl.dispatchEvent(new KeyboardEvent("keyup", { key: " ", bubbles: true }));
    editorEl.dispatchEvent(new KeyboardEvent("keydown", { key: "Backspace", bubbles: true }));
    editorEl.dispatchEvent(new KeyboardEvent("keyup", { key: "Backspace", bubbles: true }));

    // Store for recovery
    editorEl.setAttribute("data-synced-value", content || "");
  }

  function updateManusElement(type, content) {
    if (type !== MESSAGE_TYPES.BODY) {
      // Manus wiring: only BODY goes to chat box
      debugLog(`Ignoring type=${type} on manus.im (BODY-only wiring)`);
      return;
    }

    const editor = findManusEditor();
    if (!editor) {
      debugLog("Manus editor not found (chatbox contenteditable).");
      return;
    }

    debugLog(`Updating Manus chat editor with len=${(content || "").length}`);
    setManusEditorContent(editor, content || "");

    // Restore if editor clears on focus
    editor.addEventListener(
      "focus",
      function () {
        const syncedValue = this.getAttribute("data-synced-value") || "";
        const current = (this.textContent || "").trim();
        if (syncedValue && !current) {
          debugLog("Editor was empty on focus; restoring synced value");
          setManusEditorContent(this, syncedValue);
        }
      },
      { passive: true }
    );
  }

  function clickManusSend() {
    const sendBtn = findManusSendButton();
    if (!sendBtn) {
      debugLog("Send button not found on manus.im");
      return;
    }

    // If disabled, we’ll still try after a short wait (often enables after input)
    const isDisabled =
      sendBtn.disabled ||
      sendBtn.getAttribute("aria-disabled") === "true" ||
      sendBtn.classList.contains("cursor-not-allowed");

    if (isDisabled) {
      debugLog("Send button looks disabled; retrying in 250ms...");
      setTimeout(() => {
        const btn2 = findManusSendButton();
        if (btn2 && !(btn2.disabled || btn2.getAttribute("aria-disabled") === "true")) {
          debugLog("Send button enabled on retry; clicking send");
          btn2.click();
        } else if (btn2) {
          debugLog("Send button still disabled on retry; clicking anyway");
          btn2.click();
        }
      }, 250);
      return;
    }

    debugLog("Clicking Manus send button now");
    sendBtn.click();
  }

  function handleManus() {
    let lastTimestamp = 0;
    let lastButtonTimestamp = 0;
    let sendInProgress = false;

    debugLog("Manus handler initialized");

    setInterval(() => {
      // Handle triggers from React app
      const buttonTrigger = GM_getValue("buttonTrigger");
      if (buttonTrigger && buttonTrigger.timestamp > lastButtonTimestamp) {
        lastButtonTimestamp = buttonTrigger.timestamp;

        if (buttonTrigger.status === "pending" && buttonTrigger.action === "send") {
          debugLog("Received SEND trigger from React app");
          sendInProgress = true;

          // Safety timeout
          setTimeout(() => {
            sendInProgress = false;
          }, 5000);

          // Populate editor from stored body, then send
          const bodyVal =
            (GM_getValue("syncedText")?.type === MESSAGE_TYPES.BODY
              ? GM_getValue("syncedText")?.content
              : null) || GM_getValue("lastBody") || "";

          if (!bodyVal) {
            debugLog("No body content available to send.");
            sendInProgress = false;
            return;
          }

          updateManusElement(MESSAGE_TYPES.BODY, bodyVal);

          // Give the app a moment to enable the send button
          setTimeout(() => {
            clickManusSend();
            setTimeout(() => {
              sendInProgress = false;
              debugLog("Send flow complete; resuming normal sync");
            }, 800);
          }, 200);
        } else if (buttonTrigger.status === "pending" && buttonTrigger.action === "compose") {
          // Manus doesn’t have “compose”; ignore safely
          debugLog("Received COMPOSE trigger (ignored on manus.im)");
        }
      }

      // Regular sync (when body changes)
      if (!sendInProgress) {
        const syncedData = GM_getValue("syncedText");
        if (syncedData && syncedData.timestamp > lastTimestamp) {
          lastTimestamp = syncedData.timestamp;
          debugLog(`New synced data received type=${syncedData.type}`);
          updateManusElement(syncedData.type, syncedData.content);
        }
      }
    }, 500);
  }

  // Track whether React tab is open (your original pattern)
  async function checkReactOpen() {
    GM_setValue("reactOpen", !window.location.hostname.includes("manus.im"));
  }
  checkReactOpen();
  setInterval(checkReactOpen, 1000);

  // Boot
  debugLog("Waiting 1000ms for page to load...");
  setTimeout(() => {
    const host = window.location.hostname;
    if (host.includes("manus.im")) {
      debugLog("Initializing Manus handler (v2.0)");
      handleManus();
    } else {
      debugLog("Initializing React app handler (v2.0)");
      handleReactApp();
    }
  }, 1000);
})();