文章

Tampermonkey-Cambly

Work Result Show

Tutorial

Template Code: vite-plugin-monkey

示例prompt,使用通义千问完成

@cambly-video-script 当前项目是一个示例demo,仅参考即可,请实现以下功能,这是一个tampermonkey插件, 实现如下效果,当页面调用当页面调用具体接口时,请在页面上展示一个翻译后的一个对话框。它的内容是外教和学生的聊天记录。其中外教的userId是动态变化的,但是学生的userid是固定的68beb1c5e6435a90fdd6c303,它的userid是 XXX。 请实现当调用 https://www.cambly.com/model/lesson_transcript/xxxx?language=en&interfaceLanguage=zh_CN接口时,获取其返回结果,他的结果是 @example.json ,请解析结果,并且在页面展示内容为 teacher: student: teacher: student:

LLM Request

<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { GM_xmlhttpRequest } from '$';

interface SentenceWithContext {
  sentence: string;
  context: string;
}

const props = defineProps<{
  pinnedWord: { word: string; sentences: SentenceWithContext[] } | null;
  pinPosition: { x: number; y: number };
}>();

const emit = defineEmits<{
  (e: 'close'): void;
}>();

const translationResult = ref<string>('');
const isTranslating = ref<boolean>(false);
const translatedWord = ref<string>('');

const LLM_API_CONFIG = {
  baseUrl: 'http://127.0.0.1:1234',
  translatePrompt: '你的任务是翻译给定的英文单词,提供简要释义,并使用该单词造一个日常生活中的句子。请仔细阅读以下英文单词:\n' +
      '<英文单词>\n' +
      '{{text}}\n' +
      '</英文单词>\n' +
      '在处理时,请遵循以下要求:\n' +
      '1. 翻译要准确、简洁。\n' +
      '2. 释义要简要概括该单词的主要含义。\n' +
      '3. 造句要符合日常生活场景。\n' +
      '请在标签内按照以下格式输出:\n' +
      '翻译:[在此写出单词的中文翻译]\n' +
      '释义:[在此给出简要释义]\n' +
      '[在此提供日常生活中的英语句子]\n' +
      '[在此提供日常生活中的英语句子翻译]',
  model: 'qwen/qwen3-4b-2507'
};

const translateWord = async (word: string) => {
  if (!word) return;

  translatedWord.value = word;
  isTranslating.value = true;
  translationResult.value = '';

  try {
    GM_xmlhttpRequest({
      method: 'POST',
      url: `${LLM_API_CONFIG.baseUrl}/v1/chat/completions`,
      headers: {
        'Content-Type': 'application/json'
      },
      data: JSON.stringify({
        model: LLM_API_CONFIG.model,
        messages: [
          {
            role: "user",
            content: `${LLM_API_CONFIG.translatePrompt} ${word}`
          }
        ],
        temperature: 0.7,
        max_tokens: -1,
        stream: false
      }),
      onload: (resp) => {
        try {
          const data = JSON.parse(resp.responseText);
          translationResult.value = data.choices[0]?.message?.content?.trim() || '翻译失败';
        } catch (e) {
          translationResult.value = `解析响应失败: ${e.message || e.toString()}`;
        } finally {
          isTranslating.value = false;
        }
      },
      onerror: (error) => {
        console.error('翻译出错:', error);
        translationResult.value = `翻译出错: ${error.statusText || error.toString()}`;
        isTranslating.value = false;
      }
    });
  } catch (error) {
    console.error('翻译出错:', error);
    translationResult.value = `翻译出错: ${error.message || error.toString()}`;
    isTranslating.value = false;
  }
};

const highlightWord = (text: string, word: string) => {
  if (!word) return text;

  const escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

  try {
    let regex = new RegExp(`\\b(${escapedWord})\\b`, 'gi');
    let result = text.replace(regex, '<span class="highlight-word">$1</span>');

    if (result === text) {
      regex = new RegExp(`(${escapedWord})`, 'gi');
      result = text.replace(regex, '<span class="highlight-word">$1</span>');
    }

    return result;
  } catch (e) {
    console.error('正则表达式错误:', e);
    return text;
  }
};

// 在组件挂载时向页面添加全局样式
onMounted(() => {
  // 检查是否已经添加过样式
  if (!document.getElementById('chat-display-highlight-styles')) {
    const style = document.createElement('style');
    style.id = 'chat-display-highlight-styles';
    style.textContent = `
      .highlight-word {
        font-weight: bold !important;
        color: red !important;
      }
    `;
    document.head.appendChild(style);
  }
});

