// ==UserScript==
// @name 贤者之家--在线网页聊天室
// @namespace sage_home
// @version 1.1
// @description 和所有人在线交流分享,安全匿名,无需账号,无需客户端,保护隐私,在线网页聊天室
// @match https://*/*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @run-at document-start
// @license MIT
// @connect supabase.co
// @require https://unpkg.com/@supabase/[email protected]/dist/umd/supabase.js
// @require https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.1.5/hls.min.js
// ==/UserScript==
// HLS播放器模块
const HlsPlayer = {
config: {
BUFFER_LENGTH: 10,
MAX_RETRY: 3,
ERROR_DELAY: 5000
},
init: function (videoElement, streamUrl) {
console.log('[HLS Init] 开始初始化HLS播放器', streamUrl);
if (typeof Hls === 'undefined') {
console.error('[HLS Init] Hls库未加载');
return null;
}
const hls = new Hls({
maxBufferLength: this.config.BUFFER_LENGTH,
maxMaxBufferLength: this.config.BUFFER_LENGTH * 3
});
hls.loadSource(streamUrl);
hls.attachMedia(videoElement);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
console.log('[HLS] 视频流已解析');
/* videoElement.play().catch(err => {
console.error('[HLS]播放失败:', err);
}); */
});
hls.on(Hls.Events.ERROR, (event, data) => {
if (data.fatal) {
console.error('[HLS]致命错误:', data);
}
});
return hls;
},
};
(function () {
'use strict';
// 配置参数
const CONFIG = {
SUPABASE_URL: 'https://icaugjyuwenraxxgwvzf.supabase.co',
SUPABASE_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImljYXVnanl1d2VucmF4eGd3dnpmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDI4ODcwNjcsImV4cCI6MjA1ODQ2MzA2N30.-IsrU3_NyoqDxFeNH1l2d6SgVv9pPA0uIVEA44FmuSQ',
CHAT_UI: {
position: { right: '20px', bottom: '20px' },
theme: {
primary: '#007FFF',
background: '#1A1A1A',
text: '#FFFFFF',
inputBg: '#2D2D2D'
}
}
};
// 样式管理模块
const StyleManager = (() => {
const cssVariables = `
:root {
--chat-bg: ${CONFIG.CHAT_UI.theme.background};
--chat-text: ${CONFIG.CHAT_UI.theme.text};
--primary-color: ${CONFIG.CHAT_UI.theme.primary};
--input-bg: ${CONFIG.CHAT_UI.theme.inputBg};
}
`;
const scrollbarCSS = `
#chat-messages::-webkit-scrollbar {
width: 8px;
background: transparent;
}
#chat-messages::-webkit-scrollbar-thumb {
background: #5c5c5c;
border-radius: 4px;
}
#chat-messages {
height: calc(100% - 140px);
scrollbar-width: thin;
scrollbar-color: #5c5c5c #2d2d2d;
box-sizing: border-box;
}
#input-container {
display: flex;
flex-direction: column;
align-items: stretch;
padding: 10px;
gap: 8px;
box-sizing: border-box;
height: 100px;
}
#chat-input {
width: 100%;
height: 50px;
background-color: var(--input-bg);
border: 1px solid #3A3A3A;
border-radius: 8px;
color: var(--chat-text);
resize: none;
box-sizing: border-box;
overflow-y: auto;
}
#chat-input::-webkit-scrollbar {
display: none;
}
#chat-send-button {
width: auto;
padding: 5px 10px;
background-color: var(--primary-color);
color: #FFF;
border: none;
border-radius: 8px;
cursor: pointer;
margin-left: auto;
}
.online-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #4CAF50;
margin-right: 6px;
display: inline-block;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
#chat-header {
height: 36px;
padding: 0 12px;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
}
`;
return {
inject: () => {
const style = document.createElement('style');
style.textContent = `${cssVariables} ${scrollbarCSS}`;
document.head.appendChild(style);
}
};
})();
// Supabase 客户端管理
const SupabaseClient = (() => {
let client;
return {
initialize: async () => {
client = window.supabase.createClient(
CONFIG.SUPABASE_URL,
CONFIG.SUPABASE_KEY,
{
realtime: { params: { eventsPerSecond: 10 } }
}
);
return client;
},
getClient: () => client
};
})();
// 聊天室核心功能
class ChatRoom {
constructor(supabase) {
this.supabase = supabase; // 使用全局已实例化的Supabase客户端
// this.container = this.createContainer(); // 移除对不存在方法的调用
this.initUI();
// 初始化用户和实时连接
this.initializeUser();
this.setupRealtime();
this.loadHistory();
// 页面关闭时清理资源
window.addEventListener('beforeunload', () => this.cleanup());
}
initUI() {
this.container = document.createElement('div');
this.container.id = 'chat-container';
Object.assign(this.container.style, {
position: 'fixed',
right: CONFIG.CHAT_UI.position.right,
bottom: CONFIG.CHAT_UI.position.bottom,
width: '300px',
height: '90dvh',
backgroundColor: 'var(--chat-bg)',
borderRadius: '12px',
boxShadow: '0 4px 16px rgba(0,0,0,0.2)',
zIndex: 9999,
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'column',
wordWrap: 'break-word',
overflowWrap: 'break-word',
});
this.header = document.createElement('div');
this.header.id = 'chat-header';
this.header.innerHTML = `
<div class="online-count">
<span class="online-dot"></span>
<span id="online-users">0</span> 人在线
</div>
`;
this.container.appendChild(this.header);
this.messageArea = this.createMessageArea();
this.input = this.createInput();
this.sendButton = this.createButton();
this.sendButton.addEventListener('click', () => this.sendMessage());
// 创建输入区域容器,用于水平排列输入框和发送按钮
this.inputContainer = document.createElement('div');
this.inputContainer.id = 'input-container';
this.inputContainer.append(this.input, this.sendButton);
this.container.append(this.messageArea, this.inputContainer);
document.body.appendChild(this.container);
}
createMessageArea() {
const div = document.createElement('div');
Object.assign(div.style, {
height: 'calc(100% - 120px)',
padding: '16px',
overflowY: 'auto',
color: 'var(--chat-text)'
});
div.id = 'chat-messages';
return div;
}
createInput() {
const input = document.createElement('textarea');
input.id = 'chat-input';
input.placeholder = '输入消息(Ctrl + Enter 发送)';
return input;
}
createButton() {
const button = document.createElement('button');
button.id = 'chat-send-button';
button.textContent = '发送';
return button;
}
// 新增IP获取方法
async getClientIP() {
try {
// 备选方案:使用第三方IP查询
const { ip } = await fetch('https://api.ipify.org?format=json').then(r => r.json());
return ip;
} catch (error) {
// 备选方案
console.log('获取IP失败', error);
return '0.0.0.0';
}
}
/**
* 初始化用户:检查登录状态,若无则匿名登录
*/
async initializeUser() {
try {
// GM_getValue 实现跨域一致性
this.userId = await GM_getValue('user_id');
if (this.userId) {
console.log('===已存在用户ID===', this.userId);
//GM_deleteValue('user_id');//仅测试
return;
}
else {
// 匿名登录
const { data, error } = await this.supabase.auth.signInAnonymously({
options: {
data: {
ip: await this.getClientIP(),
device_info: {
screen_resolution: `${screen.width}x${screen.height}`,
color_depth: screen.colorDepth + 'bit',
preferred_language: navigator.language,
timezone_offset: new Date().getTimezoneOffset() / 60,
hardware_concurrency: navigator.hardwareConcurrency || 'unknown',
os_platform: navigator.platform,
user_agent: navigator.userAgent.substring(0, 100)
}
}
}
});
console.log('===注册匿名用户===', data, error);
if (error) throw error;
this.userId = data.session.user.id;
GM_setValue('user_id', this.userId);
}
} catch (error) {
console.error('用户初始化失败:', error);
//alert('无法连接到聊天服务器,请刷新页面重试');
}
}
/**
* 设置实时通信:消息和在线状态
*/
async setupRealtime() {
// 统一通信频道(集成消息+在线状态)
this.messageChannel = this.supabase.channel('chat-room', {
config: {
presence: {
key: this.userId,
heartbeatInterval: 15,
statusTTL: 60
}
}
})
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'messages'
}, payload => this.addMessage(payload.new))
.on('presence', { event: 'sync' }, () => {
try {
const states = this.messageChannel.presenceState();
const onlineCount = Object.values(states).length;
this.updateOnlineCount(onlineCount);
} catch (e) {
console.error('[Presence状态同步异常]', e);
}
})
.subscribe();
// 跟踪用户在线状态
await this.messageChannel.track({
user_id: this.userId,
online_at: new Date().toISOString()
});
}
addMessage(message) {
//if (message.domain !== location.host) return; // 过滤非法消息
const isOwn = message.user_id === this.userId;
const msgElement = document.createElement('div');
// 智能内容解析与样式优化
// 消息气泡渲染组件
const renderMessageBubble = (message, isOwn) => {
const userName = message.user_id.split('-')[0] || '匿名用户';
const timeStr = new Date(message.created_at).toLocaleString('zh-CN', {
hour12: false,
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).replace(/(\d+)\/(\d+), (\d+:\d+)/, '$2-$3 $4');
return `
<div style="
margin: 8px 0;
padding: 2px 5px;
background: ${isOwn ? '#1A73E8' : '#2D2D2D'};
border-radius: 6px;
color: ${isOwn ? '#FFF' : '#E0E0E0'};
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
max-width: 100%;
align-self: ${isOwn ? 'flex-end' : 'flex-start'};">
<div style="
font-size: 0.85em;
color: ${isOwn ? 'rgba(255,255,255,0.7)' : 'rgba(224,224,224,0.7)'};
margin-bottom: 4px;">
${userName} • ${timeStr}
</div>
${createMessageContent(message)}
</div>
`;
};
// 多媒体内容解析器
const createMessageContent = (message) => {
const content = message.content;
const mediaPattern = /(https?:\/\/.*\.(?:png|jpg|gif|mp4|m3u8|webm|mp3))\b/gi;
const elements = [];
content.split('\n').forEach(text => {
let remaining = text;
let match;
while ((match = mediaPattern.exec(text)) !== null) {
const media_id = `msg_${message.id}-media_${match.index}`;
const [url] = match;
const prefix = remaining.slice(0, match.index);
if (prefix) elements.push(`<div>${prefix}</div>`);
let mediaTag = null;
if (url.match(/\.(png|jpg|gif)$/i)) {
mediaTag = `<img src="${url}?ts=${Date.now()}"
referrerpolicy="no-referrer-when-downgrade"
style="max-width: min(300px, 100%); border-radius: 4px; margin: 8px 0;">`;
}
else if (url.match(/\.(mp3)$/i)) {
mediaTag = `<audio controls style="width: 100%; margin: 8px 0;" src="${url}"></audio>`;
}
else if (url.match(/\.(mp4|webm)$/i)) {
mediaTag = `<video controls style="max-width: 100%; border-radius: 8px; margin: 8px 0;" id="${media_id}" src="${url}"></video>`;
}
else if (url.match(/\.(m3u8)$/i)) {
mediaTag = `<video controls style="max-width: 100%; border-radius: 8px; margin: 8px 0;"
id="${media_id}"
data-hls-src="${url}"
data-hls-observer="pending"></video>`;
}
elements.push(mediaTag);
remaining = remaining.slice(match.index + url.length);
}
if (remaining) elements.push(`<div>${remaining}</div>`);
});
return elements.join('');
};
// 消息渲染异常防御机制
try {
//const messageContainer = document.querySelector('#message-container');
//console.assert(messageContainer, '消息容器未找到');
const bubbleHTML = renderMessageBubble(message, isOwn);
if (typeof bubbleHTML === 'string' && bubbleHTML.length > 0) {
msgElement.innerHTML = bubbleHTML;
} else {
console.error('消息渲染异常:', { message, isOwn });
msgElement.innerHTML = `<div class="error-message">消息渲染失败</div>`;
}
} catch (e) {
console.error('消息加载失败:', e);
GM_notification({
title: '系统错误',
text: `消息加载失败: ${e.message}`,
timeout: 5000
});
}
this.messageArea.appendChild(msgElement);
// Initialize HLS for video elements
msgElement.querySelectorAll('video[data-hls-src]').forEach(video => {
const hlsSrc = video.dataset.hlsSrc;
HlsPlayer.init(video, hlsSrc);
});
this.scrollToBottom();
}
scrollToBottom() {
this.messageArea.scrollTo({
top: this.messageArea.scrollHeight,
behavior: 'smooth'
});
}
async loadHistory() {
try {
const { data, error } = await this.supabase
.from('messages')
.select('*')
.order('created_at', { ascending: false })
.limit(20);
if (error) throw error;
if (!data || data.length === 0) return;
const fragment = document.createDocumentFragment();
data.reverse().forEach(msg => { this.addMessage(msg) });
this.messageArea.appendChild(fragment);
this.scrollToBottom();
} catch (error) {
console.error('加载消息历史失败:', error);
}
}
/**
* 更新在线人数显示
* @param {number} count - 当前在线用户数量
*/
updateOnlineCount(count) {
const counter = document.getElementById('online-users');
counter.textContent = count;
counter.style.fontWeight = count > 0 ? '600' : '400';
}
async cleanup() {
// 取消所有频道订阅
if (this.messageChannel) this.supabase.removeChannel(this.messageChannel);
}
/**
* 清理资源和状态
*/
async sendMessage() {
const content = this.input.value.trim();
if (!content) return;
// 防刷机制(3秒间隔)
if (this.lastSendTime && Date.now() - this.lastSendTime < 3000) {
alert('发送过于频繁,请稍后再试');
return;
}
try {
const { error } = await this.supabase
.from('messages')
.insert({
content,
user_id: this.userId,
domain: location.host // 自动注入当前域名
});
if (!error) {
this.input.value = '';
this.lastSendTime = Date.now();
} else {
console.error('消息发送失败:', error);
//alert('消息发送失败: ' + error.message);
}
} catch (error) {
console.error('消息发送失败:', error);
//alert('消息发送失败: ' + error.message);
}
}
}
// 主初始化流程
(async () => {
StyleManager.inject();
const checkInitialization = setInterval(async () => {
if (window.supabase) {
clearInterval(checkInitialization);
const client = await SupabaseClient.initialize();
new ChatRoom(client);
}
}, 500);
})();
})();