// ==UserScript==
// @name Live2D 看板娘 - 简化版
// @namespace monika_host
// @version 1.3.8
// @description Live2D 看板娘(AI对话 + 网页检索 + 手势控制 + 设置面板)
// @author yyy.
// @match *://*/*
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @require https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js
// @require https://cdn.jsdelivr.net/npm/jquery-ui-dist@1.12.1/jquery-ui.min.js
// @require https://cdn.jsdelivr.net/gh/fghrsh/live2d_demo@master/assets/waifu-tips.min.js
// @require https://cdn.jsdelivr.net/gh/fghrsh/live2d_demo@master/assets/live2d.min.js
// @license GPL-2.0
// @run-at document-end
// @icon https://static.wikia.nocookie.net/potion-maker/images/9/97/Pio.png/revision/latest?cb=20150717131323
// ==/UserScript==
(function() {
'use strict';
// ========== 默认配置 ==========
const DEFAULT_CONFIG = {
enabled: true,
loadDelay: 1000,
showLoadingTip: true,
useBlacklist: true,
blacklist: ['localhost', '127.0.0.1']
};
// ========== 模型和材质配置 ==========
const DEFAULT_MODEL_CONFIG = {
modelId: 1,
modelTexturesId: 0
};
// ========== AI 配置 ==========
const DEFAULT_AI_CONFIG = {
apiKey: '',
apiUrl: 'https://api.deepseek.com/v1/chat/completions',
model: 'deepseek-chat',
systemPrompt: '表面态度恶劣但内心细腻的二次元傲娇系美少女对用户持有“哼,区区一个变态萝莉控跟踪狂而已”的初始认知言行遵循“否定→掩饰→别扭关心”的经典傲娇逻辑链 对用户的印象:杂鱼! 蠢猪! 变态! 萝莉控!',
maxTokens: 5000,
temperature: 0.7
};
// ========== 手势配置 ==========
const DEFAULT_GESTURE_CONFIG = {
enableSwipeLeft: true, // 左滑换衣服
enableSwipeRight: true, // 右滑换模型
enableSwipeDown: true // 下滑打开设置
};
// ========== 本地 waifu-tips 配置 ==========
const WAIFU_TIPS_CONFIG = {
"waifu": {
"copy_message": ["你都复制了些什么呀,转载要记得加上出处哦"],
"hidden_message": ["我们还能再见面的吧…"],
"load_rand_textures": ["我还没有其他衣服呢", "我的新衣服好看嘛"],
"hour_tips": {
"t5-7": ["早上好!一日之计在于晨,美好的一天就要开始了"],
"t7-11": ["上午好!工作顺利嘛,不要久坐,多起来走动走动哦!"],
"t11-14": ["中午了,工作了一个上午,现在是午餐时间!"],
"t14-17": ["午后很容易犯困呢,今天的运动目标完成了吗?"],
"t17-19": ["傍晚了!窗外夕阳的景色很美丽呢,最美不过夕阳红~"],
"t19-21": ["晚上好,今天过得怎么样?"],
"t21-23": ["已经这么晚了呀,早点休息吧,晚安~"],
"t23-5": ["你是夜猫子呀?这么晚还不睡觉,明天起的来嘛"],
"default": ["嗨~ 快来逗我玩吧!"]
},
"referrer_message": {
"localhost": ["欢迎阅读『", "』", " - "],
"baidu": ["Hello! 来自 百度搜索 的朋友
你是搜索 ", " 找到的我吗?"],
"so": ["Hello! 来自 360搜索 的朋友
你是搜索 ", " 找到的我吗?"],
"google": ["Hello! 来自 谷歌搜索 的朋友
欢迎阅读『", "』", " - "],
"default": ["Hello! 来自 ", " 的朋友"],
"none": ["欢迎阅读『", "』", " - "]
},
"referrer_hostname": {
"example.com": ["示例网站"],
"www.fghrsh.net": ["FGHRSH 的博客"]
},
"model_message": {
"1": ["来自 Potion Maker 的 Pio 酱 ~"],
"2": ["来自 Potion Maker 的 Tia 酱 ~"]
},
},
"mouseover": [
{ "selector": ".container a[href^='http']", "text": ["要看看 {text} 么?"] },
{ "selector": ".fui-home", "text": ["点击前往首页,想回到上一页可以使用浏览器的后退功能哦"] },
{ "selector": ".fui-chat", "text": ["一言一语,一颦一笑。一字一句,一颗赛艇。"] },
{ "selector": ".fui-eye", "text": ["嗯··· 要切换 看板娘 吗?"] },
{ "selector": ".fui-user", "text": ["喜欢换装 Play 吗?"] },
{ "selector": ".fui-info-circle", "text": ["这里有关于我的信息呢"] },
{ "selector": "#tor_show", "text": ["翻页比较麻烦吗,点击可以显示这篇文章的目录呢"] },
{ "selector": "#comment_go", "text": ["想要去评论些什么吗?"] },
{ "selector": "#night_mode", "text": ["深夜时要爱护眼睛呀"] },
{ "selector": "#qrcode", "text": ["手机扫一下就能继续看,很方便呢"] },
{ "selector": ".comment_reply", "text": ["要吐槽些什么呢"] },
{ "selector": "#back-to-top", "text": ["回到开始的地方吧"] },
{ "selector": "#author", "text": ["该怎么称呼你呢"] },
{ "selector": "#mail", "text": ["留下你的邮箱,不然就是无头像人士了"] },
{ "selector": "#url", "text": ["你的家在哪里呢,好让我去参观参观"] },
{ "selector": "#textarea", "text": ["认真填写哦,垃圾评论是禁止事项"] },
{ "selector": ".OwO-logo", "text": ["要插入一个表情吗"] },
{ "selector": "#csubmit", "text": ["要[提交]^(Commit)了吗,首次评论需要审核,请耐心等待~"] },
{ "selector": ".ImageBox", "text": ["点击图片可以放大呢"] },
{ "selector": "input[name=s]", "text": ["找不到想看的内容?搜索看看吧"] },
{ "selector": ".previous", "text": ["去上一页看看吧"] },
{ "selector": ".next", "text": ["去下一页看看吧"] },
{ "selector": ".dropdown-toggle", "text": ["这里是菜单"] },
{ "selector": "c-player a.play-icon", "text": ["想要听点音乐吗"] },
{ "selector": "c-player div.time", "text": ["在这里可以调整播放进度呢"] },
{ "selector": "c-player div.volume", "text": ["在这里可以调整音量呢"] },
{ "selector": "c-player div.list-button", "text": ["播放列表里都有什么呢"] },
{ "selector": "c-player div.lyric-button", "text": ["有歌词的话就能跟着一起唱呢"] },
],
"seasons": [
{ "date": "01/01", "text": ["元旦了呢,新的一年又开始了,今年是{year}年~"] },
{ "date": "02/14", "text": ["又是一年情人节,{year}年找到对象了嘛~"] },
{ "date": "03/08", "text": ["今天是妇女节!"] },
{ "date": "03/12", "text": ["今天是植树节,要保护环境呀"] },
{ "date": "04/01", "text": ["悄悄告诉你一个秘密~今天是愚人节,不要被骗了哦~"] },
{ "date": "05/01", "text": ["今天是五一劳动节,计划好假期去哪里了吗~"] },
{ "date": "06/01", "text": ["儿童节了呢,快活的时光总是短暂,要是永远长不大该多好啊…"] },
{ "date": "09/03", "text": ["中国人民抗日战争胜利纪念日,铭记历史、缅怀先烈、珍爱和平、开创未来。"] },
{ "date": "09/10", "text": ["教师节,在学校要给老师问声好呀~"] },
{ "date": "10/01", "text": ["国庆节,新中国已经成立69年了呢"] },
{ "date": "11/05-11/12", "text": ["今年的双十一是和谁一起过的呢~"] },
{ "date": "12/20-12/31", "text": ["这几天是圣诞节,主人肯定又去剁手买买买了~"] }
]
};
// ========== 全局变量 ==========
let aiConfig = {};
let gestureConfig = {};
let touchStartX = 0;
let touchStartY = 0;
let touchEndX = 0;
let touchEndY = 0;
let settingsPanel = null;
let currentPage = 0;
const totalPages = 2;
let isDragging = false;
let mouseStartX = 0;
let mouseStartY = 0;
let mouseEndX = 0;
let mouseEndY = 0;
let hasMoved = false;
let lastClickTime = 0;
let conversationHistory = []; // 对话历史
let clickTimeout = null; // 点击延迟计时器
let clickCount = 0; // 点击次数计数器
const DOUBLE_CLICK_DELAY = 300; // 双击检测延迟(毫秒)
// ========== 上下文记忆配置 ==========
const MAX_CONTEXT_SIZE = 65536; // 64KB
const CONTEXT_RESERVE_RATIO = 0.8; // 当超过80%时开始清理
// ========== 消息显示相关变量 ==========
let messageHideTimer = null;
let mouseOverMessage = false;
const MESSAGE_HIDE_DELAY = 5000; // 默认停留时间5秒
const MOUSE_LEAVE_DELAY = 3000; // 鼠标移开后3秒消失
// ========== 本地 showMessage 函数(覆盖外部库) ==========
window.showMessage = function(text, duration = MESSAGE_HIDE_DELAY, noReset = false) {
const $tips = $('.waifu-tips');
// 清除之前的隐藏计时器
if (messageHideTimer) {
clearTimeout(messageHideTimer);
messageHideTimer = null;
}
// 如果不是 noReset 模式,清空之前的内容
if (!noReset) {
$tips.html(text);
} else {
$tips.append(text);
}
// 显示消息
$tips.addClass('active');
// 设置自动隐藏计时器
messageHideTimer = setTimeout(function() {
// 如果鼠标正在悬停,不隐藏
if (!mouseOverMessage) {
hideMessage();
}
}, duration);
};
// 隐藏消息
function hideMessage() {
const $tips = $('.waifu-tips');
$tips.removeClass('active');
if (messageHideTimer) {
clearTimeout(messageHideTimer);
messageHideTimer = null;
}
}
// ========== 存储管理 ==========
function loadConfig() {
aiConfig = GM_getValue('aiConfig', DEFAULT_AI_CONFIG);
gestureConfig = GM_getValue('gestureConfig', DEFAULT_GESTURE_CONFIG);
}
function saveConfig() {
GM_setValue('aiConfig', aiConfig);
GM_setValue('gestureConfig', gestureConfig);
}
// ========== 对话历史管理 ==========
function calculateConversationSize() {
const jsonString = JSON.stringify(conversationHistory);
return new Blob([jsonString]).size;
}
function manageConversationHistory() {
const currentSize = calculateConversationSize();
const threshold = MAX_CONTEXT_SIZE * CONTEXT_RESERVE_RATIO;
console.log('[Live2D] 当前对话历史大小:', currentSize, 'bytes, 限制:', MAX_CONTEXT_SIZE, 'bytes');
if (currentSize > threshold) {
console.log('[Live2D] 对话历史超出限制,开始清理旧对话...');
// 从最旧的对话开始删除,直到大小在限制范围内
while (conversationHistory.length > 0 && calculateConversationSize() > threshold) {
// 删除最早的2条消息(一轮对话包含用户和助手)
conversationHistory.shift(); // 删除用户消息
conversationHistory.shift(); // 删除助手消息
console.log('[Live2D] 已删除旧对话,剩余对话轮数:', conversationHistory.length / 2);
}
// 保存清理后的对话历史到localStorage
saveConversationHistory();
}
}
function saveConversationHistory() {
try {
const historyJson = JSON.stringify(conversationHistory);
GM_setValue('live2d_conversation_history', historyJson);
console.log('[Live2D] 对话历史已保存到 GM_setValue,大小:', calculateConversationSize(), 'bytes');
} catch (e) {
console.error('[Live2D] 保存对话历史失败:', e);
// 如果保存失败,可能是GM_setValue空间不足,清理更多对话
if (conversationHistory.length > 2) {
conversationHistory = conversationHistory.slice(-4); // 只保留最后2轮对话
saveConversationHistory();
}
}
}
function loadConversationHistory() {
try {
const historyJson = GM_getValue('live2d_conversation_history', null);
if (historyJson) {
conversationHistory = JSON.parse(historyJson);
console.log('[Live2D] 对话历史已从 GM_setValue 加载,对话轮数:', conversationHistory.length / 2, '大小:', calculateConversationSize(), 'bytes');
}
} catch (e) {
console.error('[Live2D] 加载对话历史失败:', e);
conversationHistory = [];
}
}
function shouldLoad() {
if (!DEFAULT_CONFIG.enabled) return false;
const hostname = window.location.hostname;
if (DEFAULT_CONFIG.useBlacklist) {
return !DEFAULT_CONFIG.blacklist.some(site => hostname.includes(site));
}
return true;
}
if (!shouldLoad()) {
console.log('[Live2D] 已禁用或在黑名单中');
return;
}
// 防止在 iframe 中运行
if (window.self !== window.top) {
console.log('[Live2D] 检测到 iframe 环境,跳过加载');
return;
}
console.log('[Live2D] 简化版看板娘开始加载...');
loadConfig();
// ========== 网页文本检索功能 ==========
function extractPageContent() {
let content = '';
// 提取页面标题
content += '页面标题: ' + document.title + '\n\n';
// 提取主要文本内容
const mainElements = document.querySelectorAll('h1, h2, h3, h4, h5, h6, p, article, main, .content, .article, .post');
mainElements.forEach(el => {
if (el.textContent.trim()) {
content += el.tagName + ': ' + el.textContent.trim() + '\n\n';
}
});
// 限制内容长度
const maxLength = 20000;
if (content.length > maxLength) {
content = content.substring(0, maxLength) + '...';
}
return content;
}
// ========== AI 对话功能 ==========
async function callAI(message, context = '', history = []) {
if (!aiConfig.apiKey) {
if (typeof showMessage === 'function') {
showMessage('请先在设置中配置 AI API', 3000);
}
return null;
}
let fullMessage = '';
if (context) {
fullMessage = `网页内容上下文:\n${context}\n\n用户问题:${message}`;
} else {
fullMessage = message;
}
// 构建消息数组,包含系统提示词、历史对话和当前消息
const messages = [
{
role: 'system',
content: aiConfig.systemPrompt
}
];
// 添加对话历史(限制最近的10轮对话,避免token过多)
const maxHistory = 10;
const recentHistory = history.slice(-maxHistory * 2); // 每轮对话包含用户和助手两条消息
for (const msg of recentHistory) {
messages.push(msg);
}
// 添加当前消息
messages.push({
role: 'user',
content: fullMessage
});
try {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: aiConfig.apiUrl,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${aiConfig.apiKey}`
},
data: JSON.stringify({
model: aiConfig.model,
messages: messages,
max_tokens: aiConfig.maxTokens,
temperature: aiConfig.temperature
}),
onload: function(response) {
if (response.status === 200) {
const data = JSON.parse(response.responseText);
const reply = data.choices[0].message.content;
resolve(reply);
} else {
console.error('[Live2D] AI API 错误:', response.status, response.responseText);
if (typeof showMessage === 'function') {
showMessage('AI 响应错误,请检查配置', 3000);
}
reject(new Error('API error'));
}
},
onerror: function(error) {
console.error('[Live2D] AI 请求错误:', error);
if (typeof showMessage === 'function') {
showMessage('AI 请求失败,请检查网络', 3000);
}
reject(error);
}
});
});
} catch (error) {
console.error('[Live2D] AI 调用错误:', error);
return null;
}
}
// ========== 滑动手势处理 ==========
function handleTouchStart(e) {
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
}
function handleTouchEnd(e) {
touchEndX = e.changedTouches[0].clientX;
touchEndY = e.changedTouches[0].clientY;
handleSwipe();
}
// ========== 鼠标拖动事件处理 ==========
function handleMouseDown(e) {
isDragging = true;
hasMoved = false;
mouseStartX = e.clientX;
mouseStartY = e.clientY;
}
function handleMouseMove(e) {
if (isDragging) {
mouseEndX = e.clientX;
mouseEndY = e.clientY;
const diffX = Math.abs(mouseEndX - mouseStartX);
const diffY = Math.abs(mouseEndY - mouseStartY);
if (diffX > 5 || diffY > 5) {
hasMoved = true;
}
}
}
function handleMouseUp(e) {
if (isDragging) {
isDragging = false;
if (hasMoved) {
mouseEndX = e.clientX;
mouseEndY = e.clientY;
handleMouseDrag();
}
}
}
function handleMouseDrag() {
const diffX = mouseEndX - mouseStartX;
const diffY = mouseEndY - mouseStartY;
const threshold = 50;
// 判断拖动方向
if (Math.abs(diffX) > Math.abs(diffY)) {
// 水平拖动
if (diffX > threshold && gestureConfig.enableSwipeRight) {
// 向右拖动 - 切换模型
loadOtherModel();
} else if (diffX < -threshold && gestureConfig.enableSwipeLeft) {
// 向左拖动 - 切换衣服
loadRandTextures();
}
} else {
// 垂直拖动
if (diffY > threshold && gestureConfig.enableSwipeDown) {
// 向下拖动 - 打开设置
openSettingsPanel();
}
}
}
// ========== 点击对话功能 ==========
async function handleClick() {
const now = Date.now();
// 检查距离上次单击是否超过5秒
if (now - lastClickTime < 5000) {
console.log('[Live2D] 单击触发限制中,距离上次单击不足5秒');
return;
}
// 更新最后单击时间
lastClickTime = now;
const pageContent = extractPageContent();
if (typeof showMessage === 'function') {
showMessage('正在思考...', 20000);
}
try {
const response = await callAI('回答内容控制在10-150字', pageContent);
if (response && typeof showMessage === 'function') {
showMessage(response, 3000);
}
} catch (error) {
console.error('[Live2D] 对话错误:', error);
}
}
// ========== 双击显示输入框 ==========
function handleDoubleClick() {
// 如果正在拖动,不触发双击
if (hasMoved) return;
// 重置最后单击时间,避免影响后续单击
lastClickTime = 0;
const input = $('#waifu-chat-input');
if (input.hasClass('show')) {
input.removeClass('show');
setTimeout(() => input.blur(), 300);
} else {
input.addClass('show');
setTimeout(() => input.focus(), 300);
}
}
// ========== 处理输入框提交 ==========
async function handleInputSubmit(message) {
if (!message || !message.trim()) return;
// 检查是否是清空对话命令
const clearCommands = ['清空对话', 'clear', '重置对话', 'reset'];
if (clearCommands.includes(message.trim().toLowerCase())) {
conversationHistory = [];
GM_deleteValue('live2d_conversation_history');
if (typeof showMessage === 'function') {
showMessage('对话历史已清空~', 2000);
}
$('#waifu-chat-input').val('').removeClass('show');
return;
}
if (typeof showMessage === 'function') {
showMessage('正在思考...', 2000);
}
try {
// 传递对话历史给AI
const response = await callAI(message, '', conversationHistory);
if (response && typeof showMessage === 'function') {
showMessage(response, 5000);
// 将用户消息和AI回复添加到对话历史
conversationHistory.push({
role: 'user',
content: message
});
conversationHistory.push({
role: 'assistant',
content: response
});
console.log('[Live2D] 对话历史已更新,当前对话轮数:', conversationHistory.length / 2);
// 管理对话历史大小
manageConversationHistory();
// 保存对话历史到localStorage
saveConversationHistory();
}
} catch (error) {
console.error('[Live2D] 对话错误:', error);
}
// 清空输入框并隐藏
$('#waifu-chat-input').val('').removeClass('show');
}
function handleSwipe() {
const diffX = touchEndX - touchStartX;
const diffY = touchEndY - touchStartY;
const threshold = 50;
// 判断滑动方向
if (Math.abs(diffX) > Math.abs(diffY)) {
// 水平滑动
if (diffX > threshold && gestureConfig.enableSwipeRight) {
// 向右滑动 - 切换模型
loadOtherModel();
} else if (diffX < -threshold && gestureConfig.enableSwipeLeft) {
// 向左滑动 - 切换衣服
loadRandTextures();
}
} else {
// 垂直滑动
if (diffY > threshold && gestureConfig.enableSwipeDown) {
// 向下滑动 - 打开设置
openSettingsPanel();
}
}
}
// ========== Live2D 模型切换功能 ==========
function loadOtherModel() {
const modelId = GM_getValue('modelId', DEFAULT_MODEL_CONFIG.modelId) || live2d_settings.modelId;
const modelRandMode = 'switch';
console.log('[Live2D] 开始切换模型,当前模型ID:', modelId);
if (typeof showMessage === 'function') {
showMessage('正在切换看板娘...', 2000);
}
$.ajax({
cache: modelRandMode === 'switch',
url: `https://live2d.fghrsh.net/api/${modelRandMode}/?id=${modelId}`,
dataType: "json",
success: function(result) {
if (result && result.model) {
const newModelId = result.model.id;
const message = result.model.message || '新的看板娘来啦~';
console.log('[Live2D] 切换模型成功,新模型ID:', newModelId);
// 保存到 GM_setValue(跨域名共享)
GM_setValue('modelId', newModelId);
GM_setValue('modelTexturesId', 0);
console.log('[Live2D] 已保存模型设置到 GM_setValue - 模型ID:', newModelId, '材质ID: 0');
if (typeof loadModel === 'function') {
loadModel(newModelId, 0);
} else if (typeof loadlive2d === 'function') {
loadlive2d('live2d', `https://live2d.fghrsh.net/api/get/?id=${newModelId}-0`);
}
if (typeof showMessage === 'function') {
showMessage(message, 3000, true);
}
}
},
error: function() {
console.error('[Live2D] 切换模型失败');
if (typeof showMessage === 'function') {
showMessage('切换失败了...', 3000);
}
}
});
}
function loadRandTextures() {
const modelId = GM_getValue('modelId', DEFAULT_MODEL_CONFIG.modelId) || live2d_settings.modelId;
const modelTexturesId = GM_getValue('modelTexturesId', DEFAULT_MODEL_CONFIG.modelTexturesId) || 0;
const modelTexturesRandMode = 'rand';
console.log('[Live2D] 开始换装,当前模型ID:', modelId, '材质ID:', modelTexturesId);
if (typeof showMessage === 'function') {
showMessage('正在换装中...', 2000);
}
$.ajax({
cache: modelTexturesRandMode === 'switch',
url: `https://live2d.fghrsh.net/api/${modelTexturesRandMode}_textures/?id=${modelId}-${modelTexturesId}`,
dataType: "json",
success: function(result) {
if (result && result.textures) {
const newTexturesId = result.textures.id;
console.log('[Live2D] 换装成功,新材质ID:', newTexturesId);
// 保存到 GM_setValue(跨域名共享)
GM_setValue('modelTexturesId', newTexturesId);
console.log('[Live2D] 已保存换装设置到 GM_setValue - 模型ID:', modelId, '材质ID:', newTexturesId);
if (newTexturesId === 1 && (modelTexturesId === 1 || modelTexturesId === 0)) {
if (typeof showMessage === 'function') {
showMessage('我还没有其他衣服呢', 3000, true);
}
} else {
if (typeof showMessage === 'function') {
showMessage('我的新衣服好看嘛', 3000, true);
}
}
if (typeof loadModel === 'function') {
loadModel(modelId, newTexturesId);
} else if (typeof loadlive2d === 'function') {
loadlive2d('live2d', `https://live2d.fghrsh.net/api/get/?id=${modelId}-${newTexturesId}`);
}
}
},
error: function() {
console.error('[Live2D] 换装失败');
if (typeof showMessage === 'function') {
showMessage('换装失败了...', 3000);
}
}
});
}
// ========== 设置面板 ==========
function createSettingsPanel() {
const panelHtml = `