/**
* 一起听功能模块
* 与AI角色一起听歌聊天
*/
import { getSettings, splitAIMessages } from './config.js';
import { currentChatIndex } from './chat.js';
import { requestSave } from './save-manager.js';
import { refreshChatList } from './ui.js';
import { searchMusic, playMusic, togglePlay, getCurrentSong, formatDuration } from './music.js';
import { showToast } from './toast.js';
import { escapeHtml, sleep } from './utils.js';
// ========== SVG 图标 ==========
const LISTEN_ICON = '';
const BACK_ICON = '';
const SEARCH_ICON = '';
const PLAY_ICON = '';
const PAUSE_ICON = '';
const PREV_ICON = '';
const NEXT_ICON = '';
const CHAT_ICON = '';
const PLAYLIST_ICON = '';
const SEND_ICON = '';
const CLOSE_ICON = '';
const HEART_ICON = '';
const LOOP_ICON = '';
// ========== 状态管理 ==========
let listenState = {
isActive: false,
isConnected: false,
currentSong: null,
messages: [],
contact: null,
contactIndex: -1,
startTime: null,
isPlaying: false,
connectTimeout: null,
dotsInterval: null,
chatVisible: false,
audioElement: null,
progressInterval: null,
pauseTimeout: null, // 暂停后自动播放下一首的计时器
};
// 导出图标供其他模块使用
export { LISTEN_ICON };
// ========== 页面显示/隐藏 ==========
/**
* 显示一起听搜索页面
*/
export function showListenSearchPage() {
const page = document.getElementById('wechat-listen-search-page');
if (page) {
page.classList.remove('hidden');
// 聚焦输入框
setTimeout(() => {
const input = document.getElementById('wechat-listen-search-input');
if (input) input.focus();
}, 100);
}
}
/**
* 隐藏一起听搜索页面
*/
export function hideListenSearchPage() {
const page = document.getElementById('wechat-listen-search-page');
if (page) page.classList.add('hidden');
}
/**
* 显示等待页面
*/
function showWaitingPage(song, contact) {
const page = document.getElementById('wechat-listen-waiting-page');
if (!page) return;
const settings = getSettings();
// 调试日志
console.log('[一起听等待页面] 数据检查:', {
userAvatar: settings.userAvatar,
contactAvatar: contact.avatar,
songCover: song.cover,
contactName: contact.name
});
// 小图显示用户头像
const avatarEl = document.getElementById('wechat-listen-waiting-avatar');
if (avatarEl) {
// 先清除旧内容
avatarEl.innerHTML = '';
if (settings.userAvatar) {
avatarEl.innerHTML = ``;
} else {
avatarEl.textContent = (settings.userName || 'User').charAt(0);
}
}
// 大图显示角色头像(带雷达动画的)
const coverEl = document.getElementById('wechat-listen-waiting-cover');
if (coverEl) {
// 先清除旧值
coverEl.src = '';
coverEl.style.background = '';
if (contact.avatar) {
coverEl.src = contact.avatar;
} else {
// 如果没有头像,用纯色背景
coverEl.style.background = '#333';
}
}
// 设置角色名
const nameEl = document.getElementById('wechat-listen-waiting-name');
if (nameEl) {
nameEl.textContent = contact.name || 'TA';
}
page.classList.remove('hidden');
}
/**
* 隐藏等待页面
*/
function hideWaitingPage() {
const page = document.getElementById('wechat-listen-waiting-page');
if (page) page.classList.add('hidden');
clearInterval(listenState.dotsInterval);
}
/**
* 显示一起听主页面
*/
function showListenTogetherPage() {
const page = document.getElementById('wechat-listen-together-page');
if (!page) return;
const settings = getSettings();
const contact = listenState.contact;
const song = listenState.currentSong;
// 设置用户头像
const userAvatarEl = document.getElementById('wechat-listen-user-avatar');
if (userAvatarEl) {
if (settings.userAvatar) {
userAvatarEl.innerHTML = `
`;
} else {
userAvatarEl.textContent = (settings.userName || 'User').charAt(0);
}
}
// 设置AI头像
const aiAvatarEl = document.getElementById('wechat-listen-ai-avatar');
if (aiAvatarEl) {
const firstChar = contact.name ? contact.name.charAt(0) : '?';
if (contact.avatar) {
aiAvatarEl.innerHTML = `
`;
} else {
aiAvatarEl.textContent = firstChar;
}
}
// 设置歌曲信息
const coverEl = document.getElementById('wechat-listen-cover');
const nameEl = document.getElementById('wechat-listen-song-name');
const artistEl = document.getElementById('wechat-listen-song-artist');
if (coverEl && song.cover) coverEl.src = song.cover;
if (nameEl) nameEl.textContent = song.name || '未知歌曲';
if (artistEl) artistEl.textContent = song.artist || '未知歌手';
// 初始化播放按钮状态
updatePlayButton();
page.classList.remove('hidden');
bindListenEvents();
}
/**
* 隐藏一起听主页面
*/
function hideListenTogetherPage() {
const page = document.getElementById('wechat-listen-together-page');
if (page) page.classList.add('hidden');
}
// ========== 核心逻辑 ==========
/**
* 开始一起听
* @param {Object} song - 歌曲信息
* @param {number} contactIndex - 联系人索引
*/
export async function startListenTogether(song, contactIndex = currentChatIndex) {
if (listenState.isActive) return;
if (contactIndex < 0) {
showToast('请先选择聊天对象');
return;
}
const settings = getSettings();
const contact = settings.contacts[contactIndex];
if (!contact) {
showToast('联系人不存在');
return;
}
// 初始化状态
listenState = {
isActive: true,
isConnected: false,
currentSong: song,
messages: [],
contact: contact,
contactIndex: contactIndex,
startTime: null,
isPlaying: false,
connectTimeout: null,
dotsInterval: null,
chatVisible: false,
audioElement: null,
progressInterval: null,
pauseTimeout: null,
};
// 隐藏搜索页,显示等待页面
hideListenSearchPage();
showWaitingPage(song, contact);
// 开始等待动画
startWaitingAnimation();
// 2-4秒后AI"加入"
const joinDelay = 2000 + Math.random() * 2000;
listenState.connectTimeout = setTimeout(() => {
if (listenState.isActive && !listenState.isConnected) {
onAIJoined();
}
}, joinDelay);
}
/**
* 开始等待动画
*/
function startWaitingAnimation() {
const dotsEl = document.getElementById('wechat-listen-waiting-dots');
if (!dotsEl) return;
let dotCount = 0;
clearInterval(listenState.dotsInterval);
listenState.dotsInterval = setInterval(() => {
dotCount = (dotCount + 1) % 4;
dotsEl.textContent = '.'.repeat(dotCount || 1);
}, 500);
}
/**
* AI加入后
*/
async function onAIJoined() {
listenState.isConnected = true;
listenState.startTime = Date.now();
clearInterval(listenState.dotsInterval);
clearTimeout(listenState.connectTimeout);
// 隐藏等待页面,显示主页面
hideWaitingPage();
showListenTogetherPage();
// 开始播放音乐
await playListenSong();
// AI主动发送第一条消息
await triggerAIGreeting();
}
/**
* 播放当前歌曲
*/
async function playListenSong() {
const song = listenState.currentSong;
if (!song) return;
try {
// 使用music.js的playMusic函数
await playMusic(song.id, song.platform, song.name, song.artist);
listenState.isPlaying = true;
updatePlayButton();
startProgressUpdate();
// 监听歌曲结束事件
const audio = document.getElementById('wechat-music-audio');
if (audio) {
listenState.audioElement = audio;
audio.addEventListener('ended', onSongEnded);
}
} catch (e) {
console.error('[可乐] 一起听播放失败:', e);
showToast('播放失败');
}
}
/**
* 歌曲结束时的处理
*/
async function onSongEnded() {
if (!listenState.isActive) return;
listenState.isPlaying = false;
updatePlayButton();
// 20%几率AI换歌
if (Math.random() < 0.2) {
await aiSelectSong();
}
}
/**
* AI选择歌曲(20%几率触发)
*/
async function aiSelectSong() {
if (!listenState.isConnected || !listenState.contact) return;
try {
const { callListenTogetherAI } = await import('./ai.js');
// 获取最近5条消息
const recentMessages = listenState.messages.slice(-5);
const messagesContext = recentMessages.map(m =>
`${m.role === 'user' ? '用户' : '你'}: ${m.content}`
).join('\n');
// 构建AI选歌的prompt
const prompt = `[这首歌播放完了,请你选择下一首想听的歌,根据你们刚才的聊天氛围和你的喜好来选。
最近的对话:
${messagesContext || '(刚开始听歌)'}
请回复格式:
1. 先说一句为什么想听这首歌(简短自然,1-2句话)
2. 然后用 [换歌:歌名] 格式选择歌曲
示例:突然想听点轻快的|||[换歌:晴天]]`;
showListenTypingIndicator();
const aiResponse = await callListenTogetherAI(listenState.contact, prompt, recentMessages, listenState.currentSong);
hideListenTypingIndicator();
if (aiResponse) {
// 处理回复
const parts = splitAIMessages(aiResponse);
for (const part of parts) {
const text = filterListenMessage(part);
// 检查是否包含换歌标签
const changeSongMatch = text.match(/\[换歌[::]\s*(.+?)\]/);
if (changeSongMatch) {
const songKeyword = changeSongMatch[1].trim();
// 显示AI的说明文字(去掉换歌标签)
const displayText = text.replace(/\[换歌[::][^\]]*\]/g, '').trim();
if (displayText) {
addListenMessage('ai', displayText);
}
// 搜索并播放新歌
await changeSongByKeyword(songKeyword, true);
break;
} else if (text) {
addListenMessage('ai', text);
}
}
}
} catch (err) {
console.error('[可乐] AI选歌失败:', err);
hideListenTypingIndicator();
}
}
/**
* 根据关键词换歌
*/
async function changeSongByKeyword(keyword, isAIChange = false) {
try {
const results = await searchMusic(keyword);
if (results && results.length > 0) {
const newSong = results[0];
listenState.currentSong = newSong;
// 更新界面
const coverEl = document.getElementById('wechat-listen-cover');
const nameEl = document.getElementById('wechat-listen-song-name');
const artistEl = document.getElementById('wechat-listen-song-artist');
if (coverEl) coverEl.src = newSong.cover || '';
if (nameEl) nameEl.textContent = newSong.name || '未知歌曲';
if (artistEl) artistEl.textContent = newSong.artist || '未知歌手';
// 播放新歌
await playListenSong();
// 如果不是AI换的歌,通知AI对换歌做出反应
if (!isAIChange) {
await triggerAISongChangeReaction(newSong);
}
} else {
showToast('未找到歌曲');
}
} catch (e) {
console.error('[可乐] 换歌失败:', e);
showToast('换歌失败');
}
}
/**
* AI对用户换歌的反应
*/
async function triggerAISongChangeReaction(newSong) {
if (!listenState.isConnected || !listenState.contact) return;
try {
const { callListenTogetherAI } = await import('./ai.js');
const prompt = `[用户换了一首歌,新歌是《${newSong.name}》- ${newSong.artist}。请对换歌做出反应,表达你对这首歌的看法或感受。记得发送2-4条消息,每条换行分隔]`;
showListenTypingIndicator();
const aiResponse = await callListenTogetherAI(
listenState.contact,
prompt,
listenState.messages.slice(-5),
newSong
);
hideListenTypingIndicator();
await processAIResponse(aiResponse);
} catch (err) {
hideListenTypingIndicator();
console.error('[可乐] AI换歌反应失败:', err);
}
}
/**
* AI主动发送开场消息
*/
async function triggerAIGreeting() {
if (!listenState.isConnected || !listenState.contact) return;
showListenTypingIndicator();
try {
const { callListenTogetherAI } = await import('./ai.js');
const song = listenState.currentSong;
const prompt = `[用户邀请你一起听歌,歌曲是《${song.name}》- ${song.artist},你刚刚加入了一起听。请用你的方式自然地打个招呼,并对这首歌发表一些看法。记得发送2-4条消息,每条换行分隔,像真实聊天一样有层次感]`;
const aiResponse = await callListenTogetherAI(
listenState.contact,
prompt,
[],
song
);
hideListenTypingIndicator();
await processAIResponse(aiResponse);
} catch (err) {
hideListenTypingIndicator();
console.error('[可乐] 一起听AI开场白失败:', err);
}
}
/**
* 过滤消息 - 只允许纯文字,过滤所有特殊格式
*/
function filterListenMessage(text) {
if (!text) return '';
let reply = text.trim();
// 过滤 meme 表情包
reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
// 过滤 [表情:xxx]
reply = reply.replace(/\[表情[::][^\]]*\]/g, '').trim();
// 过滤 [照片:xxx]
reply = reply.replace(/\[照片[::][^\]]*\]/g, '').trim();
// 过滤 [语音:xxx]
reply = reply.replace(/\[语音[::][^\]]*\]/g, '').trim();
// 过滤 [音乐:xxx](但保留[换歌:xxx])
reply = reply.replace(/\[(?:分享)?音乐[::][^\]]*\]/g, '').trim();
// 过滤 [回复:xxx] 引用格式
reply = reply.replace(/\[回复[::][^\]]*\]/g, '').trim();
// 过滤中文小括号内容(动作/语气描述)
reply = reply.replace(/([^)]*)/g, '').trim();
// 过滤英文小括号内容
reply = reply.replace(/\([^)]*\)/g, '').trim();
return reply;
}
/**
* 处理AI回复 - 纯文字消息
*/
async function processAIResponse(aiResponse) {
if (!aiResponse) return;
const parts = splitAIMessages(aiResponse);
for (const part of parts) {
if (!listenState.isConnected) break;
let reply = filterListenMessage(part);
if (!reply) continue;
// 直接发送纯文字消息
showListenTypingIndicator();
await sleep(400 + Math.random() * 600);
hideListenTypingIndicator();
if (listenState.isConnected) {
addListenMessage('ai', reply);
}
}
}
/**
* 用户发送消息
*/
async function sendListenMessage() {
const input = document.getElementById('wechat-listen-input-text');
if (!input) return;
const message = input.value.trim();
if (!message || !listenState.isConnected) return;
input.value = '';
// 显示用户消息
addListenMessage('user', message);
// 显示typing
showListenTypingIndicator();
try {
const { callListenTogetherAI } = await import('./ai.js');
const song = listenState.currentSong;
const aiResponse = await callListenTogetherAI(
listenState.contact,
message,
listenState.messages.slice(0, -1),
song
);
hideListenTypingIndicator();
await processAIResponse(aiResponse);
} catch (err) {
hideListenTypingIndicator();
console.error('[可乐] 一起听消息回复失败:', err);
}
}
// ========== UI 更新 ==========
/**
* 显示typing指示器
*/
function showListenTypingIndicator() {
const messagesEl = document.getElementById('wechat-listen-messages');
if (!messagesEl) return;
messagesEl.classList.remove('hidden');
hideListenTypingIndicator();
const typingDiv = document.createElement('div');
typingDiv.className = 'wechat-listen-msg ai';
typingDiv.id = 'wechat-listen-typing';
typingDiv.innerHTML = `