// 监听 pinnedWord 的变化,当用户点击不同单词时重新翻译
watch(() => props.pinnedWord, (newVal) => {
  if (newVal) {
    translateWord(newVal.word);
  }
}, { immediate: true });

</script>

<template>
  <div
    v-if="pinnedWord"
    class="sentence-tooltip pinned"
    :style="{ left: pinPosition.x + 'px', top: pinPosition.y + 'px' }"
  >
    <div class="tooltip-header">
      <strong>单词 "{{ pinnedWord.word }}" 出现的句子:</strong>
      <button class="close-button" @click="emit('close')">✕</button>
    </div>
    <div class="sentence-list">
      <div v-for="(sentence, index) in pinnedWord.sentences" :key="index" class="sentence-item">
        <div class="sentence-text" v-html="highlightWord(sentence.sentence, pinnedWord.word)"></div>
        <div class="sentence-context">
          <pre v-html="highlightWord(sentence.context, pinnedWord.word)"></pre>
        </div>
      </div>
    </div>

    <div class="translation-section">
      <div class="translation-header">
        <strong>翻译结果:</strong>
      </div>
      <div class="translation-content">
        <div v-if="isTranslating" class="translation-loading">
          翻译中...
        </div>
        <div v-else-if="translationResult" class="translation-result">
          {{ translationResult }}
        </div>
        <div v-else class="translation-empty">
          暂无翻译结果
        </div>
      </div>
    </div>
  </div>
</template>

Request Demo Content

{
  "id": "68c4e5f848c0434f0053cd41",
  "lessonId": "68c4e5f848c0434f0053cd41",
  "transcript": [
    {
      "text": "Oh,",
      "startOffsetSeconds": 1.922,
      "userId": "60801f6441aa4ba2caec515d"
    },
    {
      "text": "Hello,",
      "startOffsetSeconds": 2.312,
      "userId": "68beb1c5e6435a90fdd6c303"
    },
    {
      "text": "I can't see you.",
      "startOffsetSeconds": 5.272,
      "userId": "60801f6441aa4ba2caec515d"
    }
  ],
  "isDeleted": false
}

Request Data Sniff

<script setup lang="ts">
import { onMounted } from "vue";

// 监听网络请求
onMounted(() => {
  // 通过覆写 XMLHttpRequest 来拦截请求
  const originalOpen = XMLHttpRequest.prototype.open;
  XMLHttpRequest.prototype.open = function (method: string, url: string | URL) {
    // 检查是否是需要拦截的请求
    console.log("请求路径:", url);
    const urlString = typeof url === 'string' ? url : url.toString();
    if (urlString.includes('/model/lesson_transcript/') && urlString.includes('language=en')) {
      const originalOnReadyStateChange = this.onreadystatechange;
      this.onreadystatechange = function () {
        if (this.readyState === 4 && this.status === 200) {
          try {
            const response = JSON.parse(this.responseText);
            console.log('拦截到Cambly对话数据:', response);
            // 将数据传递给父组件
            window.dispatchEvent(new CustomEvent('transcriptLoaded', {
              detail: response
            }));
          } catch (e) {
            console.error('解析响应失败:', e);
          }
        }
        if (originalOnReadyStateChange) {
          originalOnReadyStateChange.apply(this, arguments as any);
        }
      };
    }
    originalOpen.apply(this, arguments as any);
  };

  // 监听自定义事件
  window.addEventListener('transcriptLoaded', (event: CustomEvent) => {
    const data = event.detail;
    // 将数据传递给父组件
    window.dispatchEvent(new CustomEvent('updateTranscript', {
      detail: data.transcript
    }));
  });

  // 同时拦截 fetch 请求
  const originalFetch = window.fetch;
  window.fetch = function (input: RequestInfo | URL, init?: RequestInit) {
    const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;

    // 检查是否是需要拦截的请求
    if (url.includes('/model/lesson_transcript/') && url.includes('language=en')) {
      console.log('拦截到Cambly fetch请求');
      return originalFetch(input, init).then(response => {
        const clonedResponse = response.clone();
        clonedResponse.json().then(data => {
          console.log('拦截到Cambly对话数据 (fetch):', data);
          // 将数据传递给父组件
          window.dispatchEvent(new CustomEvent('updateTranscript', {
            detail: data.transcript
          }));
        }).catch(e => {
          console.error('解析 fetch 响应失败:', e);
        });
        return response;
      });
    }

    return originalFetch(input, init);
  } as any;
});
</script>