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>