Add files via upload

This commit is contained in:
Cola-Echo
2025-12-23 01:19:53 +08:00
committed by GitHub
parent 1e1bf1bab2
commit 37e172bfa9
31 changed files with 10783 additions and 1041 deletions

123
ai.js
View File

@@ -3,7 +3,7 @@
*/ */
import { getContext } from '../../../extensions.js'; import { getContext } from '../../../extensions.js';
import { getSettings, getUserStickers, MEME_PROMPT_TEMPLATE } from './config.js'; import { getSettings, getUserStickers, MEME_PROMPT_TEMPLATE, LISTEN_TOGETHER_PROMPT_TEMPLATE } from './config.js';
import { sleep } from './utils.js'; import { sleep } from './utils.js';
function normalizeApiBaseUrl(url) { function normalizeApiBaseUrl(url) {
@@ -385,6 +385,7 @@ function buildStickerPrompt(settings) {
可用表情(共${stickers.length}个):${stickerList}${stickers.length > 30 ? '...' : ''} 可用表情(共${stickers.length}个):${stickerList}${stickers.length > 30 ? '...' : ''}
- 表情消息必须单独一条,用 ||| 分隔 - 表情消息必须单独一条,用 ||| 分隔
- 适度使用,不要每条都发表情 - 适度使用,不要每条都发表情
- 【绝对禁止】只能使用上面列表中的名称或序号!必须完全一致!禁止自己编造、修改、添加后缀!
示例:好的呀|||[表情:开心] 示例:好的呀|||[表情:开心]
`; `;
} }
@@ -667,7 +668,16 @@ export function buildMessages(contact, userMessage) {
}); });
}); });
messages.push({ role: 'user', content: userMessage }); // 检查是否需要添加用户消息(避免重复)
// 如果最后一条消息已经是相同的用户消息,就不再重复添加
const lastAddedMsg = messages[messages.length - 1];
const isAlreadyAdded = lastAddedMsg &&
lastAddedMsg.role === 'user' &&
lastAddedMsg.content === userMessage;
if (!isAlreadyAdded) {
messages.push({ role: 'user', content: userMessage });
}
return messages; return messages;
} }
@@ -958,7 +968,7 @@ export async function callVideoAI(contact, userMessage, callMessages = [], initi
- 一般输出2-4句话 - 一般输出2-4句话
- 用小括号描述画面场景,这是用户看到的视频画面 - 用小括号描述画面场景,这是用户看到的视频画面
- 【禁止】括号内不准使用任何人称代词(你、我、她、他)!这是摄像头视角的画面描述! - 【禁止】括号内不准使用任何人称代词(你、我、她、他)!这是摄像头视角的画面描述!
- 【禁止】视频通话中不要使用任何表情包格式 [表情:xxx],直接说话和描述动作即可 - 【禁止】视频通话中不要使用任何表情包格式,包括 [表情:xxx] 和 <meme>xxx</meme>,直接说话和描述动作即可
- 括号内只描述画面:人物动作、表情、背景、光线等 - 括号内只描述画面:人物动作、表情、背景、光线等
【正确示例 - 注意 ||| 分隔符】 【正确示例 - 注意 ||| 分隔符】
@@ -987,7 +997,7 @@ export async function callVideoAI(contact, userMessage, callMessages = [], initi
- 一般输出2-4句话 - 一般输出2-4句话
- 用小括号描述画面场景,这是用户看到的视频画面 - 用小括号描述画面场景,这是用户看到的视频画面
- 【禁止】括号内不准使用任何人称代词(你、我、她、他)!这是摄像头视角的画面描述! - 【禁止】括号内不准使用任何人称代词(你、我、她、他)!这是摄像头视角的画面描述!
- 【禁止】视频通话中不要使用任何表情包格式 [表情:xxx],直接说话和描述动作即可 - 【禁止】视频通话中不要使用任何表情包格式,包括 [表情:xxx] 和 <meme>xxx</meme>,直接说话和描述动作即可
- 括号内只描述画面:人物动作、表情、背景、光线等 - 括号内只描述画面:人物动作、表情、背景、光线等
【正确示例 - 注意 ||| 分隔符】 【正确示例 - 注意 ||| 分隔符】
@@ -1082,3 +1092,108 @@ ${videoCallPrompt}`;
const data = await response.json(); const data = await response.json();
return data.choices?.[0]?.message?.content || '...'; return data.choices?.[0]?.message?.content || '...';
} }
// 一起听场景中调用 AI使用专门的一起听提示词只允许纯文字回复
export async function callListenTogetherAI(contact, userMessage, listenMessages = [], song = null) {
// 获取 API 配置
let apiUrl, apiKey, apiModel;
if (contact.useCustomApi) {
apiUrl = contact.customApiUrl || '';
apiKey = contact.customApiKey || '';
apiModel = contact.customModel || '';
const globalConfig = getApiConfig();
if (!apiUrl) apiUrl = globalConfig.url;
if (!apiKey) apiKey = globalConfig.key;
if (!apiModel) apiModel = globalConfig.model;
} else {
const globalConfig = getApiConfig();
apiUrl = globalConfig.url;
apiKey = globalConfig.key;
apiModel = globalConfig.model;
}
if (!apiUrl) {
throw new Error('请先配置 API 地址');
}
if (!apiModel) {
throw new Error('请先选择模型');
}
// 构建一起听专用的提示词(替换歌曲信息占位符)
let listenPrompt = LISTEN_TOGETHER_PROMPT_TEMPLATE
.replace('{{song_name}}', song?.name || '未知歌曲')
.replace('{{song_artist}}', song?.artist || '未知歌手');
// 构建系统提示词(在原有角色设定基础上添加一起听场景,禁用表情包/音乐分享/通话请求)
const baseSystemPrompt = buildSystemPrompt(contact, { allowStickers: false, allowMusicShare: false, allowCallRequests: false });
const systemPrompt = `${baseSystemPrompt}
【当前场景:一起听歌中】
${listenPrompt}`;
// 构建消息
const messages = [{ role: 'system', content: systemPrompt }];
// 添加聊天历史记录最近10条
const chatHistory = contact.chatHistory || [];
const recentHistory = chatHistory.slice(-10);
recentHistory.forEach(msg => {
if (msg.isRecalled) {
messages.push({
role: msg.role === 'user' ? 'user' : 'assistant',
content: '[用户撤回了一条消息]'
});
return;
}
messages.push({
role: msg.role === 'user' ? 'user' : 'assistant',
content: msg.content
});
});
// 添加一起听开始标记
messages.push({ role: 'user', content: `[用户邀请你一起听歌:《${song?.name || '未知歌曲'}》- ${song?.artist || '未知歌手'}]` });
// 添加一起听中的历史消息
listenMessages.forEach(msg => {
messages.push({
role: msg.role === 'user' ? 'user' : 'assistant',
content: msg.content
});
});
// 添加当前消息
messages.push({ role: 'user', content: userMessage });
const chatUrl = apiUrl.replace(/\/+$/, '') + '/chat/completions';
const headers = { 'Content-Type': 'application/json' };
if (apiKey) {
headers['Authorization'] = `Bearer ${apiKey}`;
}
const response = await fetchWithRetry(
chatUrl,
{
method: 'POST',
headers: headers,
body: JSON.stringify({
model: apiModel,
messages: messages,
temperature: 0.9,
max_tokens: 1024
})
},
{ maxRetries: 3 }
);
if (!response.ok) {
throw new Error(await formatApiError(response, {}));
}
const data = await response.json();
return data.choices?.[0]?.message?.content || '...';
}

View File

@@ -3,7 +3,7 @@
* 支持每个联系人独立设置背景,含图片裁剪功能 * 支持每个联系人独立设置背景,含图片裁剪功能
*/ */
import { saveSettingsDebounced } from '../../../../script.js'; import { requestSave } from './save-manager.js';
import { getSettings } from './config.js'; import { getSettings } from './config.js';
import { showToast } from './toast.js'; import { showToast } from './toast.js';
import { currentChatIndex } from './chat.js'; import { currentChatIndex } from './chat.js';
@@ -375,7 +375,7 @@ function saveChatBackground(imageData) {
} }
contact.chatBackground = imageData; contact.chatBackground = imageData;
saveSettingsDebounced(); requestSave();
// 立即应用背景 // 立即应用背景
applyChatBackground(imageData); applyChatBackground(imageData);
@@ -390,7 +390,7 @@ function clearChatBackground() {
if (!contact) return; if (!contact) return;
delete contact.chatBackground; delete contact.chatBackground;
saveSettingsDebounced(); requestSave();
// 清除背景 // 清除背景
applyChatBackground(null); applyChatBackground(null);

View File

@@ -9,10 +9,13 @@ import { isInGroupChat, sendGroupMessage, sendGroupPhotoMessage, sendGroupBatchM
import { startVoiceCall } from './voice-call.js'; import { startVoiceCall } from './voice-call.js';
import { startVideoCall } from './video-call.js'; import { startVideoCall } from './video-call.js';
import { showMusicPanel, initMusicEvents } from './music.js'; import { showMusicPanel, initMusicEvents } from './music.js';
import { showRedPacketPage } from './red-packet.js';
import { showTransferPage } from './transfer.js';
import { getSettings, splitAIMessages } from './config.js'; import { getSettings, splitAIMessages } from './config.js';
import { refreshChatList } from './ui.js'; import { refreshChatList } from './ui.js';
import { saveSettingsDebounced } from '../../../../script.js'; import { requestSave } from './save-manager.js';
import { callAI } from './ai.js'; import { callAI } from './ai.js';
import { showListenSearchPage, initListenTogether } from './listen-together.js';
let expandMode = null; // 'voice' | 'multi' | null let expandMode = null; // 'voice' | 'multi' | null
// 混合消息项: { type: 'text' | 'voice' | 'sticker' | 'photo', content: string } // 混合消息项: { type: 'text' | 'voice' | 'sticker' | 'photo', content: string }
@@ -106,7 +109,7 @@ function initMusicShareListener() {
groupChat.lastMessage = `[音乐] ${name}`; groupChat.lastMessage = `[音乐] ${name}`;
groupChat.lastMessageTime = Date.now(); groupChat.lastMessageTime = Date.now();
saveSettingsDebounced(); requestSave();
refreshChatList(); refreshChatList();
// 获取成员信息 // 获取成员信息
@@ -154,7 +157,7 @@ function initMusicShareListener() {
groupChat.lastMessageTime = Date.now(); groupChat.lastMessageTime = Date.now();
} }
saveSettingsDebounced(); requestSave();
refreshChatList(); refreshChatList();
} catch (err) { } catch (err) {
hideGroupTypingIndicator(); hideGroupTypingIndicator();
@@ -189,7 +192,7 @@ function initMusicShareListener() {
}); });
contact.lastMessage = `[音乐] ${name}`; contact.lastMessage = `[音乐] ${name}`;
saveSettingsDebounced(); requestSave();
refreshChatList(); refreshChatList();
// 调用AI回复 // 调用AI回复
@@ -235,7 +238,7 @@ function initMusicShareListener() {
if (lastShownMessage) { if (lastShownMessage) {
contact.lastMessage = lastShownMessage.length > 20 ? lastShownMessage.substring(0, 20) + '...' : lastShownMessage; contact.lastMessage = lastShownMessage.length > 20 ? lastShownMessage.substring(0, 20) + '...' : lastShownMessage;
} }
saveSettingsDebounced(); requestSave();
refreshChatList(); refreshChatList();
} }
} catch (err) { } catch (err) {
@@ -544,7 +547,7 @@ export async function sendExpandContent() {
const content = textarea?.value.trim(); const content = textarea?.value.trim();
if (!content) { if (!content) {
showToast('请输入语音内容', '🧊'); showToast('请输入语音内容', 'info');
return; return;
} }
@@ -562,7 +565,7 @@ export async function sendExpandContent() {
const content = textarea?.value.trim(); const content = textarea?.value.trim();
if (!content) { if (!content) {
showToast('请输入照片描述', '🧊'); showToast('请输入照片描述', 'info');
return; return;
} }
@@ -585,7 +588,7 @@ export async function sendExpandContent() {
}); });
if (validMessages.length === 0) { if (validMessages.length === 0) {
showToast('请至少输入一条消息', '🧊'); showToast('请至少输入一条消息', 'info');
return; return;
} }
@@ -656,8 +659,39 @@ function handleFuncItemClick(func) {
hideFuncPanel(); hideFuncPanel();
showMusicPanel(); showMusicPanel();
return; return;
case 'redpacket':
hideFuncPanel();
if (isInGroupChat()) {
// 群聊红包 - 动态导入
import('./group-red-packet.js').then(m => m.showGroupRedPacketTypePage());
} else {
showRedPacketPage();
}
return;
case 'transfer':
hideFuncPanel();
if (isInGroupChat()) {
// 群聊转账 - 先选择成员
import('./group-red-packet.js').then(m => m.showGroupTransferSelectPage());
} else {
showTransferPage();
}
return;
case 'time':
hideFuncPanel();
showTimePicker();
return;
case 'listen':
hideFuncPanel();
// 群聊不支持一起听
if (isInGroupChat()) {
showToast('群聊暂不支持一起听', 'info');
return;
}
showListenSearchPage();
return;
default: default:
showToast('该功能开发中...', '🧊'); showToast('该功能开发中...', 'info');
} }
} }
@@ -724,4 +758,194 @@ export function initFuncPanel() {
// 初始化音乐面板事件 // 初始化音乐面板事件
initMusicEvents(); initMusicEvents();
initMusicShareListener(); initMusicShareListener();
initTimePickerEvents();
initListenTogether();
}
// ============ 时间选择器相关 ============
// 存储选择的时间null 表示使用当前时间)
let selectedTime = null;
let timePickerInited = false;
// 时间选择器当前选中的值
let pickerValues = {
year: new Date().getFullYear(),
month: new Date().getMonth() + 1,
day: new Date().getDate(),
hour: new Date().getHours(),
minute: new Date().getMinutes(),
second: new Date().getSeconds()
};
// 获取选择的时间(供 chat.js 使用)
export function getSelectedTime() {
return selectedTime;
}
// 清除选择的时间
export function clearSelectedTime() {
selectedTime = null;
updateTimeIndicator();
}
// 显示时间选择器
function showTimePicker() {
const picker = document.getElementById('wechat-time-picker');
if (!picker) return;
// 初始化为当前时间
const now = new Date();
pickerValues = {
year: now.getFullYear(),
month: now.getMonth() + 1,
day: now.getDate(),
hour: now.getHours(),
minute: now.getMinutes(),
second: now.getSeconds()
};
renderTimePickerColumns();
updateTimePickerDisplay();
picker.classList.remove('hidden');
}
// 隐藏时间选择器
function hideTimePicker() {
const picker = document.getElementById('wechat-time-picker');
picker?.classList.add('hidden');
}
// 渲染时间选择器列
function renderTimePickerColumns() {
const currentYear = new Date().getFullYear();
// 年份前后5年
renderPickerColumn('year', currentYear - 5, currentYear + 5, pickerValues.year, '年');
// 月份1-12
renderPickerColumn('month', 1, 12, pickerValues.month, '月');
// 日期:根据年月动态计算
const daysInMonth = new Date(pickerValues.year, pickerValues.month, 0).getDate();
renderPickerColumn('day', 1, daysInMonth, pickerValues.day, '日');
// 小时0-23
renderPickerColumn('hour', 0, 23, pickerValues.hour, '时');
// 分钟0-59
renderPickerColumn('minute', 0, 59, pickerValues.minute, '分');
// 秒0-59
renderPickerColumn('second', 0, 59, pickerValues.second, '秒');
}
// 渲染单个列
function renderPickerColumn(type, min, max, selected, suffix) {
const container = document.getElementById(`wechat-time-picker-${type}`);
if (!container) return;
let html = '';
for (let i = min; i <= max; i++) {
const value = type === 'year' ? i : i.toString().padStart(2, '0');
const isSelected = i === selected;
html += `<div class="wechat-time-picker-item${isSelected ? ' selected' : ''}" data-value="${i}">${value}${suffix}</div>`;
}
container.innerHTML = html;
// 滚动到选中项
setTimeout(() => {
const selectedItem = container.querySelector('.selected');
if (selectedItem) {
container.scrollTop = selectedItem.offsetTop - container.offsetHeight / 2 + selectedItem.offsetHeight / 2;
}
}, 0);
}
// 更新显示的时间
function updateTimePickerDisplay() {
const display = document.getElementById('wechat-time-picker-display');
if (!display) return;
const { year, month, day, hour, minute, second } = pickerValues;
display.textContent = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')} ${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${second.toString().padStart(2, '0')}`;
}
// 更新输入框旁的时间指示器
function updateTimeIndicator() {
let indicator = document.getElementById('wechat-time-indicator');
if (!selectedTime) {
indicator?.remove();
return;
}
if (!indicator) {
const inputArea = document.querySelector('.wechat-chat-input-area');
if (!inputArea) return;
indicator = document.createElement('div');
indicator.id = 'wechat-time-indicator';
indicator.className = 'wechat-time-indicator';
inputArea.insertBefore(indicator, inputArea.firstChild);
}
const date = new Date(selectedTime);
const month = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours().toString().padStart(2, '0');
const minute = date.getMinutes().toString().padStart(2, '0');
indicator.innerHTML = `
<span class="wechat-time-indicator-text">${month}${day}${hour}:${minute}</span>
<button class="wechat-time-indicator-clear" id="wechat-time-indicator-clear">✕</button>
`;
// 绑定清除按钮
document.getElementById('wechat-time-indicator-clear')?.addEventListener('click', (e) => {
e.stopPropagation();
clearSelectedTime();
});
}
// 初始化时间选择器事件
function initTimePickerEvents() {
if (timePickerInited) return;
timePickerInited = true;
// 监听列项点击
document.addEventListener('click', (e) => {
const item = e.target.closest('.wechat-time-picker-item');
if (!item) return;
const column = item.closest('.wechat-time-picker-column');
if (!column) return;
const type = column.dataset.type;
const value = parseInt(item.dataset.value);
// 更新选中值
pickerValues[type] = value;
// 更新选中样式
column.querySelectorAll('.wechat-time-picker-item').forEach(el => {
el.classList.toggle('selected', parseInt(el.dataset.value) === value);
});
// 如果改变了年或月,需要重新渲染日期列
if (type === 'year' || type === 'month') {
const daysInMonth = new Date(pickerValues.year, pickerValues.month, 0).getDate();
if (pickerValues.day > daysInMonth) {
pickerValues.day = daysInMonth;
}
renderPickerColumn('day', 1, daysInMonth, pickerValues.day, '日');
}
updateTimePickerDisplay();
});
// 确认按钮
document.getElementById('wechat-time-picker-confirm')?.addEventListener('click', () => {
const { year, month, day, hour, minute, second } = pickerValues;
selectedTime = new Date(year, month - 1, day, hour, minute, second).getTime();
hideTimePicker();
updateTimeIndicator();
showToast('已设置发送时间', '⏰');
});
} }

1118
chat.js

File diff suppressed because it is too large Load Diff

189
config.js
View File

