mirror of
https://github.com/Cola-Echo/Cola.git
synced 2026-06-06 03:35:50 +00:00
Add files via upload
This commit is contained in:
123
ai.js
123
ai.js
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
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';
|
||||
|
||||
function normalizeApiBaseUrl(url) {
|
||||
@@ -385,6 +385,7 @@ function buildStickerPrompt(settings) {
|
||||
可用表情(共${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;
|
||||
}
|
||||
@@ -958,7 +968,7 @@ export async function callVideoAI(contact, userMessage, callMessages = [], initi
|
||||
- 一般输出2-4句话
|
||||
- 用小括号描述画面场景,这是用户看到的视频画面
|
||||
- 【禁止】括号内不准使用任何人称代词(你、我、她、他)!这是摄像头视角的画面描述!
|
||||
- 【禁止】视频通话中不要使用任何表情包格式如 [表情:xxx],直接说话和描述动作即可
|
||||
- 【禁止】视频通话中不要使用任何表情包格式,包括 [表情:xxx] 和 <meme>xxx</meme>,直接说话和描述动作即可
|
||||
- 括号内只描述画面:人物动作、表情、背景、光线等
|
||||
|
||||
【正确示例 - 注意 ||| 分隔符】
|
||||
@@ -987,7 +997,7 @@ export async function callVideoAI(contact, userMessage, callMessages = [], initi
|
||||
- 一般输出2-4句话
|
||||
- 用小括号描述画面场景,这是用户看到的视频画面
|
||||
- 【禁止】括号内不准使用任何人称代词(你、我、她、他)!这是摄像头视角的画面描述!
|
||||
- 【禁止】视频通话中不要使用任何表情包格式如 [表情:xxx],直接说话和描述动作即可
|
||||
- 【禁止】视频通话中不要使用任何表情包格式,包括 [表情:xxx] 和 <meme>xxx</meme>,直接说话和描述动作即可
|
||||
- 括号内只描述画面:人物动作、表情、背景、光线等
|
||||
|
||||
【正确示例 - 注意 ||| 分隔符】
|
||||
@@ -1082,3 +1092,108 @@ ${videoCallPrompt}`;
|
||||
const data = await response.json();
|
||||
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 || '...';
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 支持每个联系人独立设置背景,含图片裁剪功能
|
||||
*/
|
||||
|
||||
import { saveSettingsDebounced } from '../../../../script.js';
|
||||
import { requestSave } from './save-manager.js';
|
||||
import { getSettings } from './config.js';
|
||||
import { showToast } from './toast.js';
|
||||
import { currentChatIndex } from './chat.js';
|
||||
@@ -375,7 +375,7 @@ function saveChatBackground(imageData) {
|
||||
}
|
||||
|
||||
contact.chatBackground = imageData;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
|
||||
// 立即应用背景
|
||||
applyChatBackground(imageData);
|
||||
@@ -390,7 +390,7 @@ function clearChatBackground() {
|
||||
if (!contact) return;
|
||||
|
||||
delete contact.chatBackground;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
|
||||
// 清除背景
|
||||
applyChatBackground(null);
|
||||
|
||||
@@ -9,10 +9,13 @@ import { isInGroupChat, sendGroupMessage, sendGroupPhotoMessage, sendGroupBatchM
|
||||
import { startVoiceCall } from './voice-call.js';
|
||||
import { startVideoCall } from './video-call.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 { refreshChatList } from './ui.js';
|
||||
import { saveSettingsDebounced } from '../../../../script.js';
|
||||
import { requestSave } from './save-manager.js';
|
||||
import { callAI } from './ai.js';
|
||||
import { showListenSearchPage, initListenTogether } from './listen-together.js';
|
||||
|
||||
let expandMode = null; // 'voice' | 'multi' | null
|
||||
// 混合消息项: { type: 'text' | 'voice' | 'sticker' | 'photo', content: string }
|
||||
@@ -106,7 +109,7 @@ function initMusicShareListener() {
|
||||
|
||||
groupChat.lastMessage = `[音乐] ${name}`;
|
||||
groupChat.lastMessageTime = Date.now();
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
|
||||
// 获取成员信息
|
||||
@@ -154,7 +157,7 @@ function initMusicShareListener() {
|
||||
groupChat.lastMessageTime = Date.now();
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
} catch (err) {
|
||||
hideGroupTypingIndicator();
|
||||
@@ -189,7 +192,7 @@ function initMusicShareListener() {
|
||||
});
|
||||
|
||||
contact.lastMessage = `[音乐] ${name}`;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
|
||||
// 调用AI回复
|
||||
@@ -235,7 +238,7 @@ function initMusicShareListener() {
|
||||
if (lastShownMessage) {
|
||||
contact.lastMessage = lastShownMessage.length > 20 ? lastShownMessage.substring(0, 20) + '...' : lastShownMessage;
|
||||
}
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -544,7 +547,7 @@ export async function sendExpandContent() {
|
||||
const content = textarea?.value.trim();
|
||||
|
||||
if (!content) {
|
||||
showToast('请输入语音内容', '🧊');
|
||||
showToast('请输入语音内容', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -562,7 +565,7 @@ export async function sendExpandContent() {
|
||||
const content = textarea?.value.trim();
|
||||
|
||||
if (!content) {
|
||||
showToast('请输入照片描述', '🧊');
|
||||
showToast('请输入照片描述', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -585,7 +588,7 @@ export async function sendExpandContent() {
|
||||
});
|
||||
|
||||
if (validMessages.length === 0) {
|
||||
showToast('请至少输入一条消息', '🧊');
|
||||
showToast('请至少输入一条消息', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -656,8 +659,39 @@ function handleFuncItemClick(func) {
|
||||
hideFuncPanel();
|
||||
showMusicPanel();
|
||||
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:
|
||||
showToast('该功能开发中...', '🧊');
|
||||
showToast('该功能开发中...', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -724,4 +758,194 @@ export function initFuncPanel() {
|
||||
// 初始化音乐面板事件
|
||||
initMusicEvents();
|
||||
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('已设置发送时间', '⏰');
|
||||
});
|
||||
}
|
||||
|
||||
189
config.js
189
config.js
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 配置、常量、默认设<EFBFBD>?
|
||||
* 配置、常量、默认设置
|
||||
*/
|
||||
|
||||
import { extension_settings } from '../../../extensions.js';
|
||||
@@ -7,19 +7,19 @@ import { extension_settings } from '../../../extensions.js';
|
||||
// 插件名称
|
||||
export const extensionName = 'wechat-simulator';
|
||||
|
||||
// Meme 表情包列表(catbox.moe<EFBFBD>?
|
||||
// Meme 表情包列表(catbox.moe)
|
||||
export const MEME_STICKERS = [
|
||||
'告到小狗法庭iaordo.jpg',
|
||||
'小猫伸爪f6nqiq.gif',
|
||||
'谢谢宝贝我现在那里好<EFBFBD>?62o48.jpg',
|
||||
'阿弥陀<EFBFBD>?cwm60.jpg',
|
||||
'谢谢宝贝我现在那里好硬862o48.jpg',
|
||||
'阿弥陀佛9cwm60.jpg',
|
||||
'你好美你长得像我爱人hmpkra.jpg',
|
||||
'我老实了i3ws7s.jpg',
|
||||
'蹭蹭你贴贴你1of415.gif',
|
||||
'喜欢你egvwqb.jpg',
|
||||
'我在哭t343od.jpg',
|
||||
'不干活就没饭<EFBFBD>?qnrgh.jpg',
|
||||
'擦眼<EFBFBD>?gno7e.jpg',
|
||||
'不干活就没饭吃2qnrgh.jpg',
|
||||
'擦眼泪9gno7e.jpg',
|
||||
'小狗摇尾巴hmdj2k.gif',
|
||||
'爱你舔舔你ola7gd.jpg',
|
||||
'不高兴x6lv1t.jpg',
|
||||
@@ -47,7 +47,7 @@ export const MEME_STICKERS = [
|
||||
'目移69jgvg.jpg',
|
||||
'上钩了cormmk.jpg',
|
||||
'无语了我哭了0awxky.jpg',
|
||||
'你嫌我丢<EFBFBD>?d71mm.jpg',
|
||||
'你嫌我丢人8d71mm.jpg',
|
||||
'笑不出来xkop14.jpg',
|
||||
'别欺负小狗啊u4t3t3.jpg',
|
||||
'他妈的真是被看扁了ime5rz.jpg',
|
||||
@@ -67,7 +67,7 @@ export const MEME_STICKERS = [
|
||||
'失望eug1e6.jpeg',
|
||||
'狂犬病发作xb3naz.jpg',
|
||||
'我是狗吗ma9azs.jpg',
|
||||
'一笑了<EFBFBD>?llb46.jpg',
|
||||
'一笑了之9llb46.jpg',
|
||||
'装可怜lcglz1.jpg',
|
||||
'小狗撒欢6j6y6a.gif',
|
||||
'狗舔舔esw5e2.gif',
|
||||
@@ -88,7 +88,7 @@ export const MEME_STICKERS = [
|
||||
'被逮捕了uzeywu.jpg',
|
||||
'看呆mqnepo.jpg',
|
||||
'我的理性在远去t9e065.jpg',
|
||||
'偷亲一<EFBFBD>?jgvb1.gif',
|
||||
'偷亲一口1jgvb1.gif',
|
||||
'震惊v5n2ve.jpg',
|
||||
'爷怒了49r80k.jpg',
|
||||
'愤怒伤心e7lr3s.jpg',
|
||||
@@ -102,19 +102,19 @@ export const MEME_STICKERS = [
|
||||
'你太可爱我喜欢你ubhai8.jpg',
|
||||
'惊吓tp9uvd.jpg',
|
||||
'脸红星星眼dsfs7o.jpg',
|
||||
'被揍了哭<EFBFBD>?1x5zq.jpg',
|
||||
'被揍了哭哭81x5zq.jpg',
|
||||
'嘬嘬fg5gx3.jpg',
|
||||
'超大声哭<EFBFBD>?86h5v.jpg',
|
||||
'超大声哭哭186h5v.jpg',
|
||||
'是的主人yvrgdc.jpg'
|
||||
];
|
||||
|
||||
// Meme 表情包提示词模板
|
||||
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>小熊跳舞122o4w.gif</meme>|||你太搞笑<EFBFBD>?
|
||||
<meme>喜欢你egvwqb.jpg</meme>|||我真的好喜欢<EFBFBD>?
|
||||
哈哈哈笑死|||<meme>小熊跳舞122o4w.gif</meme>|||你太搞笑了
|
||||
<meme>喜欢你egvwqb.jpg</meme>|||我真的好喜欢你
|
||||
|
||||
【错误示<EFBFBD>?- 绝对禁止】:
|
||||
好想<EFBFBD>?meme>xxx</meme> <EFBFBD>?错误!表情包没有用|||分开
|
||||
<meme>不存在的表情.jpg</meme> <EFBFBD>?错误!编造了不存在的文件<EFBFBD>?
|
||||
【错误示例 - 绝对禁止】:
|
||||
好想你<meme>xxx</meme> ← 错误!表情包没有用|||分开
|
||||
<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 = {
|
||||
darkMode: true,
|
||||
|
||||
/**
|
||||
* 【自动注入提示词<EFBFBD>?
|
||||
* 【自动注入提示词】
|
||||
* 开启后会自动将微信消息格式提示词注入到作者注释中
|
||||
* 提示词模板见下方 authorNoteTemplate
|
||||
* 如需自定义格式,修改 authorNoteTemplate 即可
|
||||
@@ -151,7 +184,7 @@ export const defaultSettings = {
|
||||
phonePosition: null,
|
||||
userAvatar: '',
|
||||
wechatId: 'SillyTavern',
|
||||
// 收藏/世界<EFBFBD>?用户设定
|
||||
// 收藏/世界书/用户设定
|
||||
selectedLorebooks: [],
|
||||
userPersonas: [],
|
||||
|
||||
@@ -174,16 +207,20 @@ export const defaultSettings = {
|
||||
groupSelectedModel: '',
|
||||
groupModelList: [],
|
||||
|
||||
// 上下文设<EFBFBD>?
|
||||
// 上下文设置
|
||||
contextEnabled: false,
|
||||
contextLevel: 5,
|
||||
contextTags: [],
|
||||
walletAmount: '5773.89',
|
||||
// 支付密码(6位数字)
|
||||
paymentPassword: '666666',
|
||||
// 自定义总结模板(留空则使用默认)
|
||||
customSummaryTemplate: '',
|
||||
|
||||
/**
|
||||
* 【哈基米破限<EFBFBD>?
|
||||
* 【哈基米破限】
|
||||
* 针对 Gemini 2.5 Pro 等模型的输出截断问题
|
||||
* 开启后会在系统提示词头部注入一<EFBFBD>?创作沙盒"提示<EFBFBD>?
|
||||
* 开启后会在系统提示词头部注入一段"创作沙盒"提示词
|
||||
* 帮助解除模型的自我审查,让输出更完整
|
||||
*/
|
||||
hakimiBreakLimit: false,
|
||||
@@ -199,65 +236,65 @@ export const defaultSettings = {
|
||||
// 错误日志
|
||||
errorLogs: [],
|
||||
|
||||
// 表情(用户添加的单个表情<EFBFBD>?
|
||||
// 表情(用户添加的单个表情)
|
||||
stickers: [],
|
||||
|
||||
// 用户表情开<EFBFBD>?
|
||||
// 用户表情开关
|
||||
userStickersEnabled: true,
|
||||
|
||||
// Meme 表情包功能开<EFBFBD>?
|
||||
// Meme 表情包功能开关
|
||||
memeStickersEnabled: false,
|
||||
|
||||
/**
|
||||
* 【群聊提示词注入<EFBFBD>?
|
||||
* 开启后会将 groupAuthorNote 注入到群聊系统提示词<EFBFBD>?
|
||||
* 【群聊提示词注入】
|
||||
* 开启后会将 groupAuthorNote 注入到群聊系统提示词中
|
||||
* 如需自定义群聊格式,修改下方 groupAuthorNote 即可
|
||||
*/
|
||||
groupAutoInjectPrompt: true,
|
||||
groupAuthorNote: `[群聊回复格式指南]
|
||||
这是一个微信群聊场景,你需要扮演群内的角色进行回复<EFBFBD>?
|
||||
这是一个微信群聊场景,你需要扮演群内的角色进行回复。
|
||||
|
||||
【核心规则<EFBFBD>?
|
||||
1. 每个角色只能使用自己的专属设定,不能使用其他角色的设<EFBFBD>?
|
||||
【核心规则】
|
||||
1. 每个角色只能使用自己的专属设定,不能使用其他角色的设定
|
||||
2. 每个角色只扮演自己,不能代替其他角色说话
|
||||
3. 使用 [角色名]: 内容 的格式回<EFBFBD>?
|
||||
4. 多个角色回复时,<EFBFBD>?||| 分隔
|
||||
3. 使用 [角色名]: 内容 的格式回复
|
||||
4. 多个角色回复时,用 ||| 分隔
|
||||
5. 同一角色可以发送多条消息,也用 ||| 分隔
|
||||
|
||||
【消息风格<EFBFBD>?
|
||||
- 每条消息保持简短自然,像真实微信聊天一样(1-3句话为宜<EFBFBD>?
|
||||
- 可以使用表情符号增加表现<EFBFBD>?
|
||||
【消息风格】
|
||||
- 每条消息保持简短自然,像真实微信聊天一样(1-3句话为宜)
|
||||
- 可以使用表情符号增加表现力
|
||||
- 保持角色性格,让对话有趣生动
|
||||
- 角色之间可以互动、吐槽、附和、反驳等
|
||||
|
||||
【回复要求<EFBFBD>?
|
||||
- 根据聊天内容自然判断哪些角色会回复,不需要所有人都说<EFBFBD>?
|
||||
【回复要求】
|
||||
- 根据聊天内容自然判断哪些角色会回复,不需要所有人都说话
|
||||
- 语音消息格式:[角色名]: [语音:内容]
|
||||
- 语音消息必须独立发<EFBFBD>?
|
||||
- 语音消息必须独立发送
|
||||
|
||||
示例<EFBFBD>?
|
||||
[角色A]: 你说得对|||[角色B]: 我不太同意诶|||[角色A]: 为什么啊<EFBFBD>?
|
||||
示例:
|
||||
[角色A]: 你说得对|||[角色B]: 我不太同意诶|||[角色A]: 为什么啊?
|
||||
[角色A]: [语音:哈哈哈笑死我了]
|
||||
[角色B]: @角色A 你是不是傻|||开玩笑的啦`,
|
||||
userGroupAuthorNote: '', // 用户自定义群聊提示词(界面显示用,留空则使用内置模板)
|
||||
};
|
||||
|
||||
// 作者注释模<EFBFBD>?
|
||||
export const authorNoteTemplate = `【可乐不加冰 消息格式指南】每次必须使用以下格式输出消息内容,不用生成除此之外的任何其他内容和文本。不得省略任何格式部分<EFBFBD>?
|
||||
// 作者注释模板
|
||||
export const authorNoteTemplate = `【可乐不加冰 消息格式指南】每次必须使用以下格式输出消息内容,不用生成除此之外的任何其他内容和文本。不得省略任何格式部分。
|
||||
|
||||
【核心规<EFBFBD>?- 必须遵守<EFBFBD>?
|
||||
- 每条消息都是独立的,<EFBFBD>?||| 分隔多条消息
|
||||
- 保持消息简短自然,像真实微信聊天(1-3句话为宜<EFBFBD>?
|
||||
- 绝对禁止代替{{user}}发言,严禁替{{user}}回复消息,严禁扮演{{user}},严禁捏造输出{{user}}的消<EFBFBD>?
|
||||
【核心规则 - 必须遵守】
|
||||
- 每条消息都是独立的,用 ||| 分隔多条消息
|
||||
- 保持消息简短自然,像真实微信聊天(1-3句话为宜)
|
||||
- 绝对禁止代替{{user}}发言,严禁替{{user}}回复消息,严禁扮演{{user}},严禁捏造输出{{user}}的消息
|
||||
- 只输出角色的消息内容,禁止添加任何旁白、心理描写或场景说明
|
||||
|
||||
【消息数量规<EFBFBD>?- 重要<EFBFBD>?
|
||||
- 根据情境动态调整消息数量,通常1-7条不<EFBFBD>?
|
||||
- 禁止固定每次回复的消息数<EFBFBD>?
|
||||
【消息数量规则 - 重要】
|
||||
- 根据情境动态调整消息数量,通常1-7条不等
|
||||
- 禁止固定每次回复的消息数量
|
||||
- 模拟真实聊天节奏
|
||||
|
||||
【消息类型格式<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_SUFFIX = '的聊天';
|
||||
|
||||
// 生成世界书名<EFBFBD>?
|
||||
// 生成世界书名称
|
||||
export function generateLorebookName(contactName) {
|
||||
return `${LOREBOOK_NAME_PREFIX}${contactName}${LOREBOOK_NAME_SUFFIX}`;
|
||||
}
|
||||
@@ -316,8 +353,8 @@ export function getUserStickers(settings = getSettings()) {
|
||||
// 解析 <meme> 标签,替换为图片 HTML
|
||||
export function parseMemeTag(text) {
|
||||
if (!text || typeof text !== 'string') return text;
|
||||
// 匹配 <meme>任意描述+文件ID.扩展<EFBFBD>?/meme>,只捕获文件ID部分
|
||||
// 使用 .*? 替代 [\u4e00-\u9fa5]*? 以支持包含特殊字符(<EFBFBD>?! ? 等)的表情名<EFBFBD>?
|
||||
// 匹配 <meme>任意描述+文件ID.扩展名</meme>,只捕获文件ID部分
|
||||
// 使用 .*? 替代 [\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 `<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);
|
||||
}
|
||||
|
||||
// 智能分割AI消息:处<EFBFBD>?||| 分隔符,并将 meme/语音/照片/音乐 标签与其他文字分开
|
||||
// 智能分割AI消息:处理 ||| 分隔符,并将 meme/语音/照片/音乐 标签与其他文字分开
|
||||
export function splitAIMessages(response) {
|
||||
if (!response || typeof response !== 'string') return [];
|
||||
|
||||
// 第一步:<EFBFBD>?||| 分隔
|
||||
// 第一步:用 ||| 分隔
|
||||
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 });
|
||||
}
|
||||
|
||||
// 查找音乐标签(带冒号格式<EFBFBD>?
|
||||
// 查找音乐标签(带冒号格式)
|
||||
const musicRegexLocal1 = new RegExp(musicRegexWithColon.source, 'g');
|
||||
while ((match = musicRegexLocal1.exec(part)) !== null) {
|
||||
specialTags.push({ tag: match[0], index: match.index });
|
||||
}
|
||||
|
||||
// 查找音乐标签(无冒号格式<EFBFBD>?
|
||||
// 查找音乐标签(无冒号格式)
|
||||
const musicRegexLocal2 = new RegExp(musicRegexNoColon.source, 'g');
|
||||
while ((match = musicRegexLocal2.exec(part)) !== null) {
|
||||
// 避免重复匹配(如果已经被带冒号的匹配到)
|
||||
@@ -415,7 +452,7 @@ export function splitAIMessages(response) {
|
||||
specialTags.push({ tag: match[0], index: match.index });
|
||||
}
|
||||
|
||||
// 如果没有特殊标签,直接添<EFBFBD>?
|
||||
// 如果没有特殊标签,直接添加
|
||||
if (specialTags.length === 0) {
|
||||
result.push(part);
|
||||
continue;
|
||||
@@ -424,7 +461,7 @@ export function splitAIMessages(response) {
|
||||
// 调试日志
|
||||
console.log('[可乐] splitAIMessages 分割:', { part, specialTags });
|
||||
|
||||
// 按位置排<EFBFBD>?
|
||||
// 按位置排序
|
||||
specialTags.sort((a, b) => a.index - b.index);
|
||||
|
||||
// 分割消息
|
||||
@@ -440,7 +477,7 @@ export function splitAIMessages(response) {
|
||||
lastEnd = index + tag.length;
|
||||
}
|
||||
|
||||
// 添加最后一个标签后的文<EFBFBD>?
|
||||
// 添加最后一个标签后的文字
|
||||
if (lastEnd < part.length) {
|
||||
const after = part.substring(lastEnd).trim();
|
||||
if (after) result.push(after);
|
||||
@@ -467,7 +504,7 @@ function applyDefaults(target, defaults) {
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化设<EFBFBD>?
|
||||
// 初始化设置
|
||||
export function loadSettings() {
|
||||
extension_settings[extensionName] = extension_settings[extensionName] || {};
|
||||
const settings = extension_settings[extensionName];
|
||||
@@ -485,8 +522,8 @@ export function loadSettings() {
|
||||
}
|
||||
if (settings.userPersona) delete settings.userPersona;
|
||||
|
||||
// 迁移:旧<EFBFBD>?aiStickers -> stickers(“添加的单个表情”)
|
||||
// 说明:如果用户已经有自己<EFBFBD>?stickers,则不再合并<EFBFBD>?aiStickers(避免把旧默<EFBFBD>?catbox 列表灌进去)<EFBFBD>?
|
||||
// 迁移:旧的 aiStickers -> stickers("添加的单个表情")
|
||||
// 说明:如果用户已经有自己的 stickers,则不再合并旧 aiStickers(避免把旧默认 catbox 列表灌进去)。
|
||||
const hasUserStickers = Array.isArray(settings.stickers) &&
|
||||
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 = [];
|
||||
|
||||
// 迁移:旧<EFBFBD>?aiStickersEnabled -> userStickersEnabled
|
||||
// 迁移:旧的 aiStickersEnabled -> userStickersEnabled
|
||||
if (settings.aiStickersEnabled !== undefined) {
|
||||
if (settings.userStickersEnabled === undefined) {
|
||||
settings.userStickersEnabled = settings.aiStickersEnabled;
|
||||
|
||||
59
contacts.js
59
contacts.js
@@ -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 { generateContactsList } from './ui.js';
|
||||
import { showToast } from './toast.js';
|
||||
import { selectAndCrop } from './cropper.js';
|
||||
|
||||
// 当前换头像的联系人索引
|
||||
let pendingAvatarContactIndex = -1;
|
||||
@@ -40,7 +41,7 @@ export function addContact(characterData) {
|
||||
customHakimiBreakLimit: false
|
||||
});
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshContactsList();
|
||||
return true;
|
||||
}
|
||||
@@ -65,7 +66,7 @@ export function deleteContact(index) {
|
||||
deleteContactLorebooks(contact);
|
||||
|
||||
settings.contacts.splice(index, 1);
|
||||
saveSettingsDebounced();
|
||||
saveNow();
|
||||
refreshContactsList();
|
||||
}
|
||||
}
|
||||
@@ -109,7 +110,7 @@ export function deleteGroupChat(groupIndex) {
|
||||
deleteGroupLorebooks(group, settings);
|
||||
|
||||
groupChats.splice(groupIndex, 1);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshContactsList();
|
||||
// 同时刷新聊天列表
|
||||
import('./ui.js').then(m => m.refreshChatList());
|
||||
@@ -142,41 +143,21 @@ function deleteGroupLorebooks(group, settings) {
|
||||
// 更换角色头像(在设置弹窗中使用)
|
||||
export function changeContactAvatar(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) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file || pendingAvatarContactIndex < 0) return;
|
||||
// 使用裁剪器选择并裁剪头像(1:1比例)
|
||||
selectAndCrop(1, (croppedImage) => {
|
||||
if (pendingAvatarContactIndex < 0) return;
|
||||
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
const settings = getSettings();
|
||||
if (settings.contacts[pendingAvatarContactIndex]) {
|
||||
settings.contacts[pendingAvatarContactIndex].avatar = event.target.result;
|
||||
saveSettingsDebounced();
|
||||
refreshContactsList();
|
||||
// 更新弹窗中的头像预览
|
||||
updateContactSettingsAvatar(pendingAvatarContactIndex);
|
||||
showToast('角色头像已更换');
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} catch (err) {
|
||||
console.error('[可乐] 更换角色头像失败:', err);
|
||||
showToast('更换头像失败: ' + err.message, '❌');
|
||||
}
|
||||
e.target.value = '';
|
||||
});
|
||||
}
|
||||
input.click();
|
||||
const settings = getSettings();
|
||||
if (settings.contacts[pendingAvatarContactIndex]) {
|
||||
settings.contacts[pendingAvatarContactIndex].avatar = croppedImage;
|
||||
requestSave();
|
||||
refreshContactsList();
|
||||
// 更新弹窗中的头像预览
|
||||
updateContactSettingsAvatar(pendingAvatarContactIndex);
|
||||
showToast('角色头像已更换');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新弹窗中的头像预览
|
||||
@@ -262,7 +243,7 @@ export function saveContactSettings() {
|
||||
// 保存哈基米破限
|
||||
contact.customHakimiBreakLimit = document.getElementById('wechat-contact-hakimi-toggle')?.classList.contains('on') || false;
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
showToast('角色设置已保存');
|
||||
|
||||
// 关闭弹窗
|
||||
@@ -450,7 +431,7 @@ function deleteContactDirect(index) {
|
||||
deleteContactLorebooks(contact);
|
||||
|
||||
settings.contacts.splice(index, 1);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshContactsList();
|
||||
}
|
||||
|
||||
|
||||
390
cropper.js
Normal file
390
cropper.js
Normal 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();
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* 表情面板功能
|
||||
*/
|
||||
|
||||
import { saveSettingsDebounced } from '../../../../script.js';
|
||||
import { requestSave } from './save-manager.js';
|
||||
import { getSettings } from './config.js';
|
||||
import { showToast } from './toast.js';
|
||||
import { isInGroupChat } from './group-chat.js';
|
||||
@@ -244,7 +244,7 @@ function addStickersFromInput(inputs) {
|
||||
// 检查是否已存在
|
||||
const exists = settings.stickers.some(s => s.url === url);
|
||||
if (exists) {
|
||||
showToast(`已存在: ${name}`, '🧊');
|
||||
showToast(`已存在: ${name}`, 'info');
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ function addStickersFromInput(inputs) {
|
||||
}
|
||||
|
||||
if (addedCount > 0) {
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshEmojiGrid();
|
||||
showToast(`已添加 ${addedCount} 个表情`);
|
||||
}
|
||||
@@ -299,7 +299,7 @@ function addStickerFromFile() {
|
||||
}
|
||||
|
||||
if (addedCount > 0) {
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshEmojiGrid();
|
||||
showToast(`已添加 ${addedCount} 个表情`);
|
||||
}
|
||||
@@ -369,7 +369,7 @@ function deleteSticker(index) {
|
||||
|
||||
if (index >= 0 && index < stickers.length) {
|
||||
stickers.splice(index, 1);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshEmojiGrid();
|
||||
showToast('表情已删除');
|
||||
}
|
||||
@@ -392,7 +392,7 @@ export function initEmojiPanel() {
|
||||
|
||||
const tabName = tab.dataset.tab;
|
||||
if (tabName === 'search') {
|
||||
showToast('搜索功能开发中...', '🧊');
|
||||
showToast('搜索功能开发中...', 'info');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
30
favorites.js
30
favorites.js
@@ -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 { getSettings } from './config.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 (confirm(`确定移除「${persona.name || '用户设定'}」?`)) {
|
||||
settings.userPersonas.splice(index, 1);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshFavoritesList();
|
||||
showToast('已移除');
|
||||
}
|
||||
@@ -199,7 +199,7 @@ export function removeFavoritesItem(type, index) {
|
||||
if (!lorebook) return;
|
||||
if (confirm(`确定移除「${lorebook.name}」?`)) {
|
||||
settings.selectedLorebooks.splice(index, 1);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshFavoritesList();
|
||||
showToast('已移除');
|
||||
}
|
||||
@@ -381,7 +381,7 @@ function showNewPersonaModal() {
|
||||
|
||||
settings.userPersonas.push({ name, content, enabled: true, addedTime: timeStr });
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
modal.remove();
|
||||
refreshFavoritesList();
|
||||
});
|
||||
@@ -412,7 +412,7 @@ function bindPersonaPanelEvents(panel, personaIdx) {
|
||||
if (settings.userPersonas?.[personaIdx]) {
|
||||
settings.userPersonas[personaIdx].name = name;
|
||||
settings.userPersonas[personaIdx].content = content;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
showToast('已保存');
|
||||
refreshFavoritesList();
|
||||
closeUserPersonaDetail();
|
||||
@@ -443,7 +443,7 @@ function bindPersonaPanelEvents(panel, personaIdx) {
|
||||
panel.querySelector('#wechat-persona-delete').addEventListener('click', () => {
|
||||
if (confirm('确定删除此用户设定?')) {
|
||||
settings.userPersonas.splice(personaIdx, 1);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
closeUserPersonaDetail();
|
||||
refreshFavoritesList();
|
||||
}
|
||||
@@ -477,7 +477,7 @@ async function syncPersonaToTavern(name, content) {
|
||||
|
||||
// 保存设置
|
||||
if (typeof SillyTavern !== 'undefined' && SillyTavern.saveSettingsDebounced) {
|
||||
await SillyTavern.saveSettingsDebounced();
|
||||
await SillyTavern.requestSave();
|
||||
}
|
||||
|
||||
// 尝试执行同步命令
|
||||
@@ -628,7 +628,7 @@ function bindLorebookPanelEvents(panel, lorebook, lorebookIdx) {
|
||||
const settings = getSettings();
|
||||
if (settings.selectedLorebooks?.[lorebookIdx]?.entries?.[entryIdx]) {
|
||||
settings.selectedLorebooks[lorebookIdx].entries[entryIdx].enabled = toggle.checked;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
// 同步到酒馆
|
||||
await syncLorebookToTavern(lorebook.name, lorebookIdx);
|
||||
}
|
||||
@@ -695,7 +695,7 @@ function bindLorebookPanelEvents(panel, lorebook, lorebookIdx) {
|
||||
entry.comment = comment;
|
||||
entry.keys = keys;
|
||||
entry.content = content;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
|
||||
// 同步到酒馆
|
||||
btn.disabled = true;
|
||||
@@ -735,7 +735,7 @@ function bindLorebookPanelEvents(panel, lorebook, lorebookIdx) {
|
||||
if (confirm(`确定移除「${lorebook.name}」?`)) {
|
||||
const settings = getSettings();
|
||||
settings.selectedLorebooks.splice(lorebookIdx, 1);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
closeLorebookDetail();
|
||||
refreshFavoritesList();
|
||||
}
|
||||
@@ -871,7 +871,7 @@ export async function refreshLorebookFromTavern(name, lorebookIdx) {
|
||||
if (settings.selectedLorebooks?.[lorebookIdx]) {
|
||||
settings.selectedLorebooks[lorebookIdx].entries = entries;
|
||||
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')}`;
|
||||
|
||||
settings.userPersonas.push({ name: name || '用户设定', content, enabled: true, addedTime: timeStr });
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
|
||||
modal.remove();
|
||||
refreshFavoritesList();
|
||||
@@ -1079,7 +1079,7 @@ export async function addLorebookToFavorites(name) {
|
||||
fromCharacter: false // 标记为全局世界书
|
||||
});
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshFavoritesList('global');
|
||||
showToast(`已导入「${name}」为全局世界书`);
|
||||
} catch (err) {
|
||||
@@ -1171,7 +1171,7 @@ export async function syncCharacterBookToTavern(charData) {
|
||||
});
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
|
||||
// 尝试同步到酒馆世界书系统
|
||||
if (typeof saveWorldInfo === 'function') {
|
||||
|
||||
128
group-chat.js
128
group-chat.js
@@ -2,7 +2,7 @@
|
||||
* 群聊功能
|
||||
*/
|
||||
|
||||
import { saveSettingsDebounced } from '../../../../script.js';
|
||||
import { requestSave, saveNow } from './save-manager.js';
|
||||
import { getContext } from '../../../extensions.js';
|
||||
import { getSettings, SUMMARY_MARKER_PREFIX, getUserStickers, parseMemeTag, MEME_PROMPT_TEMPLATE, splitAIMessages } from './config.js';
|
||||
import { showToast } from './toast.js';
|
||||
@@ -26,9 +26,28 @@ const GROUP_CHAT_MAX_AI_MEMBERS = 3;
|
||||
// 检查群聊记录是否需要总结提醒
|
||||
function checkGroupSummaryReminder(groupChat) {
|
||||
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);
|
||||
groupChat.memberIds = trimmed;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
|
||||
if (toast) {
|
||||
showToast(`群聊最多 ${GROUP_CHAT_MAX_AI_MEMBERS} 个成员(+你=4),已自动裁剪`, '⚠️');
|
||||
@@ -400,7 +419,7 @@ export function showGroupCreateModal() {
|
||||
const apiKey = keyInput?.value?.trim();
|
||||
|
||||
if (!apiUrl) {
|
||||
showToast('请先填写API地址', '🧊');
|
||||
showToast('请先填写API地址', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -417,7 +436,7 @@ export function showGroupCreateModal() {
|
||||
models.map(m => `<option value="${m}" ${m === currentValue ? 'selected' : ''}>${m}</option>`).join('');
|
||||
showToast(`获取到 ${models.length} 个模型`);
|
||||
} else {
|
||||
showToast('未找到可用模型', '🧊');
|
||||
showToast('未找到可用模型', 'info');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[可乐] 获取模型失败:', err);
|
||||
@@ -446,7 +465,7 @@ export function showGroupCreateModal() {
|
||||
// 更新图标
|
||||
apiToggle.textContent = contact.useCustomApi ? '⚙️' : '▼';
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
};
|
||||
|
||||
item.querySelector('.wechat-group-api-url')?.addEventListener('change', saveApiConfig);
|
||||
@@ -462,7 +481,7 @@ export function showGroupCreateModal() {
|
||||
|
||||
hakimiToggle.classList.toggle('on');
|
||||
contact.customHakimiBreakLimit = hakimiToggle.classList.contains('on');
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -561,7 +580,7 @@ export function createGroupChat() {
|
||||
if (!settings.groupChats) settings.groupChats = [];
|
||||
settings.groupChats.push(groupChat);
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
closeGroupCreateModal();
|
||||
|
||||
@@ -640,6 +659,69 @@ function renderGroupChatHistory(groupChat, members, chatHistory) {
|
||||
const isSticker = msg.isSticker === true;
|
||||
const isPhoto = msg.isPhoto === 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') {
|
||||
// 用户消息
|
||||
@@ -2054,7 +2136,7 @@ async function syncGroupMembersLorebooks(members, settings) {
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2132,7 +2214,7 @@ export async function sendGroupMessage(messageText, isMultipleMessages = false,
|
||||
}
|
||||
|
||||
// 立即保存,确保用户消息不会丢失
|
||||
saveSettingsDebounced();
|
||||
saveNow();
|
||||
|
||||
// 显示打字指示器
|
||||
showGroupTypingIndicator(members[0]?.name, members[0]?.id);
|
||||
@@ -2278,7 +2360,7 @@ export async function sendGroupMessage(messageText, isMultipleMessages = false,
|
||||
}
|
||||
groupChat.lastMessageTime = Date.now();
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
checkGroupSummaryReminder(groupChat);
|
||||
|
||||
@@ -2287,7 +2369,7 @@ export async function sendGroupMessage(messageText, isMultipleMessages = false,
|
||||
console.error('[可乐] 群聊 AI 调用失败:', err);
|
||||
|
||||
appendGroupMessage('assistant', `⚠️ ${err.message}`, '系统', null, false);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2354,7 +2436,7 @@ export async function sendGroupStickerMessage(stickerUrl, description = '') {
|
||||
groupChat.lastMessageTime = msgTimestamp;
|
||||
|
||||
// 立即保存,确保用户消息不会丢失
|
||||
saveSettingsDebounced();
|
||||
saveNow();
|
||||
|
||||
// 显示消息
|
||||
appendGroupStickerMessage('user', stickerUrl);
|
||||
@@ -2410,14 +2492,14 @@ export async function sendGroupStickerMessage(stickerUrl, description = '') {
|
||||
}
|
||||
groupChat.lastMessageTime = Date.now();
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
checkGroupSummaryReminder(groupChat);
|
||||
|
||||
} catch (err) {
|
||||
hideGroupTypingIndicator();
|
||||
console.error('[可乐] 群聊表情消息 AI 调用失败:', err);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
appendGroupMessage('assistant', `⚠️ ${err.message}`, '系统', null, false);
|
||||
}
|
||||
@@ -2515,7 +2597,7 @@ export async function sendGroupPhotoMessage(description) {
|
||||
groupChat.lastMessageTime = msgTimestamp;
|
||||
|
||||
// 立即保存,确保用户消息不会丢失
|
||||
saveSettingsDebounced();
|
||||
saveNow();
|
||||
|
||||
// 显示消息
|
||||
appendGroupPhotoMessage('user', description);
|
||||
@@ -2565,14 +2647,14 @@ export async function sendGroupPhotoMessage(description) {
|
||||
}
|
||||
groupChat.lastMessageTime = Date.now();
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
checkGroupSummaryReminder(groupChat);
|
||||
|
||||
} catch (err) {
|
||||
hideGroupTypingIndicator();
|
||||
console.error('[可乐] 群聊照片消息 AI 调用失败:', err);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
appendGroupMessage('assistant', `⚠️ ${err.message}`, '系统', null, false);
|
||||
}
|
||||
@@ -2751,7 +2833,7 @@ export async function sendGroupBatchMessages(messages) {
|
||||
groupChat.lastMessageTime = msgTimestamp;
|
||||
|
||||
// 立即保存,确保用户消息不会丢失
|
||||
saveSettingsDebounced();
|
||||
saveNow();
|
||||
|
||||
// 第二步:调用AI(一次性)
|
||||
showGroupTypingIndicator(members[0]?.name, members[0]?.id);
|
||||
@@ -2798,14 +2880,14 @@ export async function sendGroupBatchMessages(messages) {
|
||||
}
|
||||
groupChat.lastMessageTime = Date.now();
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
checkGroupSummaryReminder(groupChat);
|
||||
|
||||
} catch (err) {
|
||||
hideGroupTypingIndicator();
|
||||
console.error('[可乐] 群聊批量消息 AI 调用失败:', err);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
appendGroupMessage('assistant', `⚠️ ${err.message}`, '系统', null);
|
||||
}
|
||||
|
||||
1733
group-red-packet.js
Normal file
1733
group-red-packet.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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 { escapeHtml } from './utils.js';
|
||||
import { showToast } from './toast.js';
|
||||
@@ -48,7 +48,7 @@ export function addErrorLog(error, context = '') {
|
||||
settings.errorLogs = settings.errorLogs.slice(0, MAX_LOGS);
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
return logEntry;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export function addErrorLog(error, context = '') {
|
||||
export function clearErrorLogs() {
|
||||
const settings = getSettings();
|
||||
settings.errorLogs = [];
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
}
|
||||
|
||||
// 刷新日志列表显示
|
||||
@@ -185,7 +185,7 @@ export function toggleHistoryItem(index, enabled) {
|
||||
const settings = getSettings();
|
||||
if (settings.selectedLorebooks?.[index]) {
|
||||
settings.selectedLorebooks[index].enabled = enabled;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
showToast(enabled ? '已启用' : '已禁用');
|
||||
}
|
||||
}
|
||||
@@ -257,7 +257,7 @@ export function showHistoryDetail(index) {
|
||||
const entryIdx = parseInt(toggle.dataset.entryIndex);
|
||||
if (settings.selectedLorebooks?.[index]?.entries?.[entryIdx]) {
|
||||
settings.selectedLorebooks[index].entries[entryIdx].enabled = toggle.checked;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
49
icons.js
Normal file
49
icons.js
Normal 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
1282
listen-together.js
Normal file
File diff suppressed because it is too large
Load Diff
177
main.js
177
main.js
@@ -4,12 +4,13 @@
|
||||
|
||||
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 { generatePhoneHTML } from './phone-html.js';
|
||||
import { showPage, refreshChatList, updateMePageInfo, getUserPersonaFromST, updateTabBadge } from './ui.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 { 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 { initChatBackground } from './chat-background.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) {
|
||||
return (models || []).map(m => {
|
||||
@@ -76,7 +81,7 @@ async function refreshModelSelect() {
|
||||
const apiKey = document.getElementById('wechat-api-key')?.value?.trim() || settings.apiKey || '';
|
||||
|
||||
if (!apiUrl) {
|
||||
showToast('请先填写 API 地址', '🧊');
|
||||
showToast('请先填写 API 地址', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -94,7 +99,7 @@ async function refreshModelSelect() {
|
||||
modelIds.map(id => `<option value="${id}">${id}</option>`).join('');
|
||||
|
||||
settings.modelList = modelIds;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
showToast(`获取到 ${modelIds.length} 个模型`);
|
||||
} catch (err) {
|
||||
console.error('[可乐] 获取模型列表失败:', err);
|
||||
@@ -248,7 +253,7 @@ function bindEvents() {
|
||||
if (groupChat) {
|
||||
groupChat.chatHistory = [];
|
||||
groupChat.lastMessage = '';
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
openGroupChat(groupIndex); // 刷新群聊界面
|
||||
showToast('群聊记录已清空');
|
||||
}
|
||||
@@ -264,7 +269,7 @@ function bindEvents() {
|
||||
if (contact) {
|
||||
contact.chatHistory = [];
|
||||
contact.lastMessage = '';
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
openChat(currentChatIndex); // 刷新聊天界面
|
||||
showToast('聊天记录已清空');
|
||||
}
|
||||
@@ -369,7 +374,7 @@ function bindEvents() {
|
||||
if (contentDiv) {
|
||||
contentDiv.classList.toggle('hidden', !settings.autoInjectPrompt);
|
||||
}
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
if (settings.autoInjectPrompt) injectAuthorNote();
|
||||
});
|
||||
|
||||
@@ -377,7 +382,7 @@ function bindEvents() {
|
||||
document.getElementById('wechat-save-author-note')?.addEventListener('click', () => {
|
||||
const settings = getSettings();
|
||||
settings.authorNoteCustom = document.getElementById('wechat-author-note-content')?.value || '';
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
showToast('作者注释模板已保存');
|
||||
});
|
||||
|
||||
@@ -391,7 +396,7 @@ function bindEvents() {
|
||||
if (contentDiv) {
|
||||
contentDiv.classList.toggle('hidden', !settings.hakimiBreakLimit);
|
||||
}
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
showToast(settings.hakimiBreakLimit ? '哈基米破限已开启' : '哈基米破限已关闭');
|
||||
});
|
||||
|
||||
@@ -399,7 +404,7 @@ function bindEvents() {
|
||||
document.getElementById('wechat-save-hakimi')?.addEventListener('click', () => {
|
||||
const settings = getSettings();
|
||||
settings.hakimiCustomPrompt = document.getElementById('wechat-hakimi-prompt')?.value || '';
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
showToast('破限提示词已保存');
|
||||
});
|
||||
|
||||
@@ -415,7 +420,7 @@ function bindEvents() {
|
||||
settings.memeStickersEnabled = !settings.memeStickersEnabled;
|
||||
const toggle = document.getElementById('wechat-meme-stickers-toggle');
|
||||
toggle?.classList.toggle('on', settings.memeStickersEnabled);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
showToast(settings.memeStickersEnabled ? 'Meme表情包已启用' : 'Meme表情包已禁用');
|
||||
});
|
||||
|
||||
@@ -532,7 +537,7 @@ function bindEvents() {
|
||||
const fetchBtn = document.getElementById('wechat-contact-fetch-model');
|
||||
|
||||
if (!apiUrl) {
|
||||
showToast('请先填写API地址', '🧊');
|
||||
showToast('请先填写API地址', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -547,7 +552,7 @@ function bindEvents() {
|
||||
modelList.innerHTML = models.map(m => `<option value="${m}">`).join('');
|
||||
showToast(`获取到 ${models.length} 个模型`);
|
||||
} else {
|
||||
showToast('未找到可用模型', '🧊');
|
||||
showToast('未找到可用模型', 'info');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[可乐] 获取模型失败:', err);
|
||||
@@ -566,11 +571,11 @@ function bindEvents() {
|
||||
const testBtn = document.getElementById('wechat-contact-test-api');
|
||||
|
||||
if (!apiUrl) {
|
||||
showToast('请先填写API地址', '🧊');
|
||||
showToast('请先填写API地址', 'info');
|
||||
return;
|
||||
}
|
||||
if (!model) {
|
||||
showToast('请先填写或选择模型', '🧊');
|
||||
showToast('请先填写或选择模型', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -601,7 +606,7 @@ function bindEvents() {
|
||||
|
||||
const data = await response.json();
|
||||
const reply = data.choices?.[0]?.message?.content || '';
|
||||
showToast(`✅ 连接成功!回复: ${reply.substring(0, 20)}...`);
|
||||
showToast(`连接成功!回复: ${reply.substring(0, 20)}...`, 'success');
|
||||
} catch (err) {
|
||||
console.error('[可乐] 测试连接失败:', err);
|
||||
showToast('❌ 连接失败: ' + err.message, '⚠️');
|
||||
@@ -622,7 +627,7 @@ function bindEvents() {
|
||||
if (contentDiv) {
|
||||
contentDiv.classList.toggle('hidden', !settings.groupAutoInjectPrompt);
|
||||
}
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
showToast(settings.groupAutoInjectPrompt ? '群聊提示词注入已开启' : '群聊提示词注入已关闭');
|
||||
});
|
||||
|
||||
@@ -630,7 +635,7 @@ function bindEvents() {
|
||||
document.getElementById('wechat-save-group-note')?.addEventListener('click', () => {
|
||||
const settings = getSettings();
|
||||
settings.userGroupAuthorNote = document.getElementById('wechat-group-author-note')?.value || '';
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
showToast('群聊作者注释已保存');
|
||||
});
|
||||
|
||||
@@ -722,6 +727,10 @@ function bindEvents() {
|
||||
initEmojiPanel();
|
||||
initChatBackground();
|
||||
initMoments();
|
||||
initRedPacketEvents();
|
||||
initTransferEvents();
|
||||
initGroupRedPacket();
|
||||
initCropper();
|
||||
|
||||
// 展开面板
|
||||
document.getElementById('wechat-expand-close')?.addEventListener('click', closeExpandPanel);
|
||||
@@ -816,7 +825,7 @@ function bindEvents() {
|
||||
document.getElementById('wechat-context-enabled')?.addEventListener('change', (e) => {
|
||||
const settings = getSettings();
|
||||
settings.contextEnabled = e.target.checked;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
syncContextEnabledUI(settings.contextEnabled);
|
||||
});
|
||||
|
||||
@@ -824,7 +833,7 @@ function bindEvents() {
|
||||
document.getElementById('wechat-context-slider')?.addEventListener('input', (e) => {
|
||||
const settings = getSettings();
|
||||
settings.contextLevel = parseInt(e.target.value);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
document.getElementById('wechat-context-value').textContent = e.target.value;
|
||||
});
|
||||
|
||||
@@ -836,7 +845,7 @@ function bindEvents() {
|
||||
const index = parseInt(e.target.dataset.index);
|
||||
if (Array.isArray(settings.contextTags) && index >= 0 && index < settings.contextTags.length) {
|
||||
settings.contextTags.splice(index, 1);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshContextTags();
|
||||
}
|
||||
return;
|
||||
@@ -848,7 +857,7 @@ function bindEvents() {
|
||||
settings.contextTags = Array.isArray(settings.contextTags) ? settings.contextTags : [];
|
||||
if (!settings.contextTags.includes(tagName.trim())) {
|
||||
settings.contextTags.push(tagName.trim());
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshContextTags();
|
||||
}
|
||||
}
|
||||
@@ -861,11 +870,54 @@ function bindEvents() {
|
||||
const amount = input?.value || '0.00';
|
||||
const settings = getSettings();
|
||||
settings.walletAmount = amount;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
updateWalletAmountDisplay();
|
||||
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 配置
|
||||
document.getElementById('wechat-summary-key-toggle')?.addEventListener('click', () => {
|
||||
const input = document.getElementById('wechat-summary-key');
|
||||
@@ -879,7 +931,7 @@ function bindEvents() {
|
||||
const modelSelect = document.getElementById('wechat-summary-model');
|
||||
|
||||
if (!url || !key) {
|
||||
if (statusEl) statusEl.textContent = '🧊 请先填写 URL 和 Key';
|
||||
if (statusEl) statusEl.innerHTML = `${ICON_INFO} 请先填写 URL 和 Key`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -888,7 +940,7 @@ function bindEvents() {
|
||||
try {
|
||||
const models = await fetchModelListFromApi(url, key);
|
||||
if (models.length === 0) {
|
||||
if (statusEl) statusEl.textContent = '🧊 未找到可用模型';
|
||||
if (statusEl) statusEl.innerHTML = `${ICON_INFO} 未找到可用模型`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -899,9 +951,9 @@ function bindEvents() {
|
||||
|
||||
const settings = getSettings();
|
||||
settings.summaryModelList = models;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
|
||||
if (statusEl) statusEl.textContent = `✅ 获取到 ${models.length} 个模型`;
|
||||
if (statusEl) statusEl.innerHTML = `${ICON_SUCCESS} 获取到 ${models.length} 个模型`;
|
||||
} catch (err) {
|
||||
console.error('[可乐] 获取模型列表失败:', err);
|
||||
if (statusEl) statusEl.textContent = `⚠️ 获取失败: ${err.message}`;
|
||||
@@ -915,11 +967,11 @@ function bindEvents() {
|
||||
const model = document.getElementById('wechat-summary-model')?.value;
|
||||
|
||||
if (!url || !key) {
|
||||
if (statusEl) statusEl.textContent = '🧊 请先填写 URL 和 Key';
|
||||
if (statusEl) statusEl.innerHTML = `${ICON_INFO} 请先填写 URL 和 Key`;
|
||||
return;
|
||||
}
|
||||
if (!model) {
|
||||
if (statusEl) statusEl.textContent = '🧊 请先选择模型';
|
||||
if (statusEl) statusEl.innerHTML = `${ICON_INFO} 请先选择模型`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -938,7 +990,7 @@ function bindEvents() {
|
||||
throw new Error(errData.error?.message || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
if (statusEl) statusEl.textContent = '✅ 连接成功!';
|
||||
if (statusEl) statusEl.innerHTML = `${ICON_SUCCESS} 连接成功!`;
|
||||
} catch (err) {
|
||||
console.error('[可乐] 测试连接失败:', err);
|
||||
if (statusEl) statusEl.textContent = `⚠️ 连接失败: ${err.message}`;
|
||||
@@ -955,16 +1007,16 @@ function bindEvents() {
|
||||
settings.summaryApiUrl = urlInput?.value?.trim() || '';
|
||||
settings.summaryApiKey = keyInput?.value?.trim() || '';
|
||||
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);
|
||||
});
|
||||
|
||||
document.getElementById('wechat-summary-model')?.addEventListener('change', (e) => {
|
||||
const settings = getSettings();
|
||||
settings.summarySelectedModel = e.target.value;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
});
|
||||
|
||||
document.getElementById('wechat-summary-execute')?.addEventListener('click', () => {
|
||||
@@ -993,14 +1045,6 @@ function bindEvents() {
|
||||
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', () => {
|
||||
openMomentsPage();
|
||||
@@ -1011,7 +1055,7 @@ function bindEvents() {
|
||||
item.addEventListener('click', () => {
|
||||
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') {
|
||||
allPanels.filter(p => p !== 'wechat-summary-panel').forEach(p => document.getElementById(p)?.classList.add('hidden'));
|
||||
@@ -1053,8 +1097,22 @@ function bindEvents() {
|
||||
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 || '该';
|
||||
showToast(`"${label}" 功能开发中...`, '🧊');
|
||||
showToast(`"${label}" 功能开发中...`, 'info');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1194,7 +1252,7 @@ function bindEvents() {
|
||||
if (!confirm('确定要清空所有联系人吗?')) return;
|
||||
const settings = getSettings();
|
||||
settings.contacts = [];
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshContactsList();
|
||||
showToast('已清空所有联系人');
|
||||
});
|
||||
@@ -1213,7 +1271,7 @@ function bindEvents() {
|
||||
reader.onload = function (event) {
|
||||
const settings = getSettings();
|
||||
settings.userAvatar = event.target.result;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
updateMePageInfo();
|
||||
showToast('头像已更换');
|
||||
};
|
||||
@@ -1251,7 +1309,7 @@ function bindEvents() {
|
||||
settings.apiUrl = apiUrl;
|
||||
settings.apiKey = apiKey;
|
||||
settings.selectedModel = selectedModel;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
|
||||
showToast('API 配置已保存');
|
||||
});
|
||||
@@ -1267,12 +1325,12 @@ function bindEvents() {
|
||||
modelInput.addEventListener('change', (e) => {
|
||||
const settings = getSettings();
|
||||
settings.selectedModel = e.target.value.trim();
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
});
|
||||
modelInput.addEventListener('input', (e) => {
|
||||
const settings = getSettings();
|
||||
settings.selectedModel = e.target.value.trim();
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1282,7 +1340,7 @@ function bindEvents() {
|
||||
const apiKey = document.getElementById('wechat-api-key')?.value.trim() || '';
|
||||
|
||||
if (!apiUrl) {
|
||||
showToast('请先填写 API 地址', '🧊');
|
||||
showToast('请先填写 API 地址', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1321,7 +1379,7 @@ function bindEvents() {
|
||||
|
||||
const settings = getSettings();
|
||||
settings.selectedModel = modelName.trim();
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
showToast('模型已设置');
|
||||
}
|
||||
}
|
||||
@@ -1352,7 +1410,7 @@ function bindEvents() {
|
||||
const select = document.getElementById('wechat-group-model-select');
|
||||
|
||||
if (!apiUrl) {
|
||||
showToast('请先填写群聊 API 地址', '🧊');
|
||||
showToast('请先填写群聊 API 地址', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1372,7 +1430,7 @@ function bindEvents() {
|
||||
}
|
||||
|
||||
settings.groupModelList = modelIds;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
showToast(`获取到 ${modelIds.length} 个模型`);
|
||||
} catch (err) {
|
||||
console.error('[可乐] 获取群聊模型列表失败:', err);
|
||||
@@ -1400,7 +1458,7 @@ function bindEvents() {
|
||||
|
||||
const settings = getSettings();
|
||||
settings.groupSelectedModel = modelName.trim();
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
showToast('群聊模型已设置');
|
||||
}
|
||||
}
|
||||
@@ -1412,7 +1470,7 @@ function bindEvents() {
|
||||
const apiKey = document.getElementById('wechat-group-api-key')?.value.trim() || '';
|
||||
|
||||
if (!apiUrl) {
|
||||
showToast('请先填写群聊 API 地址', '🧊');
|
||||
showToast('请先填写群聊 API 地址', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1446,7 +1504,7 @@ function bindEvents() {
|
||||
settings.groupApiUrl = apiUrl;
|
||||
settings.groupApiKey = apiKey;
|
||||
settings.groupSelectedModel = selectedModel;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
|
||||
showToast('群聊 API 配置已保存');
|
||||
});
|
||||
@@ -1457,12 +1515,12 @@ function bindEvents() {
|
||||
groupModelInput.addEventListener('change', (e) => {
|
||||
const settings = getSettings();
|
||||
settings.groupSelectedModel = e.target.value.trim();
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
});
|
||||
groupModelInput.addEventListener('input', (e) => {
|
||||
const settings = getSettings();
|
||||
settings.groupSelectedModel = e.target.value.trim();
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1481,7 +1539,7 @@ function bindEvents() {
|
||||
|
||||
const settings = getSettings();
|
||||
settings.summarySelectedModel = modelName.trim();
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
showToast('总结模型已设置');
|
||||
}
|
||||
}
|
||||
@@ -1524,7 +1582,7 @@ function init() {
|
||||
loadSettings();
|
||||
const settings = getSettings();
|
||||
if (seedDefaultUserPersonaFromST(settings)) {
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
}
|
||||
|
||||
const phoneHTML = generatePhoneHTML();
|
||||
@@ -1559,6 +1617,9 @@ function init() {
|
||||
// 初始化错误捕获
|
||||
initErrorCapture();
|
||||
|
||||
// 初始化页面卸载保存
|
||||
setupUnloadSave();
|
||||
|
||||
setInterval(() => {
|
||||
const phone = document.getElementById('wechat-phone');
|
||||
if (!phone || phone.classList.contains('hidden')) return;
|
||||
|
||||
106
message-menu.js
106
message-menu.js
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
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 { showToast } from './toast.js';
|
||||
import { getContext } from '../../../extensions.js';
|
||||
@@ -23,6 +23,7 @@ let pendingQuote = null;
|
||||
// 菜单项配置
|
||||
const menuItems = [
|
||||
{ id: 'copy', icon: 'copy', text: '复制' },
|
||||
{ id: 'transcribe', icon: 'transcribe', text: '转文字', voiceOnly: true },
|
||||
{ id: 'quote', icon: 'quote', text: '引用' },
|
||||
{ id: 'recall', icon: 'recall', text: '撤回', userOnly: true },
|
||||
{ id: 'delete', icon: 'delete', text: '删除' },
|
||||
@@ -35,6 +36,12 @@ const icons = {
|
||||
<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"/>
|
||||
</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">
|
||||
<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"/>
|
||||
@@ -56,7 +63,7 @@ const icons = {
|
||||
};
|
||||
|
||||
// 创建菜单DOM
|
||||
function createMenuElement(isUserMessage = false) {
|
||||
function createMenuElement(isUserMessage = false, isVoiceMessage = false, voiceTextVisible = false) {
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'wechat-msg-menu hidden';
|
||||
menu.id = 'wechat-msg-menu';
|
||||
@@ -67,13 +74,22 @@ function createMenuElement(isUserMessage = false) {
|
||||
menuItems.forEach(item => {
|
||||
// 跳过仅用户可用的菜单项(如果当前不是用户消息)
|
||||
if (item.userOnly && !isUserMessage) return;
|
||||
// 跳过仅语音消息可用的菜单项(如果当前不是语音消息)
|
||||
if (item.voiceOnly && !isVoiceMessage) return;
|
||||
|
||||
const menuItem = document.createElement('div');
|
||||
menuItem.className = 'wechat-msg-menu-item';
|
||||
menuItem.dataset.action = item.id;
|
||||
|
||||
// 转文字按钮根据状态显示不同文本
|
||||
let text = item.text;
|
||||
if (item.id === 'transcribe' && voiceTextVisible) {
|
||||
text = '收起文字';
|
||||
}
|
||||
|
||||
menuItem.innerHTML = `
|
||||
<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);
|
||||
});
|
||||
@@ -111,12 +127,33 @@ export function showMessageMenu(msgElement, msgIndex, event) {
|
||||
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');
|
||||
if (menu) {
|
||||
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);
|
||||
bindMenuEvents(menu);
|
||||
|
||||
@@ -185,13 +222,16 @@ function bindMenuEvents(menu) {
|
||||
if (!menuItem) return;
|
||||
|
||||
const action = menuItem.dataset.action;
|
||||
handleMenuAction(action, currentMenuMsgIndex);
|
||||
// 传递菜单上存储的语音数据
|
||||
const voiceId = menu.dataset.voiceId;
|
||||
const voiceContent = menu.dataset.voiceContent;
|
||||
handleMenuAction(action, currentMenuMsgIndex, voiceId, voiceContent);
|
||||
hideMessageMenu();
|
||||
});
|
||||
}
|
||||
|
||||
// 处理菜单操作
|
||||
function handleMenuAction(action, msgIndex) {
|
||||
function handleMenuAction(action, msgIndex, voiceId = '', voiceContent = '') {
|
||||
const settings = getSettings();
|
||||
const groupIndex = getCurrentGroupIndex();
|
||||
let chatHistory, contact, groupChat;
|
||||
@@ -215,6 +255,22 @@ function handleMenuAction(action, msgIndex) {
|
||||
case 'copy':
|
||||
copyMessage(msg.content);
|
||||
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':
|
||||
quoteMessage(msg, groupIndex >= 0, groupChat);
|
||||
break;
|
||||
@@ -258,6 +314,12 @@ function copyMessage(content) {
|
||||
|
||||
// 引用消息 - 设置待引用状态
|
||||
function quoteMessage(msg, isGroupChat = false, groupChat = null) {
|
||||
// 不允许引用撤回的消息
|
||||
if (msg.isRecalled) {
|
||||
showToast('无法引用已撤回的消息');
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = getSettings();
|
||||
const context = getContext();
|
||||
|
||||
@@ -391,7 +453,7 @@ export function setQuote(quote) {
|
||||
// 删除消息
|
||||
function deleteMessage(msgIndex, contact) {
|
||||
contact.chatHistory.splice(msgIndex, 1);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
// 刷新聊天界面
|
||||
openChat(currentChatIndex);
|
||||
showToast('已删除');
|
||||
@@ -413,7 +475,7 @@ async function recallMessage(msgIndex, contact) {
|
||||
msg.originalContent = msg.content;
|
||||
msg.content = '';
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
// 刷新聊天界面
|
||||
openChat(currentChatIndex);
|
||||
showToast('已撤回');
|
||||
@@ -423,7 +485,22 @@ async function recallMessage(msgIndex, contact) {
|
||||
showTypingIndicator(contact);
|
||||
|
||||
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();
|
||||
|
||||
@@ -455,7 +532,7 @@ async function recallMessage(msgIndex, contact) {
|
||||
}
|
||||
|
||||
contact.lastMessage = aiMessages[aiMessages.length - 1];
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
|
||||
} catch (err) {
|
||||
hideTypingIndicator();
|
||||
@@ -469,7 +546,7 @@ function deleteGroupMessage(msgIndex, groupChat) {
|
||||
if (groupIndex < 0) return;
|
||||
|
||||
groupChat.chatHistory.splice(msgIndex, 1);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
// 刷新群聊界面
|
||||
openGroupChat(groupIndex);
|
||||
showToast('已删除');
|
||||
@@ -494,7 +571,7 @@ async function recallGroupMessage(msgIndex, groupChat) {
|
||||
msg.originalContent = msg.content;
|
||||
msg.content = '';
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
// 刷新群聊界面
|
||||
openGroupChat(groupIndex);
|
||||
showToast('已撤回');
|
||||
@@ -502,7 +579,8 @@ async function recallGroupMessage(msgIndex, groupChat) {
|
||||
|
||||
// 绑定消息气泡事件
|
||||
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) => {
|
||||
if (bubble.dataset.menuBound) return;
|
||||
@@ -522,8 +600,6 @@ export function bindMessageBubbleEvents(container) {
|
||||
isLongPress = false;
|
||||
return;
|
||||
}
|
||||
// 语音气泡点击展开文本,不显示菜单
|
||||
if (bubble.classList.contains('wechat-voice-bubble')) return;
|
||||
|
||||
e.stopPropagation();
|
||||
showMessageMenu(bubble, getRealMsgIndex(container, msgElement), e);
|
||||
|
||||
410
moments.js
410
moments.js
@@ -6,11 +6,12 @@
|
||||
* - 用户评论后角色会回复
|
||||
*/
|
||||
|
||||
import { saveSettingsDebounced } from '../../../../script.js';
|
||||
import { requestSave } from './save-manager.js';
|
||||
import { getContext } from '../../../extensions.js';
|
||||
import { getSettings } from './config.js';
|
||||
import { showToast, showNotificationBanner } from './toast.js';
|
||||
import { sleep } from './utils.js';
|
||||
import { selectAndCrop } from './cropper.js';
|
||||
|
||||
// 当前正在查看的联系人索引
|
||||
let currentContactIndex = null;
|
||||
@@ -175,34 +176,26 @@ function updateMomentsProfile(contactIndex) {
|
||||
* 更换朋友圈封面
|
||||
*/
|
||||
function changeMomentsCover() {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
input.onchange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const settings = getSettings();
|
||||
// 使用裁剪器选择并裁剪封面(16:9比例)
|
||||
selectAndCrop(16 / 9, (croppedImage) => {
|
||||
const settings = getSettings();
|
||||
|
||||
if (currentContactIndex !== null && settings.contacts[currentContactIndex]) {
|
||||
settings.contacts[currentContactIndex].momentsCover = event.target.result;
|
||||
} else {
|
||||
settings.momentsCover = event.target.result;
|
||||
}
|
||||
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);
|
||||
if (currentContactIndex !== null && settings.contacts[currentContactIndex]) {
|
||||
settings.contacts[currentContactIndex].momentsCover = croppedImage;
|
||||
} else {
|
||||
settings.momentsCover = croppedImage;
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
requestSave();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
renderMomentsList(currentContactIndex);
|
||||
}
|
||||
|
||||
@@ -756,15 +749,25 @@ async function sendUserComment() {
|
||||
|
||||
targetMoment.comments.push(newComment);
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
hideCommentInput();
|
||||
renderMomentsList(currentContactIndex);
|
||||
|
||||
// 触发角色回复(异步)- 只有联系人的朋友圈才会回复
|
||||
// 触发角色回复(异步)
|
||||
if (contactIndexForReply !== null && targetContactId !== 'user') {
|
||||
// 情况1:联系人的朋友圈 - 联系人回复用户
|
||||
setTimeout(() => {
|
||||
generateContactReplyToComment(contactIndexForReply, targetMomentIndex, userName, commentText);
|
||||
}, 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理评论内容,移除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) {
|
||||
const settings = getSettings();
|
||||
const context = getContext();
|
||||
const characters = [];
|
||||
|
||||
// 获取联系人的角色数据
|
||||
@@ -865,6 +891,9 @@ function extractCharactersFromLorebook(contact) {
|
||||
const charData = rawData.data || rawData;
|
||||
const charName = charData.name || contact.name || '';
|
||||
|
||||
// 获取用户名,用于排除用户
|
||||
const userName = context?.name1 || settings.wechatId || '';
|
||||
|
||||
// 方法1: 从 selectedLorebooks 中查找与当前角色匹配的世界书
|
||||
const selectedLorebooks = settings.selectedLorebooks || [];
|
||||
|
||||
@@ -889,8 +918,8 @@ function extractCharactersFromLorebook(contact) {
|
||||
// 提取所有有内容的条目,不再限制名称长度和关键词过滤
|
||||
if (entry.keys && entry.keys.length > 0) {
|
||||
const name = entry.keys[0];
|
||||
// 只排除角色本人,其他条目全部包含
|
||||
if (name && name !== charName) {
|
||||
// 排除角色本人和用户
|
||||
if (name && name !== charName && name !== userName) {
|
||||
characters.push({
|
||||
name: name,
|
||||
content: entry.content || ''
|
||||
@@ -909,8 +938,8 @@ function extractCharactersFromLorebook(contact) {
|
||||
// 提取所有有内容的条目
|
||||
if (entry.keys && entry.keys.length > 0) {
|
||||
const name = entry.keys[0];
|
||||
// 只排除角色本人
|
||||
if (name && name !== charName) {
|
||||
// 排除角色本人和用户
|
||||
if (name && name !== charName && name !== userName) {
|
||||
characters.push({
|
||||
name: name,
|
||||
content: entry.content || ''
|
||||
@@ -959,25 +988,19 @@ export async function generateNewMomentForContact(contactIndex) {
|
||||
if (!settings.momentsData) settings.momentsData = {};
|
||||
if (!settings.momentsData[contact.id]) settings.momentsData[contact.id] = [];
|
||||
|
||||
// 提取世界书中的人物用于评论
|
||||
const characters = extractCharactersFromLorebook(contact);
|
||||
|
||||
// 随机生成 3-4 条评论
|
||||
const comments = await generateCommentsFromCharacters(contact, momentContent.text, characters);
|
||||
|
||||
// 创建新动态
|
||||
// 创建新动态(不自动生成评论,等用户主动评论后AI再回复)
|
||||
const newMoment = {
|
||||
id: Date.now().toString(),
|
||||
text: momentContent.text,
|
||||
images: momentContent.images || [],
|
||||
timestamp: Date.now(),
|
||||
likes: [],
|
||||
comments: comments
|
||||
comments: []
|
||||
};
|
||||
|
||||
// 添加到列表开头
|
||||
settings.momentsData[contact.id].unshift(newMoment);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
|
||||
showNotificationBanner('微信', `${contact.name}发布了一条朋友圈`);
|
||||
renderMomentsList(currentContactIndex);
|
||||
@@ -993,6 +1016,7 @@ export async function generateNewMomentForContact(contactIndex) {
|
||||
*/
|
||||
async function generateMomentContent(contact) {
|
||||
const settings = getSettings();
|
||||
const context = getContext();
|
||||
|
||||
// 获取 API 配置
|
||||
let apiUrl, apiKey, apiModel;
|
||||
@@ -1020,12 +1044,56 @@ async function generateMomentContent(contact) {
|
||||
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%纯文字)
|
||||
const withImages = Math.random() < 0.6;
|
||||
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}张图片的朋友圈,请按以下格式输出:
|
||||
文案内容
|
||||
@@ -1035,11 +1103,26 @@ ${withImages ? `这是一条带${imageCount}张图片的朋友圈,请按以下
|
||||
|
||||
图片描述要具体生动,1-2句话描述图片内容(如:她在咖啡厅的自拍,手里拿着拿铁,阳光洒在脸上)` : '这是一条纯文字朋友圈,直接输出文案内容即可,不要带任何图片标签'}
|
||||
|
||||
【内容要求】
|
||||
【内容要求 - 非常重要!】
|
||||
${isChatRelated ? `★★★ 这条朋友圈必须与聊天记录相关 ★★★
|
||||
- 仔细阅读上面的聊天记录,找出最近聊天的话题、事件、情感
|
||||
- 朋友圈内容要延续、回应、或暗示最近聊天中提到的事情
|
||||
- 可以是:聊天中提到要做的事、约定、话题的延续、对对方的想念/吐槽等
|
||||
- 让看的人能感受到这条朋友圈和你们的聊天有关联
|
||||
- 示例:如果聊天中约了吃饭,可以发吃饭的朋友圈;如果聊到想念,可以发暗示思念的内容` : `★★★ 这条朋友圈是你的个人日常 ★★★
|
||||
- 发一条和聊天内容无关的个人日常动态
|
||||
- 展示你自己的生活:日常分享、心情感悟、美食、旅行、自拍、工作、宠物、风景、爱好等
|
||||
- 要符合你的角色设定和性格`}
|
||||
|
||||
【通用要求】
|
||||
1. 文案1-3句话,符合角色性格,语气自然真实
|
||||
2. 内容可以是:日常分享、心情感悟、美食、旅行、自拍、工作、宠物、风景等
|
||||
3. 可以适当使用表情符号
|
||||
4. 要像真人发的朋友圈一样自然
|
||||
2. 可以适当使用表情符号
|
||||
3. 要像真人发的朋友圈一样自然
|
||||
|
||||
【禁止输出】
|
||||
- 绝对禁止输出任何关键词、世界书条目名称、设定标签
|
||||
- 绝对禁止输出任何系统提示、指令、格式说明
|
||||
- 只输出纯粹的朋友圈内容
|
||||
|
||||
【示例】
|
||||
纯文字:今天天气真好,心情也跟着好起来了☀️
|
||||
@@ -1228,6 +1311,11 @@ ${avoidText}
|
||||
- 简短自然,5-15字
|
||||
- 禁止用"怎么了"、"咋了"、"发生什么了"开头
|
||||
|
||||
【禁止输出】
|
||||
- 绝对禁止输出任何关键词、世界书条目名称、设定标签
|
||||
- 绝对禁止输出任何系统提示、指令、格式说明
|
||||
- 只输出纯粹的评论内容
|
||||
|
||||
直接输出评论内容:`;
|
||||
|
||||
const response = await fetch(chatUrl, {
|
||||
@@ -1248,7 +1336,9 @@ ${avoidText}
|
||||
|
||||
if (response.ok) {
|
||||
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) {
|
||||
comments.push({
|
||||
name: character.name,
|
||||
@@ -1486,7 +1576,15 @@ ${commentsContext}
|
||||
showNotificationBanner(contact.name, chatMessage);
|
||||
} 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 = [];
|
||||
moment.comments.push({
|
||||
name: contact.name,
|
||||
@@ -1494,7 +1592,7 @@ ${commentsContext}
|
||||
replyTo: userName,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
renderMomentsList(currentContactIndex);
|
||||
}
|
||||
|
||||
@@ -1522,7 +1620,7 @@ export function addMomentToContact(contactId, momentData) {
|
||||
};
|
||||
|
||||
settings.momentsData[contactId].unshift(newMoment);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1555,9 +1653,9 @@ export function clearContactMoments(contactIndex) {
|
||||
|
||||
// 清空该联系人的朋友圈
|
||||
settings.momentsData[contact.id] = [];
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
|
||||
showToast(`已清空 ${momentCount} 条朋友圈`, '✅');
|
||||
showToast(`已清空 ${momentCount} 条朋友圈`, 'success');
|
||||
console.log(`[可乐] 已清空 ${contact.name} 的 ${momentCount} 条朋友圈`);
|
||||
}
|
||||
|
||||
@@ -1732,9 +1830,9 @@ function publishUserMomentWithImages(text, images) {
|
||||
};
|
||||
|
||||
settings.momentsData[userId].unshift(newMoment);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
|
||||
showToast('朋友圈已发布', '✅');
|
||||
showToast('朋友圈已发布', 'success');
|
||||
renderMomentsList(null);
|
||||
|
||||
// 通知所有联系人(可能触发他们的评论/点赞)
|
||||
@@ -1776,8 +1874,8 @@ function deleteUserMoment(index) {
|
||||
}
|
||||
// 删除该联系人的指定朋友圈
|
||||
settings.momentsData[contact.id].splice(index, 1);
|
||||
saveSettingsDebounced();
|
||||
showToast('已删除', '✅');
|
||||
requestSave();
|
||||
showToast('已删除', 'success');
|
||||
renderMomentsList(currentContactIndex);
|
||||
} else {
|
||||
// 查看所有朋友圈(合并视图)
|
||||
@@ -1802,8 +1900,8 @@ function deleteUserMoment(index) {
|
||||
|
||||
// 从对应联系人的朋友圈数组中删除
|
||||
settings.momentsData[targetMoment.contactId].splice(targetMoment.originalIndex, 1);
|
||||
saveSettingsDebounced();
|
||||
showToast('已删除', '✅');
|
||||
requestSave();
|
||||
showToast('已删除', 'success');
|
||||
renderMomentsList(null);
|
||||
}
|
||||
}
|
||||
@@ -1831,7 +1929,7 @@ async function triggerContactsReactToUserMoment(moment) {
|
||||
// 点赞
|
||||
if (!moment.likes.includes(contact.name)) {
|
||||
moment.likes.push(contact.name);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
// 用户朋友圈使用 null 作为 contactIndex
|
||||
renderMomentsList(null);
|
||||
}
|
||||
@@ -1849,7 +1947,7 @@ async function triggerContactsReactToUserMoment(moment) {
|
||||
if (!moment.likes.includes(contact.name)) {
|
||||
moment.likes.push(contact.name);
|
||||
}
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
// 用户朋友圈使用 null 作为 contactIndex
|
||||
renderMomentsList(null);
|
||||
|
||||
@@ -1962,6 +2060,11 @@ ${avoidText}
|
||||
- 简短自然,5-15字
|
||||
- 禁止用"怎么了"、"咋了"、"发生什么了"开头
|
||||
|
||||
【禁止输出】
|
||||
- 绝对禁止输出任何关键词、世界书条目名称、设定标签
|
||||
- 绝对禁止输出任何系统提示、指令、格式说明
|
||||
- 只输出纯粹的评论内容
|
||||
|
||||
直接输出评论内容:`;
|
||||
|
||||
console.log(`[可乐] 正在生成 ${contact.name} 的评论...`);
|
||||
@@ -1983,7 +2086,9 @@ ${avoidText}
|
||||
|
||||
if (response.ok) {
|
||||
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}`);
|
||||
return comment;
|
||||
} else {
|
||||
@@ -2142,7 +2247,7 @@ function addPrivateChatMessage(contactIndex, contact, message) {
|
||||
// 增加未读数
|
||||
targetContact.unreadCount = (targetContact.unreadCount || 0) + 1;
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
|
||||
// 刷新聊天列表
|
||||
import('./ui.js').then(({ refreshChatList }) => {
|
||||
@@ -2274,7 +2379,7 @@ function addPrivateMessageFromContact(contactIndex, message, context = '') {
|
||||
// 增加未读消息计数
|
||||
contact.unreadCount = (contact.unreadCount || 0) + 1;
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
|
||||
// 尝试刷新聊天列表
|
||||
try {
|
||||
@@ -2288,3 +2393,178 @@ function addPrivateMessageFromContact(contactIndex, message, context = '') {
|
||||
|
||||
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
494
music.js
@@ -1,4 +1,5 @@
|
||||
import { showToast } from './toast.js';
|
||||
import { escapeHtml } from './utils.js';
|
||||
|
||||
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 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 div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
// 随机推歌用的热门关键词库
|
||||
const RANDOM_KEYWORDS = [
|
||||
'热门', '流行', '抖音', '网红', '经典', '怀旧', '情歌', '伤感',
|
||||
'轻音乐', '纯音乐', '钢琴', '吉他', '民谣', '摇滚', '电音', 'DJ',
|
||||
'周杰伦', '林俊杰', '邓紫棋', '薛之谦', '毛不易', '陈奕迅', '王菲',
|
||||
'Taylor Swift', 'Ed Sheeran', 'Bruno Mars', 'Adele', 'BTS',
|
||||
'日语', '韩语', '粤语', '古风', '国风', '说唱', 'rap',
|
||||
'治愈', '励志', '甜蜜', '浪漫', '夜晚', '清晨', '放松'
|
||||
];
|
||||
|
||||
// 已播放过的歌曲ID(避免重复推荐)
|
||||
let playedSongIds = new Set();
|
||||
|
||||
// 是否已显示过随机推歌提示
|
||||
let hasShownRandomToast = false;
|
||||
|
||||
export function formatDuration(seconds) {
|
||||
if (seconds === null || seconds === undefined || isNaN(seconds)) return '--:--';
|
||||
@@ -113,18 +123,18 @@ export async function fetchLyrics(song) {
|
||||
function createSingleLineLyrics() {
|
||||
if (document.getElementById('wechat-single-lyrics')) return;
|
||||
|
||||
var phoneContainer = document.getElementById('wechat-phone');
|
||||
let phoneContainer = document.getElementById('wechat-phone');
|
||||
if (!phoneContainer) return;
|
||||
|
||||
// 生成颜色按钮HTML
|
||||
var colorBtnsHtml = '';
|
||||
for (var i = 0; i < LYRICS_COLORS.length; i++) {
|
||||
var c = LYRICS_COLORS[i];
|
||||
var activeClass = (c === lyricsColor) ? ' active' : '';
|
||||
let colorBtnsHtml = '';
|
||||
for (let i = 0; i < LYRICS_COLORS.length; i++) {
|
||||
let c = LYRICS_COLORS[i];
|
||||
let activeClass = (c === lyricsColor) ? ' active' : '';
|
||||
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-colors">' + colorBtnsHtml + '</div>' +
|
||||
'<button class="wechat-single-lyrics-lock">' + UNLOCK_ICON + '</button>' +
|
||||
@@ -135,11 +145,11 @@ function createSingleLineLyrics() {
|
||||
}
|
||||
|
||||
function initSingleLineLyricsEvents() {
|
||||
var panel = document.getElementById('wechat-single-lyrics');
|
||||
let panel = document.getElementById('wechat-single-lyrics');
|
||||
if (!panel) return;
|
||||
|
||||
var lockBtn = panel.querySelector('.wechat-single-lyrics-lock');
|
||||
var colorsContainer = panel.querySelector('.wechat-single-lyrics-colors');
|
||||
let lockBtn = panel.querySelector('.wechat-single-lyrics-lock');
|
||||
let colorsContainer = panel.querySelector('.wechat-single-lyrics-colors');
|
||||
|
||||
if (lockBtn) {
|
||||
lockBtn.addEventListener('click', function(e) {
|
||||
@@ -153,27 +163,27 @@ function initSingleLineLyricsEvents() {
|
||||
// 颜色按钮点击事件
|
||||
if (colorsContainer) {
|
||||
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;
|
||||
e.stopPropagation();
|
||||
|
||||
var newColor = btn.dataset.color;
|
||||
let newColor = btn.dataset.color;
|
||||
if (newColor && LYRICS_COLORS.indexOf(newColor) >= 0) {
|
||||
lyricsColor = newColor;
|
||||
|
||||
// 更新文字颜色
|
||||
var textEl = panel.querySelector('.wechat-single-lyrics-text');
|
||||
let textEl = panel.querySelector('.wechat-single-lyrics-text');
|
||||
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.add('color-' + newColor);
|
||||
}
|
||||
|
||||
// 更新按钮激活状态
|
||||
var allBtns = colorsContainer.querySelectorAll('.wechat-lyrics-color-btn');
|
||||
for (var j = 0; j < allBtns.length; j++) {
|
||||
let allBtns = colorsContainer.querySelectorAll('.wechat-lyrics-color-btn');
|
||||
for (let j = 0; j < allBtns.length; j++) {
|
||||
allBtns[j].classList.remove('active');
|
||||
}
|
||||
btn.classList.add('active');
|
||||
@@ -190,8 +200,8 @@ function initSingleLineLyricsEvents() {
|
||||
});
|
||||
|
||||
// 拖拽功能(仅在未锁定时)- 支持上下左右移动
|
||||
var isDragging = false;
|
||||
var startX, startY, initialX, initialY;
|
||||
let isDragging = false;
|
||||
let startX, startY, initialX, initialY;
|
||||
|
||||
panel.addEventListener('mousedown', startDrag);
|
||||
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-lyrics-color-btn')) return;
|
||||
isDragging = true;
|
||||
var rect = panel.getBoundingClientRect();
|
||||
var phoneRect = document.getElementById('wechat-phone').getBoundingClientRect();
|
||||
let rect = panel.getBoundingClientRect();
|
||||
let phoneRect = document.getElementById('wechat-phone').getBoundingClientRect();
|
||||
initialX = rect.left - phoneRect.left;
|
||||
initialY = rect.top - phoneRect.top;
|
||||
if (e.type === 'touchstart') {
|
||||
@@ -221,7 +231,7 @@ function initSingleLineLyricsEvents() {
|
||||
function drag(e) {
|
||||
if (!isDragging) return;
|
||||
e.preventDefault();
|
||||
var clientX, clientY;
|
||||
let clientX, clientY;
|
||||
if (e.type === 'touchmove') {
|
||||
clientX = e.touches[0].clientX;
|
||||
clientY = e.touches[0].clientY;
|
||||
@@ -229,13 +239,13 @@ function initSingleLineLyricsEvents() {
|
||||
clientX = e.clientX;
|
||||
clientY = e.clientY;
|
||||
}
|
||||
var dx = clientX - startX;
|
||||
var dy = clientY - startY;
|
||||
var phoneEl = document.getElementById('wechat-phone');
|
||||
var phoneRect = phoneEl.getBoundingClientRect();
|
||||
var panelWidth = panel.offsetWidth || 200;
|
||||
var newX = Math.max(0, Math.min(phoneRect.width - panelWidth, initialX + dx));
|
||||
var newY = Math.max(0, Math.min(phoneRect.height - 40, initialY + dy));
|
||||
let dx = clientX - startX;
|
||||
let dy = clientY - startY;
|
||||
let phoneEl = document.getElementById('wechat-phone');
|
||||
let phoneRect = phoneEl.getBoundingClientRect();
|
||||
let panelWidth = panel.offsetWidth || 200;
|
||||
let newX = Math.max(0, Math.min(phoneRect.width - panelWidth, initialX + dx));
|
||||
let newY = Math.max(0, Math.min(phoneRect.height - 40, initialY + dy));
|
||||
panel.style.left = newX + 'px';
|
||||
panel.style.top = newY + 'px';
|
||||
panel.style.transform = 'none';
|
||||
@@ -254,7 +264,7 @@ function initSingleLineLyricsEvents() {
|
||||
|
||||
function showSingleLineLyrics() {
|
||||
createSingleLineLyrics();
|
||||
var panel = document.getElementById('wechat-single-lyrics');
|
||||
let panel = document.getElementById('wechat-single-lyrics');
|
||||
if (panel) {
|
||||
panel.classList.remove('hidden');
|
||||
singleLineLyricsVisible = true;
|
||||
@@ -263,7 +273,7 @@ function showSingleLineLyrics() {
|
||||
}
|
||||
|
||||
function hideSingleLineLyrics() {
|
||||
var panel = document.getElementById('wechat-single-lyrics');
|
||||
let panel = document.getElementById('wechat-single-lyrics');
|
||||
if (panel) {
|
||||
panel.classList.add('hidden');
|
||||
singleLineLyricsVisible = false;
|
||||
@@ -277,14 +287,14 @@ function toggleSingleLineLyrics() {
|
||||
showSingleLineLyrics();
|
||||
}
|
||||
// 更新迷你播放器按钮状态
|
||||
var lyricsBtn = document.querySelector('.wechat-music-mini-lyrics-btn');
|
||||
let lyricsBtn = document.querySelector('.wechat-music-mini-lyrics-btn');
|
||||
if (lyricsBtn) {
|
||||
lyricsBtn.classList.toggle('active', singleLineLyricsVisible);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSingleLineLyricsText() {
|
||||
var textEl = document.querySelector('.wechat-single-lyrics-text');
|
||||
let textEl = document.querySelector('.wechat-single-lyrics-text');
|
||||
if (!textEl) return;
|
||||
|
||||
if (!currentSong || !currentSong.lyrics) {
|
||||
@@ -305,11 +315,11 @@ function updateSingleLineLyricsText() {
|
||||
function updateSingleLineLyricsHighlight(currentTime) {
|
||||
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;
|
||||
|
||||
var activeIndex = -1;
|
||||
for (var i = parsedLyrics.length - 1; i >= 0; i--) {
|
||||
let activeIndex = -1;
|
||||
for (let i = parsedLyrics.length - 1; i >= 0; i--) {
|
||||
if (currentTime >= parsedLyrics[i].time) {
|
||||
activeIndex = i;
|
||||
break;
|
||||
@@ -323,14 +333,14 @@ function updateSingleLineLyricsHighlight(currentTime) {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 浮动歌词面板(保留但不使用) ==========
|
||||
// ========== 浮动歌词面板 ==========
|
||||
function createFloatingLyrics() {
|
||||
if (document.getElementById('wechat-floating-lyrics')) return;
|
||||
|
||||
var phoneContainer = document.getElementById('wechat-phone');
|
||||
let phoneContainer = document.getElementById('wechat-phone');
|
||||
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">' +
|
||||
'<span class="wechat-floating-lyrics-title">歌词</span>' +
|
||||
'<button class="wechat-floating-lyrics-close">' + CLOSE_ICON + '</button>' +
|
||||
@@ -343,11 +353,11 @@ function createFloatingLyrics() {
|
||||
}
|
||||
|
||||
function initFloatingLyricsEvents() {
|
||||
var panel = document.getElementById('wechat-floating-lyrics');
|
||||
let panel = document.getElementById('wechat-floating-lyrics');
|
||||
if (!panel) return;
|
||||
|
||||
var header = panel.querySelector('.wechat-floating-lyrics-header');
|
||||
var closeBtn = panel.querySelector('.wechat-floating-lyrics-close');
|
||||
let header = panel.querySelector('.wechat-floating-lyrics-header');
|
||||
let closeBtn = panel.querySelector('.wechat-floating-lyrics-close');
|
||||
|
||||
closeBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
@@ -355,8 +365,8 @@ function initFloatingLyricsEvents() {
|
||||
});
|
||||
|
||||
// 拖拽(在手机容器内)
|
||||
var isDragging = false;
|
||||
var startX, startY, initialX, initialY;
|
||||
let isDragging = false;
|
||||
let startX, startY, initialX, initialY;
|
||||
|
||||
header.addEventListener('mousedown', startDrag);
|
||||
header.addEventListener('touchstart', startDrag, { passive: false });
|
||||
@@ -364,8 +374,8 @@ function initFloatingLyricsEvents() {
|
||||
function startDrag(e) {
|
||||
if (e.target.closest('.wechat-floating-lyrics-close')) return;
|
||||
isDragging = true;
|
||||
var rect = panel.getBoundingClientRect();
|
||||
var phoneRect = document.getElementById('wechat-phone').getBoundingClientRect();
|
||||
let rect = panel.getBoundingClientRect();
|
||||
let phoneRect = document.getElementById('wechat-phone').getBoundingClientRect();
|
||||
initialX = rect.left - phoneRect.left;
|
||||
initialY = rect.top - phoneRect.top;
|
||||
if (e.type === 'touchstart') {
|
||||
@@ -385,7 +395,7 @@ function initFloatingLyricsEvents() {
|
||||
function drag(e) {
|
||||
if (!isDragging) return;
|
||||
e.preventDefault();
|
||||
var clientX, clientY;
|
||||
let clientX, clientY;
|
||||
if (e.type === 'touchmove') {
|
||||
clientX = e.touches[0].clientX;
|
||||
clientY = e.touches[0].clientY;
|
||||
@@ -393,12 +403,12 @@ function initFloatingLyricsEvents() {
|
||||
clientX = e.clientX;
|
||||
clientY = e.clientY;
|
||||
}
|
||||
var dx = clientX - startX;
|
||||
var dy = clientY - startY;
|
||||
var phoneEl = document.getElementById('wechat-phone');
|
||||
var phoneRect = phoneEl.getBoundingClientRect();
|
||||
var newX = Math.max(0, Math.min(phoneRect.width - 280, initialX + dx));
|
||||
var newY = Math.max(0, Math.min(phoneRect.height - 100, initialY + dy));
|
||||
let dx = clientX - startX;
|
||||
let dy = clientY - startY;
|
||||
let phoneEl = document.getElementById('wechat-phone');
|
||||
let phoneRect = phoneEl.getBoundingClientRect();
|
||||
let newX = Math.max(0, Math.min(phoneRect.width - 280, initialX + dx));
|
||||
let newY = Math.max(0, Math.min(phoneRect.height - 100, initialY + dy));
|
||||
panel.style.left = newX + 'px';
|
||||
panel.style.top = newY + 'px';
|
||||
}
|
||||
@@ -497,10 +507,10 @@ function updateLyricsHighlight(currentTime) {
|
||||
function createMiniPlayer() {
|
||||
if (document.getElementById('wechat-music-mini')) return;
|
||||
|
||||
var phoneContainer = document.getElementById('wechat-phone');
|
||||
let phoneContainer = document.getElementById('wechat-phone');
|
||||
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">' +
|
||||
'<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>' +
|
||||
@@ -522,7 +532,7 @@ function createMiniPlayer() {
|
||||
'<div class="wechat-music-mini-controls">' +
|
||||
'<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-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-close">' + CLOSE_ICON + '</button>' +
|
||||
'</div>' +
|
||||
@@ -537,14 +547,14 @@ function initMiniPlayerEvents() {
|
||||
if (miniPlayerInited) return;
|
||||
miniPlayerInited = true;
|
||||
|
||||
var mini = document.getElementById('wechat-music-mini');
|
||||
var btn = mini.querySelector('.wechat-music-mini-btn');
|
||||
var panel = mini.querySelector('.wechat-music-mini-panel');
|
||||
var playBtn = mini.querySelector('.wechat-music-mini-play');
|
||||
var modeBtn = mini.querySelector('.wechat-music-mini-mode');
|
||||
var lyricsBtn = mini.querySelector('.wechat-music-mini-lyrics-btn');
|
||||
var playlistBtn = mini.querySelector('.wechat-music-mini-playlist');
|
||||
var closeBtn = mini.querySelector('.wechat-music-mini-close');
|
||||
let mini = document.getElementById('wechat-music-mini');
|
||||
let btn = mini.querySelector('.wechat-music-mini-btn');
|
||||
let panel = mini.querySelector('.wechat-music-mini-panel');
|
||||
let playBtn = mini.querySelector('.wechat-music-mini-play');
|
||||
let modeBtn = mini.querySelector('.wechat-music-mini-mode');
|
||||
let lyricsBtn = mini.querySelector('.wechat-music-mini-lyrics-btn');
|
||||
let playlistBtn = mini.querySelector('.wechat-music-mini-playlist');
|
||||
let closeBtn = mini.querySelector('.wechat-music-mini-close');
|
||||
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
@@ -583,18 +593,18 @@ function initMiniPlayerEvents() {
|
||||
});
|
||||
|
||||
// 进度条拖动
|
||||
var slider = mini.querySelector('.wechat-music-mini-slider');
|
||||
var currentTimeEl = mini.querySelector('.wechat-music-mini-current');
|
||||
var durationEl = mini.querySelector('.wechat-music-mini-duration');
|
||||
var isSeeking = false;
|
||||
let slider = mini.querySelector('.wechat-music-mini-slider');
|
||||
let currentTimeEl = mini.querySelector('.wechat-music-mini-current');
|
||||
let durationEl = mini.querySelector('.wechat-music-mini-duration');
|
||||
let isSeeking = false;
|
||||
|
||||
if (slider) {
|
||||
slider.addEventListener('input', function(e) {
|
||||
e.stopPropagation();
|
||||
isSeeking = true;
|
||||
var audio = document.getElementById('wechat-music-audio');
|
||||
let audio = document.getElementById('wechat-music-audio');
|
||||
if (audio && audio.duration) {
|
||||
var seekTime = (slider.value / 100) * audio.duration;
|
||||
let seekTime = (slider.value / 100) * audio.duration;
|
||||
if (currentTimeEl) {
|
||||
currentTimeEl.textContent = formatDuration(seekTime);
|
||||
}
|
||||
@@ -603,7 +613,7 @@ function initMiniPlayerEvents() {
|
||||
|
||||
slider.addEventListener('change', function(e) {
|
||||
e.stopPropagation();
|
||||
var audio = document.getElementById('wechat-music-audio');
|
||||
let audio = document.getElementById('wechat-music-audio');
|
||||
if (audio && audio.duration) {
|
||||
audio.currentTime = (slider.value / 100) * audio.duration;
|
||||
}
|
||||
@@ -618,7 +628,7 @@ function initMiniPlayerEvents() {
|
||||
// 监听音频进度更新
|
||||
document.addEventListener('wechat-music-timeupdate', function(e) {
|
||||
if (isSeeking) return;
|
||||
var detail = e.detail || {};
|
||||
let detail = e.detail || {};
|
||||
if (slider && typeof detail.progress === 'number') {
|
||||
slider.value = detail.progress;
|
||||
}
|
||||
@@ -638,8 +648,8 @@ function initMiniPlayerEvents() {
|
||||
});
|
||||
|
||||
// 拖拽(在手机容器内)
|
||||
var isDragging = false;
|
||||
var startX, startY, initialX, initialY;
|
||||
let isDragging = false;
|
||||
let startX, startY, initialX, initialY;
|
||||
|
||||
btn.addEventListener('mousedown', startDrag);
|
||||
btn.addEventListener('touchstart', startDrag, { passive: false });
|
||||
@@ -647,8 +657,8 @@ function initMiniPlayerEvents() {
|
||||
function startDrag(e) {
|
||||
if (e.target.closest('.wechat-music-mini-panel')) return;
|
||||
isDragging = true;
|
||||
var rect = mini.getBoundingClientRect();
|
||||
var phoneRect = document.getElementById('wechat-phone').getBoundingClientRect();
|
||||
let rect = mini.getBoundingClientRect();
|
||||
let phoneRect = document.getElementById('wechat-phone').getBoundingClientRect();
|
||||
initialX = rect.left - phoneRect.left;
|
||||
initialY = rect.top - phoneRect.top;
|
||||
if (e.type === 'touchstart') {
|
||||
@@ -667,7 +677,7 @@ function initMiniPlayerEvents() {
|
||||
function drag(e) {
|
||||
if (!isDragging) return;
|
||||
e.preventDefault();
|
||||
var clientX, clientY;
|
||||
let clientX, clientY;
|
||||
if (e.type === 'touchmove') {
|
||||
clientX = e.touches[0].clientX;
|
||||
clientY = e.touches[0].clientY;
|
||||
@@ -675,12 +685,12 @@ function initMiniPlayerEvents() {
|
||||
clientX = e.clientX;
|
||||
clientY = e.clientY;
|
||||
}
|
||||
var dx = clientX - startX;
|
||||
var dy = clientY - startY;
|
||||
var phoneEl = document.getElementById('wechat-phone');
|
||||
var phoneRect = phoneEl.getBoundingClientRect();
|
||||
var newX = Math.max(0, Math.min(phoneRect.width - 50, initialX + dx));
|
||||
var newY = Math.max(0, Math.min(phoneRect.height - 50, initialY + dy));
|
||||
let dx = clientX - startX;
|
||||
let dy = clientY - startY;
|
||||
let phoneEl = document.getElementById('wechat-phone');
|
||||
let phoneRect = phoneEl.getBoundingClientRect();
|
||||
let newX = Math.max(0, Math.min(phoneRect.width - 50, initialX + dx));
|
||||
let newY = Math.max(0, Math.min(phoneRect.height - 50, initialY + dy));
|
||||
mini.style.left = newX + 'px';
|
||||
mini.style.top = newY + 'px';
|
||||
mini.style.right = 'auto';
|
||||
@@ -714,7 +724,7 @@ function cyclePlayMode() {
|
||||
|
||||
// 更新模式按钮图标
|
||||
function updateModeButtonIcon() {
|
||||
var modeBtn = document.querySelector('.wechat-music-mini-mode');
|
||||
let modeBtn = document.querySelector('.wechat-music-mini-mode');
|
||||
if (!modeBtn) return;
|
||||
|
||||
if (playMode === 'single') {
|
||||
@@ -728,21 +738,225 @@ function updateModeButtonIcon() {
|
||||
|
||||
// 播放下一首
|
||||
function playNext() {
|
||||
if (playlist.length === 0) return;
|
||||
|
||||
var nextIndex;
|
||||
// 单曲循环模式:重新播放当前歌曲
|
||||
if (playMode === 'single') {
|
||||
nextIndex = currentPlayIndex;
|
||||
} else if (playMode === 'random') {
|
||||
nextIndex = Math.floor(Math.random() * playlist.length);
|
||||
} else {
|
||||
nextIndex = (currentPlayIndex + 1) % playlist.length;
|
||||
let audio = document.getElementById('wechat-music-audio');
|
||||
if (audio) {
|
||||
audio.currentTime = 0;
|
||||
audio.play().then(function() {
|
||||
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) {
|
||||
var song = playlist[nextIndex];
|
||||
let song = playlist[nextIndex];
|
||||
currentPlayIndex = nextIndex;
|
||||
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() {
|
||||
if (document.getElementById('wechat-music-playlist-panel')) return;
|
||||
|
||||
var phoneContainer = document.getElementById('wechat-phone');
|
||||
let phoneContainer = document.getElementById('wechat-phone');
|
||||
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">' +
|
||||
'<span class="wechat-playlist-title">播放列表</span>' +
|
||||
'<button class="wechat-playlist-clear">清空</button>' +
|
||||
@@ -767,12 +981,12 @@ function createPlaylistPanel() {
|
||||
}
|
||||
|
||||
function initPlaylistPanelEvents() {
|
||||
var panel = document.getElementById('wechat-music-playlist-panel');
|
||||
let panel = document.getElementById('wechat-music-playlist-panel');
|
||||
if (!panel) return;
|
||||
|
||||
var closeBtn = panel.querySelector('.wechat-playlist-close');
|
||||
var clearBtn = panel.querySelector('.wechat-playlist-clear');
|
||||
var content = panel.querySelector('.wechat-playlist-content');
|
||||
let closeBtn = panel.querySelector('.wechat-playlist-close');
|
||||
let clearBtn = panel.querySelector('.wechat-playlist-clear');
|
||||
let content = panel.querySelector('.wechat-playlist-content');
|
||||
|
||||
closeBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
@@ -788,10 +1002,10 @@ function initPlaylistPanelEvents() {
|
||||
});
|
||||
|
||||
content.addEventListener('click', function(e) {
|
||||
var item = e.target.closest('.wechat-playlist-item');
|
||||
let item = e.target.closest('.wechat-playlist-item');
|
||||
if (!item) return;
|
||||
|
||||
var index = parseInt(item.dataset.index);
|
||||
let index = parseInt(item.dataset.index);
|
||||
if (isNaN(index)) return;
|
||||
|
||||
if (e.target.closest('.wechat-playlist-item-del')) {
|
||||
@@ -806,7 +1020,7 @@ function initPlaylistPanelEvents() {
|
||||
} else {
|
||||
// 播放选中歌曲
|
||||
currentPlayIndex = index;
|
||||
var song = playlist[index];
|
||||
let song = playlist[index];
|
||||
playMusic(song.id, song.platform, song.name, song.artist);
|
||||
renderPlaylist();
|
||||
}
|
||||
@@ -815,7 +1029,7 @@ function initPlaylistPanelEvents() {
|
||||
|
||||
function showPlaylistPanel() {
|
||||
createPlaylistPanel();
|
||||
var panel = document.getElementById('wechat-music-playlist-panel');
|
||||
let panel = document.getElementById('wechat-music-playlist-panel');
|
||||
if (panel) {
|
||||
panel.classList.remove('hidden');
|
||||
renderPlaylist();
|
||||
@@ -823,14 +1037,14 @@ function showPlaylistPanel() {
|
||||
}
|
||||
|
||||
function hidePlaylistPanel() {
|
||||
var panel = document.getElementById('wechat-music-playlist-panel');
|
||||
let panel = document.getElementById('wechat-music-playlist-panel');
|
||||
if (panel) {
|
||||
panel.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function togglePlaylistPanel() {
|
||||
var panel = document.getElementById('wechat-music-playlist-panel');
|
||||
let panel = document.getElementById('wechat-music-playlist-panel');
|
||||
if (panel && !panel.classList.contains('hidden')) {
|
||||
hidePlaylistPanel();
|
||||
} else {
|
||||
@@ -839,7 +1053,7 @@ function togglePlaylistPanel() {
|
||||
}
|
||||
|
||||
function renderPlaylist() {
|
||||
var content = document.querySelector('.wechat-playlist-content');
|
||||
let content = document.querySelector('.wechat-playlist-content');
|
||||
if (!content) return;
|
||||
|
||||
if (playlist.length === 0) {
|
||||
@@ -847,10 +1061,10 @@ function renderPlaylist() {
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '';
|
||||
for (var i = 0; i < playlist.length; i++) {
|
||||
var song = playlist[i];
|
||||
var isActive = i === currentPlayIndex;
|
||||
let html = '';
|
||||
for (let i = 0; i < playlist.length; i++) {
|
||||
let song = playlist[i];
|
||||
let isActive = i === currentPlayIndex;
|
||||
html += '<div class="wechat-playlist-item' + (isActive ? ' active' : '') + '" data-index="' + i + '">' +
|
||||
'<div class="wechat-playlist-item-info">' +
|
||||
'<span class="wechat-playlist-item-name">' + escapeHtml(song.name) + '</span>' +
|
||||
@@ -865,11 +1079,26 @@ function renderPlaylist() {
|
||||
// 添加到播放列表
|
||||
function addToPlaylist(song) {
|
||||
// 检查是否已存在
|
||||
var exists = playlist.some(function(s) {
|
||||
return s.id === song.id && s.platform === song.platform;
|
||||
});
|
||||
if (!exists) {
|
||||
let existIndex = -1;
|
||||
for (let i = 0; i < playlist.length; i++) {
|
||||
if (playlist[i].id === song.id && playlist[i].platform === song.platform) {
|
||||
existIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existIndex >= 0) {
|
||||
// 已存在,移到最后(最新播放)
|
||||
playlist.splice(existIndex, 1);
|
||||
playlist.push(song);
|
||||
} else {
|
||||
// 不存在,添加到最后
|
||||
playlist.push(song);
|
||||
}
|
||||
|
||||
// 限制最多10首,删除最早的
|
||||
while (playlist.length > 10) {
|
||||
playlist.shift();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -883,11 +1112,11 @@ function showMiniPlayer() {
|
||||
}
|
||||
|
||||
function hideMiniPlayer() {
|
||||
var mini = document.getElementById('wechat-music-mini');
|
||||
let mini = document.getElementById('wechat-music-mini');
|
||||
if (mini) {
|
||||
mini.classList.add('hidden');
|
||||
miniPlayerExpanded = false;
|
||||
var panel = mini.querySelector('.wechat-music-mini-panel');
|
||||
let panel = mini.querySelector('.wechat-music-mini-panel');
|
||||
if (panel) panel.classList.add('hidden');
|
||||
}
|
||||
hideSingleLineLyrics();
|
||||
@@ -980,7 +1209,7 @@ export async function playMusic(id, platform, name, artist) {
|
||||
// 添加到播放列表
|
||||
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) {
|
||||
currentPlayIndex = i;
|
||||
break;
|
||||
@@ -988,12 +1217,12 @@ export async function playMusic(id, platform, name, artist) {
|
||||
}
|
||||
|
||||
const player = document.getElementById('wechat-music-player');
|
||||
var audio = document.getElementById('wechat-music-audio');
|
||||
var playBtn = document.getElementById('wechat-music-player-play');
|
||||
let audio = document.getElementById('wechat-music-audio');
|
||||
let playBtn = document.getElementById('wechat-music-player-play');
|
||||
|
||||
// 如果 audio 元素不存在,动态创建一个
|
||||
if (!audio) {
|
||||
var phoneContainer = document.getElementById('wechat-phone');
|
||||
let phoneContainer = document.getElementById('wechat-phone');
|
||||
if (phoneContainer) {
|
||||
audio = document.createElement('audio');
|
||||
audio.id = 'wechat-music-audio';
|
||||
@@ -1002,10 +1231,11 @@ export async function playMusic(id, platform, name, artist) {
|
||||
// 添加事件监听器
|
||||
audio.addEventListener('ended', function() {
|
||||
isPlaying = false;
|
||||
var btn = document.getElementById('wechat-music-player-play');
|
||||
let btn = document.getElementById('wechat-music-player-play');
|
||||
if (btn) btn.innerHTML = PLAY_ICON;
|
||||
updateMiniPlayerState();
|
||||
if (playlist.length > 0) {
|
||||
// 根据播放模式自动播放下一首(单曲循环或有播放列表时)
|
||||
if (playMode === 'single' || playlist.length > 0) {
|
||||
playNext();
|
||||
}
|
||||
});
|
||||
@@ -1014,7 +1244,7 @@ export async function playMusic(id, platform, name, artist) {
|
||||
updateLyricsHighlight(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', {
|
||||
detail: {
|
||||
currentTime: audio.currentTime,
|
||||
@@ -1134,7 +1364,7 @@ export function initMusicEvents() {
|
||||
const searchInput = document.getElementById('wechat-music-search-input');
|
||||
let searchTimeout = null;
|
||||
|
||||
var doSearch = async function(keyword) {
|
||||
let doSearch = async function(keyword) {
|
||||
if (!keyword) return;
|
||||
showLoading();
|
||||
try {
|
||||
@@ -1181,8 +1411,8 @@ export function initMusicEvents() {
|
||||
const playBtn = document.getElementById('wechat-music-player-play');
|
||||
if (playBtn) playBtn.innerHTML = PLAY_ICON;
|
||||
updateMiniPlayerState();
|
||||
// 根据播放模式自动播放下一首
|
||||
if (playlist.length > 0) {
|
||||
// 根据播放模式自动播放下一首(单曲循环或有播放列表时)
|
||||
if (playMode === 'single' || playlist.length > 0) {
|
||||
playNext();
|
||||
}
|
||||
});
|
||||
@@ -1192,7 +1422,7 @@ export function initMusicEvents() {
|
||||
updateLyricsHighlight(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', {
|
||||
detail: {
|
||||
currentTime: audio.currentTime,
|
||||
@@ -1214,7 +1444,7 @@ export async function aiShareMusic(keyword) {
|
||||
if (!keyword || !keyword.trim()) return null;
|
||||
|
||||
try {
|
||||
var results = await searchMusic(keyword);
|
||||
let results = await searchMusic(keyword);
|
||||
if (results && results.length > 0) {
|
||||
// 返回第一个搜索结果
|
||||
return results[0];
|
||||
|
||||
606
phone-html.js
606
phone-html.js
@@ -6,6 +6,7 @@
|
||||
import { getSettings, defaultSettings, MEME_PROMPT_TEMPLATE, MEME_STICKERS } from './config.js';
|
||||
import { getCurrentTime, escapeHtml } from './utils.js';
|
||||
import { getUserAvatarHTML, generateChatList, generateContactsList } from './ui.js';
|
||||
import { ICON_RED_PACKET, ICON_RED_PACKET_LARGE, ICON_USER } from './icons.js';
|
||||
|
||||
// 生成手机界面 HTML
|
||||
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="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="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 class="wechat-func-page" data-page="1">
|
||||
<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="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="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="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="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>
|
||||
</div>
|
||||
@@ -269,6 +270,36 @@ export function generatePhoneHTML() {
|
||||
<button class="wechat-btn wechat-expand-send" id="wechat-expand-send">发送</button>
|
||||
</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-tabs">
|
||||
@@ -303,7 +334,14 @@ export function generatePhoneHTML() {
|
||||
${generateVoiceCallPageHTML()}
|
||||
${generateVideoCallPageHTML()}
|
||||
${generateMusicPanelHTML()}
|
||||
${generateListenTogetherHTML()}
|
||||
${generateMomentsPageHTML()}
|
||||
${generateRedPacketPageHTML(settings)}
|
||||
${generateOpenRedPacketHTML()}
|
||||
${generateRedPacketDetailHTML(settings)}
|
||||
${generateTransferPageHTML()}
|
||||
${generateReceiveTransferPageHTML()}
|
||||
${generateTransferRefundConfirmHTML()}
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的文件输入 -->
|
||||
@@ -385,15 +423,21 @@ function generateDiscoverPageHTML() {
|
||||
<!-- 朋友圈 -->
|
||||
<div class="wechat-discover-group">
|
||||
<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">
|
||||
<circle cx="12" cy="12" r="10" stroke="#f59e0b" stroke-width="0" fill="#f59e0b"/>
|
||||
<circle cx="8" cy="10" r="1.5" fill="white"/>
|
||||
<circle cx="12" cy="7" r="1.5" fill="white"/>
|
||||
<circle cx="16" cy="10" r="1.5" fill="white"/>
|
||||
<circle cx="10" cy="14" r="1.5" fill="white"/>
|
||||
<circle cx="14" cy="14" r="1.5" fill="white"/>
|
||||
<circle cx="12" cy="17" r="1.5" fill="white"/>
|
||||
<circle cx="12" cy="12" r="11" fill="#1a1a1a"/>
|
||||
<path d="M12 3 L14.5 7.5 L12 12 Z" fill="#ff0000"/>
|
||||
<path d="M14.5 7.5 L19.5 6.5 L12 12 Z" fill="#ff8800"/>
|
||||
<path d="M19.5 6.5 L21 12 L12 12 Z" fill="#ffff00"/>
|
||||
<path d="M21 12 L19.5 17.5 L12 12 Z" fill="#00ff00"/>
|
||||
<path d="M19.5 17.5 L14.5 16.5 L12 12 Z" fill="#00ffff"/>
|
||||
<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>
|
||||
</div>
|
||||
<span class="wechat-discover-item-text">朋友圈</span>
|
||||
@@ -404,154 +448,6 @@ function generateDiscoverPageHTML() {
|
||||
</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>
|
||||
|
||||
<!-- 底部标签栏 -->
|
||||
@@ -623,7 +519,7 @@ function generateSettingsPageHTML(settings) {
|
||||
<div class="wechat-settings-item wechat-settings-item-vertical">
|
||||
<span class="wechat-settings-label">模型选择</span>
|
||||
<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('')}
|
||||
</select>
|
||||
<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">
|
||||
<span class="wechat-settings-label">模型选择</span>
|
||||
<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('')}
|
||||
</select>
|
||||
<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-body">
|
||||
<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('')}
|
||||
</select>
|
||||
</div>
|
||||
@@ -873,9 +769,6 @@ function generateServicePageHTML(settings) {
|
||||
<div style="font-size: 10px; color: #666 !important; margin-top: 4px;">每行一个表情包文件名</div>
|
||||
</div>
|
||||
<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输出 <meme>文件名</meme> 时自动渲染为图片
|
||||
</div>
|
||||
</div>
|
||||
<div class="wechat-service-section">
|
||||
<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="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="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 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>
|
||||
</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>
|
||||
`;
|
||||
@@ -1106,18 +1033,12 @@ function generateVideoCallPageHTML() {
|
||||
return `
|
||||
<!-- 视频通话页面 -->
|
||||
<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">
|
||||
<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>
|
||||
</button>
|
||||
<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>
|
||||
</div>
|
||||
<button class="wechat-video-call-switch" id="wechat-video-call-switch" title="切换摄像头">
|
||||
@@ -1125,13 +1046,13 @@ function generateVideoCallPageHTML() {
|
||||
</button>
|
||||
</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-status" id="wechat-video-call-status">等待对方接受邀请</div>
|
||||
</div>
|
||||
|
||||
<!-- 本地视频/头像小窗 -->
|
||||
<!-- 右上角用户头像小窗(长方形) -->
|
||||
<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>
|
||||
@@ -1300,3 +1221,340 @@ export function generateMusicPanelHTML() {
|
||||
</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>
|
||||
`;
|
||||
}
|
||||
|
||||
8
phone.js
8
phone.js
@@ -2,7 +2,7 @@
|
||||
* 手机面板:显示/隐藏、自动居中、拖拽定位
|
||||
*/
|
||||
|
||||
import { saveSettingsDebounced } from '../../../../script.js';
|
||||
import { requestSave } from './save-manager.js';
|
||||
import { getSettings } from './config.js';
|
||||
import { getCurrentTime } from './utils.js';
|
||||
|
||||
@@ -136,7 +136,7 @@ export function setupPhoneDrag() {
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top + rect.height / 2,
|
||||
};
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
};
|
||||
|
||||
statusbar.addEventListener('mousedown', handleStart);
|
||||
@@ -151,7 +151,7 @@ export function setupPhoneDrag() {
|
||||
phoneManuallyPositioned = false;
|
||||
const settings = getSettings();
|
||||
delete settings.phonePosition;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
centerPhoneInViewport({ force: true });
|
||||
});
|
||||
}
|
||||
@@ -203,7 +203,7 @@ export function togglePhone() {
|
||||
|
||||
phone.classList.toggle('hidden');
|
||||
settings.phoneVisible = !phone.classList.contains('hidden');
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
|
||||
if (settings.phoneVisible) {
|
||||
const timeEl = document.querySelector('.wechat-statusbar-time');
|
||||
|
||||
572
red-packet.js
Normal file
572
red-packet.js
Normal 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
140
save-manager.js
Normal 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();
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* 设置页/服务页相关的 UI 逻辑(不包含业务模块)
|
||||
*/
|
||||
|
||||
import { saveSettingsDebounced } from '../../../../script.js';
|
||||
import { requestSave } from './save-manager.js';
|
||||
import { getSettings } from './config.js';
|
||||
|
||||
export function toggleDarkMode() {
|
||||
@@ -14,7 +14,7 @@ export function toggleDarkMode() {
|
||||
settings.darkMode = !settings.darkMode;
|
||||
phone.classList.toggle('wechat-dark', settings.darkMode);
|
||||
toggle.classList.toggle('on', settings.darkMode);
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
}
|
||||
|
||||
export function refreshContextTags() {
|
||||
|
||||
@@ -30,7 +30,7 @@ export function injectAuthorNote() {
|
||||
return;
|
||||
}
|
||||
|
||||
showToast('无法找到作者注释区域', '🧊');
|
||||
showToast('无法找到作者注释区域', 'info');
|
||||
console.log('作者注释模板:', template);
|
||||
} catch (err) {
|
||||
console.error('[可乐] 注入作者注释失败:', err);
|
||||
|
||||
19
summary.js
19
summary.js
@@ -2,7 +2,7 @@
|
||||
* 总结功能
|
||||
*/
|
||||
|
||||
import { saveSettingsDebounced } from '../../../../script.js';
|
||||
import { requestSave } from './save-manager.js';
|
||||
import { getContext } from '../../../extensions.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';
|
||||
@@ -291,12 +291,20 @@ export function insertSummaryMarker(cupNumber, selectedFilter = null) {
|
||||
}
|
||||
});
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
}
|
||||
|
||||
// 生成总结提示词
|
||||
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 => {
|
||||
prompt += `\n--- ${chat.contactName} ---\n`;
|
||||
@@ -479,7 +488,7 @@ export function saveEntryToFavorites(entry, cupNumber, lorebookName) {
|
||||
|
||||
lorebook.entries.push(newEntry);
|
||||
lorebook.lastUpdated = timeStr;
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
|
||||
return lorebook;
|
||||
}
|
||||
@@ -761,7 +770,7 @@ export async function rollbackSummary() {
|
||||
updateProgress('✅ 世界书已清空,已删除...');
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
|
||||
// 3) 尝试同步删除酒馆世界书条目(或整个世界书)
|
||||
try {
|
||||
|
||||
20
toast.js
20
toast.js
@@ -2,7 +2,17 @@
|
||||
* 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');
|
||||
if (!phone) return;
|
||||
|
||||
@@ -14,7 +24,13 @@ export function showToast(message, icon = '✅', durationMs = 2000) {
|
||||
|
||||
const iconEl = document.createElement('span');
|
||||
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');
|
||||
textEl.textContent = message;
|
||||
|
||||
481
transfer.js
Normal file
481
transfer.js
Normal 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();
|
||||
}
|
||||
}
|
||||
33
utils.js
33
utils.js
@@ -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() {
|
||||
const now = new Date();
|
||||
@@ -10,11 +26,25 @@ export function getCurrentTime() {
|
||||
|
||||
// HTML 转义
|
||||
export function escapeHtml(text) {
|
||||
if (text == null) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
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) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
@@ -22,7 +52,8 @@ export function sleep(ms) {
|
||||
|
||||
// 根据内容长度计算语音秒数
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { getSettings, splitAIMessages } from './config.js';
|
||||
import { currentChatIndex } from './chat.js';
|
||||
import { saveSettingsDebounced } from '../../../../script.js';
|
||||
import { requestSave } from './save-manager.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-actions')?.classList.add('hidden');
|
||||
incomingEl.classList.remove('hidden');
|
||||
@@ -126,15 +126,13 @@ function showCallPage() {
|
||||
// 隐藏来电界面
|
||||
document.getElementById('wechat-video-call-incoming')?.classList.add('hidden');
|
||||
|
||||
// 设置头像 - 使用更安全的方式避免 onerror 内联处理器问题
|
||||
// 设置角色头像(中间圆形)
|
||||
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) : '?';
|
||||
|
||||
setAvatarSafe(avatarEl, videoCallState.contactAvatar, firstChar);
|
||||
setAvatarSafe(remoteAvatarEl, videoCallState.contactAvatar, firstChar);
|
||||
|
||||
// 设置本地头像
|
||||
// 设置用户头像(右上角长方形小窗)
|
||||
const localAvatarEl = document.getElementById('wechat-video-call-local-avatar');
|
||||
if (localAvatarEl) {
|
||||
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');
|
||||
if (statusEl) {
|
||||
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');
|
||||
|
||||
// 重置时间显示
|
||||
@@ -213,8 +205,9 @@ function onVideoCallConnected() {
|
||||
clearInterval(videoCallState.dotsInterval);
|
||||
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-actions')?.classList.remove('hidden');
|
||||
|
||||
@@ -344,7 +337,7 @@ export function hangupVideoCall() {
|
||||
// AI 对通话结束做出反应(所有情况都触发)
|
||||
triggerVideoCallEndReaction(contact, callStatus, videoCallState.initiator, videoCallState.messages);
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
}
|
||||
|
||||
@@ -396,8 +389,8 @@ function appendVideoCallRecordMessage(role, status, duration, contact) {
|
||||
if (status === 'connected') {
|
||||
callRecordHTML = `
|
||||
<div class="wechat-call-record wechat-video-call-record">
|
||||
<span class="wechat-call-record-text">视频通话 ${duration}</span>
|
||||
${cameraIconSVG}
|
||||
<span class="wechat-call-record-text">视频通话 ${duration}</span>
|
||||
</div>
|
||||
`;
|
||||
} else if (status === 'cancelled') {
|
||||
@@ -719,9 +712,15 @@ async function triggerVideoCallEndReaction(contact, callStatus, initiator, callM
|
||||
// 已接通的视频通话正常结束
|
||||
if (callMessages && callMessages.length > 0) {
|
||||
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 {
|
||||
reactionPrompt = '[你们刚才视频通话结束了。请对通话结束做出自然的反应,可以表达挂断后的心情或期待下次视频。回复1-2句话即可,简短自然。]';
|
||||
reactionPrompt = '[视频通话刚刚挂断了,现在回到微信文字聊天。请对"挂断"做出简单反应,不要假设通话中发生了什么。回复1句话,符合你的性格。]';
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
@@ -776,7 +775,7 @@ async function triggerVideoCallEndReaction(contact, callStatus, initiator, callM
|
||||
}
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
} catch (err) {
|
||||
console.error('[可乐] AI视频通话结束反应失败:', err);
|
||||
@@ -812,6 +811,8 @@ async function sendVideoCallMessage() {
|
||||
if (!videoCallState.isConnected) break;
|
||||
|
||||
let reply = part.trim();
|
||||
// 过滤掉 <meme> 标签(视频通话只输出纯文字)
|
||||
reply = reply.replace(/<\s*meme\s*>[\s\S]*?<\s*\/\s*meme\s*>/gi, '').trim();
|
||||
reply = reply.replace(/\[.*?\]/g, '').trim();
|
||||
|
||||
if (reply) {
|
||||
@@ -855,12 +856,14 @@ function showVideoCallTypingIndicator() {
|
||||
hideVideoCallTypingIndicator();
|
||||
|
||||
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.innerHTML = `
|
||||
<span class="wechat-typing-dot"></span>
|
||||
<span class="wechat-typing-dot"></span>
|
||||
<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>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesEl.appendChild(typingDiv);
|
||||
@@ -890,13 +893,6 @@ function addVideoCallMessage(role, content) {
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
}
|
||||
|
||||
// HTML转义
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 初始化
|
||||
export function initVideoCall() {
|
||||
// 事件绑定将在显示页面时进行
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
|
||||
import { getSettings, splitAIMessages } from './config.js';
|
||||
import { currentChatIndex } from './chat.js';
|
||||
import { saveSettingsDebounced } from '../../../../script.js';
|
||||
import { requestSave } from './save-manager.js';
|
||||
import { refreshChatList } from './ui.js';
|
||||
import { escapeHtml } from './utils.js';
|
||||
|
||||
// 通话状态
|
||||
let callState = {
|
||||
@@ -318,7 +319,7 @@ export function hangupCall() {
|
||||
// AI 对通话结束做出反应(所有情况都触发)
|
||||
triggerCallEndReaction(contact, callStatus, callState.initiator, callState.messages);
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
}
|
||||
|
||||
@@ -372,16 +373,16 @@ function appendCallRecordMessage(role, status, duration, contact) {
|
||||
// 已接通:显示通话时长
|
||||
callRecordHTML = `
|
||||
<div class="wechat-call-record">
|
||||
<span class="wechat-call-record-text">通话时长 ${duration}</span>
|
||||
${phoneIconSVG}
|
||||
<span class="wechat-call-record-text">通话时长 ${duration}</span>
|
||||
</div>
|
||||
`;
|
||||
} else if (status === 'cancelled') {
|
||||
// 用户发起未接通:已取消(绿色)
|
||||
callRecordHTML = `
|
||||
<div class="wechat-call-record">
|
||||
<span class="wechat-call-record-text">已取消</span>
|
||||
${phoneIconSVG}
|
||||
<span class="wechat-call-record-text">已取消</span>
|
||||
</div>
|
||||
`;
|
||||
} else if (status === 'rejected') {
|
||||
@@ -618,9 +619,15 @@ async function triggerCallEndReaction(contact, callStatus, initiator, callMessag
|
||||
// 根据通话内容生成回复
|
||||
if (callMessages && callMessages.length > 0) {
|
||||
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 {
|
||||
reactionPrompt = '[你们刚才通完电话挂断了。请对通话结束做出自然的反应,可以表达挂断后的心情或期待下次通话。回复1-2句话即可,简短自然。]';
|
||||
reactionPrompt = '[语音通话刚刚挂断了,现在回到微信文字聊天。请对"挂断"做出简单反应,不要假设通话中发生了什么。回复1句话,符合你的性格。]';
|
||||
}
|
||||
} else {
|
||||
return; // 未知状态不处理
|
||||
@@ -678,7 +685,7 @@ async function triggerCallEndReaction(contact, callStatus, initiator, callMessag
|
||||
}
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
requestSave();
|
||||
refreshChatList();
|
||||
} catch (err) {
|
||||
console.error('[可乐] AI通话结束反应失败:', err);
|
||||
@@ -776,12 +783,14 @@ function showCallTypingIndicator() {
|
||||
hideCallTypingIndicator();
|
||||
|
||||
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.innerHTML = `
|
||||
<span class="wechat-typing-dot"></span>
|
||||
<span class="wechat-typing-dot"></span>
|
||||
<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>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesEl.appendChild(typingDiv);
|
||||
@@ -829,13 +838,6 @@ function renderCallMessages() {
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
}
|
||||
|
||||
// HTML转义
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 初始化
|
||||
export function initVoiceCall() {
|
||||
// 事件绑定将在显示页面时进行
|
||||
|
||||
Reference in New Issue
Block a user