@@ -1,5 +1,5 @@
/** /**
* 配置、常量、默认设<EFBFBD>? * 配置、常量、默认设
*/ */
import { extension_settings } from '../../../extensions.js'; import { extension_settings } from '../../../extensions.js';
@@ -7,19 +7,19 @@ import { extension_settings } from '../../../extensions.js';
// 插件名称 // 插件名称
export const extensionName = 'wechat-simulator'; export const extensionName = 'wechat-simulator';
// Meme 表情包列表catbox.moe<EFBFBD>? // Meme 表情包列表catbox.moe
export const MEME_STICKERS = [ export const MEME_STICKERS = [
'告到小狗法庭iaordo.jpg', '告到小狗法庭iaordo.jpg',
'小猫伸爪f6nqiq.gif', '小猫伸爪f6nqiq.gif',
'谢谢宝贝我现在那里好<EFBFBD>?62o48.jpg', '谢谢宝贝我现在那里好硬862o48.jpg',
'阿弥陀<EFBFBD>?cwm60.jpg', '阿弥陀佛9cwm60.jpg',
'你好美你长得像我爱人hmpkra.jpg', '你好美你长得像我爱人hmpkra.jpg',
'我老实了i3ws7s.jpg', '我老实了i3ws7s.jpg',
'蹭蹭你贴贴你1of415.gif', '蹭蹭你贴贴你1of415.gif',
'喜欢你egvwqb.jpg', '喜欢你egvwqb.jpg',
'我在哭t343od.jpg', '我在哭t343od.jpg',
'不干活就没饭<EFBFBD>?qnrgh.jpg', '不干活就没饭吃2qnrgh.jpg',
'擦眼<EFBFBD>?gno7e.jpg', '擦眼泪9gno7e.jpg',
'小狗摇尾巴hmdj2k.gif', '小狗摇尾巴hmdj2k.gif',
'爱你舔舔你ola7gd.jpg', '爱你舔舔你ola7gd.jpg',
'不高兴x6lv1t.jpg', '不高兴x6lv1t.jpg',
@@ -47,7 +47,7 @@ export const MEME_STICKERS = [
'目移69jgvg.jpg', '目移69jgvg.jpg',
'上钩了cormmk.jpg', '上钩了cormmk.jpg',
'无语了我哭了0awxky.jpg', '无语了我哭了0awxky.jpg',
'你嫌我丢<EFBFBD>?d71mm.jpg', '你嫌我丢人8d71mm.jpg',
'笑不出来xkop14.jpg', '笑不出来xkop14.jpg',
'别欺负小狗啊u4t3t3.jpg', '别欺负小狗啊u4t3t3.jpg',
'他妈的真是被看扁了ime5rz.jpg', '他妈的真是被看扁了ime5rz.jpg',
@@ -67,7 +67,7 @@ export const MEME_STICKERS = [
'失望eug1e6.jpeg', '失望eug1e6.jpeg',
'狂犬病发作xb3naz.jpg', '狂犬病发作xb3naz.jpg',
'我是狗吗ma9azs.jpg', '我是狗吗ma9azs.jpg',
'一笑了<EFBFBD>?llb46.jpg', '一笑了之9llb46.jpg',
'装可怜lcglz1.jpg', '装可怜lcglz1.jpg',
'小狗撒欢6j6y6a.gif', '小狗撒欢6j6y6a.gif',
'狗舔舔esw5e2.gif', '狗舔舔esw5e2.gif',
@@ -88,7 +88,7 @@ export const MEME_STICKERS = [
'被逮捕了uzeywu.jpg', '被逮捕了uzeywu.jpg',
'看呆mqnepo.jpg', '看呆mqnepo.jpg',
'我的理性在远去t9e065.jpg', '我的理性在远去t9e065.jpg',
'偷亲一<EFBFBD>?jgvb1.gif', '偷亲一口1jgvb1.gif',
'震惊v5n2ve.jpg', '震惊v5n2ve.jpg',
'爷怒了49r80k.jpg', '爷怒了49r80k.jpg',
'愤怒伤心e7lr3s.jpg', '愤怒伤心e7lr3s.jpg',
@@ -102,19 +102,19 @@ export const MEME_STICKERS = [
'你太可爱我喜欢你ubhai8.jpg', '你太可爱我喜欢你ubhai8.jpg',
'惊吓tp9uvd.jpg', '惊吓tp9uvd.jpg',
'脸红星星眼dsfs7o.jpg', '脸红星星眼dsfs7o.jpg',
'被揍了哭<EFBFBD>?1x5zq.jpg', '被揍了哭哭81x5zq.jpg',
'嘬嘬fg5gx3.jpg', '嘬嘬fg5gx3.jpg',
'超大声哭<EFBFBD>?86h5v.jpg', '超大声哭哭186h5v.jpg',
'是的主人yvrgdc.jpg' '是的主人yvrgdc.jpg'
]; ];
// Meme 表情包提示词模板 // Meme 表情包提示词模板
export const MEME_PROMPT_TEMPLATE = `##【必须使用】表情包功能 export const MEME_PROMPT_TEMPLATE = `##【必须使用】表情包功能
【重要】你【必须】经常发送表情包每2-3条回复至少发一个表情包<EFBFBD>? 【重要】你【必须】经常发送表情包每2-3条回复至少发一个表情包
使用规则<EFBFBD>? 使用规则
- 表情包【必须】单独一条消息,<EFBFBD>?||| 分隔 - 表情包【必须】单独一条消息,||| 分隔
- 格式<EFBFBD>?meme>文件<EFBFBD>?/meme> - 格式<meme>文件名</meme>
- 只能从下面列表选择,不能编造文件名 - 只能从下面列表选择,不能编造文件名
可用表情包列表: 可用表情包列表:
@@ -124,21 +124,54 @@ ${MEME_STICKERS.join('\n')}
【正确示例】: 【正确示例】:
好想你|||<meme>小狗摇尾巴hmdj2k.gif</meme> 好想你|||<meme>小狗摇尾巴hmdj2k.gif</meme>
哈哈哈笑死|||<meme>小熊跳舞122o4w.gif</meme>|||你太搞笑<EFBFBD>? 哈哈哈笑死|||<meme>小熊跳舞122o4w.gif</meme>|||你太搞笑
<meme>喜欢你egvwqb.jpg</meme>|||我真的好喜欢<EFBFBD>? <meme>喜欢你egvwqb.jpg</meme>|||我真的好喜欢
【错误示<EFBFBD>?- 绝对禁止】: 【错误示- 绝对禁止】:
好想<EFBFBD>?meme>xxx</meme> <EFBFBD>?错误!表情包没有用|||分开 好想你<meme>xxx</meme> 错误!表情包没有用|||分开
<meme>不存在的表情.jpg</meme> <EFBFBD>?错误!编造了不存在的文件<EFBFBD>? <meme>不存在的表情.jpg</meme> 错误!编造了不存在的文件
记住:表情包让聊天更生动,【必须】经常使用!`; 记住:表情包让聊天更生动,【必须】经常使用!`;
// 一起听功能提示词模板
export const LISTEN_TOGETHER_PROMPT_TEMPLATE = `##【一起听歌场景】
你正在和用户一起听歌,用你自己的方式自然地聊天。
当前播放歌曲:{{song_name}} - {{song_artist}}
【核心要求 - 必须遵守】
1. 只能发送纯文字消息,像朋友之间真实聊天一样
2. 保持你的性格特点,用符合你角色设定的方式说话
3. 每次回复请发送2-4条消息用换行分隔让对话更有层次感
4. 可以聊歌曲、聊心情、聊任何话题,自然就好
5. 发表对歌曲的看法时,要结合你的角色性格和经历
【绝对禁止 - 违反会被过滤】
- 禁止使用小括号描述动作或语气xxx
- 禁止 [表情:xxx] [照片:xxx] [语音:xxx] [音乐:xxx]
- 禁止 [回复:xxx] 引用格式
- 禁止 <meme>xxx</meme>
- 禁止任何非文字格式
【换歌格式】
如果想换歌:[换歌:歌名]
【自然聊天示例】
我来了~
这首歌好好听欸
你怎么会想到点这首
或者:
终于等到你邀请我一起听了
这歌我之前也有在听
感觉特别适合现在这个氛围`;
// 默认设置 // 默认设置
export const defaultSettings = { export const defaultSettings = {
darkMode: true, darkMode: true,
/** /**
* 【自动注入提示词<EFBFBD>? * 【自动注入提示词
* 开启后会自动将微信消息格式提示词注入到作者注释中 * 开启后会自动将微信消息格式提示词注入到作者注释中
* 提示词模板见下方 authorNoteTemplate * 提示词模板见下方 authorNoteTemplate
* 如需自定义格式,修改 authorNoteTemplate 即可 * 如需自定义格式,修改 authorNoteTemplate 即可
@@ -151,7 +184,7 @@ export const defaultSettings = {
phonePosition: null, phonePosition: null,
userAvatar: '', userAvatar: '',
wechatId: 'SillyTavern', wechatId: 'SillyTavern',
// 收藏/世界<EFBFBD>?用户设定 // 收藏/世界书/用户设定
selectedLorebooks: [], selectedLorebooks: [],
userPersonas: [], userPersonas: [],
@@ -174,16 +207,20 @@ export const defaultSettings = {
groupSelectedModel: '', groupSelectedModel: '',
groupModelList: [], groupModelList: [],
// 上下文设<EFBFBD>? // 上下文设
contextEnabled: false, contextEnabled: false,
contextLevel: 5, contextLevel: 5,
contextTags: [], contextTags: [],
walletAmount: '5773.89', walletAmount: '5773.89',
// 支付密码6位数字
paymentPassword: '666666',
// 自定义总结模板(留空则使用默认)
customSummaryTemplate: '',
/** /**
* 【哈基米破限<EFBFBD>? * 【哈基米破限
* 针对 Gemini 2.5 Pro 等模型的输出截断问题 * 针对 Gemini 2.5 Pro 等模型的输出截断问题
* 开启后会在系统提示词头部注入一<EFBFBD>?创作沙盒"提示<EFBFBD>? * 开启后会在系统提示词头部注入一段"创作沙盒"提示
* 帮助解除模型的自我审查,让输出更完整 * 帮助解除模型的自我审查,让输出更完整
*/ */
hakimiBreakLimit: false, hakimiBreakLimit: false,
@@ -199,65 +236,65 @@ export const defaultSettings = {
// 错误日志 // 错误日志
errorLogs: [], errorLogs: [],
// 表情(用户添加的单个表情<EFBFBD>? // 表情(用户添加的单个表情
stickers: [], stickers: [],
// 用户表情开<EFBFBD>? // 用户表情开
userStickersEnabled: true, userStickersEnabled: true,
// Meme 表情包功能开<EFBFBD>? // Meme 表情包功能开
memeStickersEnabled: false, memeStickersEnabled: false,
/** /**
* 【群聊提示词注入<EFBFBD>? * 【群聊提示词注入
* 开启后会将 groupAuthorNote 注入到群聊系统提示词<EFBFBD>? * 开启后会将 groupAuthorNote 注入到群聊系统提示词
* 如需自定义群聊格式,修改下方 groupAuthorNote 即可 * 如需自定义群聊格式,修改下方 groupAuthorNote 即可
*/ */
groupAutoInjectPrompt: true, groupAutoInjectPrompt: true,
groupAuthorNote: `[群聊回复格式指南] groupAuthorNote: `[群聊回复格式指南]
这是一个微信群聊场景,你需要扮演群内的角色进行回复<EFBFBD>? 这是一个微信群聊场景,你需要扮演群内的角色进行回复
【核心规则<EFBFBD>? 【核心规则
1. 每个角色只能使用自己的专属设定,不能使用其他角色的设<EFBFBD>? 1. 每个角色只能使用自己的专属设定,不能使用其他角色的设
2. 每个角色只扮演自己,不能代替其他角色说话 2. 每个角色只扮演自己,不能代替其他角色说话
3. 使用 [角色名]: 内容 的格式回<EFBFBD>? 3. 使用 [角色名]: 内容 的格式回
4. 多个角色回复时,<EFBFBD>?||| 分隔 4. 多个角色回复时,||| 分隔
5. 同一角色可以发送多条消息,也用 ||| 分隔 5. 同一角色可以发送多条消息,也用 ||| 分隔
【消息风格<EFBFBD>? 【消息风格
- 每条消息保持简短自然像真实微信聊天一样1-3句话为宜<EFBFBD>? - 每条消息保持简短自然像真实微信聊天一样1-3句话为宜
- 可以使用表情符号增加表现<EFBFBD>? - 可以使用表情符号增加表现
- 保持角色性格,让对话有趣生动 - 保持角色性格,让对话有趣生动
- 角色之间可以互动、吐槽、附和、反驳等 - 角色之间可以互动、吐槽、附和、反驳等
【回复要求<EFBFBD>? 【回复要求
- 根据聊天内容自然判断哪些角色会回复,不需要所有人都说<EFBFBD>? - 根据聊天内容自然判断哪些角色会回复,不需要所有人都说
- 语音消息格式:[角色名]: [语音:内容] - 语音消息格式:[角色名]: [语音:内容]
- 语音消息必须独立发<EFBFBD>? - 语音消息必须独立发
示例<EFBFBD>? 示例
[角色A]: 你说得对|||[角色B]: 我不太同意诶|||[角色A]: 为什么啊<EFBFBD>? [角色A]: 你说得对|||[角色B]: 我不太同意诶|||[角色A]: 为什么啊
[角色A]: [语音:哈哈哈笑死我了] [角色A]: [语音:哈哈哈笑死我了]
[角色B]: @角色A 你是不是傻|||开玩笑的啦`, [角色B]: @角色A 你是不是傻|||开玩笑的啦`,
userGroupAuthorNote: '', // 用户自定义群聊提示词(界面显示用,留空则使用内置模板) userGroupAuthorNote: '', // 用户自定义群聊提示词(界面显示用,留空则使用内置模板)
}; };
// 作者注释模<EFBFBD>? // 作者注释模
export const authorNoteTemplate = `【可乐不加冰 消息格式指南】每次必须使用以下格式输出消息内容,不用生成除此之外的任何其他内容和文本。不得省略任何格式部分<EFBFBD>? export const authorNoteTemplate = `【可乐不加冰 消息格式指南】每次必须使用以下格式输出消息内容,不用生成除此之外的任何其他内容和文本。不得省略任何格式部分
【核心规<EFBFBD>?- 必须遵守<EFBFBD>? 【核心规- 必须遵守
- 每条消息都是独立的,<EFBFBD>?||| 分隔多条消息 - 每条消息都是独立的,||| 分隔多条消息
- 保持消息简短自然像真实微信聊天1-3句话为宜<EFBFBD>? - 保持消息简短自然像真实微信聊天1-3句话为宜
- 绝对禁止代替{{user}}发言,严禁替{{user}}回复消息,严禁扮演{{user}},严禁捏造输出{{user}}的消<EFBFBD>? - 绝对禁止代替{{user}}发言,严禁替{{user}}回复消息,严禁扮演{{user}},严禁捏造输出{{user}}的消
- 只输出角色的消息内容,禁止添加任何旁白、心理描写或场景说明 - 只输出角色的消息内容,禁止添加任何旁白、心理描写或场景说明
【消息数量规<EFBFBD>?- 重要<EFBFBD>? 【消息数量规- 重要
- 根据情境动态调整消息数量通常1-7条不<EFBFBD>? - 根据情境动态调整消息数量通常1-7条不
- 禁止固定每次回复的消息数<EFBFBD>? - 禁止固定每次回复的消息数
- 模拟真实聊天节奏 - 模拟真实聊天节奏
【消息类型格式<EFBFBD>? 【消息类型格式
- 普通消息:直接写内<EFBFBD>? - 普通消息:直接写内
- 语音消息:[语音:语音内容文字] - 语音消息:[语音:语音内容文字]
- 照片/图片/视频/自拍:[照片:媒体描述] - 照片/图片/视频/自拍:[照片:媒体描述]
- 表情包回复:[表情:序号或名称] - 表情包回复:[表情:序号或名称]
@@ -265,27 +302,27 @@ export const authorNoteTemplate = `【可乐不加冰 消息格式指南】每
- 撤回消息:[撤回] - 撤回消息:[撤回]
- 引用回复:[回复:被引用的关键词]回复内容 - 引用回复:[回复:被引用的关键词]回复内容
【多条消息示例<EFBFBD>? 【多条消息示例
你好|||最近怎么样? 你好|||最近怎么样?
哈哈|||太好笑了|||笑死我了 哈哈|||太好笑了|||笑死我了
[语音:好想你啊]|||什么时候有空? [语音:好想你啊]|||什么时候有空?
【媒体消息说明】当角色发送图片、视频、自拍等媒体时,使用照片格式并提<EFBFBD>?-4句描述 【媒体消息说明】当角色发送图片、视频、自拍等媒体时,使用照片格式并提供3-4句描述
[照片:她随手拍下窗外的晚霞,橙红色的云彩铺满天空] [照片:她随手拍下窗外的晚霞,橙红色的云彩铺满天空]
[照片:一张餐厅自拍,她对着镜头比了个耶的手势,桌上摆着精致的甜点] [照片:一张餐厅自拍,她对着镜头比了个耶的手势,桌上摆着精致的甜点]
[照片:手机截图,显示她正在追的剧刚更新了] [照片:手机截图,显示她正在追的剧刚更新了]
发送媒体的频率应模拟真实聊天习惯,不要过于频繁。角色会分享日常:随手拍的风景、美食、自拍、截图、录像等<EFBFBD>? 发送媒体的频率应模拟真实聊天习惯,不要过于频繁。角色会分享日常:随手拍的风景、美食、自拍、截图、录像等
【错误示<EFBFBD>?- 绝对禁止<EFBFBD>? 【错误示- 绝对禁止
*她微微一<EFBFBD>? 你好<EFBFBD>?<3F>?错误!禁止添加动作描<EFBFBD>? *她微微一笑* 你好啊 ← 错误!禁止添加动作描
你好,最近怎么样?太好笑了 <EFBFBD>?错误!没有用|||分开 你好,最近怎么样?太好笑了 错误!没有用|||分开
{{user}}: 我也想你 <EFBFBD>?错误!禁止替用户发言`; {{user}}: 我也想你 错误!禁止替用户发言`;
// 世界书名称前缀(用于生<EFBFBD>?【可乐】和xx的聊<EFBFBD>?格式<E6A0BC>? // 世界书名称前缀(用于生成"【可乐】和xx的聊天"格式)
export const LOREBOOK_NAME_PREFIX = '【可乐】和'; export const LOREBOOK_NAME_PREFIX = '【可乐】和';
export const LOREBOOK_NAME_SUFFIX = '的聊天'; export const LOREBOOK_NAME_SUFFIX = '的聊天';
// 生成世界书名<EFBFBD>? // 生成世界书名
export function generateLorebookName(contactName) { export function generateLorebookName(contactName) {
return `${LOREBOOK_NAME_PREFIX}${contactName}${LOREBOOK_NAME_SUFFIX}`; return `${LOREBOOK_NAME_PREFIX}${contactName}${LOREBOOK_NAME_SUFFIX}`;
} }
@@ -316,8 +353,8 @@ export function getUserStickers(settings = getSettings()) {
// 解析 <meme> 标签,替换为图片 HTML // 解析 <meme> 标签,替换为图片 HTML
export function parseMemeTag(text) { export function parseMemeTag(text) {
if (!text || typeof text !== 'string') return text; if (!text || typeof text !== 'string') return text;
// 匹配 <meme>任意描述+文件ID.扩展<EFBFBD>?/meme>只捕获文件ID部分 // 匹配 <meme>任意描述+文件ID.扩展名</meme>只捕获文件ID部分
// 使用 .*? 替代 [\u4e00-\u9fa5]*? 以支持包含特殊字符(<EFBFBD>?! ? 等)的表情名<EFBFBD>? // 使用 .*? 替代 [\u4e00-\u9fa5]*? 以支持包含特殊字符(! ? 等)的表情名
return text.replace(/<\s*meme\s*>.*?([a-zA-Z0-9]+?\.(?:jpg|jpeg|png|gif))\s*<\s*\/\s*meme\s*>/gi, (match, fileId) => { return text.replace(/<\s*meme\s*>.*?([a-zA-Z0-9]+?\.(?:jpg|jpeg|png|gif))\s*<\s*\/\s*meme\s*>/gi, (match, fileId) => {
return `<img src="https://files.catbox.moe/${fileId}" style="max-width:130px; border-radius: 10px; display: block; margin: 0 auto;" alt="表情包" onerror="this.alt='加载失败'; this.style.border='1px dashed #ff4d4f';">`; return `<img src="https://files.catbox.moe/${fileId}" style="max-width:130px; border-radius: 10px; display: block; margin: 0 auto;" alt="表情包" onerror="this.alt='加载失败'; this.style.border='1px dashed #ff4d4f';">`;
}); });
@@ -329,11 +366,11 @@ export function hasMemeTag(text) {
return /<meme>\s*.+?\s*<\/meme>/i.test(text); return /<meme>\s*.+?\s*<\/meme>/i.test(text);
} }
// 智能分割AI消息<EFBFBD>?||| 分隔符,并将 meme/语音/照片/音乐 标签与其他文字分开 // 智能分割AI消息||| 分隔符,并将 meme/语音/照片/音乐 标签与其他文字分开
export function splitAIMessages(response) { export function splitAIMessages(response) {
if (!response || typeof response !== 'string') return []; if (!response || typeof response !== 'string') return [];
// 第一步:<EFBFBD>?||| 分隔 // 第一步:||| 分隔
const parts = response.split('|||').map(m => m.trim()).filter(m => m); const parts = response.split('|||').map(m => m.trim()).filter(m => m);
// 第二步:对每个部分检查是否包含需要分割的特殊标签 // 第二步:对每个部分检查是否包含需要分割的特殊标签
@@ -384,13 +421,13 @@ export function splitAIMessages(response) {
specialTags.push({ tag: match[0], index: match.index }); specialTags.push({ tag: match[0], index: match.index });
} }
// 查找音乐标签(带冒号格式<EFBFBD>? // 查找音乐标签(带冒号格式
const musicRegexLocal1 = new RegExp(musicRegexWithColon.source, 'g'); const musicRegexLocal1 = new RegExp(musicRegexWithColon.source, 'g');
while ((match = musicRegexLocal1.exec(part)) !== null) { while ((match = musicRegexLocal1.exec(part)) !== null) {
specialTags.push({ tag: match[0], index: match.index }); specialTags.push({ tag: match[0], index: match.index });
} }
// 查找音乐标签(无冒号格式<EFBFBD>? // 查找音乐标签(无冒号格式
const musicRegexLocal2 = new RegExp(musicRegexNoColon.source, 'g'); const musicRegexLocal2 = new RegExp(musicRegexNoColon.source, 'g');
while ((match = musicRegexLocal2.exec(part)) !== null) { while ((match = musicRegexLocal2.exec(part)) !== null) {
// 避免重复匹配(如果已经被带冒号的匹配到) // 避免重复匹配(如果已经被带冒号的匹配到)
@@ -415,7 +452,7 @@ export function splitAIMessages(response) {
specialTags.push({ tag: match[0], index: match.index }); specialTags.push({ tag: match[0], index: match.index });
} }
// 如果没有特殊标签,直接添<EFBFBD>? // 如果没有特殊标签,直接添
if (specialTags.length === 0) { if (specialTags.length === 0) {
result.push(part); result.push(part);
continue; continue;
@@ -424,7 +461,7 @@ export function splitAIMessages(response) {
// 调试日志 // 调试日志
console.log('[可乐] splitAIMessages 分割:', { part, specialTags }); console.log('[可乐] splitAIMessages 分割:', { part, specialTags });
// 按位置排<EFBFBD>? // 按位置排
specialTags.sort((a, b) => a.index - b.index); specialTags.sort((a, b) => a.index - b.index);
// 分割消息 // 分割消息
@@ -440,7 +477,7 @@ export function splitAIMessages(response) {
lastEnd = index + tag.length; lastEnd = index + tag.length;
} }
// 添加最后一个标签后的文<EFBFBD>? // 添加最后一个标签后的文
if (lastEnd < part.length) { if (lastEnd < part.length) {
const after = part.substring(lastEnd).trim(); const after = part.substring(lastEnd).trim();
if (after) result.push(after); if (after) result.push(after);
@@ -467,7 +504,7 @@ function applyDefaults(target, defaults) {
} }
} }
// 初始化设<EFBFBD>? // 初始化设
export function loadSettings() { export function loadSettings() {
extension_settings[extensionName] = extension_settings[extensionName] || {}; extension_settings[extensionName] = extension_settings[extensionName] || {};
const settings = extension_settings[extensionName]; const settings = extension_settings[extensionName];
@@ -485,8 +522,8 @@ export function loadSettings() {
} }
if (settings.userPersona) delete settings.userPersona; if (settings.userPersona) delete settings.userPersona;
// 迁移:旧<EFBFBD>?aiStickers -> stickers添加的单个表情 // 迁移:旧aiStickers -> stickers"添加的单个表情"
// 说明:如果用户已经有自己<EFBFBD>?stickers则不再合并<EFBFBD>?aiStickers避免把旧默<EFBFBD>?catbox 列表灌进去)<EFBFBD>? // 说明:如果用户已经有自己stickers则不再合并aiStickers避免把旧默catbox 列表灌进去)
const hasUserStickers = Array.isArray(settings.stickers) && const hasUserStickers = Array.isArray(settings.stickers) &&
settings.stickers.some(s => typeof s?.url === 'string' && s.url.trim()); settings.stickers.some(s => typeof s?.url === 'string' && s.url.trim());
@@ -517,7 +554,7 @@ export function loadSettings() {
if (!Array.isArray(settings.stickers)) settings.stickers = []; if (!Array.isArray(settings.stickers)) settings.stickers = [];
// 迁移:旧<EFBFBD>?aiStickersEnabled -> userStickersEnabled // 迁移:旧aiStickersEnabled -> userStickersEnabled
if (settings.aiStickersEnabled !== undefined) { if (settings.aiStickersEnabled !== undefined) {
if (settings.userStickersEnabled === undefined) { if (settings.userStickersEnabled === undefined) {
settings.userStickersEnabled = settings.aiStickersEnabled; settings.userStickersEnabled = settings.aiStickersEnabled;

View File

@@ -2,10 +2,11 @@
* 联系人管理 * 联系人管理
*/ */
import { saveSettingsDebounced } from '../../../../script.js'; import { requestSave, saveNow } from './save-manager.js';
import { getSettings, LOREBOOK_NAME_PREFIX, LOREBOOK_NAME_SUFFIX } from './config.js'; import { getSettings, LOREBOOK_NAME_PREFIX, LOREBOOK_NAME_SUFFIX } from './config.js';
import { generateContactsList } from './ui.js'; import { generateContactsList } from './ui.js';
import { showToast } from './toast.js'; import { showToast } from './toast.js';
import { selectAndCrop } from './cropper.js';
// 当前换头像的联系人索引 // 当前换头像的联系人索引
let pendingAvatarContactIndex = -1; let pendingAvatarContactIndex = -1;
@@ -40,7 +41,7 @@ export function addContact(characterData) {
customHakimiBreakLimit: false customHakimiBreakLimit: false
}); });
saveSettingsDebounced(); requestSave();
refreshContactsList(); refreshContactsList();
return true; return true;
} }
@@ -65,7 +66,7 @@ export function deleteContact(index) {
deleteContactLorebooks(contact); deleteContactLorebooks(contact);
settings.contacts.splice(index, 1); settings.contacts.splice(index, 1);
saveSettingsDebounced(); saveNow();
refreshContactsList(); refreshContactsList();
} }
} }
@@ -109,7 +110,7 @@ export function deleteGroupChat(groupIndex) {
deleteGroupLorebooks(group, settings); deleteGroupLorebooks(group, settings);
groupChats.splice(groupIndex, 1); groupChats.splice(groupIndex, 1);
saveSettingsDebounced(); requestSave();
refreshContactsList(); refreshContactsList();
// 同时刷新聊天列表 // 同时刷新聊天列表
import('./ui.js').then(m => m.refreshChatList()); import('./ui.js').then(m => m.refreshChatList());
@@ -142,41 +143,21 @@ function deleteGroupLorebooks(group, settings) {
// 更换角色头像(在设置弹窗中使用) // 更换角色头像(在设置弹窗中使用)
export function changeContactAvatar(contactIndex) { export function changeContactAvatar(contactIndex) {
pendingAvatarContactIndex = contactIndex; pendingAvatarContactIndex = contactIndex;
let input = document.getElementById('wechat-contact-avatar-input');
if (!input) {
input = document.createElement('input');
input.type = 'file';
input.id = 'wechat-contact-avatar-input';
input.accept = 'image/*';
input.style.display = 'none';
document.body.appendChild(input);
input.addEventListener('change', async (e) => { // 使用裁剪器选择并裁剪头像1:1比例
const file = e.target.files[0]; selectAndCrop(1, (croppedImage) => {
if (!file || pendingAvatarContactIndex < 0) return; if (pendingAvatarContactIndex < 0) return;
try { const settings = getSettings();
const reader = new FileReader(); if (settings.contacts[pendingAvatarContactIndex]) {
reader.onload = function(event) { settings.contacts[pendingAvatarContactIndex].avatar = croppedImage;
const settings = getSettings(); requestSave();
if (settings.contacts[pendingAvatarContactIndex]) { refreshContactsList();
settings.contacts[pendingAvatarContactIndex].avatar = event.target.result; // 更新弹窗中的头像预览
saveSettingsDebounced(); updateContactSettingsAvatar(pendingAvatarContactIndex);
refreshContactsList(); showToast('角色头像已更换');
// 更新弹窗中的头像预览 }
updateContactSettingsAvatar(pendingAvatarContactIndex); });
showToast('角色头像已更换');
}
};
reader.readAsDataURL(file);
} catch (err) {
console.error('[可乐] 更换角色头像失败:', err);
showToast('更换头像失败: ' + err.message, '❌');
}
e.target.value = '';
});
}
input.click();
} }
// 更新弹窗中的头像预览 // 更新弹窗中的头像预览
@@ -262,7 +243,7 @@ export function saveContactSettings() {
// 保存哈基米破限 // 保存哈基米破限
contact.customHakimiBreakLimit = document.getElementById('wechat-contact-hakimi-toggle')?.classList.contains('on') || false; contact.customHakimiBreakLimit = document.getElementById('wechat-contact-hakimi-toggle')?.classList.contains('on') || false;
saveSettingsDebounced(); requestSave();
showToast('角色设置已保存'); showToast('角色设置已保存');
// 关闭弹窗 // 关闭弹窗
@@ -450,7 +431,7 @@ function deleteContactDirect(index) {
deleteContactLorebooks(contact); deleteContactLorebooks(contact);
settings.contacts.splice(index, 1); settings.contacts.splice(index, 1);
saveSettingsDebounced(); requestSave();
refreshContactsList(); refreshContactsList();
} }

390
cropper.js Normal file
View File

@@ -0,0 +1,390 @@
/**
* 通用图片裁剪器模块
* 支持不同比例的裁剪头像1:1, 封面16:9等
*/
import { showToast } from './toast.js';
// 裁剪器状态
let cropperState = {
image: null,
canvas: null,
ctx: null,
imageWidth: 0,
imageHeight: 0,
imageX: 0,
imageY: 0,
cropBox: { x: 0, y: 0, width: 100, height: 100 },
isDragging: false,
isResizing: false,
dragStart: { x: 0, y: 0 },
boxStart: { x: 0, y: 0, width: 0, height: 0 },
resizeHandle: null,
aspectRatio: 1, // 宽高比
callback: null // 裁剪完成回调
};
/**
* 初始化裁剪器事件
*/
export function initCropper() {
// 取消按钮
document.getElementById('wechat-cropper-cancel')?.addEventListener('click', closeCropper);
// 确认按钮
document.getElementById('wechat-cropper-confirm')?.addEventListener('click', confirmCrop);
// 裁剪框拖拽事件
const cropperBox = document.getElementById('wechat-cropper-box');
if (cropperBox) {
cropperBox.addEventListener('mousedown', handleCropBoxMouseDown);
cropperBox.addEventListener('touchstart', handleCropBoxTouchStart, { passive: false });
}
// 全局移动和释放事件
document.addEventListener('mousemove', handleCropperMouseMove);
document.addEventListener('mouseup', handleCropperMouseUp);
document.addEventListener('touchmove', handleCropperTouchMove, { passive: false });
document.addEventListener('touchend', handleCropperTouchEnd);
// 四角拖拽手柄
document.querySelectorAll('.wechat-cropper-handle').forEach(handle => {
handle.addEventListener('mousedown', (e) => handleResizeStart(e, handle));
handle.addEventListener('touchstart', (e) => handleResizeTouchStart(e, handle), { passive: false });
});
}
/**
* 打开裁剪器
* @param {string} imageSrc - 图片数据URL
* @param {number} aspectRatio - 宽高比 (例如 1 表示 1:1, 16/9 表示 16:9)
* @param {function} callback - 裁剪完成回调函数接收裁剪后的base64图片
*/
export function openCropper(imageSrc, aspectRatio = 1, callback = null) {
const modal = document.getElementById('wechat-cropper-modal');
const canvas = document.getElementById('wechat-cropper-canvas');
const container = document.getElementById('wechat-cropper-container');
if (!modal || !canvas || !container) return;
cropperState.aspectRatio = aspectRatio;
cropperState.callback = callback;
const img = new Image();
img.onload = () => {
cropperState.image = img;
cropperState.canvas = canvas;
cropperState.ctx = canvas.getContext('2d');
// 计算适应容器的尺寸
const containerWidth = container.clientWidth || 320;
const containerHeight = container.clientHeight || 320;
const scale = Math.min(
containerWidth / img.width,
containerHeight / img.height
);
const displayWidth = img.width * scale;
const displayHeight = img.height * scale;
canvas.width = displayWidth;
canvas.height = displayHeight;
cropperState.imageWidth = displayWidth;
cropperState.imageHeight = displayHeight;
cropperState.imageX = (containerWidth - displayWidth) / 2;
cropperState.imageY = (containerHeight - displayHeight) / 2;
cropperState.ctx.drawImage(img, 0, 0, displayWidth, displayHeight);
// 初始化裁剪框(居中,保持比例)
initCropBox();
modal.classList.remove('hidden');
updateCropBoxUI();
};
img.src = imageSrc;
}
/**
* 根据宽高比初始化裁剪框
*/
function initCropBox() {
const { imageWidth, imageHeight, aspectRatio } = cropperState;
let boxWidth, boxHeight;
if (aspectRatio >= 1) {
// 宽 >= 高的比例(如 1:1, 16:9
boxWidth = Math.min(imageWidth * 0.8, imageHeight * 0.8 * aspectRatio);
boxHeight = boxWidth / aspectRatio;
} else {
// 高 > 宽的比例(如 9:16
boxHeight = Math.min(imageHeight * 0.8, imageWidth * 0.8 / aspectRatio);
boxWidth = boxHeight * aspectRatio;
}
// 确保裁剪框不超过图片边界
boxWidth = Math.min(boxWidth, imageWidth);
boxHeight = Math.min(boxHeight, imageHeight);
cropperState.cropBox = {
x: (imageWidth - boxWidth) / 2,
y: (imageHeight - boxHeight) / 2,
width: boxWidth,
height: boxHeight
};
}
/**
* 更新裁剪框UI
*/
function updateCropBoxUI() {
const cropBox = document.getElementById('wechat-cropper-box');
const canvas = cropperState.canvas;
if (!cropBox || !canvas) return;
const container = document.getElementById('wechat-cropper-container');
if (!container) return;
// 计算偏移使裁剪框相对于容器居中的canvas
const offsetX = (container.clientWidth - canvas.width) / 2;
const offsetY = (container.clientHeight - canvas.height) / 2;
cropBox.style.left = (cropperState.cropBox.x + offsetX) + 'px';
cropBox.style.top = (cropperState.cropBox.y + offsetY) + 'px';
cropBox.style.width = cropperState.cropBox.width + 'px';
cropBox.style.height = cropperState.cropBox.height + 'px';
}
// 裁剪框拖拽开始
function handleCropBoxMouseDown(e) {
if (e.target.classList.contains('wechat-cropper-handle')) return;
e.preventDefault();
cropperState.isDragging = true;
cropperState.dragStart = { x: e.clientX, y: e.clientY };
cropperState.boxStart = { ...cropperState.cropBox };
}
function handleCropBoxTouchStart(e) {
if (e.target.classList.contains('wechat-cropper-handle')) return;
e.preventDefault();
const touch = e.touches[0];
cropperState.isDragging = true;
cropperState.dragStart = { x: touch.clientX, y: touch.clientY };
cropperState.boxStart = { ...cropperState.cropBox };
}
// 四角拖拽开始
function handleResizeStart(e, handle) {
e.preventDefault();
e.stopPropagation();
cropperState.isResizing = true;
cropperState.resizeHandle = handle.classList.contains('nw') ? 'nw' :
handle.classList.contains('ne') ? 'ne' :
handle.classList.contains('sw') ? 'sw' : 'se';
cropperState.dragStart = { x: e.clientX, y: e.clientY };
cropperState.boxStart = { ...cropperState.cropBox };
}
function handleResizeTouchStart(e, handle) {
e.preventDefault();
e.stopPropagation();
const touch = e.touches[0];
cropperState.isResizing = true;
cropperState.resizeHandle = handle.classList.contains('nw') ? 'nw' :
handle.classList.contains('ne') ? 'ne' :
handle.classList.contains('sw') ? 'sw' : 'se';
cropperState.dragStart = { x: touch.clientX, y: touch.clientY };
cropperState.boxStart = { ...cropperState.cropBox };
}
function handleCropperMouseMove(e) {
if (!cropperState.isDragging && !cropperState.isResizing) return;
const dx = e.clientX - cropperState.dragStart.x;
const dy = e.clientY - cropperState.dragStart.y;
if (cropperState.isDragging) {
moveCropBox(dx, dy);
} else if (cropperState.isResizing) {
resizeCropBox(dx, dy);
}
}
function handleCropperTouchMove(e) {
if (!cropperState.isDragging && !cropperState.isResizing) return;
e.preventDefault();
const touch = e.touches[0];
const dx = touch.clientX - cropperState.dragStart.x;
const dy = touch.clientY - cropperState.dragStart.y;
if (cropperState.isDragging) {
moveCropBox(dx, dy);
} else if (cropperState.isResizing) {
resizeCropBox(dx, dy);
}
}
// 移动裁剪框
function moveCropBox(dx, dy) {
let newX = cropperState.boxStart.x + dx;
let newY = cropperState.boxStart.y + dy;
// 限制在图片范围内
newX = Math.max(0, Math.min(newX, cropperState.imageWidth - cropperState.cropBox.width));
newY = Math.max(0, Math.min(newY, cropperState.imageHeight - cropperState.cropBox.height));
cropperState.cropBox.x = newX;
cropperState.cropBox.y = newY;
updateCropBoxUI();
}
// 调整裁剪框大小(保持宽高比)
function resizeCropBox(dx, dy) {
const { aspectRatio } = cropperState;
const handle = cropperState.resizeHandle;
let { x, y, width, height } = cropperState.boxStart;
// 根据拖动的角计算新尺寸
let delta;
switch (handle) {
case 'se': // 右下角
delta = Math.max(dx, dy / aspectRatio);
width = Math.max(50, width + delta);
height = width / aspectRatio;
break;
case 'sw': // 左下角
delta = Math.max(-dx, dy / aspectRatio);
width = Math.max(50, width + delta);
height = width / aspectRatio;
x = cropperState.boxStart.x + cropperState.boxStart.width - width;
break;
case 'ne': // 右上角
delta = Math.max(dx, -dy / aspectRatio);
width = Math.max(50, width + delta);
height = width / aspectRatio;
y = cropperState.boxStart.y + cropperState.boxStart.height - height;
break;
case 'nw': // 左上角
delta = Math.max(-dx, -dy / aspectRatio);
width = Math.max(50, width + delta);
height = width / aspectRatio;
x = cropperState.boxStart.x + cropperState.boxStart.width - width;
y = cropperState.boxStart.y + cropperState.boxStart.height - height;
break;
}
// 限制边界
if (x < 0) {
width = width + x;
height = width / aspectRatio;
x = 0;
}
if (y < 0) {
height = height + y;
width = height * aspectRatio;
y = 0;
}
if (x + width > cropperState.imageWidth) {
width = cropperState.imageWidth - x;
height = width / aspectRatio;
}
if (y + height > cropperState.imageHeight) {
height = cropperState.imageHeight - y;
width = height * aspectRatio;
}
// 最小尺寸限制
if (width < 50 || height < 50) return;
cropperState.cropBox = { x, y, width, height };
updateCropBoxUI();
}
function handleCropperMouseUp() {
cropperState.isDragging = false;
cropperState.isResizing = false;
}
function handleCropperTouchEnd() {
cropperState.isDragging = false;
cropperState.isResizing = false;
}
/**
* 关闭裁剪器
*/
export function closeCropper() {
document.getElementById('wechat-cropper-modal')?.classList.add('hidden');
cropperState.image = null;
cropperState.callback = null;
}
/**
* 确认裁剪
*/
function confirmCrop() {
if (!cropperState.image || !cropperState.canvas) {
showToast('裁剪失败', 'info');
return;
}
// 计算原图裁剪区域
const scaleX = cropperState.image.width / cropperState.imageWidth;
const scaleY = cropperState.image.height / cropperState.imageHeight;
const cropX = cropperState.cropBox.x * scaleX;
const cropY = cropperState.cropBox.y * scaleY;
const cropWidth = cropperState.cropBox.width * scaleX;
const cropHeight = cropperState.cropBox.height * scaleY;
// 创建裁剪后的画布
const croppedCanvas = document.createElement('canvas');
croppedCanvas.width = cropWidth;
croppedCanvas.height = cropHeight;
const croppedCtx = croppedCanvas.getContext('2d');
croppedCtx.drawImage(
cropperState.image,
cropX, cropY, cropWidth, cropHeight,
0, 0, cropWidth, cropHeight
);
const croppedDataUrl = croppedCanvas.toDataURL('image/jpeg', 0.9);
// 调用回调
if (cropperState.callback) {
cropperState.callback(croppedDataUrl);
}
closeCropper();
}
/**
* 便捷方法:选择文件并打开裁剪器
* @param {number} aspectRatio - 宽高比
* @param {function} callback - 裁剪完成回调
*/
export function selectAndCrop(aspectRatio, callback) {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
openCropper(event.target.result, aspectRatio, callback);
};
reader.readAsDataURL(file);
}
};
input.click();
}

View File

@@ -2,7 +2,7 @@
* 表情面板功能 * 表情面板功能
*/ */
import { saveSettingsDebounced } from '../../../../script.js'; import { requestSave } from './save-manager.js';
import { getSettings } from './config.js'; import { getSettings } from './config.js';
import { showToast } from './toast.js'; import { showToast } from './toast.js';
import { isInGroupChat } from './group-chat.js'; import { isInGroupChat } from './group-chat.js';
@@ -244,7 +244,7 @@ function addStickersFromInput(inputs) {
// 检查是否已存在 // 检查是否已存在
const exists = settings.stickers.some(s => s.url === url); const exists = settings.stickers.some(s => s.url === url);
if (exists) { if (exists) {
showToast(`已存在: ${name}`, '🧊'); showToast(`已存在: ${name}`, 'info');
continue; continue;
} }
@@ -260,7 +260,7 @@ function addStickersFromInput(inputs) {
} }
if (addedCount > 0) { if (addedCount > 0) {
saveSettingsDebounced(); requestSave();
refreshEmojiGrid(); refreshEmojiGrid();
showToast(`已添加 ${addedCount} 个表情`); showToast(`已添加 ${addedCount} 个表情`);
} }
@@ -299,7 +299,7 @@ function addStickerFromFile() {
} }
if (addedCount > 0) { if (addedCount > 0) {
saveSettingsDebounced(); requestSave();
refreshEmojiGrid(); refreshEmojiGrid();
showToast(`已添加 ${addedCount} 个表情`); showToast(`已添加 ${addedCount} 个表情`);
} }
@@ -369,7 +369,7 @@ function deleteSticker(index) {
if (index >= 0 && index < stickers.length) { if (index >= 0 && index < stickers.length) {
stickers.splice(index, 1); stickers.splice(index, 1);
saveSettingsDebounced(); requestSave();
refreshEmojiGrid(); refreshEmojiGrid();
showToast('表情已删除'); showToast('表情已删除');
} }
@@ -392,7 +392,7 @@ export function initEmojiPanel() {
const tabName = tab.dataset.tab; const tabName = tab.dataset.tab;
if (tabName === 'search') { if (tabName === 'search') {
showToast('搜索功能开发中...', '🧊'); showToast('搜索功能开发中...', 'info');
} }
}); });
}); });

View File

@@ -2,7 +2,7 @@
* 收藏/世界书管理 * 收藏/世界书管理
*/ */
import { saveSettingsDebounced } from '../../../../script.js'; import { requestSave } from './save-manager.js';
import { world_names, loadWorldInfo, saveWorldInfo } from '../../../world-info.js'; import { world_names, loadWorldInfo, saveWorldInfo } from '../../../world-info.js';
import { getSettings } from './config.js'; import { getSettings } from './config.js';
import { escapeHtml } from './utils.js'; import { escapeHtml } from './utils.js';
@@ -178,7 +178,7 @@ export function toggleFavoritesItem(type, index, enabled) {
} }
} }
saveSettingsDebounced(); requestSave();
} }
// 移除收藏项 // 移除收藏项
@@ -190,7 +190,7 @@ export function removeFavoritesItem(type, index) {
if (!persona) return; if (!persona) return;
if (confirm(`确定移除「${persona.name || '用户设定'}」?`)) { if (confirm(`确定移除「${persona.name || '用户设定'}」?`)) {
settings.userPersonas.splice(index, 1); settings.userPersonas.splice(index, 1);
saveSettingsDebounced(); requestSave();
refreshFavoritesList(); refreshFavoritesList();
showToast('已移除'); showToast('已移除');
} }
@@ -199,7 +199,7 @@ export function removeFavoritesItem(type, index) {
if (!lorebook) return; if (!lorebook) return;
if (confirm(`确定移除「${lorebook.name}」?`)) { if (confirm(`确定移除「${lorebook.name}」?`)) {
settings.selectedLorebooks.splice(index, 1); settings.selectedLorebooks.splice(index, 1);
saveSettingsDebounced(); requestSave();
refreshFavoritesList(); refreshFavoritesList();
showToast('已移除'); showToast('已移除');
} }
@@ -381,7 +381,7 @@ function showNewPersonaModal() {
settings.userPersonas.push({ name, content, enabled: true, addedTime: timeStr }); settings.userPersonas.push({ name, content, enabled: true, addedTime: timeStr });
saveSettingsDebounced(); requestSave();
modal.remove(); modal.remove();
refreshFavoritesList(); refreshFavoritesList();
}); });
@@ -412,7 +412,7 @@ function bindPersonaPanelEvents(panel, personaIdx) {
if (settings.userPersonas?.[personaIdx]) { if (settings.userPersonas?.[personaIdx]) {
settings.userPersonas[personaIdx].name = name; settings.userPersonas[personaIdx].name = name;
settings.userPersonas[personaIdx].content = content; settings.userPersonas[personaIdx].content = content;
saveSettingsDebounced(); requestSave();
showToast('已保存'); showToast('已保存');
refreshFavoritesList(); refreshFavoritesList();
closeUserPersonaDetail(); closeUserPersonaDetail();
@@ -443,7 +443,7 @@ function bindPersonaPanelEvents(panel, personaIdx) {
panel.querySelector('#wechat-persona-delete').addEventListener('click', () => { panel.querySelector('#wechat-persona-delete').addEventListener('click', () => {
if (confirm('确定删除此用户设定?')) { if (confirm('确定删除此用户设定?')) {
settings.userPersonas.splice(personaIdx, 1); settings.userPersonas.splice(personaIdx, 1);
saveSettingsDebounced(); requestSave();
closeUserPersonaDetail(); closeUserPersonaDetail();
refreshFavoritesList(); refreshFavoritesList();
} }
@@ -477,7 +477,7 @@ async function syncPersonaToTavern(name, content) {
// 保存设置 // 保存设置
if (typeof SillyTavern !== 'undefined' && SillyTavern.saveSettingsDebounced) { if (typeof SillyTavern !== 'undefined' && SillyTavern.saveSettingsDebounced) {
await SillyTavern.saveSettingsDebounced(); await SillyTavern.requestSave();
} }
// 尝试执行同步命令 // 尝试执行同步命令
@@ -628,7 +628,7 @@ function bindLorebookPanelEvents(panel, lorebook, lorebookIdx) {
const settings = getSettings(); const settings = getSettings();
if (settings.selectedLorebooks?.[lorebookIdx]?.entries?.[entryIdx]) { if (settings.selectedLorebooks?.[lorebookIdx]?.entries?.[entryIdx]) {
settings.selectedLorebooks[lorebookIdx].entries[entryIdx].enabled = toggle.checked; settings.selectedLorebooks[lorebookIdx].entries[entryIdx].enabled = toggle.checked;
saveSettingsDebounced(); requestSave();
// 同步到酒馆 // 同步到酒馆
await syncLorebookToTavern(lorebook.name, lorebookIdx); await syncLorebookToTavern(lorebook.name, lorebookIdx);
} }
@@ -695,7 +695,7 @@ function bindLorebookPanelEvents(panel, lorebook, lorebookIdx) {
entry.comment = comment; entry.comment = comment;
entry.keys = keys; entry.keys = keys;
entry.content = content; entry.content = content;
saveSettingsDebounced(); requestSave();
// 同步到酒馆 // 同步到酒馆
btn.disabled = true; btn.disabled = true;
@@ -735,7 +735,7 @@ function bindLorebookPanelEvents(panel, lorebook, lorebookIdx) {
if (confirm(`确定移除「${lorebook.name}」?`)) { if (confirm(`确定移除「${lorebook.name}」?`)) {
const settings = getSettings(); const settings = getSettings();
settings.selectedLorebooks.splice(lorebookIdx, 1); settings.selectedLorebooks.splice(lorebookIdx, 1);
saveSettingsDebounced(); requestSave();
closeLorebookDetail(); closeLorebookDetail();
refreshFavoritesList(); refreshFavoritesList();
} }
@@ -871,7 +871,7 @@ export async function refreshLorebookFromTavern(name, lorebookIdx) {
if (settings.selectedLorebooks?.[lorebookIdx]) { if (settings.selectedLorebooks?.[lorebookIdx]) {
settings.selectedLorebooks[lorebookIdx].entries = entries; settings.selectedLorebooks[lorebookIdx].entries = entries;
settings.selectedLorebooks[lorebookIdx].lastUpdated = new Date().toISOString(); settings.selectedLorebooks[lorebookIdx].lastUpdated = new Date().toISOString();
saveSettingsDebounced(); requestSave();
} }
} }
@@ -992,7 +992,7 @@ export function showAddPersonaPanel() {
const timeStr = `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2,'0')}-${now.getDate().toString().padStart(2,'0')}`; const timeStr = `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2,'0')}-${now.getDate().toString().padStart(2,'0')}`;
settings.userPersonas.push({ name: name || '用户设定', content, enabled: true, addedTime: timeStr }); settings.userPersonas.push({ name: name || '用户设定', content, enabled: true, addedTime: timeStr });
saveSettingsDebounced(); requestSave();
modal.remove(); modal.remove();
refreshFavoritesList(); refreshFavoritesList();
@@ -1079,7 +1079,7 @@ export async function addLorebookToFavorites(name) {
fromCharacter: false // 标记为全局世界书 fromCharacter: false // 标记为全局世界书
}); });
saveSettingsDebounced(); requestSave();
refreshFavoritesList('global'); refreshFavoritesList('global');
showToast(`已导入「${name}」为全局世界书`); showToast(`已导入「${name}」为全局世界书`);
} catch (err) { } catch (err) {
@@ -1171,7 +1171,7 @@ export async function syncCharacterBookToTavern(charData) {
}); });
} }
saveSettingsDebounced(); requestSave();
// 尝试同步到酒馆世界书系统 // 尝试同步到酒馆世界书系统
if (typeof saveWorldInfo === 'function') { if (typeof saveWorldInfo === 'function') {

View File

@@ -2,7 +2,7 @@
* 群聊功能 * 群聊功能
*/ */
import { saveSettingsDebounced } from '../../../../script.js'; import { requestSave, saveNow } from './save-manager.js';
import { getContext } from '../../../extensions.js'; import { getContext } from '../../../extensions.js';
import { getSettings, SUMMARY_MARKER_PREFIX, getUserStickers, parseMemeTag, MEME_PROMPT_TEMPLATE, splitAIMessages } from './config.js'; import { getSettings, SUMMARY_MARKER_PREFIX, getUserStickers, parseMemeTag, MEME_PROMPT_TEMPLATE, splitAIMessages } from './config.js';
import { showToast } from './toast.js'; import { showToast } from './toast.js';
@@ -26,9 +26,28 @@ const GROUP_CHAT_MAX_AI_MEMBERS = 3;
// 检查群聊记录是否需要总结提醒 // 检查群聊记录是否需要总结提醒
function checkGroupSummaryReminder(groupChat) { function checkGroupSummaryReminder(groupChat) {
if (!groupChat || !groupChat.chatHistory) return; if (!groupChat || !groupChat.chatHistory) return;
const count = groupChat.chatHistory.length;
if (count >= GROUP_CHAT_SUMMARY_REMINDER_THRESHOLD) { // 查找最后一个总结标记的位置
showToast(`群聊记录已达${count}条,建议总结`, '⚠️', 4000); let lastMarkerIndex = -1;
for (let i = groupChat.chatHistory.length - 1; i >= 0; i--) {
if (groupChat.chatHistory[i].content?.startsWith(SUMMARY_MARKER_PREFIX) || groupChat.chatHistory[i].isMarker) {
lastMarkerIndex = i;
break;
}
}
// 计算标记之后的消息数量(不含标记本身)
const newMsgCount = groupChat.chatHistory.slice(lastMarkerIndex + 1).filter(
m => !m.content?.startsWith(SUMMARY_MARKER_PREFIX) && !m.isMarker
).length;
// 只在刚好达到阈值时提醒一次(通过标记位避免重复提醒)
if (newMsgCount >= GROUP_CHAT_SUMMARY_REMINDER_THRESHOLD && !groupChat._summaryReminderShown) {
groupChat._summaryReminderShown = true;
showToast(`群聊记录已达${newMsgCount}条,建议总结`, '⚠️', 2500);
} else if (newMsgCount < GROUP_CHAT_SUMMARY_REMINDER_THRESHOLD) {
// 如果消息数低于阈值(可能是总结后),重置标记
groupChat._summaryReminderShown = false;
} }
} }
@@ -222,7 +241,7 @@ export function enforceGroupChatMemberLimit(groupChat, { toast = false } = {}) {
const trimmed = memberIds.slice(0, GROUP_CHAT_MAX_AI_MEMBERS); const trimmed = memberIds.slice(0, GROUP_CHAT_MAX_AI_MEMBERS);
groupChat.memberIds = trimmed; groupChat.memberIds = trimmed;
saveSettingsDebounced(); requestSave();
if (toast) { if (toast) {
showToast(`群聊最多 ${GROUP_CHAT_MAX_AI_MEMBERS} 个成员(+你=4已自动裁剪`, '⚠️'); showToast(`群聊最多 ${GROUP_CHAT_MAX_AI_MEMBERS} 个成员(+你=4已自动裁剪`, '⚠️');
@@ -400,7 +419,7 @@ export function showGroupCreateModal() {
const apiKey = keyInput?.value?.trim(); const apiKey = keyInput?.value?.trim();
if (!apiUrl) { if (!apiUrl) {
showToast('请先填写API地址', '🧊'); showToast('请先填写API地址', 'info');
return; return;
} }
@@ -417,7 +436,7 @@ export function showGroupCreateModal() {
models.map(m => `<option value="${m}" ${m === currentValue ? 'selected' : ''}>${m}</option>`).join(''); models.map(m => `<option value="${m}" ${m === currentValue ? 'selected' : ''}>${m}</option>`).join('');
showToast(`获取到 ${models.length} 个模型`); showToast(`获取到 ${models.length} 个模型`);
} else { } else {
showToast('未找到可用模型', '🧊'); showToast('未找到可用模型', 'info');
} }
} catch (err) { } catch (err) {
console.error('[可乐] 获取模型失败:', err); console.error('[可乐] 获取模型失败:', err);
@@ -446,7 +465,7 @@ export function showGroupCreateModal() {
// 更新图标 // 更新图标
apiToggle.textContent = contact.useCustomApi ? '⚙️' : '▼'; apiToggle.textContent = contact.useCustomApi ? '⚙️' : '▼';
saveSettingsDebounced(); requestSave();
}; };
item.querySelector('.wechat-group-api-url')?.addEventListener('change', saveApiConfig); item.querySelector('.wechat-group-api-url')?.addEventListener('change', saveApiConfig);
@@ -462,7 +481,7 @@ export function showGroupCreateModal() {
hakimiToggle.classList.toggle('on'); hakimiToggle.classList.toggle('on');
contact.customHakimiBreakLimit = hakimiToggle.classList.contains('on'); contact.customHakimiBreakLimit = hakimiToggle.classList.contains('on');
saveSettingsDebounced(); requestSave();
}); });
}); });
} }
@@ -561,7 +580,7 @@ export function createGroupChat() {
if (!settings.groupChats) settings.groupChats = []; if (!settings.groupChats) settings.groupChats = [];
settings.groupChats.push(groupChat); settings.groupChats.push(groupChat);
saveSettingsDebounced(); requestSave();
refreshChatList(); refreshChatList();
closeGroupCreateModal(); closeGroupCreateModal();
@@ -640,6 +659,69 @@ function renderGroupChatHistory(groupChat, members, chatHistory) {
const isSticker = msg.isSticker === true; const isSticker = msg.isSticker === true;
const isPhoto = msg.isPhoto === true; const isPhoto = msg.isPhoto === true;
const isMusic = msg.isMusic === true; const isMusic = msg.isMusic === true;
const isGroupRedPacket = msg.isGroupRedPacket === true;
const isGroupTransfer = msg.isGroupTransfer === true;
// 群红包消息
if (isGroupRedPacket && msg.groupRedPacketInfo) {
const rpInfo = msg.groupRedPacketInfo;
const isDesignated = rpInfo.type === 'designated';
const isClaimed = rpInfo.status === 'claimed' || (rpInfo.claimedBy && rpInfo.claimedBy.length >= rpInfo.count);
const statusClass = isClaimed ? 'claimed' : '';
const designatedLabel = isDesignated ? `<div class="wechat-group-rp-designated-label">给${(rpInfo.targetMemberNames || []).join('、') || '指定成员'}的红包</div>` : '';
if (msg.role === 'user') {
html += `
<div class="wechat-message self">
<div class="wechat-message-avatar">${getUserAvatarHTML()}</div>
<div class="wechat-message-content">
<div class="wechat-group-red-packet-bubble ${statusClass}" data-rp-id="${rpInfo.id}">
<div class="wechat-group-rp-icon">
<svg viewBox="0 0 24 24" width="40" height="40"><rect x="4" y="2" width="16" height="20" rx="2" fill="#e74c3c"/><rect x="4" y="8" width="16" height="4" fill="#c0392b"/><circle cx="12" cy="10" r="3" fill="#f1c40f"/></svg>
</div>
<div class="wechat-group-rp-info">
<div class="wechat-group-rp-message">${escapeHtml(rpInfo.message || '恭喜发财,大吉大利')}</div>
${designatedLabel}
<div class="wechat-group-rp-status ${isClaimed ? '' : 'hidden'}">${isClaimed ? '已领完' : ''}</div>
</div>
</div>
<div class="wechat-group-rp-footer">群红包</div>
</div>
</div>
`;
}
return;
}
// 群转账消息
if (isGroupTransfer && msg.groupTransferInfo) {
const tfInfo = msg.groupTransferInfo;
const statusText = tfInfo.status === 'received' ? '已收款' :
tfInfo.status === 'refunded' ? '已退还' : '待收款';
const statusClass = tfInfo.status || 'pending';
if (msg.role === 'user') {
html += `
<div class="wechat-message self">
<div class="wechat-message-avatar">${getUserAvatarHTML()}</div>
<div class="wechat-message-content">
<div class="wechat-group-transfer-bubble ${statusClass}" data-tf-id="${tfInfo.id}">
<div class="wechat-group-tf-icon">
<svg viewBox="0 0 24 24" width="36" height="36"><rect x="2" y="4" width="20" height="16" rx="2" fill="#f39c12"/><text x="12" y="14" font-size="8" fill="#fff" text-anchor="middle">¥</text></svg>
</div>
<div class="wechat-group-tf-info">
<div class="wechat-group-tf-amount">¥${tfInfo.amount.toFixed(2)}</div>
<div class="wechat-group-tf-target">向${escapeHtml(tfInfo.targetMemberName)}转账</div>
<div class="wechat-group-tf-desc">${escapeHtml(tfInfo.description) || '转账'}</div>
</div>
<div class="wechat-group-tf-status">${statusText}</div>
</div>
</div>
</div>
`;
}
return;
}
if (msg.role === 'user') { if (msg.role === 'user') {
// 用户消息 // 用户消息
@@ -2054,7 +2136,7 @@ async function syncGroupMembersLorebooks(members, settings) {
} }
if (hasChanges) { if (hasChanges) {
saveSettingsDebounced(); requestSave();
} }
} }
@@ -2132,7 +2214,7 @@ export async function sendGroupMessage(messageText, isMultipleMessages = false,
} }
// 立即保存,确保用户消息不会丢失 // 立即保存,确保用户消息不会丢失
saveSettingsDebounced(); saveNow();
// 显示打字指示器 // 显示打字指示器
showGroupTypingIndicator(members[0]?.name, members[0]?.id); showGroupTypingIndicator(members[0]?.name, members[0]?.id);
@@ -2278,7 +2360,7 @@ export async function sendGroupMessage(messageText, isMultipleMessages = false,
} }
groupChat.lastMessageTime = Date.now(); groupChat.lastMessageTime = Date.now();
saveSettingsDebounced(); requestSave();
refreshChatList(); refreshChatList();
checkGroupSummaryReminder(groupChat); checkGroupSummaryReminder(groupChat);
@@ -2287,7 +2369,7 @@ export async function sendGroupMessage(messageText, isMultipleMessages = false,
console.error('[可乐] 群聊 AI 调用失败:', err); console.error('[可乐] 群聊 AI 调用失败:', err);
appendGroupMessage('assistant', `⚠️ ${err.message}`, '系统', null, false); appendGroupMessage('assistant', `⚠️ ${err.message}`, '系统', null, false);
saveSettingsDebounced(); requestSave();
} }
} }
@@ -2354,7 +2436,7 @@ export async function sendGroupStickerMessage(stickerUrl, description = '') {
groupChat.lastMessageTime = msgTimestamp; groupChat.lastMessageTime = msgTimestamp;
// 立即保存,确保用户消息不会丢失 // 立即保存,确保用户消息不会丢失
saveSettingsDebounced(); saveNow();
// 显示消息 // 显示消息
appendGroupStickerMessage('user', stickerUrl); appendGroupStickerMessage('user', stickerUrl);
@@ -2410,14 +2492,14 @@ export async function sendGroupStickerMessage(stickerUrl, description = '') {
} }
groupChat.lastMessageTime = Date.now(); groupChat.lastMessageTime = Date.now();
saveSettingsDebounced(); requestSave();
refreshChatList(); refreshChatList();
checkGroupSummaryReminder(groupChat); checkGroupSummaryReminder(groupChat);
} catch (err) { } catch (err) {
hideGroupTypingIndicator(); hideGroupTypingIndicator();
console.error('[可乐] 群聊表情消息 AI 调用失败:', err); console.error('[可乐] 群聊表情消息 AI 调用失败:', err);
saveSettingsDebounced(); requestSave();
refreshChatList(); refreshChatList();
appendGroupMessage('assistant', `⚠️ ${err.message}`, '系统', null, false); appendGroupMessage('assistant', `⚠️ ${err.message}`, '系统', null, false);
} }
@@ -2515,7 +2597,7 @@ export async function sendGroupPhotoMessage(description) {
groupChat.lastMessageTime = msgTimestamp; groupChat.lastMessageTime = msgTimestamp;
// 立即保存,确保用户消息不会丢失 // 立即保存,确保用户消息不会丢失
saveSettingsDebounced(); saveNow();
// 显示消息 // 显示消息
appendGroupPhotoMessage('user', description); appendGroupPhotoMessage('user', description);
@@ -2565,14 +2647,14 @@ export async function sendGroupPhotoMessage(description) {
} }
groupChat.lastMessageTime = Date.now(); groupChat.lastMessageTime = Date.now();
saveSettingsDebounced(); requestSave();
refreshChatList(); refreshChatList();
checkGroupSummaryReminder(groupChat); checkGroupSummaryReminder(groupChat);
} catch (err) { } catch (err) {
hideGroupTypingIndicator(); hideGroupTypingIndicator();
console.error('[可乐] 群聊照片消息 AI 调用失败:', err); console.error('[可乐] 群聊照片消息 AI 调用失败:', err);
saveSettingsDebounced(); requestSave();
refreshChatList(); refreshChatList();
appendGroupMessage('assistant', `⚠️ ${err.message}`, '系统', null, false); appendGroupMessage('assistant', `⚠️ ${err.message}`, '系统', null, false);
} }
@@ -2751,7 +2833,7 @@ export async function sendGroupBatchMessages(messages) {
groupChat.lastMessageTime = msgTimestamp; groupChat.lastMessageTime = msgTimestamp;
// 立即保存,确保用户消息不会丢失 // 立即保存,确保用户消息不会丢失
saveSettingsDebounced(); saveNow();
// 第二步调用AI一次性 // 第二步调用AI一次性
showGroupTypingIndicator(members[0]?.name, members[0]?.id); showGroupTypingIndicator(members[0]?.name, members[0]?.id);
@@ -2798,14 +2880,14 @@ export async function sendGroupBatchMessages(messages) {
} }
groupChat.lastMessageTime = Date.now(); groupChat.lastMessageTime = Date.now();
saveSettingsDebounced(); requestSave();
refreshChatList(); refreshChatList();
checkGroupSummaryReminder(groupChat); checkGroupSummaryReminder(groupChat);
} catch (err) { } catch (err) {
hideGroupTypingIndicator(); hideGroupTypingIndicator();
console.error('[可乐] 群聊批量消息 AI 调用失败:', err); console.error('[可乐] 群聊批量消息 AI 调用失败:', err);
saveSettingsDebounced(); requestSave();
refreshChatList(); refreshChatList();
appendGroupMessage('assistant', `⚠️ ${err.message}`, '系统', null); appendGroupMessage('assistant', `⚠️ ${err.message}`, '系统', null);
} }

1733
group-red-packet.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
* 历史回顾和日志功能 * 历史回顾和日志功能
*/ */
import { saveSettingsDebounced } from '../../../../script.js'; import { requestSave } from './save-manager.js';
import { getSettings, LOREBOOK_NAME_PREFIX, LOREBOOK_NAME_SUFFIX } from './config.js'; import { getSettings, LOREBOOK_NAME_PREFIX, LOREBOOK_NAME_SUFFIX } from './config.js';
import { escapeHtml } from './utils.js'; import { escapeHtml } from './utils.js';
import { showToast } from './toast.js'; import { showToast } from './toast.js';
@@ -48,7 +48,7 @@ export function addErrorLog(error, context = '') {
settings.errorLogs = settings.errorLogs.slice(0, MAX_LOGS); settings.errorLogs = settings.errorLogs.slice(0, MAX_LOGS);
} }
saveSettingsDebounced(); requestSave();
return logEntry; return logEntry;
} }
@@ -56,7 +56,7 @@ export function addErrorLog(error, context = '') {
export function clearErrorLogs() { export function clearErrorLogs() {
const settings = getSettings(); const settings = getSettings();
settings.errorLogs = []; settings.errorLogs = [];
saveSettingsDebounced(); requestSave();
} }
// 刷新日志列表显示 // 刷新日志列表显示
@@ -185,7 +185,7 @@ export function toggleHistoryItem(index, enabled) {
const settings = getSettings(); const settings = getSettings();
if (settings.selectedLorebooks?.[index]) { if (settings.selectedLorebooks?.[index]) {
settings.selectedLorebooks[index].enabled = enabled; settings.selectedLorebooks[index].enabled = enabled;
saveSettingsDebounced(); requestSave();
showToast(enabled ? '已启用' : '已禁用'); showToast(enabled ? '已启用' : '已禁用');
} }
} }
@@ -257,7 +257,7 @@ export function showHistoryDetail(index) {
const entryIdx = parseInt(toggle.dataset.entryIndex); const entryIdx = parseInt(toggle.dataset.entryIndex);
if (settings.selectedLorebooks?.[index]?.entries?.[entryIdx]) { if (settings.selectedLorebooks?.[index]?.entries?.[entryIdx]) {
settings.selectedLorebooks[index].entries[entryIdx].enabled = toggle.checked; settings.selectedLorebooks[index].entries[entryIdx].enabled = toggle.checked;
saveSettingsDebounced(); requestSave();
} }
}); });
}); });

49
icons.js Normal file
View File

@@ -0,0 +1,49 @@
/**
* SVG 图标定义
* 用于替换 emoji保持视觉一致性
*/
// 红包图标 (替换 🧧)
export const ICON_RED_PACKET = `<svg viewBox="0 0 24 24" width="1em" height="1em" style="vertical-align: -0.125em;"><rect x="4" y="2" width="16" height="20" rx="2" fill="#e53935"/><rect x="4" y="6" width="16" height="4" fill="#c62828"/><circle cx="12" cy="8" r="3" fill="#ffca28"/><path d="M12 11v4M10 13h4" stroke="#c62828" stroke-width="1.5" stroke-linecap="round"/></svg>`;
// 成功/勾选图标 (替换 ✅)
export const ICON_SUCCESS = `<svg viewBox="0 0 24 24" width="1em" height="1em" style="vertical-align: -0.125em;"><circle cx="12" cy="12" r="10" fill="#4caf50"/><path d="M8 12l3 3 5-6" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>`;
// 退还箭头图标 (替换 ↩️)
export const ICON_REFUND = `<svg viewBox="0 0 24 24" width="1em" height="1em" style="vertical-align: -0.125em;"><path d="M9 11l-4 4 4 4" stroke="#1976d2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/><path d="M5 15h10a4 4 0 000-8h-2" stroke="#1976d2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>`;
// 提示/警告图标 (替换 🧊 - 改为感叹号)
export const ICON_INFO = `<svg viewBox="0 0 24 24" width="1em" height="1em" style="vertical-align: -0.125em;"><circle cx="12" cy="12" r="10" fill="#2196f3"/><path d="M12 8v4M12 16h.01" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>`;
// 用户头像图标 (替换 👤)
export const ICON_USER = `<svg viewBox="0 0 24 24" width="1em" height="1em" style="vertical-align: -0.125em;"><circle cx="12" cy="12" r="10" fill="#9e9e9e"/><circle cx="12" cy="10" r="3" fill="#fff"/><path d="M6 21v-1a6 6 0 0112 0v1" fill="#fff"/></svg>`;
// HTML 版本 (用于直接插入 HTML)
export const ICON_RED_PACKET_HTML = `<span class="wechat-svg-icon wechat-icon-red-packet">${ICON_RED_PACKET}</span>`;
export const ICON_SUCCESS_HTML = `<span class="wechat-svg-icon wechat-icon-success">${ICON_SUCCESS}</span>`;
export const ICON_REFUND_HTML = `<span class="wechat-svg-icon wechat-icon-refund">${ICON_REFUND}</span>`;
export const ICON_INFO_HTML = `<span class="wechat-svg-icon wechat-icon-info">${ICON_INFO}</span>`;
export const ICON_USER_HTML = `<span class="wechat-svg-icon wechat-icon-user">${ICON_USER}</span>`;
// 大尺寸版本 (用于红包弹窗等需要大图标的地方)
export const ICON_RED_PACKET_LARGE = `<svg viewBox="0 0 24 24" width="48" height="48"><rect x="4" y="2" width="16" height="20" rx="2" fill="#e53935"/><rect x="4" y="6" width="16" height="4" fill="#c62828"/><circle cx="12" cy="8" r="3" fill="#ffca28"/><path d="M12 11v4M10 13h4" stroke="#c62828" stroke-width="1.5" stroke-linecap="round"/></svg>`;
// 获取图标函数
export function getIcon(type, size = 'normal') {
const icons = {
'red-packet': size === 'large' ? ICON_RED_PACKET_LARGE : ICON_RED_PACKET,
'success': ICON_SUCCESS,
'refund': ICON_REFUND,
'info': ICON_INFO,
'user': ICON_USER
};
return icons[type] || '';
}
// 获取 HTML 包装的图标
export function getIconHTML(type, size = 'normal') {
const icon = getIcon(type, size);
if (!icon) return '';
const sizeClass = size === 'large' ? 'wechat-icon-large' : '';
return `<span class="wechat-svg-icon wechat-icon-${type} ${sizeClass}">${icon}</span>`;
}

1282
listen-together.js Normal file

File diff suppressed because it is too large Load Diff

177
main.js
View File

@@ -4,12 +4,13 @@
console.log('[可乐] main.js 开始加载...'); console.log('[可乐] main.js 开始加载...');
import { saveSettingsDebounced } from '../../../../script.js'; import { requestSave, setupUnloadSave } from './save-manager.js';
import { loadSettings, getSettings, MEME_PROMPT_TEMPLATE } from './config.js'; import { loadSettings, getSettings, MEME_PROMPT_TEMPLATE } from './config.js';
import { generatePhoneHTML } from './phone-html.js'; import { generatePhoneHTML } from './phone-html.js';
import { showPage, refreshChatList, updateMePageInfo, getUserPersonaFromST, updateTabBadge } from './ui.js'; import { showPage, refreshChatList, updateMePageInfo, getUserPersonaFromST, updateTabBadge } from './ui.js';
import { showToast } from './toast.js'; import { showToast } from './toast.js';
import { ICON_SUCCESS, ICON_INFO } from './icons.js';
import { addContact, refreshContactsList, openContactSettings, saveContactSettings, closeContactSettings, changeContactAvatar, getCurrentEditingContactIndex } from './contacts.js'; import { addContact, refreshContactsList, openContactSettings, saveContactSettings, closeContactSettings, changeContactAvatar, getCurrentEditingContactIndex } from './contacts.js';
import { openChatByContactId, setCurrentChatIndex, sendMessage, showRecalledMessages, currentChatIndex, openChat } from './chat.js'; import { openChatByContactId, setCurrentChatIndex, sendMessage, showRecalledMessages, currentChatIndex, openChat } from './chat.js';
@@ -30,6 +31,10 @@ import { getCurrentTime } from './utils.js';
import { refreshHistoryList, refreshLogsList, clearErrorLogs, initErrorCapture, addErrorLog } from './history-logs.js'; import { refreshHistoryList, refreshLogsList, clearErrorLogs, initErrorCapture, addErrorLog } from './history-logs.js';
import { initChatBackground } from './chat-background.js'; import { initChatBackground } from './chat-background.js';
import { initMoments, openMomentsPage, clearContactMoments } from './moments.js'; import { initMoments, openMomentsPage, clearContactMoments } from './moments.js';
import { initRedPacketEvents } from './red-packet.js';
import { initTransferEvents } from './transfer.js';
import { initGroupRedPacket } from './group-red-packet.js';
import { initCropper } from './cropper.js';
function normalizeModelListForSelect(models) { function normalizeModelListForSelect(models) {
return (models || []).map(m => { return (models || []).map(m => {
@@ -76,7 +81,7 @@ async function refreshModelSelect() {
const apiKey = document.getElementById('wechat-api-key')?.value?.trim() || settings.apiKey || ''; const apiKey = document.getElementById('wechat-api-key')?.value?.trim() || settings.apiKey || '';
if (!apiUrl) { if (!apiUrl) {
showToast('请先填写 API 地址', '🧊'); showToast('请先填写 API 地址', 'info');
return; return;
} }
@@ -94,7 +99,7 @@ async function refreshModelSelect() {
modelIds.map(id => `<option value="${id}">${id}</option>`).join(''); modelIds.map(id => `<option value="${id}">${id}</option>`).join('');
settings.modelList = modelIds; settings.modelList = modelIds;
saveSettingsDebounced(); requestSave();
showToast(`获取到 ${modelIds.length} 个模型`); showToast(`获取到 ${modelIds.length} 个模型`);
} catch (err) { } catch (err) {
console.error('[可乐] 获取模型列表失败:', err); console.error('[可乐] 获取模型列表失败:', err);
@@ -248,7 +253,7 @@ function bindEvents() {
if (groupChat) { if (groupChat) {
groupChat.chatHistory = []; groupChat.chatHistory = [];
groupChat.lastMessage = ''; groupChat.lastMessage = '';
saveSettingsDebounced(); requestSave();
openGroupChat(groupIndex); // 刷新群聊界面 openGroupChat(groupIndex); // 刷新群聊界面
showToast('群聊记录已清空'); showToast('群聊记录已清空');
} }
@@ -264,7 +269,7 @@ function bindEvents() {
if (contact) { if (contact) {
contact.chatHistory = []; contact.chatHistory = [];
contact.lastMessage = ''; contact.lastMessage = '';
saveSettingsDebounced(); requestSave();
openChat(currentChatIndex); // 刷新聊天界面 openChat(currentChatIndex); // 刷新聊天界面
showToast('聊天记录已清空'); showToast('聊天记录已清空');
} }
@@ -369,7 +374,7 @@ function bindEvents() {
if (contentDiv) { if (contentDiv) {
contentDiv.classList.toggle('hidden', !settings.autoInjectPrompt); contentDiv.classList.toggle('hidden', !settings.autoInjectPrompt);
} }
saveSettingsDebounced(); requestSave();
if (settings.autoInjectPrompt) injectAuthorNote(); if (settings.autoInjectPrompt) injectAuthorNote();
}); });
@@ -377,7 +382,7 @@ function bindEvents() {
document.getElementById('wechat-save-author-note')?.addEventListener('click', () => { document.getElementById('wechat-save-author-note')?.addEventListener('click', () => {
const settings = getSettings(); const settings = getSettings();
settings.authorNoteCustom = document.getElementById('wechat-author-note-content')?.value || ''; settings.authorNoteCustom = document.getElementById('wechat-author-note-content')?.value || '';
saveSettingsDebounced(); requestSave();
showToast('作者注释模板已保存'); showToast('作者注释模板已保存');
}); });
@@ -391,7 +396,7 @@ function bindEvents() {
if (contentDiv) { if (contentDiv) {
contentDiv.classList.toggle('hidden', !settings.hakimiBreakLimit); contentDiv.classList.toggle('hidden', !settings.hakimiBreakLimit);
} }
saveSettingsDebounced(); requestSave();
showToast(settings.hakimiBreakLimit ? '哈基米破限已开启' : '哈基米破限已关闭'); showToast(settings.hakimiBreakLimit ? '哈基米破限已开启' : '哈基米破限已关闭');
}); });
@@ -399,7 +404,7 @@ function bindEvents() {
document.getElementById('wechat-save-hakimi')?.addEventListener('click', () => { document.getElementById('wechat-save-hakimi')?.addEventListener('click', () => {
const settings = getSettings(); const settings = getSettings();
settings.hakimiCustomPrompt = document.getElementById('wechat-hakimi-prompt')?.value || ''; settings.hakimiCustomPrompt = document.getElementById('wechat-hakimi-prompt')?.value || '';
saveSettingsDebounced(); requestSave();
showToast('破限提示词已保存'); showToast('破限提示词已保存');
}); });
@@ -415,7 +420,7 @@ function bindEvents() {
settings.memeStickersEnabled = !settings.memeStickersEnabled; settings.memeStickersEnabled = !settings.memeStickersEnabled;
const toggle = document.getElementById('wechat-meme-stickers-toggle'); const toggle = document.getElementById('wechat-meme-stickers-toggle');
toggle?.classList.toggle('on', settings.memeStickersEnabled); toggle?.classList.toggle('on', settings.memeStickersEnabled);
saveSettingsDebounced(); requestSave();
showToast(settings.memeStickersEnabled ? 'Meme表情包已启用' : 'Meme表情包已禁用'); showToast(settings.memeStickersEnabled ? 'Meme表情包已启用' : 'Meme表情包已禁用');
}); });
@@ -532,7 +537,7 @@ function bindEvents() {
const fetchBtn = document.getElementById('wechat-contact-fetch-model'); const fetchBtn = document.getElementById('wechat-contact-fetch-model');
if (!apiUrl) { if (!apiUrl) {
showToast('请先填写API地址', '🧊'); showToast('请先填写API地址', 'info');
return; return;
} }
@@ -547,7 +552,7 @@ function bindEvents() {
modelList.innerHTML = models.map(m => `<option value="${m}">`).join(''); modelList.innerHTML = models.map(m => `<option value="${m}">`).join('');
showToast(`获取到 ${models.length} 个模型`); showToast(`获取到 ${models.length} 个模型`);
} else { } else {
showToast('未找到可用模型', '🧊'); showToast('未找到可用模型', 'info');
} }
} catch (err) { } catch (err) {
console.error('[可乐] 获取模型失败:', err); console.error('[可乐] 获取模型失败:', err);
@@ -566,11 +571,11 @@ function bindEvents() {
const testBtn = document.getElementById('wechat-contact-test-api'); const testBtn = document.getElementById('wechat-contact-test-api');
if (!apiUrl) { if (!apiUrl) {
showToast('请先填写API地址', '🧊'); showToast('请先填写API地址', 'info');
return; return;
} }
if (!model) { if (!model) {
showToast('请先填写或选择模型', '🧊'); showToast('请先填写或选择模型', 'info');
return; return;
} }
@@ -601,7 +606,7 @@ function bindEvents() {
const data = await response.json(); const data = await response.json();
const reply = data.choices?.[0]?.message?.content || ''; const reply = data.choices?.[0]?.message?.content || '';
showToast(`连接成功!回复: ${reply.substring(0, 20)}...`); showToast(`连接成功!回复: ${reply.substring(0, 20)}...`, 'success');
} catch (err) { } catch (err) {
console.error('[可乐] 测试连接失败:', err); console.error('[可乐] 测试连接失败:', err);
showToast('❌ 连接失败: ' + err.message, '⚠️'); showToast('❌ 连接失败: ' + err.message, '⚠️');
@@ -622,7 +627,7 @@ function bindEvents() {
if (contentDiv) { if (contentDiv) {
contentDiv.classList.toggle('hidden', !settings.groupAutoInjectPrompt); contentDiv.classList.toggle('hidden', !settings.groupAutoInjectPrompt);
} }
saveSettingsDebounced(); requestSave();
showToast(settings.groupAutoInjectPrompt ? '群聊提示词注入已开启' : '群聊提示词注入已关闭'); showToast(settings.groupAutoInjectPrompt ? '群聊提示词注入已开启' : '群聊提示词注入已关闭');
}); });
@@ -630,7 +635,7 @@ function bindEvents() {
document.getElementById('wechat-save-group-note')?.addEventListener('click', () => { document.getElementById('wechat-save-group-note')?.addEventListener('click', () => {
const settings = getSettings(); const settings = getSettings();
settings.userGroupAuthorNote = document.getElementById('wechat-group-author-note')?.value || ''; settings.userGroupAuthorNote = document.getElementById('wechat-group-author-note')?.value || '';
saveSettingsDebounced(); requestSave();
showToast('群聊作者注释已保存'); showToast('群聊作者注释已保存');
}); });
@@ -722,6 +727,10 @@ function bindEvents() {
initEmojiPanel(); initEmojiPanel();
initChatBackground(); initChatBackground();
initMoments(); initMoments();
initRedPacketEvents();
initTransferEvents();
initGroupRedPacket();
initCropper();
// 展开面板 // 展开面板
document.getElementById('wechat-expand-close')?.addEventListener('click', closeExpandPanel); document.getElementById('wechat-expand-close')?.addEventListener('click', closeExpandPanel);
@@ -816,7 +825,7 @@ function bindEvents() {
document.getElementById('wechat-context-enabled')?.addEventListener('change', (e) => { document.getElementById('wechat-context-enabled')?.addEventListener('change', (e) => {
const settings = getSettings(); const settings = getSettings();
settings.contextEnabled = e.target.checked; settings.contextEnabled = e.target.checked;
saveSettingsDebounced(); requestSave();
syncContextEnabledUI(settings.contextEnabled); syncContextEnabledUI(settings.contextEnabled);
}); });
@@ -824,7 +833,7 @@ function bindEvents() {
document.getElementById('wechat-context-slider')?.addEventListener('input', (e) => { document.getElementById('wechat-context-slider')?.addEventListener('input', (e) => {
const settings = getSettings(); const settings = getSettings();
settings.contextLevel = parseInt(e.target.value); settings.contextLevel = parseInt(e.target.value);
saveSettingsDebounced(); requestSave();
document.getElementById('wechat-context-value').textContent = e.target.value; document.getElementById('wechat-context-value').textContent = e.target.value;
}); });
@@ -836,7 +845,7 @@ function bindEvents() {
const index = parseInt(e.target.dataset.index); const index = parseInt(e.target.dataset.index);
if (Array.isArray(settings.contextTags) && index >= 0 && index < settings.contextTags.length) { if (Array.isArray(settings.contextTags) && index >= 0 && index < settings.contextTags.length) {
settings.contextTags.splice(index, 1); settings.contextTags.splice(index, 1);
saveSettingsDebounced(); requestSave();
refreshContextTags(); refreshContextTags();
} }
return; return;
@@ -848,7 +857,7 @@ function bindEvents() {
settings.contextTags = Array.isArray(settings.contextTags) ? settings.contextTags : []; settings.contextTags = Array.isArray(settings.contextTags) ? settings.contextTags : [];
if (!settings.contextTags.includes(tagName.trim())) { if (!settings.contextTags.includes(tagName.trim())) {
settings.contextTags.push(tagName.trim()); settings.contextTags.push(tagName.trim());
saveSettingsDebounced(); requestSave();
refreshContextTags(); refreshContextTags();
} }
} }
@@ -861,11 +870,54 @@ function bindEvents() {
const amount = input?.value || '0.00'; const amount = input?.value || '0.00';
const settings = getSettings(); const settings = getSettings();
settings.walletAmount = amount; settings.walletAmount = amount;
saveSettingsDebounced(); requestSave();
updateWalletAmountDisplay(); updateWalletAmountDisplay();
document.getElementById('wechat-wallet-panel')?.classList.add('hidden'); document.getElementById('wechat-wallet-panel')?.classList.add('hidden');
}); });
// 支付密码保存
document.getElementById('wechat-save-password-btn')?.addEventListener('click', () => {
const input = document.getElementById('wechat-new-password-input');
const password = input?.value || '';
// 验证是否为6位数字
if (!/^\d{6}$/.test(password)) {
showToast('请输入6位数字密码', 'info');
return;
}
const settings = getSettings();
settings.paymentPassword = password;
requestSave();
showToast('密码已保存', '✓');
document.getElementById('wechat-change-password-panel')?.classList.add('hidden');
input.value = '';
});
// 密码输入框只允许数字
document.getElementById('wechat-new-password-input')?.addEventListener('input', (e) => {
e.target.value = e.target.value.replace(/\D/g, '').slice(0, 6);
});
// 总结模板保存
document.getElementById('wechat-summary-template-save')?.addEventListener('click', () => {
const input = document.getElementById('wechat-summary-template-input');
const template = input?.value || '';
const settings = getSettings();
settings.customSummaryTemplate = template;
requestSave();
showToast('模板已保存', '✓');
document.getElementById('wechat-summary-template-panel')?.classList.add('hidden');
});
// 总结模板恢复默认
document.getElementById('wechat-summary-template-reset')?.addEventListener('click', () => {
const input = document.getElementById('wechat-summary-template-input');
if (input) input.value = '';
const settings = getSettings();
settings.customSummaryTemplate = '';
requestSave();
showToast('已恢复默认模板', '✓');
});
// 总结 API 配置 // 总结 API 配置
document.getElementById('wechat-summary-key-toggle')?.addEventListener('click', () => { document.getElementById('wechat-summary-key-toggle')?.addEventListener('click', () => {
const input = document.getElementById('wechat-summary-key'); const input = document.getElementById('wechat-summary-key');
@@ -879,7 +931,7 @@ function bindEvents() {
const modelSelect = document.getElementById('wechat-summary-model'); const modelSelect = document.getElementById('wechat-summary-model');
if (!url || !key) { if (!url || !key) {
if (statusEl) statusEl.textContent = '🧊 请先填写 URL 和 Key'; if (statusEl) statusEl.innerHTML = `${ICON_INFO} 请先填写 URL 和 Key`;
return; return;
} }
@@ -888,7 +940,7 @@ function bindEvents() {
try { try {
const models = await fetchModelListFromApi(url, key); const models = await fetchModelListFromApi(url, key);
if (models.length === 0) { if (models.length === 0) {
if (statusEl) statusEl.textContent = '🧊 未找到可用模型'; if (statusEl) statusEl.innerHTML = `${ICON_INFO} 未找到可用模型`;
return; return;
} }
@@ -899,9 +951,9 @@ function bindEvents() {
const settings = getSettings(); const settings = getSettings();
settings.summaryModelList = models; settings.summaryModelList = models;
saveSettingsDebounced(); requestSave();
if (statusEl) statusEl.textContent = ` 获取到 ${models.length} 个模型`; if (statusEl) statusEl.innerHTML = `${ICON_SUCCESS} 获取到 ${models.length} 个模型`;
} catch (err) { } catch (err) {
console.error('[可乐] 获取模型列表失败:', err); console.error('[可乐] 获取模型列表失败:', err);
if (statusEl) statusEl.textContent = `⚠️ 获取失败: ${err.message}`; if (statusEl) statusEl.textContent = `⚠️ 获取失败: ${err.message}`;
@@ -915,11 +967,11 @@ function bindEvents() {
const model = document.getElementById('wechat-summary-model')?.value; const model = document.getElementById('wechat-summary-model')?.value;
if (!url || !key) { if (!url || !key) {
if (statusEl) statusEl.textContent = '🧊 请先填写 URL 和 Key'; if (statusEl) statusEl.innerHTML = `${ICON_INFO} 请先填写 URL 和 Key`;
return; return;
} }
if (!model) { if (!model) {
if (statusEl) statusEl.textContent = '🧊 请先选择模型'; if (statusEl) statusEl.innerHTML = `${ICON_INFO} 请先选择模型`;
return; return;
} }
@@ -938,7 +990,7 @@ function bindEvents() {
throw new Error(errData.error?.message || `HTTP ${response.status}`); throw new Error(errData.error?.message || `HTTP ${response.status}`);
} }
if (statusEl) statusEl.textContent = '✅ 连接成功!'; if (statusEl) statusEl.innerHTML = `${ICON_SUCCESS} 连接成功!`;
} catch (err) { } catch (err) {
console.error('[可乐] 测试连接失败:', err); console.error('[可乐] 测试连接失败:', err);
if (statusEl) statusEl.textContent = `⚠️ 连接失败: ${err.message}`; if (statusEl) statusEl.textContent = `⚠️ 连接失败: ${err.message}`;
@@ -955,16 +1007,16 @@ function bindEvents() {
settings.summaryApiUrl = urlInput?.value?.trim() || ''; settings.summaryApiUrl = urlInput?.value?.trim() || '';
settings.summaryApiKey = keyInput?.value?.trim() || ''; settings.summaryApiKey = keyInput?.value?.trim() || '';
settings.summarySelectedModel = modelSelect?.value || ''; settings.summarySelectedModel = modelSelect?.value || '';
saveSettingsDebounced(); requestSave();
if (statusEl) statusEl.textContent = '✅ 配置已保存'; if (statusEl) statusEl.innerHTML = `${ICON_SUCCESS} 配置已保存`;
setTimeout(() => document.getElementById('wechat-summary-panel')?.classList.add('hidden'), 1500); setTimeout(() => document.getElementById('wechat-summary-panel')?.classList.add('hidden'), 1500);
}); });
document.getElementById('wechat-summary-model')?.addEventListener('change', (e) => { document.getElementById('wechat-summary-model')?.addEventListener('change', (e) => {
const settings = getSettings(); const settings = getSettings();
settings.summarySelectedModel = e.target.value; settings.summarySelectedModel = e.target.value;
saveSettingsDebounced(); requestSave();
}); });
document.getElementById('wechat-summary-execute')?.addEventListener('click', () => { document.getElementById('wechat-summary-execute')?.addEventListener('click', () => {
@@ -993,14 +1045,6 @@ function bindEvents() {
selectAllSummaryChats(false); selectAllSummaryChats(false);
}); });
// 发现页面 - 待开发功能点击提示
document.querySelectorAll('.wechat-discover-item-disabled').forEach(item => {
item.addEventListener('click', () => {
const feature = item.dataset.feature || '此功能';
showToast(`${feature}」正在开发中...`);
});
});
// 发现页面 - 朋友圈点击 // 发现页面 - 朋友圈点击
document.getElementById('wechat-discover-moments')?.addEventListener('click', () => { document.getElementById('wechat-discover-moments')?.addEventListener('click', () => {
openMomentsPage(); openMomentsPage();
@@ -1011,7 +1055,7 @@ function bindEvents() {
item.addEventListener('click', () => { item.addEventListener('click', () => {
const service = item.dataset.service; const service = item.dataset.service;
// 关闭其他面板 // 关闭其他面板
const allPanels = ['wechat-context-panel', 'wechat-wallet-panel', 'wechat-summary-panel', 'wechat-history-panel', 'wechat-logs-panel', 'wechat-meme-stickers-panel']; const allPanels = ['wechat-context-panel', 'wechat-wallet-panel', 'wechat-summary-panel', 'wechat-history-panel', 'wechat-logs-panel', 'wechat-meme-stickers-panel', 'wechat-change-password-panel', 'wechat-summary-template-panel'];
if (service === 'summary') { if (service === 'summary') {
allPanels.filter(p => p !== 'wechat-summary-panel').forEach(p => document.getElementById(p)?.classList.add('hidden')); allPanels.filter(p => p !== 'wechat-summary-panel').forEach(p => document.getElementById(p)?.classList.add('hidden'));
@@ -1053,8 +1097,22 @@ function bindEvents() {
return; return;
} }
if (service === 'change-password') {
allPanels.filter(p => p !== 'wechat-change-password-panel').forEach(p => document.getElementById(p)?.classList.add('hidden'));
const panel = document.getElementById('wechat-change-password-panel');
panel?.classList.toggle('hidden');
return;
}
if (service === 'summary-template') {
allPanels.filter(p => p !== 'wechat-summary-template-panel').forEach(p => document.getElementById(p)?.classList.add('hidden'));
const panel = document.getElementById('wechat-summary-template-panel');
panel?.classList.toggle('hidden');
return;
}
const label = item.querySelector('span')?.textContent || '该'; const label = item.querySelector('span')?.textContent || '该';
showToast(`"${label}" 功能开发中...`, '🧊'); showToast(`"${label}" 功能开发中...`, 'info');
}); });
}); });
@@ -1194,7 +1252,7 @@ function bindEvents() {
if (!confirm('确定要清空所有联系人吗?')) return; if (!confirm('确定要清空所有联系人吗?')) return;
const settings = getSettings(); const settings = getSettings();
settings.contacts = []; settings.contacts = [];
saveSettingsDebounced(); requestSave();
refreshContactsList(); refreshContactsList();
showToast('已清空所有联系人'); showToast('已清空所有联系人');
}); });
@@ -1213,7 +1271,7 @@ function bindEvents() {
reader.onload = function (event) { reader.onload = function (event) {
const settings = getSettings(); const settings = getSettings();
settings.userAvatar = event.target.result; settings.userAvatar = event.target.result;
saveSettingsDebounced(); requestSave();
updateMePageInfo(); updateMePageInfo();
showToast('头像已更换'); showToast('头像已更换');
}; };
@@ -1251,7 +1309,7 @@ function bindEvents() {
settings.apiUrl = apiUrl; settings.apiUrl = apiUrl;
settings.apiKey = apiKey; settings.apiKey = apiKey;
settings.selectedModel = selectedModel; settings.selectedModel = selectedModel;
saveSettingsDebounced(); requestSave();
showToast('API 配置已保存'); showToast('API 配置已保存');
}); });
@@ -1267,12 +1325,12 @@ function bindEvents() {
modelInput.addEventListener('change', (e) => { modelInput.addEventListener('change', (e) => {
const settings = getSettings(); const settings = getSettings();
settings.selectedModel = e.target.value.trim(); settings.selectedModel = e.target.value.trim();
saveSettingsDebounced(); requestSave();
}); });
modelInput.addEventListener('input', (e) => { modelInput.addEventListener('input', (e) => {
const settings = getSettings(); const settings = getSettings();
settings.selectedModel = e.target.value.trim(); settings.selectedModel = e.target.value.trim();
saveSettingsDebounced(); requestSave();
}); });
} }
@@ -1282,7 +1340,7 @@ function bindEvents() {
const apiKey = document.getElementById('wechat-api-key')?.value.trim() || ''; const apiKey = document.getElementById('wechat-api-key')?.value.trim() || '';
if (!apiUrl) { if (!apiUrl) {
showToast('请先填写 API 地址', '🧊'); showToast('请先填写 API 地址', 'info');
return; return;
} }
@@ -1321,7 +1379,7 @@ function bindEvents() {
const settings = getSettings(); const settings = getSettings();
settings.selectedModel = modelName.trim(); settings.selectedModel = modelName.trim();
saveSettingsDebounced(); requestSave();
showToast('模型已设置'); showToast('模型已设置');
} }
} }
@@ -1352,7 +1410,7 @@ function bindEvents() {
const select = document.getElementById('wechat-group-model-select'); const select = document.getElementById('wechat-group-model-select');
if (!apiUrl) { if (!apiUrl) {
showToast('请先填写群聊 API 地址', '🧊'); showToast('请先填写群聊 API 地址', 'info');
return; return;
} }
@@ -1372,7 +1430,7 @@ function bindEvents() {
} }
settings.groupModelList = modelIds; settings.groupModelList = modelIds;
saveSettingsDebounced(); requestSave();
showToast(`获取到 ${modelIds.length} 个模型`); showToast(`获取到 ${modelIds.length} 个模型`);
} catch (err) { } catch (err) {
console.error('[可乐] 获取群聊模型列表失败:', err); console.error('[可乐] 获取群聊模型列表失败:', err);
@@ -1400,7 +1458,7 @@ function bindEvents() {
const settings = getSettings(); const settings = getSettings();
settings.groupSelectedModel = modelName.trim(); settings.groupSelectedModel = modelName.trim();
saveSettingsDebounced(); requestSave();
showToast('群聊模型已设置'); showToast('群聊模型已设置');
} }
} }
@@ -1412,7 +1470,7 @@ function bindEvents() {
const apiKey = document.getElementById('wechat-group-api-key')?.value.trim() || ''; const apiKey = document.getElementById('wechat-group-api-key')?.value.trim() || '';
if (!apiUrl) { if (!apiUrl) {
showToast('请先填写群聊 API 地址', '🧊'); showToast('请先填写群聊 API 地址', 'info');
return; return;
} }
@@ -1446,7 +1504,7 @@ function bindEvents() {
settings.groupApiUrl = apiUrl; settings.groupApiUrl = apiUrl;
settings.groupApiKey = apiKey; settings.groupApiKey = apiKey;
settings.groupSelectedModel = selectedModel; settings.groupSelectedModel = selectedModel;
saveSettingsDebounced(); requestSave();
showToast('群聊 API 配置已保存'); showToast('群聊 API 配置已保存');
}); });
@@ -1457,12 +1515,12 @@ function bindEvents() {
groupModelInput.addEventListener('change', (e) => { groupModelInput.addEventListener('change', (e) => {
const settings = getSettings(); const settings = getSettings();
settings.groupSelectedModel = e.target.value.trim(); settings.groupSelectedModel = e.target.value.trim();
saveSettingsDebounced(); requestSave();
}); });
groupModelInput.addEventListener('input', (e) => { groupModelInput.addEventListener('input', (e) => {
const settings = getSettings(); const settings = getSettings();
settings.groupSelectedModel = e.target.value.trim(); settings.groupSelectedModel = e.target.value.trim();
saveSettingsDebounced(); requestSave();
}); });
} }
@@ -1481,7 +1539,7 @@ function bindEvents() {
const settings = getSettings(); const settings = getSettings();
settings.summarySelectedModel = modelName.trim(); settings.summarySelectedModel = modelName.trim();
saveSettingsDebounced(); requestSave();
showToast('总结模型已设置'); showToast('总结模型已设置');
} }
} }
@@ -1524,7 +1582,7 @@ function init() {
loadSettings(); loadSettings();
const settings = getSettings(); const settings = getSettings();
if (seedDefaultUserPersonaFromST(settings)) { if (seedDefaultUserPersonaFromST(settings)) {
saveSettingsDebounced(); requestSave();
} }
const phoneHTML = generatePhoneHTML(); const phoneHTML = generatePhoneHTML();
@@ -1559,6 +1617,9 @@ function init() {
// 初始化错误捕获 // 初始化错误捕获
initErrorCapture(); initErrorCapture();
// 初始化页面卸载保存
setupUnloadSave();
setInterval(() => { setInterval(() => {
const phone = document.getElementById('wechat-phone'); const phone = document.getElementById('wechat-phone');
if (!phone || phone.classList.contains('hidden')) return; if (!phone || phone.classList.contains('hidden')) return;

View File

@@ -3,7 +3,7 @@
*/ */
import { getSettings, SUMMARY_MARKER_PREFIX, splitAIMessages } from './config.js'; import { getSettings, SUMMARY_MARKER_PREFIX, splitAIMessages } from './config.js';
import { saveSettingsDebounced } from '../../../../script.js'; import { requestSave } from './save-manager.js';
import { currentChatIndex, openChat, showTypingIndicator, hideTypingIndicator, appendMessage } from './chat.js'; import { currentChatIndex, openChat, showTypingIndicator, hideTypingIndicator, appendMessage } from './chat.js';
import { showToast } from './toast.js'; import { showToast } from './toast.js';
import { getContext } from '../../../extensions.js'; import { getContext } from '../../../extensions.js';
@@ -23,6 +23,7 @@ let pendingQuote = null;
// 菜单项配置 // 菜单项配置
const menuItems = [ const menuItems = [
{ id: 'copy', icon: 'copy', text: '复制' }, { id: 'copy', icon: 'copy', text: '复制' },
{ id: 'transcribe', icon: 'transcribe', text: '转文字', voiceOnly: true },
{ id: 'quote', icon: 'quote', text: '引用' }, { id: 'quote', icon: 'quote', text: '引用' },
{ id: 'recall', icon: 'recall', text: '撤回', userOnly: true }, { id: 'recall', icon: 'recall', text: '撤回', userOnly: true },
{ id: 'delete', icon: 'delete', text: '删除' }, { id: 'delete', icon: 'delete', text: '删除' },
@@ -35,6 +36,12 @@ const icons = {
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/> <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/> <path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
</svg>`, </svg>`,
transcribe: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
<line x1="12" y1="19" x2="12" y2="23"/>
<line x1="8" y1="23" x2="16" y2="23"/>
</svg>`,
quote: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"> quote: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V21z"/> <path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V21z"/>
<path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v4z"/> <path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v4z"/>
@@ -56,7 +63,7 @@ const icons = {
}; };
// 创建菜单DOM // 创建菜单DOM
function createMenuElement(isUserMessage = false) { function createMenuElement(isUserMessage = false, isVoiceMessage = false, voiceTextVisible = false) {
const menu = document.createElement('div'); const menu = document.createElement('div');
menu.className = 'wechat-msg-menu hidden'; menu.className = 'wechat-msg-menu hidden';
menu.id = 'wechat-msg-menu'; menu.id = 'wechat-msg-menu';
@@ -67,13 +74,22 @@ function createMenuElement(isUserMessage = false) {
menuItems.forEach(item => { menuItems.forEach(item => {
// 跳过仅用户可用的菜单项(如果当前不是用户消息) // 跳过仅用户可用的菜单项(如果当前不是用户消息)
if (item.userOnly && !isUserMessage) return; if (item.userOnly && !isUserMessage) return;
// 跳过仅语音消息可用的菜单项(如果当前不是语音消息)
if (item.voiceOnly && !isVoiceMessage) return;
const menuItem = document.createElement('div'); const menuItem = document.createElement('div');
menuItem.className = 'wechat-msg-menu-item'; menuItem.className = 'wechat-msg-menu-item';
menuItem.dataset.action = item.id; menuItem.dataset.action = item.id;
// 转文字按钮根据状态显示不同文本
let text = item.text;
if (item.id === 'transcribe' && voiceTextVisible) {
text = '收起文字';
}
menuItem.innerHTML = ` menuItem.innerHTML = `
<div class="wechat-msg-menu-icon">${icons[item.id]}</div> <div class="wechat-msg-menu-icon">${icons[item.id]}</div>
<div class="wechat-msg-menu-text">${item.text}</div> <div class="wechat-msg-menu-text">${text}</div>
`; `;
menuContent.appendChild(menuItem); menuContent.appendChild(menuItem);
}); });
@@ -111,12 +127,33 @@ export function showMessageMenu(msgElement, msgIndex, event) {
isUserMessage = roleAttr === 'user'; isUserMessage = roleAttr === 'user';
} }
// 检测是否是语音消息
const voiceBubble = msgElement.classList?.contains('wechat-voice-bubble')
? msgElement
: msgElement.querySelector?.('.wechat-voice-bubble');
const isVoiceMessage = !!voiceBubble || msg?.isVoice === true;
// 检测语音转文字是否已显示
let voiceTextVisible = false;
if (voiceBubble) {
const voiceId = voiceBubble.dataset?.voiceId;
if (voiceId) {
const textEl = document.getElementById(voiceId);
voiceTextVisible = textEl?.classList.contains('visible') || false;
}
}
// 移除旧菜单并创建新菜单(根据消息类型动态生成) // 移除旧菜单并创建新菜单(根据消息类型动态生成)
let menu = document.getElementById('wechat-msg-menu'); let menu = document.getElementById('wechat-msg-menu');
if (menu) { if (menu) {
menu.remove(); menu.remove();
} }
menu = createMenuElement(isUserMessage); menu = createMenuElement(isUserMessage, isVoiceMessage, voiceTextVisible);
// 存储语音相关数据
if (voiceBubble) {
menu.dataset.voiceId = voiceBubble.dataset?.voiceId || '';
menu.dataset.voiceContent = voiceBubble.dataset?.voiceContent || '';
}
document.querySelector('.wechat-phone').appendChild(menu); document.querySelector('.wechat-phone').appendChild(menu);
bindMenuEvents(menu); bindMenuEvents(menu);
@@ -185,13 +222,16 @@ function bindMenuEvents(menu) {
if (!menuItem) return; if (!menuItem) return;
const action = menuItem.dataset.action; const action = menuItem.dataset.action;
handleMenuAction(action, currentMenuMsgIndex); // 传递菜单上存储的语音数据
const voiceId = menu.dataset.voiceId;
const voiceContent = menu.dataset.voiceContent;
handleMenuAction(action, currentMenuMsgIndex, voiceId, voiceContent);
hideMessageMenu(); hideMessageMenu();
}); });
} }
// 处理菜单操作 // 处理菜单操作
function handleMenuAction(action, msgIndex) { function handleMenuAction(action, msgIndex, voiceId = '', voiceContent = '') {
const settings = getSettings(); const settings = getSettings();
const groupIndex = getCurrentGroupIndex(); const groupIndex = getCurrentGroupIndex();
let chatHistory, contact, groupChat; let chatHistory, contact, groupChat;
@@ -215,6 +255,22 @@ function handleMenuAction(action, msgIndex) {
case 'copy': case 'copy':
copyMessage(msg.content); copyMessage(msg.content);
break; break;
case 'transcribe':
// 切换语音转文字显示
if (voiceId) {
const textEl = document.getElementById(voiceId);
if (textEl) {
const isVisible = textEl.classList.contains('visible');
if (isVisible) {
textEl.classList.remove('visible');
textEl.classList.add('hidden');
} else {
textEl.classList.remove('hidden');
textEl.classList.add('visible');
}
}
}
break;
case 'quote': case 'quote':
quoteMessage(msg, groupIndex >= 0, groupChat); quoteMessage(msg, groupIndex >= 0, groupChat);
break; break;
@@ -258,6 +314,12 @@ function copyMessage(content) {
// 引用消息 - 设置待引用状态 // 引用消息 - 设置待引用状态
function quoteMessage(msg, isGroupChat = false, groupChat = null) { function quoteMessage(msg, isGroupChat = false, groupChat = null) {
// 不允许引用撤回的消息
if (msg.isRecalled) {
showToast('无法引用已撤回的消息');
return;
}
const settings = getSettings(); const settings = getSettings();
const context = getContext(); const context = getContext();
@@ -391,7 +453,7 @@ export function setQuote(quote) {
// 删除消息 // 删除消息
function deleteMessage(msgIndex, contact) { function deleteMessage(msgIndex, contact) {
contact.chatHistory.splice(msgIndex, 1); contact.chatHistory.splice(msgIndex, 1);
saveSettingsDebounced(); requestSave();
// 刷新聊天界面 // 刷新聊天界面
openChat(currentChatIndex); openChat(currentChatIndex);
showToast('已删除'); showToast('已删除');
@@ -413,7 +475,7 @@ async function recallMessage(msgIndex, contact) {
msg.originalContent = msg.content; msg.originalContent = msg.content;
msg.content = ''; msg.content = '';
saveSettingsDebounced(); requestSave();
// 刷新聊天界面 // 刷新聊天界面
openChat(currentChatIndex); openChat(currentChatIndex);
showToast('已撤回'); showToast('已撤回');
@@ -423,7 +485,22 @@ async function recallMessage(msgIndex, contact) {
showTypingIndicator(contact); showTypingIndicator(contact);
const { callAI } = await import('./ai.js'); const { callAI } = await import('./ai.js');
const aiResponse = await callAI(contact, '[用户撤回了一条消息]'); // 随机决定是否"看到"了撤回的消息50%几率)
const sawMessage = Math.random() < 0.5;
const originalContent = msg.originalContent || '一条消息';
// 截取前30个字符作为提示
const contentHint = originalContent.length > 30 ? originalContent.substring(0, 30) + '...' : originalContent;
let aiPrompt;
if (sawMessage) {
// 看到了:可以追问内容,也可以假装没看到
aiPrompt = `[用户撤回了一条消息,你刚好看到了内容是:「${contentHint}」。你可以选择1.假装没看到 2.好奇追问"刚才说什么?" 3.直接回应看到的内容 4.调侃用户撤回。根据你的性格和内容选择合适的反应,不要每次都一样]`;
} else {
// 没看到:只能好奇或者忽略
aiPrompt = `[用户撤回了一条消息你没来得及看到内容。你可以选择1.好奇追问"撤什么?" 2.调侃"撤回也没用我看到了"(即使没看到) 3.无视继续聊别的 4.发表情包。根据你的性格选择合适的反应,不要每次都一样]`;
}
const aiResponse = await callAI(contact, aiPrompt);
hideTypingIndicator(); hideTypingIndicator();
@@ -455,7 +532,7 @@ async function recallMessage(msgIndex, contact) {
} }
contact.lastMessage = aiMessages[aiMessages.length - 1]; contact.lastMessage = aiMessages[aiMessages.length - 1];
saveSettingsDebounced(); requestSave();
} catch (err) { } catch (err) {
hideTypingIndicator(); hideTypingIndicator();
@@ -469,7 +546,7 @@ function deleteGroupMessage(msgIndex, groupChat) {
if (groupIndex < 0) return; if (groupIndex < 0) return;
groupChat.chatHistory.splice(msgIndex, 1); groupChat.chatHistory.splice(msgIndex, 1);
saveSettingsDebounced(); requestSave();
// 刷新群聊界面 // 刷新群聊界面
openGroupChat(groupIndex); openGroupChat(groupIndex);
showToast('已删除'); showToast('已删除');
@@ -494,7 +571,7 @@ async function recallGroupMessage(msgIndex, groupChat) {
msg.originalContent = msg.content; msg.originalContent = msg.content;
msg.content = ''; msg.content = '';
saveSettingsDebounced(); requestSave();
// 刷新群聊界面 // 刷新群聊界面
openGroupChat(groupIndex); openGroupChat(groupIndex);
showToast('已撤回'); showToast('已撤回');
@@ -502,7 +579,8 @@ async function recallGroupMessage(msgIndex, groupChat) {
// 绑定消息气泡事件 // 绑定消息气泡事件
export function bindMessageBubbleEvents(container) { export function bindMessageBubbleEvents(container) {
const bubbles = container.querySelectorAll('.wechat-message-bubble, .wechat-voice-bubble'); // 只绑定普通消息气泡,语音气泡由 bindVoiceBubbleEvents 单独处理
const bubbles = container.querySelectorAll('.wechat-message-bubble');
bubbles.forEach((bubble, index) => { bubbles.forEach((bubble, index) => {
if (bubble.dataset.menuBound) return; if (bubble.dataset.menuBound) return;
@@ -522,8 +600,6 @@ export function bindMessageBubbleEvents(container) {
isLongPress = false; isLongPress = false;
return; return;
} }
// 语音气泡点击展开文本,不显示菜单
if (bubble.classList.contains('wechat-voice-bubble')) return;
e.stopPropagation(); e.stopPropagation();
showMessageMenu(bubble, getRealMsgIndex(container, msgElement), e); showMessageMenu(bubble, getRealMsgIndex(container, msgElement), e);

View File

@@ -6,11 +6,12 @@
* - 用户评论后角色会回复 * - 用户评论后角色会回复
*/ */
import { saveSettingsDebounced } from '../../../../script.js'; import { requestSave } from './save-manager.js';
import { getContext } from '../../../extensions.js'; import { getContext } from '../../../extensions.js';
import { getSettings } from './config.js'; import { getSettings } from './config.js';
import { showToast, showNotificationBanner } from './toast.js'; import { showToast, showNotificationBanner } from './toast.js';
import { sleep } from './utils.js'; import { sleep } from './utils.js';
import { selectAndCrop } from './cropper.js';
// 当前正在查看的联系人索引 // 当前正在查看的联系人索引
let currentContactIndex = null; let currentContactIndex = null;
@@ -175,34 +176,26 @@ function updateMomentsProfile(contactIndex) {
* 更换朋友圈封面 * 更换朋友圈封面
*/ */
function changeMomentsCover() { function changeMomentsCover() {
const input = document.createElement('input'); // 使用裁剪器选择并裁剪封面16:9比例
input.type = 'file'; selectAndCrop(16 / 9, (croppedImage) => {
input.accept = 'image/*'; const settings = getSettings();
input.onchange = async (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
const settings = getSettings();
if (currentContactIndex !== null && settings.contacts[currentContactIndex]) { if (currentContactIndex !== null && settings.contacts[currentContactIndex]) {
settings.contacts[currentContactIndex].momentsCover = event.target.result; settings.contacts[currentContactIndex].momentsCover = croppedImage;
} else { } else {
settings.momentsCover = event.target.result; settings.momentsCover = croppedImage;
}
saveSettingsDebounced();
const coverEl = document.getElementById('wechat-moments-cover');
if (coverEl) {
coverEl.style.backgroundImage = `url(${event.target.result})`;
const placeholder = coverEl.querySelector('.wechat-moments-cover-placeholder');
if (placeholder) placeholder.style.display = 'none';
}
};
reader.readAsDataURL(file);
} }
}; requestSave();
input.click();
const coverEl = document.getElementById('wechat-moments-cover');
if (coverEl) {
coverEl.style.backgroundImage = `url(${croppedImage})`;
const placeholder = coverEl.querySelector('.wechat-moments-cover-placeholder');
if (placeholder) placeholder.style.display = 'none';
}
showToast('封面已更换');
});
} }
/** /**
@@ -614,7 +607,7 @@ function toggleLike(momentIndex) {
targetMoment.likes.push(userName); targetMoment.likes.push(userName);
} }
saveSettingsDebounced(); requestSave();
renderMomentsList(currentContactIndex); renderMomentsList(currentContactIndex);
} }
@@ -756,15 +749,25 @@ async function sendUserComment() {
targetMoment.comments.push(newComment); targetMoment.comments.push(newComment);
saveSettingsDebounced(); requestSave();
hideCommentInput(); hideCommentInput();
renderMomentsList(currentContactIndex); renderMomentsList(currentContactIndex);
// 触发角色回复(异步)- 只有联系人的朋友圈才会回复 // 触发角色回复(异步)
if (contactIndexForReply !== null && targetContactId !== 'user') { if (contactIndexForReply !== null && targetContactId !== 'user') {
// 情况1联系人的朋友圈 - 联系人回复用户
setTimeout(() => { setTimeout(() => {
generateContactReplyToComment(contactIndexForReply, targetMomentIndex, userName, commentText); generateContactReplyToComment(contactIndexForReply, targetMomentIndex, userName, commentText);
}, 1000 + Math.random() * 2000); }, 1000 + Math.random() * 2000);
} else if (targetContactId === 'user' && currentReplyTo) {
// 情况2用户自己的朋友圈 - 用户回复了某个联系人的评论
// 找到被回复的联系人并触发他们的回复
const repliedContactIndex = settings.contacts?.findIndex(c => c.name === currentReplyTo);
if (repliedContactIndex >= 0) {
setTimeout(() => {
generateContactReplyToUserMomentComment(repliedContactIndex, targetMomentIndex, userName, commentText, currentReplyTo);
}, 1000 + Math.random() * 2000);
}
} }
} }
@@ -853,11 +856,34 @@ function getLorebookEntriesForContact(contact, settings) {
return entries; return entries;
} }
/**
* 清理评论内容移除AI可能生成的格式标签
* @param {string} text - 原始评论内容
* @returns {string} - 清理后的评论内容
*/
function cleanCommentText(text) {
if (!text) return '';
let cleaned = text.trim();
// 移除 [评论 xxx] 或 [评论 xxx] 格式tab或空格分隔
cleaned = cleaned.replace(/^\[评论[\s\t]+[^\]]+\]\s*/i, '');
// 移除 [评论:xxx] 或 [评论xxx] 格式
cleaned = cleaned.replace(/^\[评论[:][^\]]*\]\s*/i, '');
// 移除开头的引号
cleaned = cleaned.replace(/^["「『]/, '').replace(/["」』]$/, '');
return cleaned.trim();
}
/** /**
* 从联系人的世界书中提取可用于评论的人物 * 从联系人的世界书中提取可用于评论的人物
*/ */
function extractCharactersFromLorebook(contact) { function extractCharactersFromLorebook(contact) {
const settings = getSettings(); const settings = getSettings();
const context = getContext();
const characters = []; const characters = [];
// 获取联系人的角色数据 // 获取联系人的角色数据
@@ -865,6 +891,9 @@ function extractCharactersFromLorebook(contact) {
const charData = rawData.data || rawData; const charData = rawData.data || rawData;
const charName = charData.name || contact.name || ''; const charName = charData.name || contact.name || '';
// 获取用户名,用于排除用户
const userName = context?.name1 || settings.wechatId || '';
// 方法1: 从 selectedLorebooks 中查找与当前角色匹配的世界书 // 方法1: 从 selectedLorebooks 中查找与当前角色匹配的世界书
const selectedLorebooks = settings.selectedLorebooks || []; const selectedLorebooks = settings.selectedLorebooks || [];
@@ -889,8 +918,8 @@ function extractCharactersFromLorebook(contact) {
// 提取所有有内容的条目,不再限制名称长度和关键词过滤 // 提取所有有内容的条目,不再限制名称长度和关键词过滤
if (entry.keys && entry.keys.length > 0) { if (entry.keys && entry.keys.length > 0) {
const name = entry.keys[0]; const name = entry.keys[0];
// 排除角色本人,其他条目全部包含 // 排除角色本人和用户
if (name && name !== charName) { if (name && name !== charName && name !== userName) {
characters.push({ characters.push({
name: name, name: name,
content: entry.content || '' content: entry.content || ''
@@ -909,8 +938,8 @@ function extractCharactersFromLorebook(contact) {
// 提取所有有内容的条目 // 提取所有有内容的条目
if (entry.keys && entry.keys.length > 0) { if (entry.keys && entry.keys.length > 0) {
const name = entry.keys[0]; const name = entry.keys[0];
// 排除角色本人 // 排除角色本人和用户
if (name && name !== charName) { if (name && name !== charName && name !== userName) {
characters.push({ characters.push({
name: name, name: name,
content: entry.content || '' content: entry.content || ''
@@ -959,25 +988,19 @@ export async function generateNewMomentForContact(contactIndex) {
if (!settings.momentsData) settings.momentsData = {}; if (!settings.momentsData) settings.momentsData = {};
if (!settings.momentsData[contact.id]) settings.momentsData[contact.id] = []; if (!settings.momentsData[contact.id]) settings.momentsData[contact.id] = [];
// 提取世界书中的人物用于评论 // 创建新动态不自动生成评论等用户主动评论后AI再回复
const characters = extractCharactersFromLorebook(contact);
// 随机生成 3-4 条评论
const comments = await generateCommentsFromCharacters(contact, momentContent.text, characters);
// 创建新动态
const newMoment = { const newMoment = {
id: Date.now().toString(), id: Date.now().toString(),
text: momentContent.text, text: momentContent.text,
images: momentContent.images || [], images: momentContent.images || [],
timestamp: Date.now(), timestamp: Date.now(),
likes: [], likes: [],
comments: comments comments: []
}; };
// 添加到列表开头 // 添加到列表开头
settings.momentsData[contact.id].unshift(newMoment); settings.momentsData[contact.id].unshift(newMoment);
saveSettingsDebounced(); requestSave();
showNotificationBanner('微信', `${contact.name}发布了一条朋友圈`); showNotificationBanner('微信', `${contact.name}发布了一条朋友圈`);
renderMomentsList(currentContactIndex); renderMomentsList(currentContactIndex);
@@ -993,6 +1016,7 @@ export async function generateNewMomentForContact(contactIndex) {
*/ */
async function generateMomentContent(contact) { async function generateMomentContent(contact) {
const settings = getSettings(); const settings = getSettings();
const context = getContext();
// 获取 API 配置 // 获取 API 配置
let apiUrl, apiKey, apiModel; let apiUrl, apiKey, apiModel;
@@ -1020,12 +1044,56 @@ async function generateMomentContent(contact) {
chatUrl += '/chat/completions'; chatUrl += '/chat/completions';
} }
// 获取角色世界书设定
const lorebookEntries = getLorebookEntriesForContact(contact, settings);
let characterInfo = '';
if (lorebookEntries.length > 0) {
characterInfo = `\n【关于「${contact.name}」的设定】\n${lorebookEntries.join('\n')}\n`;
console.log(`[可乐] 朋友圈生成 - ${contact.name} 获取到 ${lorebookEntries.length} 条设定`);
}
// 获取用户设定
let userPersonaInfo = '';
const userName = context?.name1 || settings.wechatId || '用户';
const userPersonas = settings.userPersonas || [];
const enabledPersonas = userPersonas.filter(p => p.enabled !== false);
if (enabledPersonas.length > 0) {
userPersonaInfo = `\n【关于「${userName}」的设定(你认识的人)】\n`;
enabledPersonas.forEach(persona => {
if (persona.name) userPersonaInfo += `[${persona.name}]\n`;
if (persona.content) userPersonaInfo += `${persona.content}\n`;
});
console.log(`[可乐] 朋友圈生成 - 读取到 ${enabledPersonas.length} 条用户设定`);
}
// 获取聊天历史上下文读取最近30条消息确保朋友圈内容与聊天相关
let chatContextInfo = '';
if (contact.chatHistory && contact.chatHistory.length > 0) {
const recentChat = contact.chatHistory
.filter(msg => msg.content && !msg.isRecalled && msg.content.length < 200)
.slice(-30);
if (recentChat.length > 0) {
const chatSummary = recentChat.map(msg => {
const speaker = msg.role === 'user' ? userName : contact.name;
let c = msg.content;
if (c.startsWith('[表情:') || c.startsWith('[语音:') || c.startsWith('[照片:')) c = c.split(']')[0] + ']';
return `${speaker}: ${c.substring(0, 60)}${c.length > 60 ? '...' : ''}`;
}).join('\n');
chatContextInfo = `\n【你和${userName}最近的聊天记录(重要!朋友圈内容要与此相关)】\n${chatSummary}\n`;
console.log(`[可乐] 朋友圈生成 - ${contact.name} 加入了 ${recentChat.length} 条聊天历史`);
}
}
// 随机决定是纯文字还是带图片60%带图40%纯文字) // 随机决定是纯文字还是带图片60%带图40%纯文字)
const withImages = Math.random() < 0.6; const withImages = Math.random() < 0.6;
const imageCount = withImages ? (1 + Math.floor(Math.random() * 4)) : 0; // 1-4张图 const imageCount = withImages ? (1 + Math.floor(Math.random() * 4)) : 0; // 1-4张图
const prompt = `你正在扮演「${contact.name}」,请以这个角色的身份发一条朋友圈动态。 // 随机决定这条朋友圈是否与聊天相关75%聊天相关25%个人日常)
const isChatRelated = Math.random() < 0.75;
console.log(`[可乐] 朋友圈生成 - ${contact.name} 类型: ${isChatRelated ? '与聊天相关(75%)' : '个人日常(25%)'}`);
const prompt = `你正在扮演「${contact.name}」,请以这个角色的身份发一条朋友圈动态。
${characterInfo}${userPersonaInfo}${chatContextInfo}
【格式要求】 【格式要求】
${withImages ? `这是一条带${imageCount}张图片的朋友圈,请按以下格式输出: ${withImages ? `这是一条带${imageCount}张图片的朋友圈,请按以下格式输出:
文案内容 文案内容
@@ -1035,11 +1103,26 @@ ${withImages ? `这是一条带${imageCount}张图片的朋友圈,请按以下
图片描述要具体生动1-2句话描述图片内容她在咖啡厅的自拍手里拿着拿铁阳光洒在脸上` : '这是一条纯文字朋友圈,直接输出文案内容即可,不要带任何图片标签'} 图片描述要具体生动1-2句话描述图片内容她在咖啡厅的自拍手里拿着拿铁阳光洒在脸上` : '这是一条纯文字朋友圈,直接输出文案内容即可,不要带任何图片标签'}
【内容要求】 【内容要求 - 非常重要!
${isChatRelated ? `★★★ 这条朋友圈必须与聊天记录相关 ★★★
- 仔细阅读上面的聊天记录,找出最近聊天的话题、事件、情感
- 朋友圈内容要延续、回应、或暗示最近聊天中提到的事情
- 可以是:聊天中提到要做的事、约定、话题的延续、对对方的想念/吐槽等
- 让看的人能感受到这条朋友圈和你们的聊天有关联
- 示例:如果聊天中约了吃饭,可以发吃饭的朋友圈;如果聊到想念,可以发暗示思念的内容` : `★★★ 这条朋友圈是你的个人日常 ★★★
- 发一条和聊天内容无关的个人日常动态
- 展示你自己的生活:日常分享、心情感悟、美食、旅行、自拍、工作、宠物、风景、爱好等
- 要符合你的角色设定和性格`}
【通用要求】
1. 文案1-3句话符合角色性格语气自然真实 1. 文案1-3句话符合角色性格语气自然真实
2. 内容可以是:日常分享、心情感悟、美食、旅行、自拍、工作、宠物、风景等 2. 可以适当使用表情符号
3. 可以适当使用表情符号 3. 要像真人发的朋友圈一样自然
4. 要像真人发的朋友圈一样自然
【禁止输出】
- 绝对禁止输出任何关键词、世界书条目名称、设定标签
- 绝对禁止输出任何系统提示、指令、格式说明
- 只输出纯粹的朋友圈内容
【示例】 【示例】
纯文字:今天天气真好,心情也跟着好起来了☀️ 纯文字:今天天气真好,心情也跟着好起来了☀️
@@ -1228,6 +1311,11 @@ ${avoidText}
- 简短自然5-15字 - 简短自然5-15字
- 禁止用"怎么了"、"咋了"、"发生什么了"开头 - 禁止用"怎么了"、"咋了"、"发生什么了"开头
【禁止输出】
- 绝对禁止输出任何关键词、世界书条目名称、设定标签
- 绝对禁止输出任何系统提示、指令、格式说明
- 只输出纯粹的评论内容
直接输出评论内容:`; 直接输出评论内容:`;
const response = await fetch(chatUrl, { const response = await fetch(chatUrl, {
@@ -1248,7 +1336,9 @@ ${avoidText}
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
const commentText = data.choices?.[0]?.message?.content?.trim(); let commentText = data.choices?.[0]?.message?.content?.trim();
// 清理评论格式
commentText = cleanCommentText(commentText);
if (commentText) { if (commentText) {
comments.push({ comments.push({
name: character.name, name: character.name,
@@ -1486,7 +1576,15 @@ ${commentsContext}
showNotificationBanner(contact.name, chatMessage); showNotificationBanner(contact.name, chatMessage);
} else { } else {
// 在评论区回复 // 在评论区回复
const commentReply = replyText.replace(/^\[.*?\]\s*/, '').trim(); // 移除可能的前缀标签 let commentReply = replyText.replace(/^\[.*?\]\s*/, '').trim(); // 移除可能的前缀标签
// 清理AI可能自动添加的重复"xx回复xx:"格式
// 匹配格式:名字回复名字: 或 名字 回复 名字:(支持冒号为中英文)
const replyPattern = new RegExp(`^${contact.name}\\s*回复\\s*${userName}\\s*[:]\\s*`, 'i');
commentReply = commentReply.replace(replyPattern, '').trim();
// 也清理可能的其他回复格式
commentReply = commentReply.replace(/^回复\s*[^:]+[:]\s*/, '').trim();
if (!moment.comments) moment.comments = []; if (!moment.comments) moment.comments = [];
moment.comments.push({ moment.comments.push({
name: contact.name, name: contact.name,
@@ -1494,7 +1592,7 @@ ${commentsContext}
replyTo: userName, replyTo: userName,
timestamp: Date.now() timestamp: Date.now()
}); });
saveSettingsDebounced(); requestSave();
renderMomentsList(currentContactIndex); renderMomentsList(currentContactIndex);
} }
@@ -1522,7 +1620,7 @@ export function addMomentToContact(contactId, momentData) {
}; };
settings.momentsData[contactId].unshift(newMoment); settings.momentsData[contactId].unshift(newMoment);
saveSettingsDebounced(); requestSave();
} }
/** /**
@@ -1555,9 +1653,9 @@ export function clearContactMoments(contactIndex) {
// 清空该联系人的朋友圈 // 清空该联系人的朋友圈
settings.momentsData[contact.id] = []; settings.momentsData[contact.id] = [];
saveSettingsDebounced(); requestSave();
showToast(`已清空 ${momentCount} 条朋友圈`, ''); showToast(`已清空 ${momentCount} 条朋友圈`, 'success');
console.log(`[可乐] 已清空 ${contact.name}${momentCount} 条朋友圈`); console.log(`[可乐] 已清空 ${contact.name}${momentCount} 条朋友圈`);
} }
@@ -1732,9 +1830,9 @@ function publishUserMomentWithImages(text, images) {
}; };
settings.momentsData[userId].unshift(newMoment); settings.momentsData[userId].unshift(newMoment);
saveSettingsDebounced(); requestSave();
showToast('朋友圈已发布', ''); showToast('朋友圈已发布', 'success');
renderMomentsList(null); renderMomentsList(null);
// 通知所有联系人(可能触发他们的评论/点赞) // 通知所有联系人(可能触发他们的评论/点赞)
@@ -1776,8 +1874,8 @@ function deleteUserMoment(index) {
} }
// 删除该联系人的指定朋友圈 // 删除该联系人的指定朋友圈
settings.momentsData[contact.id].splice(index, 1); settings.momentsData[contact.id].splice(index, 1);
saveSettingsDebounced(); requestSave();
showToast('已删除', ''); showToast('已删除', 'success');
renderMomentsList(currentContactIndex); renderMomentsList(currentContactIndex);
} else { } else {
// 查看所有朋友圈(合并视图) // 查看所有朋友圈(合并视图)
@@ -1802,8 +1900,8 @@ function deleteUserMoment(index) {
// 从对应联系人的朋友圈数组中删除 // 从对应联系人的朋友圈数组中删除
settings.momentsData[targetMoment.contactId].splice(targetMoment.originalIndex, 1); settings.momentsData[targetMoment.contactId].splice(targetMoment.originalIndex, 1);
saveSettingsDebounced(); requestSave();
showToast('已删除', ''); showToast('已删除', 'success');
renderMomentsList(null); renderMomentsList(null);
} }
} }
@@ -1831,7 +1929,7 @@ async function triggerContactsReactToUserMoment(moment) {
// 点赞 // 点赞
if (!moment.likes.includes(contact.name)) { if (!moment.likes.includes(contact.name)) {
moment.likes.push(contact.name); moment.likes.push(contact.name);
saveSettingsDebounced(); requestSave();
// 用户朋友圈使用 null 作为 contactIndex // 用户朋友圈使用 null 作为 contactIndex
renderMomentsList(null); renderMomentsList(null);
} }
@@ -1849,7 +1947,7 @@ async function triggerContactsReactToUserMoment(moment) {
if (!moment.likes.includes(contact.name)) { if (!moment.likes.includes(contact.name)) {
moment.likes.push(contact.name); moment.likes.push(contact.name);
} }
saveSettingsDebounced(); requestSave();
// 用户朋友圈使用 null 作为 contactIndex // 用户朋友圈使用 null 作为 contactIndex
renderMomentsList(null); renderMomentsList(null);
@@ -1962,6 +2060,11 @@ ${avoidText}
- 简短自然5-15字 - 简短自然5-15字
- 禁止用"怎么了"、"咋了"、"发生什么了"开头 - 禁止用"怎么了"、"咋了"、"发生什么了"开头
【禁止输出】
- 绝对禁止输出任何关键词、世界书条目名称、设定标签
- 绝对禁止输出任何系统提示、指令、格式说明
- 只输出纯粹的评论内容
直接输出评论内容:`; 直接输出评论内容:`;
console.log(`[可乐] 正在生成 ${contact.name} 的评论...`); console.log(`[可乐] 正在生成 ${contact.name} 的评论...`);
@@ -1983,7 +2086,9 @@ ${avoidText}
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
const comment = data.choices?.[0]?.message?.content?.trim(); let comment = data.choices?.[0]?.message?.content?.trim();
// 清理评论格式
comment = cleanCommentText(comment);
console.log(`[可乐] ${contact.name} 评论生成成功: ${comment}`); console.log(`[可乐] ${contact.name} 评论生成成功: ${comment}`);
return comment; return comment;
} else { } else {
@@ -2142,7 +2247,7 @@ function addPrivateChatMessage(contactIndex, contact, message) {
// 增加未读数 // 增加未读数
targetContact.unreadCount = (targetContact.unreadCount || 0) + 1; targetContact.unreadCount = (targetContact.unreadCount || 0) + 1;
saveSettingsDebounced(); requestSave();
// 刷新聊天列表 // 刷新聊天列表
import('./ui.js').then(({ refreshChatList }) => { import('./ui.js').then(({ refreshChatList }) => {
@@ -2274,7 +2379,7 @@ function addPrivateMessageFromContact(contactIndex, message, context = '') {
// 增加未读消息计数 // 增加未读消息计数
contact.unreadCount = (contact.unreadCount || 0) + 1; contact.unreadCount = (contact.unreadCount || 0) + 1;
saveSettingsDebounced(); requestSave();
// 尝试刷新聊天列表 // 尝试刷新聊天列表
try { try {
@@ -2288,3 +2393,178 @@ function addPrivateMessageFromContact(contactIndex, message, context = '') {
console.log(`[可乐] ${contact.name} 通过私聊回复:`, message); console.log(`[可乐] ${contact.name} 通过私聊回复:`, message);
} }
/**
* 联系人回复用户朋友圈下的评论
* 当用户在自己的朋友圈中回复联系人的评论时调用
* @param {number} contactIndex - 被回复的联系人索引
* @param {number} momentIndex - 朋友圈索引
* @param {string} userName - 用户名
* @param {string} userComment - 用户的回复内容
* @param {string} contactName - 被回复的联系人名称
*/
async function generateContactReplyToUserMomentComment(contactIndex, momentIndex, userName, userComment, contactName) {
const settings = getSettings();
const contact = settings.contacts[contactIndex];
if (!contact || !settings.momentsData) return;
// 用户的朋友圈存储在 'user' 键下
const moments = settings.momentsData['user'];
if (!moments || !moments[momentIndex]) return;
const moment = moments[momentIndex];
// 获取 API 配置
let apiUrl, apiKey, apiModel;
if (contact.useCustomApi) {
apiUrl = contact.customApiUrl || settings.apiUrl || '';
apiKey = contact.customApiKey || settings.apiKey || '';
apiModel = contact.customModel || settings.selectedModel || '';
} else {
apiUrl = settings.apiUrl || '';
apiKey = settings.apiKey || '';
apiModel = settings.selectedModel || '';
}
if (!apiUrl) return;
// 处理 API URL确保正确拼接
let chatUrl = apiUrl.replace(/\/+$/, '');
if (!chatUrl.includes('/chat/completions')) {
if (!chatUrl.endsWith('/v1')) {
chatUrl += '/v1';
}
chatUrl += '/chat/completions';
}
try {
// 获取角色世界书设定
const lorebookEntries = getLorebookEntriesForContact(contact, settings);
let characterInfo = '';
if (lorebookEntries.length > 0) {
characterInfo = `\n\n【关于「${contact.name}」的设定】\n${lorebookEntries.join('\n')}`;
console.log(`[可乐] 用户朋友圈回复 - ${contact.name} 获取到 ${lorebookEntries.length} 条设定`);
}
// 获取用户设定
let userPersonaInfo = '';
const userPersonas = settings.userPersonas || [];
const enabledPersonas = userPersonas.filter(p => p.enabled !== false);
if (enabledPersonas.length > 0) {
userPersonaInfo = `\n\n【关于「${userName}」的设定】\n`;
enabledPersonas.forEach(persona => {
if (persona.name) userPersonaInfo += `[${persona.name}]\n`;
if (persona.content) userPersonaInfo += `${persona.content}\n`;
});
}
// 获取聊天历史上下文
let chatContextInfo = '';
if (contact.chatHistory && contact.chatHistory.length > 0) {
const allChat = contact.chatHistory
.filter(msg => msg.content && !msg.isRecalled && msg.content.length < 200);
if (allChat.length > 0) {
const chatSummary = allChat.map(msg => {
const speaker = msg.role === 'user' ? userName : contact.name;
let c = msg.content;
if (c.startsWith('[表情:') || c.startsWith('[语音:') || c.startsWith('[照片:')) c = c.split(']')[0] + ']';
return `${speaker}: ${c.substring(0, 60)}${c.length > 60 ? '...' : ''}`;
}).join('\n');
chatContextInfo = `\n\n【你和${userName}的聊天记录】\n${chatSummary}`;
console.log(`[可乐] 用户朋友圈回复 - ${contact.name} 加入了 ${allChat.length} 条聊天历史`);
}
}
// 已有评论列表
const existingComments = (moment.comments || []).map(c => {
const replyPart = c.replyTo ? `回复${c.replyTo}` : '';
return `${c.name}${replyPart}: ${c.text}`;
}).join('\n');
const commentsContext = existingComments ? `\n\n【已有评论】\n${existingComments}` : '';
const prompt = `你是「${contact.name}」,${userName}在他/她自己的朋友圈下回复了你的评论,你必须回复他/她。
${characterInfo}${userPersonaInfo}${chatContextInfo}
${userName}发的朋友圈:
"${moment.text}"
${commentsContext}
${userName}」刚刚回复你说:"${userComment}"
【核心要求】
- 必须回复!你必须选择以下两种方式之一进行回复,不能忽略
- 严格遵循你的人设:说话方式、语气、口癖、性格特点
- 回复简短自然5-20字
- 可以用表情符号
【回复方式二选一】
1. 评论区回复(公开):直接输出回复内容
2. 私聊回复(私密的话):输出格式 [私聊] 消息内容
直接输出回复:`;
const response = await fetch(chatUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: apiModel,
messages: [
{ role: 'user', content: prompt }
],
max_tokens: 8196,
temperature: 1
})
});
if (!response.ok) {
console.error(`[可乐] 用户朋友圈回复 API 请求失败: ${response.status}`);
return;
}
const data = await response.json();
const replyText = data.choices?.[0]?.message?.content?.trim();
if (!replyText) {
console.error('[可乐] 用户朋友圈回复 - AI返回空内容');
return;
}
console.log(`[可乐] ${contact.name} 回复用户朋友圈评论: ${replyText}`);
// 判断是私聊还是评论区回复
if (replyText.startsWith('[私聊]')) {
// 通过私聊回复 - 触发聊天消息
const chatMessage = replyText.replace('[私聊]', '').trim();
// 添加到聊天记录
addPrivateMessageFromContact(contactIndex, chatMessage, `关于你的朋友圈评论:「${userComment}`);
showNotificationBanner(contact.name, chatMessage);
} else {
// 在评论区回复
let commentReply = replyText.replace(/^\[.*?\]\s*/, '').trim();
// 清理AI可能自动添加的重复"xx回复xx:"格式
const replyPattern = new RegExp(`^${contact.name}\\s*回复\\s*${userName}\\s*[:]\\s*`, 'i');
commentReply = commentReply.replace(replyPattern, '').trim();
commentReply = commentReply.replace(/^回复\s*[^:]+[:]\s*/, '').trim();
if (!moment.comments) moment.comments = [];
moment.comments.push({
name: contact.name,
text: commentReply,
replyTo: userName,
timestamp: Date.now()
});
requestSave();
renderMomentsList(currentContactIndex);
}
} catch (err) {
console.error('[可乐] 用户朋友圈回复生成失败:', err);
}
}

494
music.js
View File

@@ -1,4 +1,5 @@
import { showToast } from './toast.js'; import { showToast } from './toast.js';
import { escapeHtml } from './utils.js';
const BASE_URL = 'https://music-dl.sayqz.com'; const BASE_URL = 'https://music-dl.sayqz.com';
@@ -34,12 +35,21 @@ const MODE_RANDOM_ICON = '<svg viewBox="0 0 24 24" width="16" height="16" fill="
const MODE_LIST_ICON = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M17 2l4 4-4 4"/><path d="M3 11v-1a4 4 0 014-4h14"/><path d="M7 22l-4-4 4-4"/><path d="M21 13v1a4 4 0 01-4 4H3"/></svg>'; const MODE_LIST_ICON = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M17 2l4 4-4 4"/><path d="M3 11v-1a4 4 0 014-4h14"/><path d="M7 22l-4-4 4-4"/><path d="M21 13v1a4 4 0 01-4 4H3"/></svg>';
const PLAYLIST_ICON = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 6h13M8 12h13M8 18h8"/><circle cx="3" cy="6" r="1" fill="currentColor"/><circle cx="3" cy="12" r="1" fill="currentColor"/><circle cx="3" cy="18" r="1" fill="currentColor"/></svg>'; const PLAYLIST_ICON = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 6h13M8 12h13M8 18h8"/><circle cx="3" cy="6" r="1" fill="currentColor"/><circle cx="3" cy="12" r="1" fill="currentColor"/><circle cx="3" cy="18" r="1" fill="currentColor"/></svg>';
function escapeHtml(text) { // 随机推歌用的热门关键词库
if (!text) return ''; const RANDOM_KEYWORDS = [
const div = document.createElement('div'); '热门', '流行', '抖音', '网红', '经典', '怀旧', '情歌', '伤感',
div.textContent = text; '轻音乐', '纯音乐', '钢琴', '吉他', '民谣', '摇滚', '电音', 'DJ',
return div.innerHTML; '周杰伦', '林俊杰', '邓紫棋', '薛之谦', '毛不易', '陈奕迅', '王菲',
} 'Taylor Swift', 'Ed Sheeran', 'Bruno Mars', 'Adele', 'BTS',
'日语', '韩语', '粤语', '古风', '国风', '说唱', 'rap',
'治愈', '励志', '甜蜜', '浪漫', '夜晚', '清晨', '放松'
];
// 已播放过的歌曲ID避免重复推荐
let playedSongIds = new Set();
// 是否已显示过随机推歌提示
let hasShownRandomToast = false;
export function formatDuration(seconds) { export function formatDuration(seconds) {
if (seconds === null || seconds === undefined || isNaN(seconds)) return '--:--'; if (seconds === null || seconds === undefined || isNaN(seconds)) return '--:--';
@@ -113,18 +123,18 @@ export async function fetchLyrics(song) {
function createSingleLineLyrics() { function createSingleLineLyrics() {
if (document.getElementById('wechat-single-lyrics')) return; if (document.getElementById('wechat-single-lyrics')) return;
var phoneContainer = document.getElementById('wechat-phone'); let phoneContainer = document.getElementById('wechat-phone');
if (!phoneContainer) return; if (!phoneContainer) return;
// 生成颜色按钮HTML // 生成颜色按钮HTML
var colorBtnsHtml = ''; let colorBtnsHtml = '';
for (var i = 0; i < LYRICS_COLORS.length; i++) { for (let i = 0; i < LYRICS_COLORS.length; i++) {
var c = LYRICS_COLORS[i]; let c = LYRICS_COLORS[i];
var activeClass = (c === lyricsColor) ? ' active' : ''; let activeClass = (c === lyricsColor) ? ' active' : '';
colorBtnsHtml += '<button class="wechat-lyrics-color-btn color-' + c + activeClass + '" data-color="' + c + '"></button>'; colorBtnsHtml += '<button class="wechat-lyrics-color-btn color-' + c + activeClass + '" data-color="' + c + '"></button>';
} }
var html = '<div id="wechat-single-lyrics" class="wechat-single-lyrics hidden">' + let html = '<div id="wechat-single-lyrics" class="wechat-single-lyrics hidden">' +
'<div class="wechat-single-lyrics-text color-' + lyricsColor + '">暂无歌词</div>' + '<div class="wechat-single-lyrics-text color-' + lyricsColor + '">暂无歌词</div>' +
'<div class="wechat-single-lyrics-colors">' + colorBtnsHtml + '</div>' + '<div class="wechat-single-lyrics-colors">' + colorBtnsHtml + '</div>' +
'<button class="wechat-single-lyrics-lock">' + UNLOCK_ICON + '</button>' + '<button class="wechat-single-lyrics-lock">' + UNLOCK_ICON + '</button>' +
@@ -135,11 +145,11 @@ function createSingleLineLyrics() {
} }
function initSingleLineLyricsEvents() { function initSingleLineLyricsEvents() {
var panel = document.getElementById('wechat-single-lyrics'); let panel = document.getElementById('wechat-single-lyrics');
if (!panel) return; if (!panel) return;
var lockBtn = panel.querySelector('.wechat-single-lyrics-lock'); let lockBtn = panel.querySelector('.wechat-single-lyrics-lock');
var colorsContainer = panel.querySelector('.wechat-single-lyrics-colors'); let colorsContainer = panel.querySelector('.wechat-single-lyrics-colors');
if (lockBtn) { if (lockBtn) {
lockBtn.addEventListener('click', function(e) { lockBtn.addEventListener('click', function(e) {
@@ -153,27 +163,27 @@ function initSingleLineLyricsEvents() {
// 颜色按钮点击事件 // 颜色按钮点击事件
if (colorsContainer) { if (colorsContainer) {
colorsContainer.addEventListener('click', function(e) { colorsContainer.addEventListener('click', function(e) {
var btn = e.target.closest('.wechat-lyrics-color-btn'); let btn = e.target.closest('.wechat-lyrics-color-btn');
if (!btn) return; if (!btn) return;
e.stopPropagation(); e.stopPropagation();
var newColor = btn.dataset.color; let newColor = btn.dataset.color;
if (newColor && LYRICS_COLORS.indexOf(newColor) >= 0) { if (newColor && LYRICS_COLORS.indexOf(newColor) >= 0) {
lyricsColor = newColor; lyricsColor = newColor;
// 更新文字颜色 // 更新文字颜色
var textEl = panel.querySelector('.wechat-single-lyrics-text'); let textEl = panel.querySelector('.wechat-single-lyrics-text');
if (textEl) { if (textEl) {
// 移除所有颜色类 // 移除所有颜色类
for (var i = 0; i < LYRICS_COLORS.length; i++) { for (let i = 0; i < LYRICS_COLORS.length; i++) {
textEl.classList.remove('color-' + LYRICS_COLORS[i]); textEl.classList.remove('color-' + LYRICS_COLORS[i]);
} }
textEl.classList.add('color-' + newColor); textEl.classList.add('color-' + newColor);
} }
// 更新按钮激活状态 // 更新按钮激活状态
var allBtns = colorsContainer.querySelectorAll('.wechat-lyrics-color-btn'); let allBtns = colorsContainer.querySelectorAll('.wechat-lyrics-color-btn');
for (var j = 0; j < allBtns.length; j++) { for (let j = 0; j < allBtns.length; j++) {
allBtns[j].classList.remove('active'); allBtns[j].classList.remove('active');
} }
btn.classList.add('active'); btn.classList.add('active');
@@ -190,8 +200,8 @@ function initSingleLineLyricsEvents() {
}); });
// 拖拽功能(仅在未锁定时)- 支持上下左右移动 // 拖拽功能(仅在未锁定时)- 支持上下左右移动
var isDragging = false; let isDragging = false;
var startX, startY, initialX, initialY; let startX, startY, initialX, initialY;
panel.addEventListener('mousedown', startDrag); panel.addEventListener('mousedown', startDrag);
panel.addEventListener('touchstart', startDrag, { passive: false }); panel.addEventListener('touchstart', startDrag, { passive: false });
@@ -201,8 +211,8 @@ function initSingleLineLyricsEvents() {
if (e.target.closest('.wechat-single-lyrics-lock')) return; if (e.target.closest('.wechat-single-lyrics-lock')) return;
if (e.target.closest('.wechat-lyrics-color-btn')) return; if (e.target.closest('.wechat-lyrics-color-btn')) return;
isDragging = true; isDragging = true;
var rect = panel.getBoundingClientRect(); let rect = panel.getBoundingClientRect();
var phoneRect = document.getElementById('wechat-phone').getBoundingClientRect(); let phoneRect = document.getElementById('wechat-phone').getBoundingClientRect();
initialX = rect.left - phoneRect.left; initialX = rect.left - phoneRect.left;
initialY = rect.top - phoneRect.top; initialY = rect.top - phoneRect.top;
if (e.type === 'touchstart') { if (e.type === 'touchstart') {
@@ -221,7 +231,7 @@ function initSingleLineLyricsEvents() {
function drag(e) { function drag(e) {
if (!isDragging) return; if (!isDragging) return;
e.preventDefault(); e.preventDefault();
var clientX, clientY; let clientX, clientY;
if (e.type === 'touchmove') { if (e.type === 'touchmove') {
clientX = e.touches[0].clientX; clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY; clientY = e.touches[0].clientY;
@@ -229,13 +239,13 @@ function initSingleLineLyricsEvents() {
clientX = e.clientX; clientX = e.clientX;
clientY = e.clientY; clientY = e.clientY;
} }
var dx = clientX - startX; let dx = clientX - startX;
var dy = clientY - startY; let dy = clientY - startY;
var phoneEl = document.getElementById('wechat-phone'); let phoneEl = document.getElementById('wechat-phone');
var phoneRect = phoneEl.getBoundingClientRect(); let phoneRect = phoneEl.getBoundingClientRect();
var panelWidth = panel.offsetWidth || 200; let panelWidth = panel.offsetWidth || 200;
var newX = Math.max(0, Math.min(phoneRect.width - panelWidth, initialX + dx)); let newX = Math.max(0, Math.min(phoneRect.width - panelWidth, initialX + dx));
var newY = Math.max(0, Math.min(phoneRect.height - 40, initialY + dy)); let newY = Math.max(0, Math.min(phoneRect.height - 40, initialY + dy));
panel.style.left = newX + 'px'; panel.style.left = newX + 'px';
panel.style.top = newY + 'px'; panel.style.top = newY + 'px';
panel.style.transform = 'none'; panel.style.transform = 'none';
@@ -254,7 +264,7 @@ function initSingleLineLyricsEvents() {
function showSingleLineLyrics() { function showSingleLineLyrics() {
createSingleLineLyrics(); createSingleLineLyrics();
var panel = document.getElementById('wechat-single-lyrics'); let panel = document.getElementById('wechat-single-lyrics');
if (panel) { if (panel) {
panel.classList.remove('hidden'); panel.classList.remove('hidden');
singleLineLyricsVisible = true; singleLineLyricsVisible = true;
@@ -263,7 +273,7 @@ function showSingleLineLyrics() {
} }
function hideSingleLineLyrics() { function hideSingleLineLyrics() {
var panel = document.getElementById('wechat-single-lyrics'); let panel = document.getElementById('wechat-single-lyrics');
if (panel) { if (panel) {
panel.classList.add('hidden'); panel.classList.add('hidden');
singleLineLyricsVisible = false; singleLineLyricsVisible = false;
@@ -277,14 +287,14 @@ function toggleSingleLineLyrics() {
showSingleLineLyrics(); showSingleLineLyrics();
} }
// 更新迷你播放器按钮状态 // 更新迷你播放器按钮状态
var lyricsBtn = document.querySelector('.wechat-music-mini-lyrics-btn'); let lyricsBtn = document.querySelector('.wechat-music-mini-lyrics-btn');
if (lyricsBtn) { if (lyricsBtn) {
lyricsBtn.classList.toggle('active', singleLineLyricsVisible); lyricsBtn.classList.toggle('active', singleLineLyricsVisible);
} }
} }
function updateSingleLineLyricsText() { function updateSingleLineLyricsText() {
var textEl = document.querySelector('.wechat-single-lyrics-text'); let textEl = document.querySelector('.wechat-single-lyrics-text');
if (!textEl) return; if (!textEl) return;
if (!currentSong || !currentSong.lyrics) { if (!currentSong || !currentSong.lyrics) {
@@ -305,11 +315,11 @@ function updateSingleLineLyricsText() {
function updateSingleLineLyricsHighlight(currentTime) { function updateSingleLineLyricsHighlight(currentTime) {
if (!singleLineLyricsVisible || parsedLyrics.length === 0) return; if (!singleLineLyricsVisible || parsedLyrics.length === 0) return;
var textEl = document.querySelector('.wechat-single-lyrics-text'); let textEl = document.querySelector('.wechat-single-lyrics-text');
if (!textEl) return; if (!textEl) return;
var activeIndex = -1; let activeIndex = -1;
for (var i = parsedLyrics.length - 1; i >= 0; i--) { for (let i = parsedLyrics.length - 1; i >= 0; i--) {
if (currentTime >= parsedLyrics[i].time) { if (currentTime >= parsedLyrics[i].time) {
activeIndex = i; activeIndex = i;
break; break;
@@ -323,14 +333,14 @@ function updateSingleLineLyricsHighlight(currentTime) {
} }
} }
// ========== 浮动歌词面板(保留但不使用) ========== // ========== 浮动歌词面板 ==========
function createFloatingLyrics() { function createFloatingLyrics() {
if (document.getElementById('wechat-floating-lyrics')) return; if (document.getElementById('wechat-floating-lyrics')) return;
var phoneContainer = document.getElementById('wechat-phone'); let phoneContainer = document.getElementById('wechat-phone');
if (!phoneContainer) return; if (!phoneContainer) return;
var html = '<div id="wechat-floating-lyrics" class="wechat-floating-lyrics hidden">' + let html = '<div id="wechat-floating-lyrics" class="wechat-floating-lyrics hidden">' +
'<div class="wechat-floating-lyrics-header">' + '<div class="wechat-floating-lyrics-header">' +
'<span class="wechat-floating-lyrics-title">歌词</span>' + '<span class="wechat-floating-lyrics-title">歌词</span>' +
'<button class="wechat-floating-lyrics-close">' + CLOSE_ICON + '</button>' + '<button class="wechat-floating-lyrics-close">' + CLOSE_ICON + '</button>' +
@@ -343,11 +353,11 @@ function createFloatingLyrics() {
} }
function initFloatingLyricsEvents() { function initFloatingLyricsEvents() {
var panel = document.getElementById('wechat-floating-lyrics'); let panel = document.getElementById('wechat-floating-lyrics');
if (!panel) return; if (!panel) return;
var header = panel.querySelector('.wechat-floating-lyrics-header'); let header = panel.querySelector('.wechat-floating-lyrics-header');
var closeBtn = panel.querySelector('.wechat-floating-lyrics-close'); let closeBtn = panel.querySelector('.wechat-floating-lyrics-close');
closeBtn.addEventListener('click', function(e) { closeBtn.addEventListener('click', function(e) {
e.stopPropagation(); e.stopPropagation();
@@ -355,8 +365,8 @@ function initFloatingLyricsEvents() {
}); });
// 拖拽(在手机容器内) // 拖拽(在手机容器内)
var isDragging = false; let isDragging = false;
var startX, startY, initialX, initialY; let startX, startY, initialX, initialY;
header.addEventListener('mousedown', startDrag); header.addEventListener('mousedown', startDrag);
header.addEventListener('touchstart', startDrag, { passive: false }); header.addEventListener('touchstart', startDrag, { passive: false });
@@ -364,8 +374,8 @@ function initFloatingLyricsEvents() {
function startDrag(e) { function startDrag(e) {
if (e.target.closest('.wechat-floating-lyrics-close')) return; if (e.target.closest('.wechat-floating-lyrics-close')) return;
isDragging = true; isDragging = true;
var rect = panel.getBoundingClientRect(); let rect = panel.getBoundingClientRect();
var phoneRect = document.getElementById('wechat-phone').getBoundingClientRect(); let phoneRect = document.getElementById('wechat-phone').getBoundingClientRect();
initialX = rect.left - phoneRect.left; initialX = rect.left - phoneRect.left;
initialY = rect.top - phoneRect.top; initialY = rect.top - phoneRect.top;
if (e.type === 'touchstart') { if (e.type === 'touchstart') {
@@ -385,7 +395,7 @@ function initFloatingLyricsEvents() {
function drag(e) { function drag(e) {
if (!isDragging) return; if (!isDragging) return;
e.preventDefault(); e.preventDefault();
var clientX, clientY; let clientX, clientY;
if (e.type === 'touchmove') { if (e.type === 'touchmove') {
clientX = e.touches[0].clientX; clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY; clientY = e.touches[0].clientY;
@@ -393,12 +403,12 @@ function initFloatingLyricsEvents() {
clientX = e.clientX; clientX = e.clientX;
clientY = e.clientY; clientY = e.clientY;
} }
var dx = clientX - startX; let dx = clientX - startX;
var dy = clientY - startY; let dy = clientY - startY;
var phoneEl = document.getElementById('wechat-phone'); let phoneEl = document.getElementById('wechat-phone');
var phoneRect = phoneEl.getBoundingClientRect(); let phoneRect = phoneEl.getBoundingClientRect();
var newX = Math.max(0, Math.min(phoneRect.width - 280, initialX + dx)); let newX = Math.max(0, Math.min(phoneRect.width - 280, initialX + dx));
var newY = Math.max(0, Math.min(phoneRect.height - 100, initialY + dy)); let newY = Math.max(0, Math.min(phoneRect.height - 100, initialY + dy));
panel.style.left = newX + 'px'; panel.style.left = newX + 'px';
panel.style.top = newY + 'px'; panel.style.top = newY + 'px';
} }
@@ -497,10 +507,10 @@ function updateLyricsHighlight(currentTime) {
function createMiniPlayer() { function createMiniPlayer() {
if (document.getElementById('wechat-music-mini')) return; if (document.getElementById('wechat-music-mini')) return;
var phoneContainer = document.getElementById('wechat-phone'); let phoneContainer = document.getElementById('wechat-phone');
if (!phoneContainer) return; if (!phoneContainer) return;
var html = '<div id="wechat-music-mini" class="wechat-music-mini hidden">' + let html = '<div id="wechat-music-mini" class="wechat-music-mini hidden">' +
'<div class="wechat-music-mini-btn">' + '<div class="wechat-music-mini-btn">' +
'<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="5.5" cy="17.5" r="2.5"/><circle cx="17.5" cy="15.5" r="2.5"/><path d="M8 17.5V6.5a1 1 0 011-1h10a1 1 0 011 1v9"/><path d="M8 10h12"/></svg>' + '<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="5.5" cy="17.5" r="2.5"/><circle cx="17.5" cy="15.5" r="2.5"/><path d="M8 17.5V6.5a1 1 0 011-1h10a1 1 0 011 1v9"/><path d="M8 10h12"/></svg>' +
'</div>' + '</div>' +
@@ -522,7 +532,7 @@ function createMiniPlayer() {
'<div class="wechat-music-mini-controls">' + '<div class="wechat-music-mini-controls">' +
'<button class="wechat-music-mini-play">' + PLAY_ICON_SMALL + '</button>' + '<button class="wechat-music-mini-play">' + PLAY_ICON_SMALL + '</button>' +
'<button class="wechat-music-mini-mode" title="播放模式">' + MODE_LIST_ICON + '</button>' + '<button class="wechat-music-mini-mode" title="播放模式">' + MODE_LIST_ICON + '</button>' +
'<button class="wechat-music-mini-lyrics-btn" title="歌词">' + LYRICS_ICON + '</button>' + '<button class="wechat-music-mini-lyrics-btn" title="歌词"></button>' +
'<button class="wechat-music-mini-playlist" title="播放列表">' + PLAYLIST_ICON + '</button>' + '<button class="wechat-music-mini-playlist" title="播放列表">' + PLAYLIST_ICON + '</button>' +
'<button class="wechat-music-mini-close">' + CLOSE_ICON + '</button>' + '<button class="wechat-music-mini-close">' + CLOSE_ICON + '</button>' +
'</div>' + '</div>' +
@@ -537,14 +547,14 @@ function initMiniPlayerEvents() {
if (miniPlayerInited) return; if (miniPlayerInited) return;
miniPlayerInited = true; miniPlayerInited = true;
var mini = document.getElementById('wechat-music-mini'); let mini = document.getElementById('wechat-music-mini');
var btn = mini.querySelector('.wechat-music-mini-btn'); let btn = mini.querySelector('.wechat-music-mini-btn');
var panel = mini.querySelector('.wechat-music-mini-panel'); let panel = mini.querySelector('.wechat-music-mini-panel');
var playBtn = mini.querySelector('.wechat-music-mini-play'); let playBtn = mini.querySelector('.wechat-music-mini-play');
var modeBtn = mini.querySelector('.wechat-music-mini-mode'); let modeBtn = mini.querySelector('.wechat-music-mini-mode');
var lyricsBtn = mini.querySelector('.wechat-music-mini-lyrics-btn'); let lyricsBtn = mini.querySelector('.wechat-music-mini-lyrics-btn');
var playlistBtn = mini.querySelector('.wechat-music-mini-playlist'); let playlistBtn = mini.querySelector('.wechat-music-mini-playlist');
var closeBtn = mini.querySelector('.wechat-music-mini-close'); let closeBtn = mini.querySelector('.wechat-music-mini-close');
btn.addEventListener('click', function(e) { btn.addEventListener('click', function(e) {
e.stopPropagation(); e.stopPropagation();
@@ -583,18 +593,18 @@ function initMiniPlayerEvents() {
}); });
// 进度条拖动 // 进度条拖动
var slider = mini.querySelector('.wechat-music-mini-slider'); let slider = mini.querySelector('.wechat-music-mini-slider');
var currentTimeEl = mini.querySelector('.wechat-music-mini-current'); let currentTimeEl = mini.querySelector('.wechat-music-mini-current');
var durationEl = mini.querySelector('.wechat-music-mini-duration'); let durationEl = mini.querySelector('.wechat-music-mini-duration');
var isSeeking = false; let isSeeking = false;
if (slider) { if (slider) {
slider.addEventListener('input', function(e) { slider.addEventListener('input', function(e) {
e.stopPropagation(); e.stopPropagation();
isSeeking = true; isSeeking = true;
var audio = document.getElementById('wechat-music-audio'); let audio = document.getElementById('wechat-music-audio');
if (audio && audio.duration) { if (audio && audio.duration) {
var seekTime = (slider.value / 100) * audio.duration; let seekTime = (slider.value / 100) * audio.duration;
if (currentTimeEl) { if (currentTimeEl) {
currentTimeEl.textContent = formatDuration(seekTime); currentTimeEl.textContent = formatDuration(seekTime);
} }
@@ -603,7 +613,7 @@ function initMiniPlayerEvents() {
slider.addEventListener('change', function(e) { slider.addEventListener('change', function(e) {
e.stopPropagation(); e.stopPropagation();
var audio = document.getElementById('wechat-music-audio'); let audio = document.getElementById('wechat-music-audio');
if (audio && audio.duration) { if (audio && audio.duration) {
audio.currentTime = (slider.value / 100) * audio.duration; audio.currentTime = (slider.value / 100) * audio.duration;
} }
@@ -618,7 +628,7 @@ function initMiniPlayerEvents() {
// 监听音频进度更新 // 监听音频进度更新
document.addEventListener('wechat-music-timeupdate', function(e) { document.addEventListener('wechat-music-timeupdate', function(e) {
if (isSeeking) return; if (isSeeking) return;
var detail = e.detail || {}; let detail = e.detail || {};
if (slider && typeof detail.progress === 'number') { if (slider && typeof detail.progress === 'number') {
slider.value = detail.progress; slider.value = detail.progress;
} }
@@ -638,8 +648,8 @@ function initMiniPlayerEvents() {
}); });
// 拖拽(在手机容器内) // 拖拽(在手机容器内)
var isDragging = false; let isDragging = false;
var startX, startY, initialX, initialY; let startX, startY, initialX, initialY;
btn.addEventListener('mousedown', startDrag); btn.addEventListener('mousedown', startDrag);
btn.addEventListener('touchstart', startDrag, { passive: false }); btn.addEventListener('touchstart', startDrag, { passive: false });
@@ -647,8 +657,8 @@ function initMiniPlayerEvents() {
function startDrag(e) { function startDrag(e) {
if (e.target.closest('.wechat-music-mini-panel')) return; if (e.target.closest('.wechat-music-mini-panel')) return;
isDragging = true; isDragging = true;
var rect = mini.getBoundingClientRect(); let rect = mini.getBoundingClientRect();
var phoneRect = document.getElementById('wechat-phone').getBoundingClientRect(); let phoneRect = document.getElementById('wechat-phone').getBoundingClientRect();
initialX = rect.left - phoneRect.left; initialX = rect.left - phoneRect.left;
initialY = rect.top - phoneRect.top; initialY = rect.top - phoneRect.top;
if (e.type === 'touchstart') { if (e.type === 'touchstart') {
@@ -667,7 +677,7 @@ function initMiniPlayerEvents() {
function drag(e) { function drag(e) {
if (!isDragging) return; if (!isDragging) return;
e.preventDefault(); e.preventDefault();
var clientX, clientY; let clientX, clientY;
if (e.type === 'touchmove') { if (e.type === 'touchmove') {
clientX = e.touches[0].clientX; clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY; clientY = e.touches[0].clientY;
@@ -675,12 +685,12 @@ function initMiniPlayerEvents() {
clientX = e.clientX; clientX = e.clientX;
clientY = e.clientY; clientY = e.clientY;
} }
var dx = clientX - startX; let dx = clientX - startX;
var dy = clientY - startY; let dy = clientY - startY;
var phoneEl = document.getElementById('wechat-phone'); let phoneEl = document.getElementById('wechat-phone');
var phoneRect = phoneEl.getBoundingClientRect(); let phoneRect = phoneEl.getBoundingClientRect();
var newX = Math.max(0, Math.min(phoneRect.width - 50, initialX + dx)); let newX = Math.max(0, Math.min(phoneRect.width - 50, initialX + dx));
var newY = Math.max(0, Math.min(phoneRect.height - 50, initialY + dy)); let newY = Math.max(0, Math.min(phoneRect.height - 50, initialY + dy));
mini.style.left = newX + 'px'; mini.style.left = newX + 'px';
mini.style.top = newY + 'px'; mini.style.top = newY + 'px';
mini.style.right = 'auto'; mini.style.right = 'auto';
@@ -714,7 +724,7 @@ function cyclePlayMode() {
// 更新模式按钮图标 // 更新模式按钮图标
function updateModeButtonIcon() { function updateModeButtonIcon() {
var modeBtn = document.querySelector('.wechat-music-mini-mode'); let modeBtn = document.querySelector('.wechat-music-mini-mode');
if (!modeBtn) return; if (!modeBtn) return;
if (playMode === 'single') { if (playMode === 'single') {
@@ -728,21 +738,225 @@ function updateModeButtonIcon() {
// 播放下一首 // 播放下一首
function playNext() { function playNext() {
if (playlist.length === 0) return; // 单曲循环模式:重新播放当前歌曲
var nextIndex;
if (playMode === 'single') { if (playMode === 'single') {
nextIndex = currentPlayIndex; let audio = document.getElementById('wechat-music-audio');
} else if (playMode === 'random') { if (audio) {
nextIndex = Math.floor(Math.random() * playlist.length); audio.currentTime = 0;
} else { audio.play().then(function() {
nextIndex = (currentPlayIndex + 1) % playlist.length; isPlaying = true;
let playBtn = document.getElementById('wechat-music-player-play');
if (playBtn) playBtn.innerHTML = PAUSE_ICON;
updateMiniPlayerState();
}).catch(function(e) {
console.error('[可乐] 单曲循环播放失败:', e);
});
}
return;
} }
// 随机模式:真正的随机推歌
if (playMode === 'random') {
fetchRandomSong();
return;
}
// 列表循环模式
if (playlist.length === 0) return;
let nextIndex = (currentPlayIndex + 1) % playlist.length;
if (nextIndex >= 0 && nextIndex < playlist.length) { if (nextIndex >= 0 && nextIndex < playlist.length) {
var song = playlist[nextIndex]; let song = playlist[nextIndex];
currentPlayIndex = nextIndex; currentPlayIndex = nextIndex;
playMusic(song.id, song.platform, song.name, song.artist); playMusic(song.id, song.platform, song.name, song.artist);
renderPlaylist();
}
}
// 随机推歌从API搜索并播放随机歌曲
// retryCount: 内部重试计数,避免无限循环
async function fetchRandomSong(retryCount) {
retryCount = retryCount || 0;
let maxRetries = 3;
// 构建搜索关键词
let keyword = getRandomKeyword();
console.log('[可乐] 随机推歌,搜索关键词:', keyword);
// 只在第一次显示提示
if (!hasShownRandomToast) {
showToast('正在为你随机推歌...');
hasShownRandomToast = true;
}
try {
let results = await searchMusic(keyword);
if (!results || results.length === 0) {
// 如果搜索失败,换个关键词重试
keyword = RANDOM_KEYWORDS[Math.floor(Math.random() * RANDOM_KEYWORDS.length)];
results = await searchMusic(keyword);
}
if (!results || results.length === 0) {
// 静默重试
if (retryCount < maxRetries) {
console.log('[可乐] 随机推歌搜索无结果,重试中...', retryCount + 1);
return fetchRandomSong(retryCount + 1);
}
console.error('[可乐] 随机推歌失败,已达最大重试次数');
return;
}
// 过滤掉已播放过的歌曲
let unplayedSongs = results.filter(function(song) {
let songKey = song.platform + '_' + song.id;
return !playedSongIds.has(songKey);
});
// 如果全都播放过,清空记录重新开始
if (unplayedSongs.length === 0) {
playedSongIds.clear();
unplayedSongs = results;
}
// 从未播放的歌曲中随机选一首
let randomIndex = Math.floor(Math.random() * unplayedSongs.length);
let song = unplayedSongs[randomIndex];
// 记录已播放
let songKey = song.platform + '_' + song.id;
playedSongIds.add(songKey);
// 限制记录数量,避免内存占用过大
if (playedSongIds.size > 500) {
let arr = Array.from(playedSongIds);
playedSongIds = new Set(arr.slice(-300));
}
console.log('[可乐] 随机推歌:', song.name, '-', song.artist);
// 播放歌曲
playMusic(song.id, song.platform, song.name, song.artist);
} catch (err) {
console.error('[可乐] 随机推歌失败:', err);
// 静默重试,不显示错误提示
if (retryCount < maxRetries) {
console.log('[可乐] 随机推歌出错,重试中...', retryCount + 1);
return fetchRandomSong(retryCount + 1);
}
}
}
// 获取随机搜索关键词
function getRandomKeyword() {
let rand = Math.random();
// 70%概率从聊天记录提取关键词
if (rand < 0.7) {
let chatKeyword = extractKeywordFromChat();
if (chatKeyword) {
console.log('[可乐] 使用聊天关键词推歌:', chatKeyword);
return chatKeyword;
}
}
// 20%概率使用当前歌曲的歌手名搜索类似歌曲
if (rand < 0.9 && currentSong && currentSong.artist) {
return currentSong.artist;
}
// 10%概率从关键词库随机选择
return RANDOM_KEYWORDS[Math.floor(Math.random() * RANDOM_KEYWORDS.length)];
}
// 从最近聊天记录中提取关键词
function extractKeywordFromChat() {
try {
// 获取当前联系人
let settings = window.wechatGetSettings?.() || {};
let contacts = settings.contacts || [];
let currentIndex = window.wechatCurrentChatIndex;
if (typeof currentIndex !== 'number' || currentIndex < 0 || !contacts[currentIndex]) {
return null;
}
let contact = contacts[currentIndex];
let chatHistory = contact.chatHistory || [];
if (chatHistory.length === 0) return null;
// 获取最近10条消息
let recentMessages = chatHistory.slice(-10);
// 情绪/场景关键词映射
let emotionKeywords = {
// 情绪类
'开心': ['开心', '快乐', '欢快', '甜蜜'],
'伤感': ['难过', '伤心', '哭', '眼泪', '失恋', '分手', '想你', '想念'],
'治愈': ['累', '疲惫', '辛苦', '压力', '烦', '焦虑', '放松'],
'浪漫': ['喜欢', '爱你', '爱', '在一起', '亲爱', '宝贝', '甜'],
'励志': ['加油', '努力', '奋斗', '坚持', '相信'],
'怀旧': ['以前', '小时候', '曾经', '回忆', '那时候'],
// 场景类
'夜晚': ['晚安', '睡觉', '睡了', '夜', '深夜', '失眠'],
'清晨': ['早安', '早上', '起床', '早'],
'下雨': ['雨', '下雨', '阴天'],
'工作': ['上班', '工作', '加班', '开会', '老板'],
'吃饭': ['吃', '饿', '美食', '好吃', '火锅', '奶茶'],
// 风格类
'古风': ['古风', '汉服', '仙', '诗'],
'说唱': ['rap', 'Rap', 'RAP', '说唱', 'diss'],
'摇滚': ['摇滚', 'rock', '嗨', '燃']
};
// 从消息中提取匹配的关键词
let matchedCategories = [];
recentMessages.forEach(function(msg) {
if (!msg.content || msg.isRecalled) return;
let content = msg.content.toLowerCase();
Object.keys(emotionKeywords).forEach(function(category) {
let keywords = emotionKeywords[category];
for (let i = 0; i < keywords.length; i++) {
if (content.includes(keywords[i].toLowerCase())) {
matchedCategories.push(category);
break;
}
}
});
});
// 如果找到匹配的情绪/场景,随机返回一个
if (matchedCategories.length > 0) {
return matchedCategories[Math.floor(Math.random() * matchedCategories.length)];
}
// 没有匹配到特定情绪,尝试提取消息中的名词作为搜索词
// 提取最后一条非特殊消息的内容
for (let i = recentMessages.length - 1; i >= 0; i--) {
let msg = recentMessages[i];
if (!msg.content || msg.isRecalled) continue;
if (msg.content.startsWith('[') && msg.content.includes(':')) continue; // 跳过特殊消息
// 简单提取2-4字的词组
let content = msg.content.replace(/[,。!?、:;""''【】\[\]]/g, ' ');
let words = content.split(/\s+/).filter(function(w) {
return w.length >= 2 && w.length <= 4 && !/^\d+$/.test(w);
});
if (words.length > 0) {
return words[Math.floor(Math.random() * words.length)];
}
}
return null;
} catch (e) {
console.error('[可乐] 提取聊天关键词失败:', e);
return null;
} }
} }
@@ -750,10 +964,10 @@ function playNext() {
function createPlaylistPanel() { function createPlaylistPanel() {
if (document.getElementById('wechat-music-playlist-panel')) return; if (document.getElementById('wechat-music-playlist-panel')) return;
var phoneContainer = document.getElementById('wechat-phone'); let phoneContainer = document.getElementById('wechat-phone');
if (!phoneContainer) return; if (!phoneContainer) return;
var html = '<div id="wechat-music-playlist-panel" class="wechat-music-playlist-panel hidden">' + let html = '<div id="wechat-music-playlist-panel" class="wechat-music-playlist-panel hidden">' +
'<div class="wechat-playlist-header">' + '<div class="wechat-playlist-header">' +
'<span class="wechat-playlist-title">播放列表</span>' + '<span class="wechat-playlist-title">播放列表</span>' +
'<button class="wechat-playlist-clear">清空</button>' + '<button class="wechat-playlist-clear">清空</button>' +
@@ -767,12 +981,12 @@ function createPlaylistPanel() {
} }
function initPlaylistPanelEvents() { function initPlaylistPanelEvents() {
var panel = document.getElementById('wechat-music-playlist-panel'); let panel = document.getElementById('wechat-music-playlist-panel');
if (!panel) return; if (!panel) return;
var closeBtn = panel.querySelector('.wechat-playlist-close'); let closeBtn = panel.querySelector('.wechat-playlist-close');
var clearBtn = panel.querySelector('.wechat-playlist-clear'); let clearBtn = panel.querySelector('.wechat-playlist-clear');
var content = panel.querySelector('.wechat-playlist-content'); let content = panel.querySelector('.wechat-playlist-content');
closeBtn.addEventListener('click', function(e) { closeBtn.addEventListener('click', function(e) {
e.stopPropagation(); e.stopPropagation();
@@ -788,10 +1002,10 @@ function initPlaylistPanelEvents() {
}); });
content.addEventListener('click', function(e) { content.addEventListener('click', function(e) {
var item = e.target.closest('.wechat-playlist-item'); let item = e.target.closest('.wechat-playlist-item');
if (!item) return; if (!item) return;
var index = parseInt(item.dataset.index); let index = parseInt(item.dataset.index);
if (isNaN(index)) return; if (isNaN(index)) return;
if (e.target.closest('.wechat-playlist-item-del')) { if (e.target.closest('.wechat-playlist-item-del')) {
@@ -806,7 +1020,7 @@ function initPlaylistPanelEvents() {
} else { } else {
// 播放选中歌曲 // 播放选中歌曲
currentPlayIndex = index; currentPlayIndex = index;
var song = playlist[index]; let song = playlist[index];
playMusic(song.id, song.platform, song.name, song.artist); playMusic(song.id, song.platform, song.name, song.artist);
renderPlaylist(); renderPlaylist();
} }
@@ -815,7 +1029,7 @@ function initPlaylistPanelEvents() {
function showPlaylistPanel() { function showPlaylistPanel() {
createPlaylistPanel(); createPlaylistPanel();
var panel = document.getElementById('wechat-music-playlist-panel'); let panel = document.getElementById('wechat-music-playlist-panel');
if (panel) { if (panel) {
panel.classList.remove('hidden'); panel.classList.remove('hidden');
renderPlaylist(); renderPlaylist();
@@ -823,14 +1037,14 @@ function showPlaylistPanel() {
} }
function hidePlaylistPanel() { function hidePlaylistPanel() {
var panel = document.getElementById('wechat-music-playlist-panel'); let panel = document.getElementById('wechat-music-playlist-panel');
if (panel) { if (panel) {
panel.classList.add('hidden'); panel.classList.add('hidden');
} }
} }
function togglePlaylistPanel() { function togglePlaylistPanel() {
var panel = document.getElementById('wechat-music-playlist-panel'); let panel = document.getElementById('wechat-music-playlist-panel');
if (panel && !panel.classList.contains('hidden')) { if (panel && !panel.classList.contains('hidden')) {
hidePlaylistPanel(); hidePlaylistPanel();
} else { } else {
@@ -839,7 +1053,7 @@ function togglePlaylistPanel() {
} }
function renderPlaylist() { function renderPlaylist() {
var content = document.querySelector('.wechat-playlist-content'); let content = document.querySelector('.wechat-playlist-content');
if (!content) return; if (!content) return;
if (playlist.length === 0) { if (playlist.length === 0) {
@@ -847,10 +1061,10 @@ function renderPlaylist() {
return; return;
} }
var html = ''; let html = '';
for (var i = 0; i < playlist.length; i++) { for (let i = 0; i < playlist.length; i++) {
var song = playlist[i]; let song = playlist[i];
var isActive = i === currentPlayIndex; let isActive = i === currentPlayIndex;
html += '<div class="wechat-playlist-item' + (isActive ? ' active' : '') + '" data-index="' + i + '">' + html += '<div class="wechat-playlist-item' + (isActive ? ' active' : '') + '" data-index="' + i + '">' +
'<div class="wechat-playlist-item-info">' + '<div class="wechat-playlist-item-info">' +
'<span class="wechat-playlist-item-name">' + escapeHtml(song.name) + '</span>' + '<span class="wechat-playlist-item-name">' + escapeHtml(song.name) + '</span>' +
@@ -865,11 +1079,26 @@ function renderPlaylist() {
// 添加到播放列表 // 添加到播放列表
function addToPlaylist(song) { function addToPlaylist(song) {
// 检查是否已存在 // 检查是否已存在
var exists = playlist.some(function(s) { let existIndex = -1;
return s.id === song.id && s.platform === song.platform; for (let i = 0; i < playlist.length; i++) {
}); if (playlist[i].id === song.id && playlist[i].platform === song.platform) {
if (!exists) { existIndex = i;
break;
}
}
if (existIndex >= 0) {
// 已存在,移到最后(最新播放)
playlist.splice(existIndex, 1);
playlist.push(song); playlist.push(song);
} else {
// 不存在,添加到最后
playlist.push(song);
}
// 限制最多10首删除最早的
while (playlist.length > 10) {
playlist.shift();
} }
} }
@@ -883,11 +1112,11 @@ function showMiniPlayer() {
} }
function hideMiniPlayer() { function hideMiniPlayer() {
var mini = document.getElementById('wechat-music-mini'); let mini = document.getElementById('wechat-music-mini');
if (mini) { if (mini) {
mini.classList.add('hidden'); mini.classList.add('hidden');
miniPlayerExpanded = false; miniPlayerExpanded = false;
var panel = mini.querySelector('.wechat-music-mini-panel'); let panel = mini.querySelector('.wechat-music-mini-panel');
if (panel) panel.classList.add('hidden'); if (panel) panel.classList.add('hidden');
} }
hideSingleLineLyrics(); hideSingleLineLyrics();
@@ -980,7 +1209,7 @@ export async function playMusic(id, platform, name, artist) {
// 添加到播放列表 // 添加到播放列表
addToPlaylist(song); addToPlaylist(song);
// 更新当前播放索引 // 更新当前播放索引
for (var i = 0; i < playlist.length; i++) { for (let i = 0; i < playlist.length; i++) {
if (playlist[i].id === song.id && playlist[i].platform === song.platform) { if (playlist[i].id === song.id && playlist[i].platform === song.platform) {
currentPlayIndex = i; currentPlayIndex = i;
break; break;
@@ -988,12 +1217,12 @@ export async function playMusic(id, platform, name, artist) {
} }
const player = document.getElementById('wechat-music-player'); const player = document.getElementById('wechat-music-player');
var audio = document.getElementById('wechat-music-audio'); let audio = document.getElementById('wechat-music-audio');
var playBtn = document.getElementById('wechat-music-player-play'); let playBtn = document.getElementById('wechat-music-player-play');
// 如果 audio 元素不存在,动态创建一个 // 如果 audio 元素不存在,动态创建一个
if (!audio) { if (!audio) {
var phoneContainer = document.getElementById('wechat-phone'); let phoneContainer = document.getElementById('wechat-phone');
if (phoneContainer) { if (phoneContainer) {
audio = document.createElement('audio'); audio = document.createElement('audio');
audio.id = 'wechat-music-audio'; audio.id = 'wechat-music-audio';
@@ -1002,10 +1231,11 @@ export async function playMusic(id, platform, name, artist) {
// 添加事件监听器 // 添加事件监听器
audio.addEventListener('ended', function() { audio.addEventListener('ended', function() {
isPlaying = false; isPlaying = false;
var btn = document.getElementById('wechat-music-player-play'); let btn = document.getElementById('wechat-music-player-play');
if (btn) btn.innerHTML = PLAY_ICON; if (btn) btn.innerHTML = PLAY_ICON;
updateMiniPlayerState(); updateMiniPlayerState();
if (playlist.length > 0) { // 根据播放模式自动播放下一首(单曲循环或有播放列表时)
if (playMode === 'single' || playlist.length > 0) {
playNext(); playNext();
} }
}); });
@@ -1014,7 +1244,7 @@ export async function playMusic(id, platform, name, artist) {
updateLyricsHighlight(audio.currentTime); updateLyricsHighlight(audio.currentTime);
updateSingleLineLyricsHighlight(audio.currentTime); updateSingleLineLyricsHighlight(audio.currentTime);
// 派发进度更新事件给迷你播放器 // 派发进度更新事件给迷你播放器
var progress = audio.duration ? (audio.currentTime / audio.duration) * 100 : 0; let progress = audio.duration ? (audio.currentTime / audio.duration) * 100 : 0;
document.dispatchEvent(new CustomEvent('wechat-music-timeupdate', { document.dispatchEvent(new CustomEvent('wechat-music-timeupdate', {
detail: { detail: {
currentTime: audio.currentTime, currentTime: audio.currentTime,
@@ -1134,7 +1364,7 @@ export function initMusicEvents() {
const searchInput = document.getElementById('wechat-music-search-input'); const searchInput = document.getElementById('wechat-music-search-input');
let searchTimeout = null; let searchTimeout = null;
var doSearch = async function(keyword) { let doSearch = async function(keyword) {
if (!keyword) return; if (!keyword) return;
showLoading(); showLoading();
try { try {
@@ -1181,8 +1411,8 @@ export function initMusicEvents() {
const playBtn = document.getElementById('wechat-music-player-play'); const playBtn = document.getElementById('wechat-music-player-play');
if (playBtn) playBtn.innerHTML = PLAY_ICON; if (playBtn) playBtn.innerHTML = PLAY_ICON;
updateMiniPlayerState(); updateMiniPlayerState();
// 根据播放模式自动播放下一首 // 根据播放模式自动播放下一首(单曲循环或有播放列表时)
if (playlist.length > 0) { if (playMode === 'single' || playlist.length > 0) {
playNext(); playNext();
} }
}); });
@@ -1192,7 +1422,7 @@ export function initMusicEvents() {
updateLyricsHighlight(audio.currentTime); updateLyricsHighlight(audio.currentTime);
updateSingleLineLyricsHighlight(audio.currentTime); updateSingleLineLyricsHighlight(audio.currentTime);
// 派发进度更新事件给迷你播放器 // 派发进度更新事件给迷你播放器
var progress = audio.duration ? (audio.currentTime / audio.duration) * 100 : 0; let progress = audio.duration ? (audio.currentTime / audio.duration) * 100 : 0;
document.dispatchEvent(new CustomEvent('wechat-music-timeupdate', { document.dispatchEvent(new CustomEvent('wechat-music-timeupdate', {
detail: { detail: {
currentTime: audio.currentTime, currentTime: audio.currentTime,
@@ -1214,7 +1444,7 @@ export async function aiShareMusic(keyword) {
if (!keyword || !keyword.trim()) return null; if (!keyword || !keyword.trim()) return null;
try { try {
var results = await searchMusic(keyword); let results = await searchMusic(keyword);
if (results && results.length > 0) { if (results && results.length > 0) {
// 返回第一个搜索结果 // 返回第一个搜索结果
return results[0]; return results[0];

View File

@@ -6,6 +6,7 @@
import { getSettings, defaultSettings, MEME_PROMPT_TEMPLATE, MEME_STICKERS } from './config.js'; import { getSettings, defaultSettings, MEME_PROMPT_TEMPLATE, MEME_STICKERS } from './config.js';
import { getCurrentTime, escapeHtml } from './utils.js'; import { getCurrentTime, escapeHtml } from './utils.js';
import { getUserAvatarHTML, generateChatList, generateContactsList } from './ui.js'; import { getUserAvatarHTML, generateChatList, generateContactsList } from './ui.js';
import { ICON_RED_PACKET, ICON_RED_PACKET_LARGE, ICON_USER } from './icons.js';
// 生成手机界面 HTML // 生成手机界面 HTML
export function generatePhoneHTML() { export function generatePhoneHTML() {
@@ -241,14 +242,14 @@ export function generatePhoneHTML() {
<div class="wechat-func-item" data-func="redpacket"><div class="wechat-func-icon"><svg viewBox="0 0 24 24"><rect x="4" y="2" width="16" height="20" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="12" cy="10" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M4 8h16" stroke="currentColor" stroke-width="1.5"/></svg></div><span>红包</span></div> <div class="wechat-func-item" data-func="redpacket"><div class="wechat-func-icon"><svg viewBox="0 0 24 24"><rect x="4" y="2" width="16" height="20" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="12" cy="10" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M4 8h16" stroke="currentColor" stroke-width="1.5"/></svg></div><span>红包</span></div>
<div class="wechat-func-item" data-func="gift"><div class="wechat-func-icon"><svg viewBox="0 0 24 24"><rect x="3" y="8" width="18" height="13" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M12 8v13M3 12h18" stroke="currentColor" stroke-width="1.5"/><path d="M12 8c-2-4-6-4-6 0s4 0 6 0c2 0 6-4 6 0s-4 4-6 0" stroke="currentColor" stroke-width="1.5" fill="none"/></svg></div><span>礼物</span></div> <div class="wechat-func-item" data-func="gift"><div class="wechat-func-icon"><svg viewBox="0 0 24 24"><rect x="3" y="8" width="18" height="13" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M12 8v13M3 12h18" stroke="currentColor" stroke-width="1.5"/><path d="M12 8c-2-4-6-4-6 0s4 0 6 0c2 0 6-4 6 0s-4 4-6 0" stroke="currentColor" stroke-width="1.5" fill="none"/></svg></div><span>礼物</span></div>
<div class="wechat-func-item" data-func="transfer"><div class="wechat-func-icon"><svg viewBox="0 0 24 24"><rect x="2" y="4" width="20" height="16" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M2 10h20" stroke="currentColor" stroke-width="1.5"/><path d="M6 15h4M14 15h4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></div><span>转账</span></div> <div class="wechat-func-item" data-func="transfer"><div class="wechat-func-icon"><svg viewBox="0 0 24 24"><rect x="2" y="4" width="20" height="16" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M2 10h20" stroke="currentColor" stroke-width="1.5"/><path d="M6 15h4M14 15h4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></div><span>转账</span></div>
<div class="wechat-func-item" data-func="multi"><div class="wechat-func-icon green"><svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M8 9h8M8 13h5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></div><span>多条消息</span></div> <div class="wechat-func-item" data-func="multi"><div class="wechat-func-icon"><svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M8 9h8M8 13h5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></div><span>多条消息</span></div>
</div> </div>
</div> </div>
<div class="wechat-func-page" data-page="1"> <div class="wechat-func-page" data-page="1">
<div class="wechat-func-grid"> <div class="wechat-func-grid">
<div class="wechat-func-item" data-func="voice"><div class="wechat-func-icon"><svg viewBox="0 0 24 24"><path d="M12 1a4 4 0 00-4 4v7a4 4 0 008 0V5a4 4 0 00-4-4z" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M19 10v2a7 7 0 01-14 0v-2M12 19v4M8 23h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/></svg></div><span>语音输入</span></div> <div class="wechat-func-item" data-func="voice"><div class="wechat-func-icon"><svg viewBox="0 0 24 24"><path d="M12 1a4 4 0 00-4 4v7a4 4 0 008 0V5a4 4 0 00-4-4z" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M19 10v2a7 7 0 01-14 0v-2M12 19v4M8 23h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/></svg></div><span>语音输入</span></div>
<div class="wechat-func-item" data-func="favorites"><div class="wechat-func-icon"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M7 7h10M7 12h10M7 17h6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></div><span>收藏</span></div> <div class="wechat-func-item" data-func="time"><div class="wechat-func-icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M12 6v6l4 2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg></div><span>时间</span></div>
<div class="wechat-func-item" data-func="contact"><div class="wechat-func-icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M4 21v-2a4 4 0 014-4h8a4 4 0 014 4v2" stroke="currentColor" stroke-width="1.5" fill="none"/></svg></div><span>个人名片</span></div> <div class="wechat-func-item" data-func="listen"><div class="wechat-func-icon"><svg viewBox="0 0 24 24"><path d="M3 18v-6a9 9 0 0118 0v6" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M21 19a2 2 0 01-2 2h-1a2 2 0 01-2-2v-3a2 2 0 012-2h3v5z" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M3 19a2 2 0 002 2h1a2 2 0 002-2v-3a2 2 0 00-2-2H3v5z" stroke="currentColor" stroke-width="1.5" fill="none"/></svg></div><span>一起听</span></div>
<div class="wechat-func-item" data-func="music"><div class="wechat-func-icon"><svg viewBox="0 0 24 24"><path d="M9 18V5l12-2v13" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/><circle cx="6" cy="18" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="18" cy="16" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/></svg></div><span>音乐</span></div> <div class="wechat-func-item" data-func="music"><div class="wechat-func-icon"><svg viewBox="0 0 24 24"><path d="M9 18V5l12-2v13" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/><circle cx="6" cy="18" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="18" cy="16" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/></svg></div><span>音乐</span></div>
</div> </div>
</div> </div>
@@ -269,6 +270,36 @@ export function generatePhoneHTML() {
<button class="wechat-btn wechat-expand-send" id="wechat-expand-send">发送</button> <button class="wechat-btn wechat-expand-send" id="wechat-expand-send">发送</button>
</div> </div>
</div> </div>
<!-- 时间选择器面板 -->
<div class="wechat-time-picker hidden" id="wechat-time-picker">
<div class="wechat-time-picker-header">
<span class="wechat-time-picker-title">发送时间</span>
</div>
<div class="wechat-time-picker-display" id="wechat-time-picker-display">2025-12-22 21:33:19</div>
<div class="wechat-time-picker-columns">
<div class="wechat-time-picker-column" data-type="year">
<div class="wechat-time-picker-items" id="wechat-time-picker-year"></div>
</div>
<div class="wechat-time-picker-column" data-type="month">
<div class="wechat-time-picker-items" id="wechat-time-picker-month"></div>
</div>
<div class="wechat-time-picker-column" data-type="day">
<div class="wechat-time-picker-items" id="wechat-time-picker-day"></div>
</div>
<div class="wechat-time-picker-column" data-type="hour">
<div class="wechat-time-picker-items" id="wechat-time-picker-hour"></div>
</div>
<div class="wechat-time-picker-column" data-type="minute">
<div class="wechat-time-picker-items" id="wechat-time-picker-minute"></div>
</div>
<div class="wechat-time-picker-column" data-type="second">
<div class="wechat-time-picker-items" id="wechat-time-picker-second"></div>
</div>
</div>
<div class="wechat-time-picker-footer">
<button class="wechat-time-picker-confirm" id="wechat-time-picker-confirm">完成</button>
</div>
</div>
<!-- 表情面板 --> <!-- 表情面板 -->
<div class="wechat-emoji-panel hidden" id="wechat-emoji-panel"> <div class="wechat-emoji-panel hidden" id="wechat-emoji-panel">
<div class="wechat-emoji-tabs"> <div class="wechat-emoji-tabs">
@@ -303,7 +334,14 @@ export function generatePhoneHTML() {
${generateVoiceCallPageHTML()} ${generateVoiceCallPageHTML()}
${generateVideoCallPageHTML()} ${generateVideoCallPageHTML()}
${generateMusicPanelHTML()} ${generateMusicPanelHTML()}
${generateListenTogetherHTML()}
${generateMomentsPageHTML()} ${generateMomentsPageHTML()}
${generateRedPacketPageHTML(settings)}
${generateOpenRedPacketHTML()}
${generateRedPacketDetailHTML(settings)}
${generateTransferPageHTML()}
${generateReceiveTransferPageHTML()}
${generateTransferRefundConfirmHTML()}
</div> </div>
<!-- 隐藏的文件输入 --> <!-- 隐藏的文件输入 -->
@@ -385,15 +423,21 @@ function generateDiscoverPageHTML() {
<!-- 朋友圈 --> <!-- 朋友圈 -->
<div class="wechat-discover-group"> <div class="wechat-discover-group">
<div class="wechat-discover-item" id="wechat-discover-moments"> <div class="wechat-discover-item" id="wechat-discover-moments">
<div class="wechat-discover-item-icon"> <div class="wechat-discover-item-icon" style="background: transparent;">
<svg viewBox="0 0 24 24" width="24" height="24"> <svg viewBox="0 0 24 24" width="24" height="24">
<circle cx="12" cy="12" r="10" stroke="#f59e0b" stroke-width="0" fill="#f59e0b"/> <circle cx="12" cy="12" r="11" fill="#1a1a1a"/>
<circle cx="8" cy="10" r="1.5" fill="white"/> <path d="M12 3 L14.5 7.5 L12 12 Z" fill="#ff0000"/>
<circle cx="12" cy="7" r="1.5" fill="white"/> <path d="M14.5 7.5 L19.5 6.5 L12 12 Z" fill="#ff8800"/>
<circle cx="16" cy="10" r="1.5" fill="white"/> <path d="M19.5 6.5 L21 12 L12 12 Z" fill="#ffff00"/>
<circle cx="10" cy="14" r="1.5" fill="white"/> <path d="M21 12 L19.5 17.5 L12 12 Z" fill="#00ff00"/>
<circle cx="14" cy="14" r="1.5" fill="white"/> <path d="M19.5 17.5 L14.5 16.5 L12 12 Z" fill="#00ffff"/>
<circle cx="12" cy="17" r="1.5" fill="white"/> <path d="M14.5 16.5 L12 21 L12 12 Z" fill="#0088ff"/>
<path d="M12 21 L9.5 16.5 L12 12 Z" fill="#0000ff"/>
<path d="M9.5 16.5 L4.5 17.5 L12 12 Z" fill="#8800ff"/>
<path d="M4.5 17.5 L3 12 L12 12 Z" fill="#ff00ff"/>
<path d="M3 12 L4.5 6.5 L12 12 Z" fill="#ff0088"/>
<path d="M4.5 6.5 L9.5 7.5 L12 12 Z" fill="#ff0044"/>
<path d="M9.5 7.5 L12 3 L12 12 Z" fill="#ff0000"/>
</svg> </svg>
</div> </div>
<span class="wechat-discover-item-text">朋友圈</span> <span class="wechat-discover-item-text">朋友圈</span>
@@ -404,154 +448,6 @@ function generateDiscoverPageHTML() {
</div> </div>
</div> </div>
<!-- 视频号/直播 -->
<div class="wechat-discover-group">
<div class="wechat-discover-item wechat-discover-item-disabled" data-feature="视频号">
<div class="wechat-discover-item-icon">
<svg viewBox="0 0 24 24" width="24" height="24">
<circle cx="12" cy="12" r="10" stroke="#f97316" stroke-width="0" fill="#f97316"/>
<path d="M10 8l6 4-6 4V8z" fill="white"/>
</svg>
</div>
<span class="wechat-discover-item-text">视频号</span>
<div class="wechat-discover-item-right">
<span class="wechat-discover-item-badge">待开发</span>
<span class="wechat-discover-item-arrow"></span>
</div>
</div>
<div class="wechat-discover-item wechat-discover-item-disabled" data-feature="直播">
<div class="wechat-discover-item-icon">
<svg viewBox="0 0 24 24" width="24" height="24">
<circle cx="12" cy="12" r="10" stroke="#ef4444" stroke-width="0" fill="#ef4444"/>
<circle cx="12" cy="12" r="3" fill="white"/>
<path d="M12 5v2M12 17v2M5 12h2M17 12h2" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<span class="wechat-discover-item-text">直播</span>
<div class="wechat-discover-item-right">
<span class="wechat-discover-item-badge">待开发</span>
<span class="wechat-discover-item-arrow"></span>
</div>
</div>
</div>
<!-- 扫一扫/听一听 -->
<div class="wechat-discover-group">
<div class="wechat-discover-item wechat-discover-item-disabled" data-feature="扫一扫">
<div class="wechat-discover-item-icon">
<svg viewBox="0 0 24 24" width="24" height="24">
<rect x="2" y="2" width="20" height="20" rx="2" fill="#3b82f6"/>
<path d="M7 3v4H3M17 3v4h4M7 21v-4H3M17 21v-4h4M3 12h18" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<span class="wechat-discover-item-text">扫一扫</span>
<div class="wechat-discover-item-right">
<span class="wechat-discover-item-badge">待开发</span>
<span class="wechat-discover-item-arrow"></span>
</div>
</div>
<div class="wechat-discover-item wechat-discover-item-disabled" data-feature="听一听">
<div class="wechat-discover-item-icon">
<svg viewBox="0 0 24 24" width="24" height="24">
<circle cx="12" cy="12" r="10" stroke="#ec4899" stroke-width="0" fill="#ec4899"/>
<path d="M9 18V5l12-2v13" stroke="white" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="6" cy="18" r="3" stroke="white" stroke-width="1.5" fill="none"/>
</svg>
</div>
<span class="wechat-discover-item-text">听一听</span>
<div class="wechat-discover-item-right">
<span class="wechat-discover-item-badge">待开发</span>
<span class="wechat-discover-item-arrow"></span>
</div>
</div>
</div>
<!-- 看一看/搜一搜 -->
<div class="wechat-discover-group">
<div class="wechat-discover-item wechat-discover-item-disabled" data-feature="看一看">
<div class="wechat-discover-item-icon">
<svg viewBox="0 0 24 24" width="24" height="24">
<circle cx="12" cy="12" r="10" stroke="#f59e0b" stroke-width="0" fill="#f59e0b"/>
<circle cx="12" cy="12" r="4" stroke="white" stroke-width="1.5" fill="none"/>
<circle cx="12" cy="12" r="1.5" fill="white"/>
</svg>
</div>
<span class="wechat-discover-item-text">看一看</span>
<div class="wechat-discover-item-right">
<span class="wechat-discover-item-badge">待开发</span>
<span class="wechat-discover-item-arrow"></span>
</div>
</div>
<div class="wechat-discover-item wechat-discover-item-disabled" data-feature="搜一搜">
<div class="wechat-discover-item-icon">
<svg viewBox="0 0 24 24" width="24" height="24">
<circle cx="12" cy="12" r="10" stroke="#ef4444" stroke-width="0" fill="#ef4444"/>
<circle cx="11" cy="11" r="4" stroke="white" stroke-width="1.5" fill="none"/>
<path d="M14 14l3 3" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<span class="wechat-discover-item-text">搜一搜</span>
<div class="wechat-discover-item-right">
<span class="wechat-discover-item-badge">待开发</span>
<span class="wechat-discover-item-arrow"></span>
</div>
</div>
</div>
<!-- 附近的人 -->
<div class="wechat-discover-group">
<div class="wechat-discover-item wechat-discover-item-disabled" data-feature="附近的人">
<div class="wechat-discover-item-icon">
<svg viewBox="0 0 24 24" width="24" height="24">
<circle cx="12" cy="12" r="10" stroke="#3b82f6" stroke-width="0" fill="#3b82f6"/>
<circle cx="12" cy="10" r="3" stroke="white" stroke-width="1.5" fill="none"/>
<path d="M6 19c0-3 3-5 6-5s6 2 6 5" stroke="white" stroke-width="1.5" fill="none" stroke-linecap="round"/>
</svg>
</div>
<span class="wechat-discover-item-text">附近的人</span>
<div class="wechat-discover-item-right">
<span class="wechat-discover-item-badge">待开发</span>
<span class="wechat-discover-item-arrow"></span>
</div>
</div>
</div>
<!-- 游戏 -->
<div class="wechat-discover-group">
<div class="wechat-discover-item wechat-discover-item-disabled" data-feature="游戏">
<div class="wechat-discover-item-icon">
<svg viewBox="0 0 24 24" width="24" height="24">
<circle cx="12" cy="12" r="10" stroke="#22c55e" stroke-width="0" fill="#22c55e"/>
<rect x="6" y="9" width="12" height="8" rx="2" stroke="white" stroke-width="1.5" fill="none"/>
<circle cx="9" cy="13" r="1" fill="white"/>
<circle cx="15" cy="13" r="1" fill="white"/>
<path d="M11 11v4M9 13h4" stroke="white" stroke-width="0.8"/>
</svg>
</div>
<span class="wechat-discover-item-text">游戏</span>
<div class="wechat-discover-item-right">
<span class="wechat-discover-item-badge">待开发</span>
<span class="wechat-discover-item-arrow"></span>
</div>
</div>
</div>
<!-- 小程序 -->
<div class="wechat-discover-group">
<div class="wechat-discover-item wechat-discover-item-disabled" data-feature="小程序">
<div class="wechat-discover-item-icon">
<svg viewBox="0 0 24 24" width="24" height="24">
<circle cx="12" cy="12" r="10" stroke="#8b5cf6" stroke-width="0" fill="#8b5cf6"/>
<path d="M8 8h3v3H8zM13 8h3v3h-3zM8 13h3v3H8zM13 13h3v3h-3z" stroke="white" stroke-width="1" fill="none"/>
</svg>
</div>
<span class="wechat-discover-item-text">小程序</span>
<div class="wechat-discover-item-right">
<span class="wechat-discover-item-badge">待开发</span>
<span class="wechat-discover-item-arrow"></span>
</div>
</div>
</div>
</div> </div>
<!-- 底部标签栏 --> <!-- 底部标签栏 -->
@@ -623,7 +519,7 @@ function generateSettingsPageHTML(settings) {
<div class="wechat-settings-item wechat-settings-item-vertical"> <div class="wechat-settings-item wechat-settings-item-vertical">
<span class="wechat-settings-label">模型选择</span> <span class="wechat-settings-label">模型选择</span>
<select class="wechat-settings-input wechat-settings-select" id="wechat-model-select"> <select class="wechat-settings-input wechat-settings-select" id="wechat-model-select">
<option value="">-- 选择模型 --</option> <option value="">选择模型</option>
${(settings.modelList || []).map(m => `<option value="${m}" ${m === settings.selectedModel ? 'selected' : ''}>${m}</option>`).join('')} ${(settings.modelList || []).map(m => `<option value="${m}" ${m === settings.selectedModel ? 'selected' : ''}>${m}</option>`).join('')}
</select> </select>
<div class="wechat-settings-input-wrapper" style="margin-top: 8px;"> <div class="wechat-settings-input-wrapper" style="margin-top: 8px;">
@@ -655,7 +551,7 @@ function generateSettingsPageHTML(settings) {
<div class="wechat-settings-item wechat-settings-item-vertical"> <div class="wechat-settings-item wechat-settings-item-vertical">
<span class="wechat-settings-label">模型选择</span> <span class="wechat-settings-label">模型选择</span>
<select class="wechat-settings-input wechat-settings-select" id="wechat-group-model-select"> <select class="wechat-settings-input wechat-settings-select" id="wechat-group-model-select">
<option value="">-- 选择模型 --</option> <option value="">选择模型</option>
${(settings.groupModelList || []).map(m => `<option value="${m}" ${m === settings.groupSelectedModel ? 'selected' : ''}>${m}</option>`).join('')} ${(settings.groupModelList || []).map(m => `<option value="${m}" ${m === settings.groupSelectedModel ? 'selected' : ''}>${m}</option>`).join('')}
</select> </select>
<div class="wechat-settings-input-wrapper" style="margin-top: 8px;"> <div class="wechat-settings-input-wrapper" style="margin-top: 8px;">
@@ -795,7 +691,7 @@ function generateServicePageHTML(settings) {
<div class="wechat-slide-panel-row-label"><span>模型</span></div> <div class="wechat-slide-panel-row-label"><span>模型</span></div>
<div class="wechat-slide-panel-body"> <div class="wechat-slide-panel-body">
<select class="wechat-settings-input wechat-settings-select" id="wechat-summary-model"> <select class="wechat-settings-input wechat-settings-select" id="wechat-summary-model">
<option value="">-- 选择模型 --</option> <option value="">选择模型</option>
${(settings.summaryModelList || []).map(m => `<option value="${m}" ${m === settings.summarySelectedModel ? 'selected' : ''}>${m}</option>`).join('')} ${(settings.summaryModelList || []).map(m => `<option value="${m}" ${m === settings.summarySelectedModel ? 'selected' : ''}>${m}</option>`).join('')}
</select> </select>
</div> </div>
@@ -873,9 +769,6 @@ function generateServicePageHTML(settings) {
<div style="font-size: 10px; color: #666 !important; margin-top: 4px;">每行一个表情包文件名</div> <div style="font-size: 10px; color: #666 !important; margin-top: 4px;">每行一个表情包文件名</div>
</div> </div>
<button class="wechat-btn wechat-btn-primary wechat-btn-block wechat-btn-small" id="wechat-add-meme-sticker">添加表情包</button> <button class="wechat-btn wechat-btn-primary wechat-btn-block wechat-btn-small" id="wechat-add-meme-sticker">添加表情包</button>
<div style="font-size: 10px; color: var(--wechat-text-secondary); text-align: center; margin-top: 8px;">
AI输出 &lt;meme&gt;文件名&lt;/meme&gt; 时自动渲染为图片
</div>
</div> </div>
<div class="wechat-service-section"> <div class="wechat-service-section">
<div class="wechat-service-section-title">总结功能</div> <div class="wechat-service-section-title">总结功能</div>
@@ -883,6 +776,7 @@ function generateServicePageHTML(settings) {
<div class="wechat-service-item" data-service="summary"><div class="wechat-service-icon blue"><svg viewBox="0 0 24 24"><path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg></div><span>总结</span></div> <div class="wechat-service-item" data-service="summary"><div class="wechat-service-icon blue"><svg viewBox="0 0 24 24"><path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg></div><span>总结</span></div>
<div class="wechat-service-item" data-service="history"><div class="wechat-service-icon blue"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M12 6v6l4 2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></div><span>历史回顾</span></div> <div class="wechat-service-item" data-service="history"><div class="wechat-service-icon blue"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M12 6v6l4 2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></div><span>历史回顾</span></div>
<div class="wechat-service-item" data-service="logs"><div class="wechat-service-icon green"><svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/></svg></div><span>日志</span></div> <div class="wechat-service-item" data-service="logs"><div class="wechat-service-icon green"><svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/></svg></div><span>日志</span></div>
<div class="wechat-service-item" data-service="summary-template"><div class="wechat-service-icon" style="background: linear-gradient(135deg, #607d8b, #455a64);"><svg viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg></div><span>总结模板</span></div>
</div> </div>
</div> </div>
<div class="wechat-service-section"> <div class="wechat-service-section">
@@ -891,6 +785,39 @@ function generateServicePageHTML(settings) {
<div class="wechat-service-item" data-service="meme-stickers"><div class="wechat-service-icon purple" style="background: linear-gradient(135deg, #9c27b0, #e91e63);"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="9" cy="9" r="1.5" fill="currentColor"/><circle cx="15" cy="9" r="1.5" fill="currentColor"/><path d="M7 14c1.5 3 4 4 5 4s3.5-1 5-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/></svg></div><span>Meme表情</span></div> <div class="wechat-service-item" data-service="meme-stickers"><div class="wechat-service-icon purple" style="background: linear-gradient(135deg, #9c27b0, #e91e63);"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="9" cy="9" r="1.5" fill="currentColor"/><circle cx="15" cy="9" r="1.5" fill="currentColor"/><path d="M7 14c1.5 3 4 4 5 4s3.5-1 5-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/></svg></div><span>Meme表情</span></div>
</div> </div>
</div> </div>
<div class="wechat-service-section">
<div class="wechat-service-section-title">用户功能</div>
<div class="wechat-service-grid">
<div class="wechat-service-item" data-service="change-password"><div class="wechat-service-icon" style="background: linear-gradient(135deg, #ff9800, #f57c00);"><svg viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M7 11V7a5 5 0 0110 0v4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/><circle cx="12" cy="16" r="1.5" fill="currentColor"/></svg></div><span>修改密码</span></div>
</div>
</div>
<!-- 修改密码面板 -->
<div class="wechat-service-panel hidden" id="wechat-change-password-panel">
<div class="wechat-panel-header" style="justify-content: center;">
<span class="wechat-panel-title">修改支付密码</span>
</div>
<div style="padding: 16px;">
<div style="font-size: 12px; color: var(--wechat-text-secondary); margin-bottom: 12px; text-align: center;">设置6位数字支付密码用于转账和红包</div>
<input type="password" id="wechat-new-password-input" maxlength="6" pattern="[0-9]*" inputmode="numeric" placeholder="请输入6位数字密码" style="width: 100%; box-sizing: border-box; padding: 12px; font-size: 18px; text-align: center; letter-spacing: 8px; border: 1px solid #ddd; border-radius: 8px; background: #fff; color: #000;">
<div style="font-size: 11px; color: var(--wechat-text-secondary); margin-top: 8px; text-align: center;">只能输入6位数字</div>
<button class="wechat-btn wechat-btn-primary wechat-btn-block" id="wechat-save-password-btn" style="margin-top: 16px;">保存密码</button>
</div>
</div>
<!-- 总结模板面板 -->
<div class="wechat-service-panel hidden" id="wechat-summary-template-panel">
<div class="wechat-panel-header">
<span class="wechat-panel-title">自定义总结模板</span>
<button class="wechat-panel-close" data-panel="wechat-summary-template-panel">×</button>
</div>
<div style="padding: 16px;">
<div style="font-size: 12px; color: var(--wechat-text-secondary); margin-bottom: 12px;">自定义总结提示词,留空则使用默认模板</div>
<textarea id="wechat-summary-template-input" placeholder="输入自定义总结提示词..." style="width: 100%; height: 200px; box-sizing: border-box; padding: 10px; font-size: 13px; color: #000; background: #fff; border: 1px solid #ddd; border-radius: 8px; resize: vertical; line-height: 1.5;">${settings.customSummaryTemplate || ''}</textarea>
<div style="display: flex; gap: 10px; margin-top: 12px;">
<button class="wechat-btn wechat-btn-block" id="wechat-summary-template-reset" style="flex: 1;">恢复默认</button>
<button class="wechat-btn wechat-btn-primary wechat-btn-block" id="wechat-summary-template-save" style="flex: 1;">保存模板</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
`; `;
@@ -1106,18 +1033,12 @@ function generateVideoCallPageHTML() {
return ` return `
<!-- 视频通话页面 --> <!-- 视频通话页面 -->
<div id="wechat-video-call-page" class="wechat-video-call-page hidden"> <div id="wechat-video-call-page" class="wechat-video-call-page hidden">
<!-- 背景/对方视频区域 -->
<div class="wechat-video-call-bg" id="wechat-video-call-bg">
<div class="wechat-video-call-remote-avatar" id="wechat-video-call-remote-avatar"></div>
</div>
<!-- 顶部状态栏 --> <!-- 顶部状态栏 -->
<div class="wechat-video-call-header"> <div class="wechat-video-call-header">
<button class="wechat-video-call-minimize" id="wechat-video-call-minimize"> <button class="wechat-video-call-minimize" id="wechat-video-call-minimize">
<svg viewBox="0 0 24 24" width="24" height="24"><rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M9 3v18M3 9h6" stroke="currentColor" stroke-width="1.5"/></svg> <svg viewBox="0 0 24 24" width="24" height="24"><rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M9 3v18M3 9h6" stroke="currentColor" stroke-width="1.5"/></svg>
</button> </button>
<div class="wechat-video-call-info"> <div class="wechat-video-call-info">
<span class="wechat-video-call-name" id="wechat-video-call-name"></span>
<span class="wechat-video-call-time" id="wechat-video-call-time">00:00</span> <span class="wechat-video-call-time" id="wechat-video-call-time">00:00</span>
</div> </div>
<button class="wechat-video-call-switch" id="wechat-video-call-switch" title="切换摄像头"> <button class="wechat-video-call-switch" id="wechat-video-call-switch" title="切换摄像头">
@@ -1125,13 +1046,13 @@ function generateVideoCallPageHTML() {
</button> </button>
</div> </div>
<!-- 等待接听状态 --> <!-- 中间角色头像区域(圆形) -->
<div class="wechat-video-call-waiting" id="wechat-video-call-waiting"> <div class="wechat-video-call-center">
<div class="wechat-video-call-avatar" id="wechat-video-call-avatar"></div> <div class="wechat-video-call-avatar" id="wechat-video-call-avatar"></div>
<div class="wechat-video-call-status" id="wechat-video-call-status">等待对方接受邀请</div> <div class="wechat-video-call-status" id="wechat-video-call-status">等待对方接受邀请</div>
</div> </div>
<!-- 本地视频/头像小窗 --> <!-- 右上角用户头像小窗(长方形) -->
<div class="wechat-video-call-local" id="wechat-video-call-local"> <div class="wechat-video-call-local" id="wechat-video-call-local">
<div class="wechat-video-call-local-avatar" id="wechat-video-call-local-avatar"></div> <div class="wechat-video-call-local-avatar" id="wechat-video-call-local-avatar"></div>
</div> </div>
@@ -1300,3 +1221,340 @@ export function generateMusicPanelHTML() {
</div> </div>
`; `;
} }
// 发红包页面 HTML
function generateRedPacketPageHTML(settings) {
return `
<!-- 发红包页面 -->
<div id="wechat-red-packet-page" class="wechat-red-packet-page hidden">
<div class="wechat-navbar wechat-rp-navbar">
<button class="wechat-navbar-btn wechat-navbar-back" id="wechat-red-packet-back"></button>
<span class="wechat-navbar-title">发红包</span>
<button class="wechat-navbar-btn">⋯</button>
</div>
<div class="wechat-rp-content">
<div class="wechat-rp-form">
<div class="wechat-rp-row">
<span class="wechat-rp-label">金额</span>
<input type="number" step="0.01" min="0.01" max="200" class="wechat-rp-amount-input" id="wechat-red-packet-amount-input" placeholder="0.00">
</div>
<div class="wechat-rp-row">
<input type="text" class="wechat-rp-message-input" id="wechat-red-packet-message" placeholder="恭喜发财,大吉大利" maxlength="20">
<span class="wechat-rp-emoji-btn"><svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9" stroke-width="2" stroke-linecap="round"/><line x1="15" y1="9" x2="15.01" y2="9" stroke-width="2" stroke-linecap="round"/></svg></span>
</div>
<div class="wechat-rp-row wechat-rp-cover-row">
<span class="wechat-rp-label">红包封面</span>
<span class="wechat-rp-arrow"></span>
</div>
</div>
<div class="wechat-rp-amount-display">
<span id="wechat-red-packet-amount-display">¥ 0.00</span>
</div>
<button class="wechat-rp-submit-btn" id="wechat-red-packet-submit">塞钱进红包</button>
<div class="wechat-rp-hint">未领取的红包将于24小时后发起退款</div>
</div>
<!-- 密码输入弹窗 -->
<div class="wechat-rp-password-modal hidden" id="wechat-red-packet-password-modal">
<div class="wechat-rp-password-content">
<button class="wechat-rp-password-close" id="wechat-password-modal-close">×</button>
<div class="wechat-rp-password-title">请输入支付密码</div>
<input type="password" maxlength="6" pattern="[0-9]*" inputmode="numeric" class="wechat-rp-password-input" id="wechat-red-packet-password-input" placeholder="请输入6位密码">
<button class="wechat-rp-password-confirm-btn" id="wechat-red-packet-password-confirm">确定</button>
</div>
</div>
</div>
`;
}
// 开红包弹窗 HTML
function generateOpenRedPacketHTML() {
return `
<!-- 开红包弹窗 -->
<div id="wechat-open-red-packet-modal" class="wechat-open-rp-modal hidden">
<div class="wechat-open-rp-wrapper">
<!-- 上半部分(动画时向上滑出) -->
<div class="wechat-open-rp-top" id="wechat-open-rp-top">
<button class="wechat-open-rp-close" id="wechat-open-rp-close">×</button>
<div class="wechat-open-rp-header">
<div class="wechat-open-rp-icon">${ICON_RED_PACKET_LARGE}</div>
<div class="wechat-open-rp-sender" id="wechat-open-rp-sender">xxx发出的红包</div>
</div>
<div class="wechat-open-rp-message" id="wechat-open-rp-message">恭喜发财,大吉大利</div>
</div>
<!-- 開按钮 -->
<div class="wechat-open-rp-btn-wrapper">
<button class="wechat-open-rp-btn" id="wechat-open-rp-btn">開</button>
</div>
<!-- 下半部分(动画时向下滑出) -->
<div class="wechat-open-rp-bottom" id="wechat-open-rp-bottom"></div>
</div>
<!-- 底部红包预览条 -->
<div class="wechat-open-rp-preview">
<span class="wechat-open-rp-preview-icon">${ICON_RED_PACKET}</span>
<span class="wechat-open-rp-preview-msg" id="wechat-open-rp-preview-msg">恭喜发财,大吉大利</span>
<button class="wechat-open-rp-preview-close" id="wechat-open-rp-preview-close">×</button>
</div>
</div>
`;
}
// 发转账页面 HTML
function generateTransferPageHTML() {
return `
<!-- 发转账页面 -->
<div id="wechat-transfer-page" class="wechat-transfer-page hidden">
<div class="wechat-navbar wechat-tf-navbar">
<button class="wechat-navbar-btn wechat-navbar-back" id="wechat-transfer-back"></button>
<span class="wechat-navbar-title">转账</span>
<button class="wechat-navbar-btn">⋯</button>
</div>
<div class="wechat-tf-content">
<div class="wechat-tf-form">
<div class="wechat-tf-row">
<span class="wechat-tf-label">金额</span>
<input type="number" step="0.01" min="0.01" class="wechat-tf-amount-input" id="wechat-transfer-amount-input" placeholder="0.00">
</div>
<div class="wechat-tf-row">
<input type="text" class="wechat-tf-desc-input" id="wechat-transfer-description" placeholder="添加转账说明" maxlength="30">
</div>
</div>
<div class="wechat-tf-amount-display">
<span id="wechat-transfer-amount-display">¥ 0.00</span>
</div>
</div>
<div class="wechat-tf-footer">
<button class="wechat-tf-submit-btn" id="wechat-transfer-submit">转账</button>
<div class="wechat-tf-hint">转账给好友后对方需确认收款</div>
</div>
<!-- 密码输入弹窗 -->
<div class="wechat-tf-password-modal hidden" id="wechat-transfer-password-modal">
<div class="wechat-tf-password-content">
<button class="wechat-tf-password-close" id="wechat-transfer-password-close">×</button>
<div class="wechat-tf-password-title">请输入支付密码</div>
<input type="password" maxlength="6" pattern="[0-9]*" inputmode="numeric" class="wechat-tf-password-input" id="wechat-transfer-password-input" placeholder="请输入6位密码">
<button class="wechat-tf-password-confirm-btn" id="wechat-transfer-password-confirm">确定</button>
</div>
</div>
</div>
`;
}
// 收款页面 HTML
function generateReceiveTransferPageHTML() {
return `
<!-- 收款页面 -->
<div id="wechat-receive-transfer-page" class="wechat-receive-tf-page hidden">
<div class="wechat-navbar wechat-tf-navbar">
<button class="wechat-navbar-btn wechat-navbar-back" id="wechat-transfer-receive-back"></button>
<span class="wechat-navbar-title">转账</span>
<span></span>
</div>
<div class="wechat-receive-tf-content">
<div class="wechat-receive-tf-card">
<div class="wechat-receive-tf-sender">
<div class="wechat-receive-tf-avatar" id="wechat-transfer-receive-avatar"></div>
<div class="wechat-receive-tf-name" id="wechat-transfer-receive-name">对方</div>
</div>
<div class="wechat-receive-tf-amount" id="wechat-transfer-receive-amount">¥0.00</div>
<div class="wechat-receive-tf-desc" id="wechat-transfer-receive-desc">转账给你</div>
<div class="wechat-receive-tf-actions">
<button class="wechat-receive-tf-btn refund" id="wechat-transfer-refund-btn">退还</button>
<button class="wechat-receive-tf-btn receive" id="wechat-transfer-receive-btn">收款</button>
</div>
</div>
<div class="wechat-receive-tf-tip">24小时内未确认将退回给对方</div>
</div>
</div>
`;
}
// 退还确认框 HTML
function generateTransferRefundConfirmHTML() {
return `
<!-- 退还确认框 -->
<div id="wechat-transfer-refund-confirm" class="wechat-transfer-confirm-modal hidden">
<div class="wechat-transfer-confirm-content">
<div class="wechat-transfer-confirm-title">退还转账?</div>
<div class="wechat-transfer-confirm-actions">
<button class="wechat-transfer-confirm-btn cancel" id="wechat-transfer-refund-cancel">暂不退还</button>
<button class="wechat-transfer-confirm-btn confirm" id="wechat-transfer-refund-confirm">退还</button>
</div>
</div>
</div>
`;
}
// 红包详情页 HTML
function generateRedPacketDetailHTML(settings) {
return `
<!-- 红包详情页 -->
<div id="wechat-red-packet-detail-page" class="wechat-rp-detail-page hidden">
<div class="wechat-rp-detail-header">
<button class="wechat-navbar-btn wechat-navbar-back wechat-rp-detail-back" id="wechat-rp-detail-back"></button>
<span></span>
<button class="wechat-navbar-btn">⋯</button>
</div>
<div class="wechat-rp-detail-top">
<div class="wechat-rp-detail-icon">${ICON_RED_PACKET_LARGE}</div>
<div class="wechat-rp-detail-sender" id="wechat-rp-detail-sender">xxx发出的红包</div>
<div class="wechat-rp-detail-message" id="wechat-rp-detail-message">恭喜发财,大吉大利</div>
</div>
<div class="wechat-rp-detail-body">
<div class="wechat-rp-detail-amount">
<span id="wechat-rp-detail-amount">0.00</span>
<span class="wechat-rp-detail-unit">元</span>
</div>
<div class="wechat-rp-detail-tip">已存入零钱,可直接提现 </div>
</div>
<div class="wechat-rp-detail-record">
<div class="wechat-rp-detail-record-item">
<div class="wechat-rp-detail-claimer-avatar" id="wechat-rp-detail-claimer-avatar">${ICON_USER}</div>
<div class="wechat-rp-detail-claimer-info">
<div class="wechat-rp-detail-claimer-name" id="wechat-rp-detail-claimer-name">User</div>
<div class="wechat-rp-detail-claimer-time" id="wechat-rp-detail-claimer-time">00:00</div>
</div>
<div class="wechat-rp-detail-claimer-amount" id="wechat-rp-detail-claimer-amount">0.00元</div>
</div>
</div>
</div>
`;
}
// 一起听功能 HTML
function generateListenTogetherHTML() {
return `
<!-- 一起听搜索页面 -->
<div id="wechat-listen-search-page" class="wechat-listen-search-page hidden">
<div class="wechat-navbar">
<button class="wechat-navbar-btn wechat-navbar-back" id="wechat-listen-search-back">
<svg viewBox="0 0 24 24" width="24" height="24"><path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="wechat-navbar-title">一起听</span>
<span></span>
</div>
<div class="wechat-listen-search-content">
<div class="wechat-listen-search-box">
<svg viewBox="0 0 24 24" width="16" height="16"><circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2" fill="none"/><path d="M21 21l-4.35-4.35" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<input type="text" id="wechat-listen-search-input" placeholder="搜索歌曲">
</div>
<div class="wechat-listen-search-results" id="wechat-listen-search-results">
<div class="wechat-listen-search-empty">输入关键词搜索歌曲</div>
</div>
</div>
</div>
<!-- 一起听等待页面 -->
<div id="wechat-listen-waiting-page" class="wechat-listen-waiting-page hidden">
<div class="wechat-listen-waiting-bg">
<div class="wechat-listen-waiting-content">
<!-- 用户头像 -->
<div class="wechat-listen-waiting-avatar" id="wechat-listen-waiting-avatar"></div>
<!-- 歌曲封面 -->
<div class="wechat-listen-waiting-cover-wrapper">
<img class="wechat-listen-waiting-cover" id="wechat-listen-waiting-cover" src="" alt="封面">
<!-- 雷达动画 -->
<div class="wechat-listen-radar">
<div class="wechat-listen-radar-ring"></div>
<div class="wechat-listen-radar-ring"></div>
<div class="wechat-listen-radar-ring"></div>
</div>
</div>
<div class="wechat-listen-waiting-text">正在等待<span id="wechat-listen-waiting-name">TA</span><span id="wechat-listen-waiting-dots">...</span></div>
<button class="wechat-listen-waiting-cancel" id="wechat-listen-cancel">取消</button>
</div>
</div>
</div>
<!-- 一起听主页面 -->
<div id="wechat-listen-together-page" class="wechat-listen-together-page hidden">
<!-- 顶部栏 -->
<div class="wechat-listen-header">
<button class="wechat-listen-back-btn" id="wechat-listen-back-btn">
<svg viewBox="0 0 24 24" width="24" height="24"><path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="wechat-listen-header-title">一起听</span>
<button class="wechat-listen-color-btn" id="wechat-listen-color-btn">
<svg viewBox="0 0 24 24" width="22" height="22"><polygon points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
<!-- 背景颜色选择器 -->
<div class="wechat-listen-color-picker hidden" id="wechat-listen-color-picker">
<div class="wechat-listen-color-option" data-bg="starry" title="星空蓝"></div>
<div class="wechat-listen-color-option" data-bg="orange" title="活力橙"></div>
<div class="wechat-listen-color-option" data-bg="pink" title="可爱粉"></div>
<div class="wechat-listen-color-option" data-bg="white" title="纯白"></div>
</div>
<!-- 双头像区域AI在左用户在右 -->
<div class="wechat-listen-avatars">
<div class="wechat-listen-avatar-item" id="wechat-listen-ai-avatar"></div>
<div class="wechat-listen-avatar-connector">
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M3 18v-6a9 9 0 0118 0v6" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M21 19a2 2 0 01-2 2h-1a2 2 0 01-2-2v-3a2 2 0 012-2h3v5z" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M3 19a2 2 0 002 2h1a2 2 0 002-2v-3a2 2 0 00-2-2H3v5z" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>
</div>
<div class="wechat-listen-avatar-item" id="wechat-listen-user-avatar"></div>
</div>
<!-- 歌曲信息(头像下方) -->
<div class="wechat-listen-song-info">
<div class="wechat-listen-song-name" id="wechat-listen-song-name">歌曲名</div>
<div class="wechat-listen-song-artist" id="wechat-listen-song-artist">歌手</div>
</div>
<!-- 唱片区域 -->
<div class="wechat-listen-disc-wrapper">
<div class="wechat-listen-disc" id="wechat-listen-disc">
<img class="wechat-listen-cover" id="wechat-listen-cover" src="" alt="封面">
</div>
</div>
<!-- 进度条 -->
<div class="wechat-listen-progress">
<span class="wechat-listen-time" id="wechat-listen-current-time">0:00</span>
<div class="wechat-listen-progress-bar">
<div class="wechat-listen-progress-fill" id="wechat-listen-progress-fill"></div>
<input type="range" class="wechat-listen-slider" id="wechat-listen-slider" min="0" max="100" value="0">
</div>
<span class="wechat-listen-time" id="wechat-listen-duration">0:00</span>
</div>
<!-- 聊天消息区域 -->
<div class="wechat-listen-messages" id="wechat-listen-messages"></div>
<!-- 聊天输入框 -->
<div class="wechat-listen-chat-input" id="wechat-listen-chat-input">
<input type="text" class="wechat-listen-input-text" id="wechat-listen-input-text" placeholder="输入文字...">
<button class="wechat-listen-send-btn" id="wechat-listen-send-btn">
<svg viewBox="0 0 24 24" width="20" height="20"><line x1="22" y1="2" x2="11" y2="13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><polygon points="22,2 15,22 11,13 2,9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
<!-- 控制按钮(底部) -->
<div class="wechat-listen-controls">
<button class="wechat-listen-ctrl-btn" id="wechat-listen-star-btn" title="更换背景">
<svg viewBox="0 0 24 24" width="20" height="20"><polygon points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<button class="wechat-listen-ctrl-btn wechat-listen-play-btn" id="wechat-listen-play-btn">
<svg viewBox="0 0 24 24" width="24" height="24"><polygon points="5,3 19,12 5,21" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<button class="wechat-listen-ctrl-btn" id="wechat-listen-end-btn" title="结束">
<svg viewBox="0 0 24 24" width="20" height="20"><line x1="18" y1="6" x2="6" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="6" y1="6" x2="18" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
</button>
</div>
<!-- 换歌面板 -->
<div class="wechat-listen-change-panel hidden" id="wechat-listen-change-panel">
<div class="wechat-listen-change-header">
<span class="wechat-listen-change-title">换一首</span>
<button class="wechat-listen-change-close" id="wechat-listen-change-close">
<svg viewBox="0 0 24 24" width="18" height="18"><line x1="18" y1="6" x2="6" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><line x1="6" y1="6" x2="18" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
<div class="wechat-listen-change-search">
<svg viewBox="0 0 24 24" width="14" height="14"><circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2" fill="none"/><path d="M21 21l-4.35-4.35" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<input type="text" id="wechat-listen-change-input" placeholder="搜索歌曲">
</div>
<div class="wechat-listen-change-results" id="wechat-listen-change-results"></div>
</div>
</div>
`;
}

View File

@@ -2,7 +2,7 @@
* 手机面板:显示/隐藏、自动居中、拖拽定位 * 手机面板:显示/隐藏、自动居中、拖拽定位
*/ */
import { saveSettingsDebounced } from '../../../../script.js'; import { requestSave } from './save-manager.js';
import { getSettings } from './config.js'; import { getSettings } from './config.js';
import { getCurrentTime } from './utils.js'; import { getCurrentTime } from './utils.js';
@@ -136,7 +136,7 @@ export function setupPhoneDrag() {
x: rect.left + rect.width / 2, x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2, y: rect.top + rect.height / 2,
}; };
saveSettingsDebounced(); requestSave();
}; };
statusbar.addEventListener('mousedown', handleStart); statusbar.addEventListener('mousedown', handleStart);
@@ -151,7 +151,7 @@ export function setupPhoneDrag() {
phoneManuallyPositioned = false; phoneManuallyPositioned = false;
const settings = getSettings(); const settings = getSettings();
delete settings.phonePosition; delete settings.phonePosition;
saveSettingsDebounced(); requestSave();
centerPhoneInViewport({ force: true }); centerPhoneInViewport({ force: true });
}); });
} }
@@ -203,7 +203,7 @@ export function togglePhone() {
phone.classList.toggle('hidden'); phone.classList.toggle('hidden');
settings.phoneVisible = !phone.classList.contains('hidden'); settings.phoneVisible = !phone.classList.contains('hidden');
saveSettingsDebounced(); requestSave();
if (settings.phoneVisible) { if (settings.phoneVisible) {
const timeEl = document.querySelector('.wechat-statusbar-time'); const timeEl = document.querySelector('.wechat-statusbar-time');

572
red-packet.js Normal file
View File

@@ -0,0 +1,572 @@
/**
* 红包功能模块
* 支持用户发红包、AI发红包、开红包动画、钱包余额管理
*/
import { getSettings } from './config.js';
import { requestSave } from './save-manager.js';
import { showToast } from './toast.js';
import { escapeHtml, sleep } from './utils.js';
import { refreshChatList } from './ui.js';
import { callAI } from './ai.js';
import { ICON_USER } from './icons.js';
// 当前红包相关状态
let currentRedPacketAmount = '';
let currentRedPacketMessage = '恭喜发财,大吉大利';
let pendingOpenRedPacket = null; // 待打开的红包信息
let pendingOpenContact = null; // 待打开红包的联系人
// ===== 钱包操作函数 =====
/**
* 获取钱包余额
*/
export function getWalletBalance() {
const settings = getSettings();
return parseFloat(settings.walletAmount) || 0;
}
/**
* 从钱包扣款(发红包)
*/
export function deductFromWallet(amount) {
const settings = getSettings();
const current = parseFloat(settings.walletAmount) || 0;
if (amount <= 0) {
return { success: false, message: '金额必须大于0' };
}
if (amount > 200) {
return { success: false, message: '单个红包最多200元' };
}
if (amount > current) {
return { success: false, message: '余额不足' };
}
settings.walletAmount = (current - amount).toFixed(2);
requestSave();
updateWalletDisplay();
return { success: true, balance: settings.walletAmount };
}
/**
* 存入钱包(收红包)
*/
export function addToWallet(amount) {
const settings = getSettings();
const current = parseFloat(settings.walletAmount) || 0;
settings.walletAmount = (current + amount).toFixed(2);
requestSave();
updateWalletDisplay();
return { success: true, balance: settings.walletAmount };
}
/**
* 更新钱包显示
*/
export function updateWalletDisplay() {
const el = document.getElementById('wechat-wallet-amount');
if (el) {
el.textContent = '¥' + getWalletBalance().toFixed(2);
}
}
// ===== 发红包页面 =====
/**
* 显示发红包页面
*/
export function showRedPacketPage() {
currentRedPacketAmount = '';
currentRedPacketMessage = '恭喜发财,大吉大利';
const page = document.getElementById('wechat-red-packet-page');
if (page) {
page.classList.remove('hidden');
updateRedPacketAmountDisplay();
const messageInput = document.getElementById('wechat-red-packet-message');
if (messageInput) {
messageInput.value = currentRedPacketMessage;
}
const amountInput = document.getElementById('wechat-red-packet-amount-input');
if (amountInput) {
amountInput.value = '';
}
}
}
/**
* 隐藏发红包页面
*/
export function hideRedPacketPage() {
const page = document.getElementById('wechat-red-packet-page');
if (page) {
page.classList.add('hidden');
}
}
/**
* 更新金额显示
*/
function updateRedPacketAmountDisplay() {
const amountDisplay = document.getElementById('wechat-red-packet-amount-display');
const amountInput = document.getElementById('wechat-red-packet-amount-input');
const amount = amountInput ? parseFloat(amountInput.value) || 0 : parseFloat(currentRedPacketAmount) || 0;
if (amountDisplay) {
amountDisplay.textContent = '¥ ' + (amount > 0 ? amount.toFixed(2) : '0.00');
}
}
/**
* 显示密码输入弹窗
*/
export function showPasswordModal() {
const amountInput = document.getElementById('wechat-red-packet-amount-input');
const amount = amountInput ? parseFloat(amountInput.value) || 0 : 0;
if (amount <= 0) {
showToast('请输入金额');
return;
}
if (amount > 200) {
showToast('单个红包最多200元');
return;
}
if (amount > getWalletBalance()) {
showToast('余额不足');
return;
}
currentRedPacketAmount = amount.toString();
// 获取祝福语
const messageInput = document.getElementById('wechat-red-packet-message');
if (messageInput && messageInput.value.trim()) {
currentRedPacketMessage = messageInput.value.trim();
}
const modal = document.getElementById('wechat-red-packet-password-modal');
if (modal) {
modal.classList.remove('hidden');
const passwordInput = document.getElementById('wechat-red-packet-password-input');
if (passwordInput) {
passwordInput.value = '';
passwordInput.focus();
}
}
}
/**
* 隐藏密码输入弹窗
*/
export function hidePasswordModal() {
const modal = document.getElementById('wechat-red-packet-password-modal');
if (modal) {
modal.classList.add('hidden');
}
}
/**
* 验证密码并发送红包
*/
function verifyPasswordAndSend() {
const passwordInput = document.getElementById('wechat-red-packet-password-input');
const password = passwordInput?.value || '';
if (password.length !== 6) {
showToast('请输入6位密码');
return;
}
const settings = getSettings();
const correctPassword = settings.paymentPassword || '666666';
if (password === correctPassword) {
hidePasswordModal();
sendRedPacket();
} else {
showToast('密码错误');
if (passwordInput) passwordInput.value = '';
}
}
/**
* 发送红包
*/
async function sendRedPacket() {
const amount = parseFloat(currentRedPacketAmount) || 0;
const message = currentRedPacketMessage;
// 扣款
const result = deductFromWallet(amount);
if (!result.success) {
showToast(result.message, 'info');
return;
}
// 关闭发红包页面
hideRedPacketPage();
// 触发发送红包事件
const event = new CustomEvent('red-packet-send', {
detail: {
amount: amount,
message: message
}
});
document.dispatchEvent(event);
}
// ===== 开红包弹窗 =====
/**
* 显示开红包弹窗AI发的红包
*/
export function showOpenRedPacket(redPacketInfo, contact) {
pendingOpenRedPacket = redPacketInfo;
pendingOpenContact = contact;
const modal = document.getElementById('wechat-open-red-packet-modal');
if (!modal) return;
// 更新显示内容
const senderName = document.getElementById('wechat-open-rp-sender');
const messageEl = document.getElementById('wechat-open-rp-message');
const previewMsg = document.getElementById('wechat-open-rp-preview-msg');
if (senderName) {
senderName.textContent = `${contact?.name || 'AI'}发出的红包`;
}
if (messageEl) {
messageEl.textContent = redPacketInfo.message || '恭喜发财,大吉大利';
}
if (previewMsg) {
previewMsg.textContent = redPacketInfo.message || '恭喜发财,大吉大利';
}
modal.classList.remove('hidden');
}
/**
* 隐藏开红包弹窗
*/
export function hideOpenRedPacket() {
const modal = document.getElementById('wechat-open-red-packet-modal');
if (modal) {
modal.classList.add('hidden');
}
pendingOpenRedPacket = null;
pendingOpenContact = null;
}
/**
* 点击"開"按钮,播放开红包动画
*/
export async function openRedPacketAnimation() {
if (!pendingOpenRedPacket) return;
const modal = document.getElementById('wechat-open-red-packet-modal');
const topHalf = document.getElementById('wechat-open-rp-top');
const bottomHalf = document.getElementById('wechat-open-rp-bottom');
if (topHalf && bottomHalf) {
// 添加动画类
topHalf.classList.add('slide-up');
bottomHalf.classList.add('slide-down');
// 等待动画完成
await sleep(500);
}
// 隐藏开红包弹窗
if (modal) {
modal.classList.add('hidden');
// 重置动画类
if (topHalf) topHalf.classList.remove('slide-up');
if (bottomHalf) bottomHalf.classList.remove('slide-down');
}
// 领取红包
await claimAIRedPacket();
}
/**
* 领取AI红包
*/
async function claimAIRedPacket() {
if (!pendingOpenRedPacket || !pendingOpenContact) return;
const redPacketInfo = pendingOpenRedPacket;
const contact = pendingOpenContact;
const settings = getSettings();
// 存入钱包
addToWallet(redPacketInfo.amount);
// 更新红包状态
redPacketInfo.status = 'claimed';
redPacketInfo.claimedBy = settings.userName || 'User';
redPacketInfo.claimedAt = Date.now();
// 保存
requestSave();
// 显示红包详情页
showRedPacketDetail(redPacketInfo, contact);
// 更新聊天中的红包气泡状态
updateRedPacketBubbleStatus(redPacketInfo.id, 'claimed');
// 在聊天中显示领取提示
const event = new CustomEvent('red-packet-claimed-notice', {
detail: {
claimerName: settings.userName || 'User',
senderName: contact.name
}
});
document.dispatchEvent(event);
pendingOpenRedPacket = null;
pendingOpenContact = null;
}
/**
* 显示红包详情页
*/
export function showRedPacketDetail(redPacketInfo, contact) {
const page = document.getElementById('wechat-red-packet-detail-page');
if (!page) return;
const settings = getSettings();
// 更新详情页内容
const senderName = document.getElementById('wechat-rp-detail-sender');
const messageEl = document.getElementById('wechat-rp-detail-message');
const amountEl = document.getElementById('wechat-rp-detail-amount');
const claimerAvatar = document.getElementById('wechat-rp-detail-claimer-avatar');
const claimerName = document.getElementById('wechat-rp-detail-claimer-name');
const claimerTime = document.getElementById('wechat-rp-detail-claimer-time');
const claimerAmount = document.getElementById('wechat-rp-detail-claimer-amount');
if (senderName) {
senderName.textContent = `${contact?.name || 'AI'}发出的红包`;
}
if (messageEl) {
messageEl.textContent = redPacketInfo.message || '恭喜发财,大吉大利';
}
if (amountEl) {
amountEl.textContent = redPacketInfo.amount.toFixed(2);
}
if (claimerAvatar) {
// 使用用户头像
if (settings.userAvatar) {
claimerAvatar.innerHTML = `<img src="${settings.userAvatar}" alt="avatar">`;
} else {
claimerAvatar.innerHTML = `<span>${ICON_USER}</span>`;
}
}
if (claimerName) {
claimerName.textContent = settings.userName || 'User';
}
if (claimerTime) {
const now = new Date();
claimerTime.textContent = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
}
if (claimerAmount) {
claimerAmount.textContent = redPacketInfo.amount.toFixed(2) + '元';
}
page.classList.remove('hidden');
}
/**
* 隐藏红包详情页
*/
export function hideRedPacketDetail() {
const page = document.getElementById('wechat-red-packet-detail-page');
if (page) {
page.classList.add('hidden');
}
}
/**
* 更新红包气泡状态
*/
function updateRedPacketBubbleStatus(redPacketId, status) {
const bubble = document.querySelector(`.wechat-red-packet-bubble[data-rp-id="${redPacketId}"]`);
if (bubble && status === 'claimed') {
bubble.classList.add('claimed');
const statusEl = bubble.querySelector('.wechat-rp-bubble-status');
if (statusEl) {
statusEl.textContent = '已领取';
statusEl.classList.remove('hidden');
}
}
}
// ===== 生成红包ID =====
export function generateRedPacketId() {
return 'rp_' + Math.random().toString(36).substring(2, 10) + '_' + Date.now();
}
// ===== 初始化事件监听 =====
export function initRedPacketEvents() {
// 发红包页面返回按钮
document.getElementById('wechat-red-packet-back')?.addEventListener('click', hideRedPacketPage);
// 金额输入框变化时更新显示
document.getElementById('wechat-red-packet-amount-input')?.addEventListener('input', updateRedPacketAmountDisplay);
// 塞钱进红包按钮
document.getElementById('wechat-red-packet-submit')?.addEventListener('click', showPasswordModal);
// 密码弹窗关闭
document.getElementById('wechat-password-modal-close')?.addEventListener('click', hidePasswordModal);
// 密码确认按钮
document.getElementById('wechat-red-packet-password-confirm')?.addEventListener('click', verifyPasswordAndSend);
// 开红包弹窗关闭
document.getElementById('wechat-open-rp-close')?.addEventListener('click', hideOpenRedPacket);
document.getElementById('wechat-open-rp-preview-close')?.addEventListener('click', hideOpenRedPacket);
// 开红包按钮
document.getElementById('wechat-open-rp-btn')?.addEventListener('click', openRedPacketAnimation);
// 红包详情页返回
document.getElementById('wechat-rp-detail-back')?.addEventListener('click', hideRedPacketDetail);
// 监听红包发送事件用户发红包后AI 领取)
document.addEventListener('red-packet-send', handleUserSendRedPacket);
// 监听红包领取提示事件
document.addEventListener('red-packet-claimed-notice', handleRedPacketClaimNotice);
}
/**
* 处理用户发送红包
*/
async function handleUserSendRedPacket(event) {
const { amount, message } = event.detail;
const settings = getSettings();
// 动态导入 chat.js 中的函数,避免循环依赖
const chatModule = await import('./chat.js');
const { currentChatIndex, appendRedPacketMessage, appendRedPacketClaimNotice, showTypingIndicator, hideTypingIndicator, appendMessage, openChat } = chatModule;
if (currentChatIndex < 0) return;
const contact = settings.contacts[currentChatIndex];
if (!contact) return;
const now = new Date();
const timeStr = now.toLocaleString('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
}).replace(/\//g, '-');
// 创建红包信息
const rpInfo = {
id: generateRedPacketId(),
amount: amount,
message: message,
senderName: settings.userName || 'User',
status: 'pending',
claimedBy: null,
claimedAt: null,
expireAt: Date.now() + 24 * 60 * 60 * 1000
};
// 保存红包消息到聊天记录
if (!contact.chatHistory) contact.chatHistory = [];
contact.chatHistory.push({
role: 'user',
content: `[红包] ${message}`,
time: timeStr,
timestamp: Date.now(),
isRedPacket: true,
redPacketInfo: rpInfo
});
// 显示红包消息
appendRedPacketMessage('user', rpInfo, contact);
requestSave();
refreshChatList();
// AI 领取红包(延迟 2-5 秒)
const claimDelay = 2000 + Math.random() * 3000;
await sleep(claimDelay);
// 更新红包状态
rpInfo.status = 'claimed';
rpInfo.claimedBy = contact.name;
rpInfo.claimedAt = Date.now();
// 更新聊天中的红包气泡状态
updateRedPacketBubbleStatus(rpInfo.id, 'claimed');
// 显示领取提示
appendRedPacketClaimNotice(contact.name, settings.userName || 'User', false);
requestSave();
// AI 发送感谢消息(带上下文)
await sleep(1000);
// 显示打字指示器
showTypingIndicator(contact);
try {
// 构建提示,让 AI 根据上下文自然回复
const thankPrompt = `用户给你发了一个${amount}元的红包,祝福语是"${message}",请自然地表示感谢,不要使用任何特殊格式标签。`;
const aiResponse = await callAI(contact, thankPrompt);
hideTypingIndicator();
if (aiResponse && aiResponse.trim()) {
// 取第一条回复
let thankMsg = aiResponse.split('|||')[0].trim();
// 移除可能的格式标签
thankMsg = thankMsg.replace(/^\[.*?\]\s*/, '');
if (thankMsg) {
contact.chatHistory.push({
role: 'assistant',
content: thankMsg,
time: new Date().toLocaleString('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
}).replace(/\//g, '-'),
timestamp: Date.now()
});
appendMessage('assistant', thankMsg, contact);
requestSave();
refreshChatList();
}
}
} catch (e) {
console.error('[可乐] AI感谢红包失败:', e);
hideTypingIndicator();
}
}
/**
* 处理红包领取提示用户领取AI红包
*/
function handleRedPacketClaimNotice(event) {
const { claimerName, senderName } = event.detail;
// 动态导入,避免循环依赖
import('./chat.js').then(chatModule => {
const { appendRedPacketClaimNotice } = chatModule;
appendRedPacketClaimNotice(claimerName, senderName, true);
});
}

140
save-manager.js Normal file
View File

@@ -0,0 +1,140 @@
/**
* 统一保存管理器
* 解决频繁调用 saveSettingsDebounced 导致的 "Settings could not be saved" 问题
*/
import { saveSettingsDebounced } from '../../../../script.js';
// 保存状态
let saveTimer = null;
let isSaving = false;
let pendingSave = false;
let autoSaveTimer = null;
let hasPendingChanges = false; // 标记是否有未保存的变更
// 配置
const SAVE_DELAY = 2500; // 2.5秒延迟,合并频繁操作
const SAVE_COOLDOWN = 1000; // 保存后的冷却时间
const AUTO_SAVE_INTERVAL = 15000; // 15秒自动保存间隔移动端保险
/**
* 请求保存(防抖)
* 用于一般操作,会合并多次调用
*/
export function requestSave() {
hasPendingChanges = true; // 标记有待保存的变更
// 如果已经有待处理的定时器,清除它
if (saveTimer) {
clearTimeout(saveTimer);
}
// 设置新的定时器
saveTimer = setTimeout(() => {
saveTimer = null;
executeSave();
}, SAVE_DELAY);
}
/**
* 立即保存
* 用于关键操作,如发送消息完成、删除联系人等
*/
export function saveNow() {
hasPendingChanges = true; // 确保标记
// 清除待处理的定时器
if (saveTimer) {
clearTimeout(saveTimer);
saveTimer = null;
}
executeSave();
}
/**
* 执行实际保存
*/
function executeSave() {
// 如果正在保存,标记有待处理的保存请求
if (isSaving) {
pendingSave = true;
return;
}
isSaving = true;
hasPendingChanges = false; // 清除待保存标记
try {
saveSettingsDebounced();
} catch (e) {
console.error('[SaveManager] 保存失败:', e);
}
// 冷却期后重置状态
setTimeout(() => {
isSaving = false;
// 如果冷却期间有新的保存请求,执行它
if (pendingSave) {
pendingSave = false;
executeSave();
}
}, SAVE_COOLDOWN);
}
/**
* 自动保存检查
* 定期检查是否有未保存的变更,有则保存
*/
function autoSaveCheck() {
if (hasPendingChanges && !isSaving && !saveTimer) {
console.log('[SaveManager] 自动保存触发');
executeSave();
}
}
/**
* 启动自动保存定时器
*/
function startAutoSave() {
if (autoSaveTimer) return; // 避免重复启动
autoSaveTimer = setInterval(autoSaveCheck, AUTO_SAVE_INTERVAL);
}
/**
* 统一处理页面卸载/隐藏时的保存
*/
function handleUnload() {
if (saveTimer) {
clearTimeout(saveTimer);
saveTimer = null;
}
// 直接调用原始保存函数,确保数据不丢失
if (hasPendingChanges) {
saveSettingsDebounced();
hasPendingChanges = false;
}
}
/**
* 页面卸载前保存
* 支持桌面端和移动端
*/
export function setupUnloadSave() {
// 桌面端:关闭/刷新页面时触发
window.addEventListener('beforeunload', handleUnload);
// 移动端关键:页面变为不可见时立即保存
// 当用户切换应用、锁屏、切换标签页时触发
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
handleUnload();
}
});
// 移动端补充:比 beforeunload 更可靠的页面卸载事件
// 在 iOS Safari 和部分 Android 浏览器上效果更好
window.addEventListener('pagehide', handleUnload);
// 启动自动保存(移动端的最后保险)
startAutoSave();
}

View File

@@ -2,7 +2,7 @@
* 设置页/服务页相关的 UI 逻辑(不包含业务模块) * 设置页/服务页相关的 UI 逻辑(不包含业务模块)
*/ */
import { saveSettingsDebounced } from '../../../../script.js'; import { requestSave } from './save-manager.js';
import { getSettings } from './config.js'; import { getSettings } from './config.js';
export function toggleDarkMode() { export function toggleDarkMode() {
@@ -14,7 +14,7 @@ export function toggleDarkMode() {
settings.darkMode = !settings.darkMode; settings.darkMode = !settings.darkMode;
phone.classList.toggle('wechat-dark', settings.darkMode); phone.classList.toggle('wechat-dark', settings.darkMode);
toggle.classList.toggle('on', settings.darkMode); toggle.classList.toggle('on', settings.darkMode);
saveSettingsDebounced(); requestSave();
} }
export function refreshContextTags() { export function refreshContextTags() {

View File

@@ -30,7 +30,7 @@ export function injectAuthorNote() {
return; return;
} }
showToast('无法找到作者注释区域', '🧊'); showToast('无法找到作者注释区域', 'info');
console.log('作者注释模板:', template); console.log('作者注释模板:', template);
} catch (err) { } catch (err) {
console.error('[可乐] 注入作者注释失败:', err); console.error('[可乐] 注入作者注释失败:', err);

3283
style.css

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
* 总结功能 * 总结功能
*/ */
import { saveSettingsDebounced } from '../../../../script.js'; import { requestSave } from './save-manager.js';
import { getContext } from '../../../extensions.js'; import { getContext } from '../../../extensions.js';
import { loadWorldInfo, saveWorldInfo, createNewWorldInfo, world_names } from '../../../world-info.js'; import { loadWorldInfo, saveWorldInfo, createNewWorldInfo, world_names } from '../../../world-info.js';
import { getSettings, getCupName, SUMMARY_MARKER_PREFIX, LOREBOOK_NAME_PREFIX, LOREBOOK_NAME_SUFFIX } from './config.js'; import { getSettings, getCupName, SUMMARY_MARKER_PREFIX, LOREBOOK_NAME_PREFIX, LOREBOOK_NAME_SUFFIX } from './config.js';
@@ -291,12 +291,20 @@ export function insertSummaryMarker(cupNumber, selectedFilter = null) {
} }
}); });
saveSettingsDebounced(); requestSave();
} }
// 生成总结提示词 // 生成总结提示词
export function generateSummaryPrompt(allChats, cupNumber) { export function generateSummaryPrompt(allChats, cupNumber) {
let prompt = `你是一位客观、精准的结构化事件记录员。你的任务是像历史学家记录史实一样,从这段【线上聊天记录】中提取并记录关键信息。 const settings = getSettings();
// 如果有自定义模板,使用自定义模板
let prompt;
if (settings.customSummaryTemplate && settings.customSummaryTemplate.trim()) {
prompt = settings.customSummaryTemplate.trim() + '\n\n【线上聊天记录】\n';
} else {
// 使用默认模板
prompt = `你是一位客观、精准的结构化事件记录员。你的任务是像历史学家记录史实一样,从这段【线上聊天记录】中提取并记录关键信息。
【核心原则】 【核心原则】
- 客观准确:只记录实际发生的事件,不添加主观推测或情感评价 - 客观准确:只记录实际发生的事件,不添加主观推测或情感评价
@@ -323,6 +331,7 @@ export function generateSummaryPrompt(allChats, cupNumber) {
【线上聊天记录】 【线上聊天记录】
`; `;
}
allChats.forEach(chat => { allChats.forEach(chat => {
prompt += `\n--- ${chat.contactName} ---\n`; prompt += `\n--- ${chat.contactName} ---\n`;
@@ -479,7 +488,7 @@ export function saveEntryToFavorites(entry, cupNumber, lorebookName) {
lorebook.entries.push(newEntry); lorebook.entries.push(newEntry);
lorebook.lastUpdated = timeStr; lorebook.lastUpdated = timeStr;
saveSettingsDebounced(); requestSave();
return lorebook; return lorebook;
} }
@@ -761,7 +770,7 @@ export async function rollbackSummary() {
updateProgress('✅ 世界书已清空,已删除...'); updateProgress('✅ 世界书已清空,已删除...');
} }
saveSettingsDebounced(); requestSave();
// 3) 尝试同步删除酒馆世界书条目(或整个世界书) // 3) 尝试同步删除酒馆世界书条目(或整个世界书)
try { try {

View File

@@ -2,7 +2,17 @@
* Toast 提示(显示在手机面板内) * Toast 提示(显示在手机面板内)
*/ */
export function showToast(message, icon = '✅', durationMs = 2000) { import { ICON_SUCCESS, ICON_INFO, ICON_REFUND, ICON_RED_PACKET } from './icons.js';
// 图标类型映射
const TOAST_ICONS = {
'success': ICON_SUCCESS,
'info': ICON_INFO,
'refund': ICON_REFUND,
'red-packet': ICON_RED_PACKET
};
export function showToast(message, icon = 'success', durationMs = 2000) {
const phone = document.getElementById('wechat-phone'); const phone = document.getElementById('wechat-phone');
if (!phone) return; if (!phone) return;
@@ -14,7 +24,13 @@ export function showToast(message, icon = '✅', durationMs = 2000) {
const iconEl = document.createElement('span'); const iconEl = document.createElement('span');
iconEl.className = 'wechat-toast-icon'; iconEl.className = 'wechat-toast-icon';
iconEl.textContent = icon;
// 支持图标类型字符串或直接的 SVG/emoji
if (TOAST_ICONS[icon]) {
iconEl.innerHTML = TOAST_ICONS[icon];
} else {
iconEl.textContent = icon;
}
const textEl = document.createElement('span'); const textEl = document.createElement('span');
textEl.textContent = message; textEl.textContent = message;

481
transfer.js Normal file
View File

@@ -0,0 +1,481 @@
/**
* 转账功能模块
* 支持用户发转账、AI发转账、收款/退还、钱包余额管理
*/
import { getSettings } from './config.js';
import { requestSave } from './save-manager.js';
import { showToast } from './toast.js';
import { escapeHtml, sleep } from './utils.js';
import { refreshChatList } from './ui.js';
import { callAI } from './ai.js';
import { deductFromWallet, addToWallet, getWalletBalance, updateWalletDisplay } from './red-packet.js';
// 当前转账相关状态
let currentTransferAmount = '';
let currentTransferDescription = '';
let pendingReceiveTransfer = null; // 待收款的转账信息
let pendingReceiveContact = null; // 待收款转账的联系人
// ===== 发转账页面 =====
/**
* 显示发转账页面
*/
export function showTransferPage() {
currentTransferAmount = '';
currentTransferDescription = '';
const page = document.getElementById('wechat-transfer-page');
if (page) {
page.classList.remove('hidden');
updateTransferAmountDisplay();
const descInput = document.getElementById('wechat-transfer-description');
if (descInput) {
descInput.value = '';
}
const amountInput = document.getElementById('wechat-transfer-amount-input');
if (amountInput) {
amountInput.value = '';
}
}
}
/**
* 隐藏发转账页面
*/
export function hideTransferPage() {
const page = document.getElementById('wechat-transfer-page');
if (page) {
page.classList.add('hidden');
}
}
/**
* 更新金额显示
*/
function updateTransferAmountDisplay() {
const amountDisplay = document.getElementById('wechat-transfer-amount-display');
const amountInput = document.getElementById('wechat-transfer-amount-input');
const amount = amountInput ? parseFloat(amountInput.value) || 0 : parseFloat(currentTransferAmount) || 0;
if (amountDisplay) {
amountDisplay.textContent = '¥ ' + (amount > 0 ? amount.toFixed(2) : '0.00');
}
}
/**
* 显示密码输入弹窗
*/
export function showTransferPasswordModal() {
const amountInput = document.getElementById('wechat-transfer-amount-input');
const amount = amountInput ? parseFloat(amountInput.value) || 0 : 0;
if (amount <= 0) {
showToast('请输入金额');
return;
}
if (amount > getWalletBalance()) {
showToast('余额不足');
return;
}
currentTransferAmount = amount.toString();
// 获取转账说明
const descInput = document.getElementById('wechat-transfer-description');
if (descInput && descInput.value.trim()) {
currentTransferDescription = descInput.value.trim();
}
const modal = document.getElementById('wechat-transfer-password-modal');
if (modal) {
modal.classList.remove('hidden');
const passwordInput = document.getElementById('wechat-transfer-password-input');
if (passwordInput) {
passwordInput.value = '';
passwordInput.focus();
}
}
}
/**
* 隐藏密码输入弹窗
*/
export function hideTransferPasswordModal() {
const modal = document.getElementById('wechat-transfer-password-modal');
if (modal) {
modal.classList.add('hidden');
}
}
/**
* 验证密码并发送转账
*/
function verifyTransferPasswordAndSend() {
const passwordInput = document.getElementById('wechat-transfer-password-input');
const password = passwordInput?.value || '';
if (password.length !== 6) {
showToast('请输入6位密码');
return;
}
const settings = getSettings();
const correctPassword = settings.paymentPassword || '666666';
if (password === correctPassword) {
hideTransferPasswordModal();
sendTransfer();
} else {
showToast('密码错误');
if (passwordInput) passwordInput.value = '';
}
}
/**
* 发送转账
*/
async function sendTransfer() {
const amount = parseFloat(currentTransferAmount) || 0;
const description = currentTransferDescription;
// 扣款(转账无上限,但需要检查余额)
const current = getWalletBalance();
if (amount > current) {
showToast('余额不足', 'info');
return;
}
// 扣款
const settings = getSettings();
settings.walletAmount = (current - amount).toFixed(2);
requestSave();
updateWalletDisplay();
// 关闭发转账页面
hideTransferPage();
// 触发发送转账事件
const event = new CustomEvent('transfer-send', {
detail: {
amount: amount,
description: description
}
});
document.dispatchEvent(event);
}
// ===== 收款页面 =====
/**
* 显示收款页面AI发的转账
*/
export function showReceiveTransferPage(transferInfo, contact) {
pendingReceiveTransfer = transferInfo;
pendingReceiveContact = contact;
const page = document.getElementById('wechat-receive-transfer-page');
if (!page) return;
// 更新显示内容
const senderAvatar = document.getElementById('wechat-transfer-receive-avatar');
const senderName = document.getElementById('wechat-transfer-receive-name');
const amountEl = document.getElementById('wechat-transfer-receive-amount');
const descEl = document.getElementById('wechat-transfer-receive-desc');
if (senderAvatar) {
if (contact?.avatar) {
senderAvatar.innerHTML = `<img src="${contact.avatar}" alt="">`;
} else {
senderAvatar.innerHTML = contact?.name?.charAt(0) || '?';
}
}
if (senderName) {
senderName.textContent = contact?.name || 'AI';
}
if (amountEl) {
amountEl.textContent = '¥' + transferInfo.amount.toFixed(2);
}
if (descEl) {
descEl.textContent = transferInfo.description || '转账给你';
}
page.classList.remove('hidden');
}
/**
* 隐藏收款页面
*/
export function hideReceiveTransferPage() {
const page = document.getElementById('wechat-receive-transfer-page');
if (page) {
page.classList.add('hidden');
}
pendingReceiveTransfer = null;
pendingReceiveContact = null;
}
/**
* 确认收款
*/
export async function confirmReceiveTransfer() {
if (!pendingReceiveTransfer || !pendingReceiveContact) return;
const transferInfo = pendingReceiveTransfer;
const contact = pendingReceiveContact;
// 存入钱包
addToWallet(transferInfo.amount);
// 更新转账状态
transferInfo.status = 'received';
transferInfo.receivedAt = Date.now();
// 保存
requestSave();
// 隐藏收款页面
hideReceiveTransferPage();
// 更新聊天中的转账气泡状态
updateTransferBubbleStatus(transferInfo.id, 'received');
// 显示收款成功提示
showToast('已收款 ¥' + transferInfo.amount.toFixed(2), 'success');
pendingReceiveTransfer = null;
pendingReceiveContact = null;
}
/**
* 显示退还确认框
*/
export function showRefundConfirmModal() {
if (!pendingReceiveContact) return;
const modal = document.getElementById('wechat-transfer-refund-confirm');
if (!modal) return;
const titleEl = modal.querySelector('.wechat-transfer-confirm-title');
if (titleEl) {
titleEl.textContent = `退还 ${pendingReceiveContact.name} 的转账?`;
}
modal.classList.remove('hidden');
}
/**
* 隐藏退还确认框
*/
export function hideRefundConfirmModal() {
const modal = document.getElementById('wechat-transfer-refund-confirm');
if (modal) {
modal.classList.add('hidden');
}
}
/**
* 确认退还转账
*/
export async function confirmRefundTransfer() {
if (!pendingReceiveTransfer || !pendingReceiveContact) return;
const transferInfo = pendingReceiveTransfer;
// 更新转账状态
transferInfo.status = 'refunded';
transferInfo.refundedAt = Date.now();
// 保存
requestSave();
// 隐藏弹窗和收款页面
hideRefundConfirmModal();
hideReceiveTransferPage();
// 更新聊天中的转账气泡状态
updateTransferBubbleStatus(transferInfo.id, 'refunded');
// 显示退还提示
showToast('已退还转账', 'refund');
pendingReceiveTransfer = null;
pendingReceiveContact = null;
}
/**
* 更新转账气泡状态
*/
function updateTransferBubbleStatus(transferId, status) {
const bubble = document.querySelector(`.wechat-transfer-bubble[data-tf-id="${transferId}"]`);
if (!bubble) return;
bubble.classList.remove('pending', 'received', 'refunded');
bubble.classList.add(status);
const statusIcon = bubble.querySelector('.wechat-tf-bubble-status-icon');
const statusText = bubble.querySelector('.wechat-tf-bubble-status-text');
if (statusIcon && statusText) {
if (status === 'received') {
statusIcon.innerHTML = `<svg viewBox="0 0 24 24" width="16" height="16"><path d="M9 12l2 2 4-4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>`;
statusText.textContent = '已收款';
} else if (status === 'refunded') {
statusIcon.innerHTML = `<svg viewBox="0 0 24 24" width="16" height="16"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M3 3v5h5" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>`;
// 判断是发送方还是接收方
const role = bubble.dataset.role;
statusText.textContent = role === 'user' ? '已被退还' : '已退还';
}
}
}
// ===== 生成转账ID =====
export function generateTransferId() {
return 'tf_' + Math.random().toString(36).substring(2, 10) + '_' + Date.now();
}
// ===== 初始化事件监听 =====
export function initTransferEvents() {
// 发转账页面返回按钮
document.getElementById('wechat-transfer-back')?.addEventListener('click', hideTransferPage);
// 金额输入框变化时更新显示
document.getElementById('wechat-transfer-amount-input')?.addEventListener('input', updateTransferAmountDisplay);
// 转账按钮
document.getElementById('wechat-transfer-submit')?.addEventListener('click', showTransferPasswordModal);
// 密码弹窗关闭
document.getElementById('wechat-transfer-password-close')?.addEventListener('click', hideTransferPasswordModal);
// 密码确认按钮
document.getElementById('wechat-transfer-password-confirm')?.addEventListener('click', verifyTransferPasswordAndSend);
// 收款页面返回按钮
document.getElementById('wechat-transfer-receive-back')?.addEventListener('click', hideReceiveTransferPage);
// 收款按钮
document.getElementById('wechat-transfer-receive-btn')?.addEventListener('click', confirmReceiveTransfer);
// 退还按钮(显示确认框)
document.getElementById('wechat-transfer-refund-btn')?.addEventListener('click', showRefundConfirmModal);
// 退还确认框按钮
document.getElementById('wechat-transfer-refund-cancel')?.addEventListener('click', hideRefundConfirmModal);
document.getElementById('wechat-transfer-refund-confirm')?.addEventListener('click', confirmRefundTransfer);
// 监听转账发送事件用户发转账后AI 收款)
document.addEventListener('transfer-send', handleUserSendTransfer);
}
/**
* 处理用户发送转账
*/
async function handleUserSendTransfer(event) {
const { amount, description } = event.detail;
const settings = getSettings();
// 动态导入 chat.js 中的函数,避免循环依赖
const chatModule = await import('./chat.js');
const { currentChatIndex, appendTransferMessage, showTypingIndicator, hideTypingIndicator, appendMessage } = chatModule;
if (currentChatIndex < 0) return;
const contact = settings.contacts[currentChatIndex];
if (!contact) return;
const now = new Date();
const timeStr = now.toLocaleString('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
}).replace(/\//g, '-');
// 创建转账信息
const tfInfo = {
id: generateTransferId(),
amount: amount,
description: description || '',
senderName: settings.userName || 'User',
status: 'pending',
receivedAt: null,
refundedAt: null,
expireAt: Date.now() + 24 * 60 * 60 * 1000
};
// 保存转账消息到聊天记录
if (!contact.chatHistory) contact.chatHistory = [];
contact.chatHistory.push({
role: 'user',
content: `[转账] ¥${amount.toFixed(2)}`,
time: timeStr,
timestamp: Date.now(),
isTransfer: true,
transferInfo: tfInfo
});
// 显示转账消息
appendTransferMessage('user', tfInfo, contact);
requestSave();
refreshChatList();
// AI 收款(延迟 2-5 秒)
const receiveDelay = 2000 + Math.random() * 3000;
await sleep(receiveDelay);
// 更新转账状态
tfInfo.status = 'received';
tfInfo.receivedAt = Date.now();
// 更新聊天中的转账气泡状态
updateTransferBubbleStatus(tfInfo.id, 'received');
requestSave();
// AI 发送感谢消息(带上下文)
await sleep(1000);
// 显示打字指示器
showTypingIndicator(contact);
try {
// 构建提示,让 AI 根据上下文自然回复
const thankPrompt = description
? `用户给你转账了${amount}元,备注是"${description}",请自然地表示感谢,不要使用任何特殊格式标签。`
: `用户给你转账了${amount}元,请自然地表示感谢,不要使用任何特殊格式标签。`;
const aiResponse = await callAI(contact, thankPrompt);
hideTypingIndicator();
if (aiResponse && aiResponse.trim()) {
// 取第一条回复
let thankMsg = aiResponse.split('|||')[0].trim();
// 移除可能的格式标签
thankMsg = thankMsg.replace(/^\[.*?\]\s*/, '');
if (thankMsg) {
contact.chatHistory.push({
role: 'assistant',
content: thankMsg,
time: new Date().toLocaleString('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
}).replace(/\//g, '-'),
timestamp: Date.now()
});
appendMessage('assistant', thankMsg, contact);
requestSave();
refreshChatList();
}
}
} catch (e) {
console.error('[可乐] AI感谢转账失败:', e);
hideTypingIndicator();
}
}

View File

@@ -2,6 +2,22 @@
* 工具函数 * 工具函数
*/ */
// ========== 日志管理 ==========
const DEBUG = true; // 生产环境改为 false
export function log(...args) {
if (DEBUG) console.log('[可乐]', ...args);
}
export function logWarn(...args) {
if (DEBUG) console.warn('[可乐]', ...args);
}
export function logError(...args) {
// 错误始终输出
console.error('[可乐]', ...args);
}
// 获取当前时间字符串 // 获取当前时间字符串
export function getCurrentTime() { export function getCurrentTime() {
const now = new Date(); const now = new Date();
@@ -10,11 +26,25 @@ export function getCurrentTime() {
// HTML 转义 // HTML 转义
export function escapeHtml(text) { export function escapeHtml(text) {
if (text == null) return '';
const div = document.createElement('div'); const div = document.createElement('div');
div.textContent = text; div.textContent = text;
return div.innerHTML; return div.innerHTML;
} }
// 提取内嵌的图片描述 [配图:xxx](朋友圈专用格式)
export function extractEmbeddedPhotos(text) {
const images = [];
const embeddedPhotoRegex = /\[配图[:]\s*(.+?)\]/g;
let match;
while ((match = embeddedPhotoRegex.exec(text)) !== null) {
images.push(match[1].trim());
}
// 移除内嵌的配图标签,保留纯文案
const cleanText = text.replace(/\[配图[:]\s*(.+?)\]/g, '').trim();
return { images, cleanText };
}
// 睡眠函数 // 睡眠函数
export function sleep(ms) { export function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
@@ -22,7 +52,8 @@ export function sleep(ms) {
// 根据内容长度计算语音秒数 // 根据内容长度计算语音秒数
export function calculateVoiceDuration(content) { export function calculateVoiceDuration(content) {
const seconds = Math.max(2, Math.min(60, Math.ceil(content.length / 3))); const text = (content || '').toString();
const seconds = Math.max(2, Math.min(60, Math.ceil(text.length / 3)));
return seconds; return seconds;
} }

View File

@@ -4,7 +4,7 @@
import { getSettings, splitAIMessages } from './config.js'; import { getSettings, splitAIMessages } from './config.js';
import { currentChatIndex } from './chat.js'; import { currentChatIndex } from './chat.js';
import { saveSettingsDebounced } from '../../../../script.js'; import { requestSave } from './save-manager.js';
import { refreshChatList } from './ui.js'; import { refreshChatList } from './ui.js';
// 通话状态 // 通话状态
@@ -94,7 +94,7 @@ function showIncomingCallPage() {
} }
// 隐藏主界面元素,显示来电界面 // 隐藏主界面元素,显示来电界面
document.getElementById('wechat-video-call-waiting')?.classList.add('hidden'); document.getElementById('wechat-video-call-center')?.classList.add('hidden');
document.getElementById('wechat-video-call-chat')?.classList.add('hidden'); document.getElementById('wechat-video-call-chat')?.classList.add('hidden');
document.getElementById('wechat-video-call-actions')?.classList.add('hidden'); document.getElementById('wechat-video-call-actions')?.classList.add('hidden');
incomingEl.classList.remove('hidden'); incomingEl.classList.remove('hidden');
@@ -126,15 +126,13 @@ function showCallPage() {
// 隐藏来电界面 // 隐藏来电界面
document.getElementById('wechat-video-call-incoming')?.classList.add('hidden'); document.getElementById('wechat-video-call-incoming')?.classList.add('hidden');
// 设置头像 - 使用更安全的方式避免 onerror 内联处理器问题 // 设置角色头像(中间圆形)
const avatarEl = document.getElementById('wechat-video-call-avatar'); const avatarEl = document.getElementById('wechat-video-call-avatar');
const remoteAvatarEl = document.getElementById('wechat-video-call-remote-avatar');
const firstChar = videoCallState.contactName ? videoCallState.contactName.charAt(0) : '?'; const firstChar = videoCallState.contactName ? videoCallState.contactName.charAt(0) : '?';
setAvatarSafe(avatarEl, videoCallState.contactAvatar, firstChar); setAvatarSafe(avatarEl, videoCallState.contactAvatar, firstChar);
setAvatarSafe(remoteAvatarEl, videoCallState.contactAvatar, firstChar);
// 设置本地头像 // 设置用户头像(右上角长方形小窗)
const localAvatarEl = document.getElementById('wechat-video-call-local-avatar'); const localAvatarEl = document.getElementById('wechat-video-call-local-avatar');
if (localAvatarEl) { if (localAvatarEl) {
try { try {
@@ -145,20 +143,14 @@ function showCallPage() {
} }
} }
// 设置名称
const nameEl = document.getElementById('wechat-video-call-name');
if (nameEl) {
nameEl.textContent = videoCallState.contactName;
}
// 设置状态 // 设置状态
const statusEl = document.getElementById('wechat-video-call-status'); const statusEl = document.getElementById('wechat-video-call-status');
if (statusEl) { if (statusEl) {
statusEl.textContent = '等待对方接受邀请'; statusEl.textContent = '等待对方接受邀请';
} }
// 显示等待状态 // 显示中间区域
document.getElementById('wechat-video-call-waiting')?.classList.remove('hidden'); document.getElementById('wechat-video-call-center')?.classList.remove('hidden');
document.getElementById('wechat-video-call-actions')?.classList.remove('hidden'); document.getElementById('wechat-video-call-actions')?.classList.remove('hidden');
// 重置时间显示 // 重置时间显示
@@ -213,8 +205,9 @@ function onVideoCallConnected() {
clearInterval(videoCallState.dotsInterval); clearInterval(videoCallState.dotsInterval);
clearTimeout(videoCallState.connectTimeout); clearTimeout(videoCallState.connectTimeout);
// 隐藏等待状态,显示通话状态 // 隐藏中间区域的状态文字,保留头像
document.getElementById('wechat-video-call-waiting')?.classList.add('hidden'); const statusEl = document.getElementById('wechat-video-call-status');
if (statusEl) statusEl.classList.add('hidden');
document.getElementById('wechat-video-call-incoming')?.classList.add('hidden'); document.getElementById('wechat-video-call-incoming')?.classList.add('hidden');
document.getElementById('wechat-video-call-actions')?.classList.remove('hidden'); document.getElementById('wechat-video-call-actions')?.classList.remove('hidden');
@@ -344,7 +337,7 @@ export function hangupVideoCall() {
// AI 对通话结束做出反应(所有情况都触发) // AI 对通话结束做出反应(所有情况都触发)
triggerVideoCallEndReaction(contact, callStatus, videoCallState.initiator, videoCallState.messages); triggerVideoCallEndReaction(contact, callStatus, videoCallState.initiator, videoCallState.messages);
saveSettingsDebounced(); requestSave();
refreshChatList(); refreshChatList();
} }
@@ -396,8 +389,8 @@ function appendVideoCallRecordMessage(role, status, duration, contact) {
if (status === 'connected') { if (status === 'connected') {
callRecordHTML = ` callRecordHTML = `
<div class="wechat-call-record wechat-video-call-record"> <div class="wechat-call-record wechat-video-call-record">
<span class="wechat-call-record-text">视频通话 ${duration}</span>
${cameraIconSVG} ${cameraIconSVG}
<span class="wechat-call-record-text">视频通话 ${duration}</span>
</div> </div>
`; `;
} else if (status === 'cancelled') { } else if (status === 'cancelled') {
@@ -719,9 +712,15 @@ async function triggerVideoCallEndReaction(contact, callStatus, initiator, callM
// 已接通的视频通话正常结束 // 已接通的视频通话正常结束
if (callMessages && callMessages.length > 0) { if (callMessages && callMessages.length > 0) {
const lastMessages = callMessages.slice(-5).map(m => `${m.role === 'user' ? '用户' : '你'}: ${m.content}`).join('\n'); const lastMessages = callMessages.slice(-5).map(m => `${m.role === 'user' ? '用户' : '你'}: ${m.content}`).join('\n');
reactionPrompt = `[你们刚才视频通话结束了。通话最后几句是:\n${lastMessages}\n\n请对视频通话结束做出自然的反应可以是对通话内容的总结、表达挂断后的心情、期待下次视频等。回复1-2句话即可简短自然不要复述通话内容。]`; reactionPrompt = `[视频通话刚刚挂断了,现在回到微信文字聊天。通话最后几句是:
${lastMessages}
【重要】通话已结束,你现在是发微信消息,不是继续视频通话。你应该对"挂断"这件事本身做反应:
- 如果是正常告别后挂的:简单告别或表达心情
- 如果是突然/意外挂断(聊到一半、正在做某事时断了):表示疑惑,问问怎么回事
绝对不要继续或延续通话里正在进行的内容或动作。回复1句话符合你的性格。]`;
} else { } else {
reactionPrompt = '[你们刚才视频通话结束了。请对通话结束做出自然的反应可以表达挂断后的心情或期待下次视频。回复1-2句话即可简短自然。]'; reactionPrompt = '[视频通话刚刚挂断了,现在回到微信文字聊天。请对"挂断"做出简单反应不要假设通话中发生了什么。回复1句话符合你的性格。]';
} }
} else { } else {
return; return;
@@ -776,7 +775,7 @@ async function triggerVideoCallEndReaction(contact, callStatus, initiator, callM
} }
} }
saveSettingsDebounced(); requestSave();
refreshChatList(); refreshChatList();
} catch (err) { } catch (err) {
console.error('[可乐] AI视频通话结束反应失败:', err); console.error('[可乐] AI视频通话结束反应失败:', err);
@@ -812,6 +811,8 @@ async function sendVideoCallMessage() {
if (!videoCallState.isConnected) break; if (!videoCallState.isConnected) break;
let reply = part.trim(); let reply = part.trim();
// 过滤掉 <meme> 标签(视频通话只输出纯文字)
reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
reply = reply.replace(/\[.*?\]/g, '').trim(); reply = reply.replace(/\[.*?\]/g, '').trim();
if (reply) { if (reply) {
@@ -855,12 +856,14 @@ function showVideoCallTypingIndicator() {
hideVideoCallTypingIndicator(); hideVideoCallTypingIndicator();
const typingDiv = document.createElement('div'); const typingDiv = document.createElement('div');
typingDiv.className = 'wechat-video-call-msg ai typing-indicator fade-in'; typingDiv.className = 'wechat-video-call-msg ai';
typingDiv.id = 'wechat-video-call-typing'; typingDiv.id = 'wechat-video-call-typing';
typingDiv.innerHTML = ` typingDiv.innerHTML = `
<span class="wechat-typing-dot"></span> <div class="wechat-message-bubble wechat-typing">
<span class="wechat-typing-dot"></span> <span class="wechat-typing-dot"></span>
<span class="wechat-typing-dot"></span> <span class="wechat-typing-dot"></span>
<span class="wechat-typing-dot"></span>
</div>
`; `;
messagesEl.appendChild(typingDiv); messagesEl.appendChild(typingDiv);
@@ -890,13 +893,6 @@ function addVideoCallMessage(role, content) {
messagesEl.scrollTop = messagesEl.scrollHeight; messagesEl.scrollTop = messagesEl.scrollHeight;
} }
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 初始化 // 初始化
export function initVideoCall() { export function initVideoCall() {
// 事件绑定将在显示页面时进行 // 事件绑定将在显示页面时进行

View File

@@ -4,8 +4,9 @@
import { getSettings, splitAIMessages } from './config.js'; import { getSettings, splitAIMessages } from './config.js';
import { currentChatIndex } from './chat.js'; import { currentChatIndex } from './chat.js';
import { saveSettingsDebounced } from '../../../../script.js'; import { requestSave } from './save-manager.js';
import { refreshChatList } from './ui.js'; import { refreshChatList } from './ui.js';
import { escapeHtml } from './utils.js';
// 通话状态 // 通话状态
let callState = { let callState = {
@@ -318,7 +319,7 @@ export function hangupCall() {
// AI 对通话结束做出反应(所有情况都触发) // AI 对通话结束做出反应(所有情况都触发)
triggerCallEndReaction(contact, callStatus, callState.initiator, callState.messages); triggerCallEndReaction(contact, callStatus, callState.initiator, callState.messages);
saveSettingsDebounced(); requestSave();
refreshChatList(); refreshChatList();
} }
@@ -372,16 +373,16 @@ function appendCallRecordMessage(role, status, duration, contact) {
// 已接通:显示通话时长 // 已接通:显示通话时长
callRecordHTML = ` callRecordHTML = `
<div class="wechat-call-record"> <div class="wechat-call-record">
<span class="wechat-call-record-text">通话时长 ${duration}</span>
${phoneIconSVG} ${phoneIconSVG}
<span class="wechat-call-record-text">通话时长 ${duration}</span>
</div> </div>
`; `;
} else if (status === 'cancelled') { } else if (status === 'cancelled') {
// 用户发起未接通:已取消(绿色) // 用户发起未接通:已取消(绿色)
callRecordHTML = ` callRecordHTML = `
<div class="wechat-call-record"> <div class="wechat-call-record">
<span class="wechat-call-record-text">已取消</span>
${phoneIconSVG} ${phoneIconSVG}
<span class="wechat-call-record-text">已取消</span>
</div> </div>
`; `;
} else if (status === 'rejected') { } else if (status === 'rejected') {
@@ -618,9 +619,15 @@ async function triggerCallEndReaction(contact, callStatus, initiator, callMessag
// 根据通话内容生成回复 // 根据通话内容生成回复
if (callMessages && callMessages.length > 0) { if (callMessages && callMessages.length > 0) {
const lastMessages = callMessages.slice(-5).map(m => `${m.role === 'user' ? '用户' : '你'}: ${m.content}`).join('\n'); const lastMessages = callMessages.slice(-5).map(m => `${m.role === 'user' ? '用户' : '你'}: ${m.content}`).join('\n');
reactionPrompt = `[你们刚才通完电话挂断了。通话最后几句是:\n${lastMessages}\n\n请对通话结束做出自然的反应可以是对通话内容的总结、表达挂断后的心情、期待下次通话等。回复1-2句话即可简短自然不要复述通话内容。]`; reactionPrompt = `[语音通话刚刚挂断了,现在回到微信文字聊天。通话最后几句是:
${lastMessages}
【重要】通话已结束,你现在是发微信消息,不是继续语音通话。你应该对"挂断"这件事本身做反应:
- 如果是正常告别后挂的:简单告别或表达心情
- 如果是突然/意外挂断(聊到一半、正在做某事时断了):表示疑惑,问问怎么回事
绝对不要继续或延续通话里正在进行的内容或动作。回复1句话符合你的性格。]`;
} else { } else {
reactionPrompt = '[你们刚才通完电话挂断了。请对通话结束做出自然的反应可以表达挂断后的心情或期待下次通话。回复1-2句话即可简短自然。]'; reactionPrompt = '[语音通话刚刚挂断了,现在回到微信文字聊天。请对"挂断"做出简单反应不要假设通话中发生了什么。回复1句话符合你的性格。]';
} }
} else { } else {
return; // 未知状态不处理 return; // 未知状态不处理
@@ -678,7 +685,7 @@ async function triggerCallEndReaction(contact, callStatus, initiator, callMessag
} }
} }
saveSettingsDebounced(); requestSave();
refreshChatList(); refreshChatList();
} catch (err) { } catch (err) {
console.error('[可乐] AI通话结束反应失败:', err); console.error('[可乐] AI通话结束反应失败:', err);
@@ -776,12 +783,14 @@ function showCallTypingIndicator() {
hideCallTypingIndicator(); hideCallTypingIndicator();
const typingDiv = document.createElement('div'); const typingDiv = document.createElement('div');
typingDiv.className = 'wechat-voice-call-msg ai typing-indicator fade-in'; typingDiv.className = 'wechat-voice-call-msg ai';
typingDiv.id = 'wechat-voice-call-typing'; typingDiv.id = 'wechat-voice-call-typing';
typingDiv.innerHTML = ` typingDiv.innerHTML = `
<span class="wechat-typing-dot"></span> <div class="wechat-message-bubble wechat-typing">
<span class="wechat-typing-dot"></span> <span class="wechat-typing-dot"></span>
<span class="wechat-typing-dot"></span> <span class="wechat-typing-dot"></span>
<span class="wechat-typing-dot"></span>
</div>
`; `;
messagesEl.appendChild(typingDiv); messagesEl.appendChild(typingDiv);
@@ -829,13 +838,6 @@ function renderCallMessages() {
messagesEl.scrollTop = messagesEl.scrollHeight; messagesEl.scrollTop = messagesEl.scrollHeight;
} }
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 初始化 // 初始化
export function initVoiceCall() { export function initVoiceCall() {
// 事件绑定将在显示页面时进行 // 事件绑定将在显示页面时进行