mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-11 21:06:56 +08:00
feat: 更新依赖项和配置,添加新通知类型
- 在 package.json 中添加了 @types/md5 和 @vueuse/integrations 依赖。 - 更新了 tsconfig.json 中的模块解析方式为 bundler。 - 在组件声明中移除了不再使用的 Naive UI 组件。 - 在弹幕窗口和设置中添加了启用动画的选项,并更新了相关样式。 - 实现了私信发送失败的通知功能,增强了用户体验。
This commit is contained in:
175
src/client/store/autoAction/expressionEvaluator.ts
Normal file
175
src/client/store/autoAction/expressionEvaluator.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* 表达式求值工具 - 用于在自动操作模板中支持简单的JavaScript表达式
|
||||
*/
|
||||
|
||||
// 表达式模式匹配
|
||||
// {{js: expression}} - 完整的JavaScript表达式
|
||||
const JS_EXPRESSION_REGEX = /\{\{\s*js:\s*(.*?)\s*\}\}/g;
|
||||
|
||||
/**
|
||||
* 处理模板中的表达式
|
||||
* @param template 包含表达式的模板字符串
|
||||
* @param context 上下文对象,包含可在表达式中访问的变量
|
||||
* @returns 处理后的字符串
|
||||
*/
|
||||
export function evaluateTemplateExpressions(template: string, context: Record<string, any>): string {
|
||||
if (!template) return "";
|
||||
|
||||
return template.replace(JS_EXPRESSION_REGEX, (match, expression) => {
|
||||
try {
|
||||
// 创建一个安全的求值函数
|
||||
const evalInContext = new Function(...Object.keys(context), `
|
||||
try {
|
||||
return ${expression};
|
||||
} catch (e) {
|
||||
return "[表达式错误: " + e.message + "]";
|
||||
}
|
||||
`);
|
||||
|
||||
// 执行表达式并返回结果
|
||||
const result = evalInContext(...Object.values(context));
|
||||
return result !== undefined ? String(result) : "";
|
||||
} catch (error) {
|
||||
console.error("表达式求值错误:", error);
|
||||
return `[表达式错误: ${(error as Error).message}]`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查模板中是否包含JavaScript表达式
|
||||
* @param template 要检查的模板字符串
|
||||
* @returns 是否包含表达式
|
||||
*/
|
||||
export function containsJsExpression(template: string): boolean {
|
||||
return JS_EXPRESSION_REGEX.test(template);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义字符串中的特殊字符,使其可以安全地在正则表达式中使用
|
||||
* @param string 要转义的字符串
|
||||
* @returns 转义后的字符串
|
||||
*/
|
||||
export function escapeRegExp(string: string): string {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* 将普通占位符格式转换为JS表达式格式
|
||||
* 例如: {{user.name}} 转换为 {{js: user.name}}
|
||||
* @param template 包含普通占位符的模板
|
||||
* @param placeholders 占位符列表
|
||||
* @returns 转换后的模板
|
||||
*/
|
||||
export function convertToJsExpressions(template: string, placeholders: {name: string, description: string}[]): string {
|
||||
let result = template;
|
||||
|
||||
placeholders.forEach(p => {
|
||||
const placeholder = p.name;
|
||||
const path = placeholder.replace(/\{\{|\}\}/g, '').trim();
|
||||
const regex = new RegExp(escapeRegExp(placeholder), 'g');
|
||||
result = result.replace(regex, `{{js: ${path}}}`);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为礼物感谢模块创建上下文对象
|
||||
* @param user 用户信息
|
||||
* @param gift 礼物信息
|
||||
* @returns 上下文对象
|
||||
*/
|
||||
export function createGiftThankContext(user: { uid: number; name: string },
|
||||
gift: { name: string; count: number; price: number }): Record<string, any> {
|
||||
return {
|
||||
user: {
|
||||
uid: user.uid,
|
||||
name: user.name,
|
||||
// 额外方法和属性
|
||||
nameLength: user.name.length,
|
||||
},
|
||||
gift: {
|
||||
name: gift.name,
|
||||
count: gift.count,
|
||||
price: gift.price,
|
||||
totalPrice: gift.count * gift.price,
|
||||
// 工具方法
|
||||
summary: `${gift.count}个${gift.name}`,
|
||||
isExpensive: gift.price >= 50
|
||||
},
|
||||
// 工具函数
|
||||
format: {
|
||||
currency: (value: number) => `¥${value.toFixed(2)}`,
|
||||
pluralize: (count: number, singular: string, plural: string) => count === 1 ? singular : plural,
|
||||
},
|
||||
// 日期时间
|
||||
date: {
|
||||
now: new Date(),
|
||||
timestamp: Date.now(),
|
||||
formatted: new Intl.DateTimeFormat('zh-CN').format(new Date())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 为入场欢迎模块创建上下文对象
|
||||
* @param user 用户信息
|
||||
* @returns 上下文对象
|
||||
*/
|
||||
export function createEntryWelcomeContext(user: { uid: number; name: string; medal?: { level: number; name: string } }): Record<string, any> {
|
||||
return {
|
||||
user: {
|
||||
uid: user.uid,
|
||||
name: user.name,
|
||||
nameLength: user.name.length,
|
||||
medal: user.medal || { level: 0, name: '' },
|
||||
hasMedal: !!user.medal
|
||||
},
|
||||
date: {
|
||||
now: new Date(),
|
||||
timestamp: Date.now(),
|
||||
formatted: new Intl.DateTimeFormat('zh-CN').format(new Date()),
|
||||
hour: new Date().getHours()
|
||||
},
|
||||
// 时间相关的便捷函数
|
||||
timeOfDay: () => {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 6) return '凌晨';
|
||||
if (hour < 12) return '上午';
|
||||
if (hour < 14) return '中午';
|
||||
if (hour < 18) return '下午';
|
||||
return '晚上';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 为自动回复模块创建上下文对象
|
||||
* @param user 用户信息
|
||||
* @param message 消息内容
|
||||
* @returns 上下文对象
|
||||
*/
|
||||
export function createAutoReplyContext(user: { uid: number; name: string; medal?: { level: number; name: string } },
|
||||
message: string): Record<string, any> {
|
||||
return {
|
||||
user: {
|
||||
uid: user.uid,
|
||||
name: user.name,
|
||||
nameLength: user.name.length,
|
||||
medal: user.medal || { level: 0, name: '' },
|
||||
hasMedal: !!user.medal
|
||||
},
|
||||
message: {
|
||||
content: message,
|
||||
length: message.length,
|
||||
containsQuestion: message.includes('?') || message.includes('?'),
|
||||
words: message.split(/\s+/).filter(Boolean)
|
||||
},
|
||||
date: {
|
||||
now: new Date(),
|
||||
timestamp: Date.now(),
|
||||
formatted: new Intl.DateTimeFormat('zh-CN').format(new Date())
|
||||
}
|
||||
};
|
||||
}
|
||||
150
src/client/store/autoAction/modules/autoReply.ts
Normal file
150
src/client/store/autoAction/modules/autoReply.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { ref, Ref } from 'vue';
|
||||
import { EventModel } from '@/api/api-models';
|
||||
import {
|
||||
AutoActionItem,
|
||||
TriggerType,
|
||||
ExecutionContext,
|
||||
RuntimeState
|
||||
} from '../types';
|
||||
import {
|
||||
formatTemplate,
|
||||
getRandomTemplate,
|
||||
shouldProcess,
|
||||
evaluateExpression
|
||||
} from '../utils';
|
||||
|
||||
/**
|
||||
* 自动回复模块
|
||||
* @param isLive 是否处于直播状态
|
||||
* @param roomId 房间ID
|
||||
* @param sendLiveDanmaku 发送弹幕函数
|
||||
*/
|
||||
export function useAutoReply(
|
||||
isLive: Ref<boolean>,
|
||||
roomId: Ref<number | undefined>,
|
||||
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>
|
||||
) {
|
||||
// 运行时数据 - 记录特定关键词的最后回复时间
|
||||
const lastReplyTimestamps = ref<{ [keyword: string]: number; }>({});
|
||||
|
||||
/**
|
||||
* 处理弹幕事件
|
||||
* @param event 弹幕事件
|
||||
* @param actions 自动操作列表
|
||||
* @param runtimeState 运行时状态
|
||||
*/
|
||||
function onDanmaku(
|
||||
event: EventModel,
|
||||
actions: AutoActionItem[],
|
||||
runtimeState: RuntimeState
|
||||
) {
|
||||
if (!roomId.value) return;
|
||||
|
||||
// 过滤出有效的自动回复操作
|
||||
const replyActions = actions.filter(action =>
|
||||
action.triggerType === TriggerType.DANMAKU &&
|
||||
action.enabled &&
|
||||
(!action.triggerConfig.onlyDuringLive || isLive.value)
|
||||
);
|
||||
|
||||
if (replyActions.length === 0) return;
|
||||
|
||||
const message = event.msg;
|
||||
const now = Date.now();
|
||||
|
||||
// 准备执行上下文
|
||||
const context: ExecutionContext = {
|
||||
event,
|
||||
roomId: roomId.value,
|
||||
variables: {
|
||||
user: {
|
||||
name: event.uname,
|
||||
uid: event.uid,
|
||||
guardLevel: event.guard_level,
|
||||
hasMedal: event.fans_medal_wearing_status,
|
||||
medalLevel: event.fans_medal_level,
|
||||
medalName: event.fans_medal_name
|
||||
},
|
||||
message: event.msg,
|
||||
timeOfDay: () => {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 6) return '凌晨';
|
||||
if (hour < 9) return '早上';
|
||||
if (hour < 12) return '上午';
|
||||
if (hour < 14) return '中午';
|
||||
if (hour < 18) return '下午';
|
||||
if (hour < 22) return '晚上';
|
||||
return '深夜';
|
||||
},
|
||||
date: {
|
||||
formatted: new Date().toLocaleString('zh-CN')
|
||||
}
|
||||
},
|
||||
timestamp: now
|
||||
};
|
||||
|
||||
// 检查每个操作
|
||||
for (const action of replyActions) {
|
||||
// 检查用户过滤条件
|
||||
if (action.triggerConfig.userFilterEnabled) {
|
||||
if (action.triggerConfig.requireMedal && !event.fans_medal_wearing_status) continue;
|
||||
if (action.triggerConfig.requireCaptain && !event.guard_level) continue;
|
||||
}
|
||||
|
||||
// 关键词和屏蔽词检查
|
||||
const keywordMatch = action.triggerConfig.keywords?.some(kw => message.includes(kw));
|
||||
if (!keywordMatch) continue;
|
||||
|
||||
const blockwordMatch = action.triggerConfig.blockwords?.some(bw => message.includes(bw));
|
||||
if (blockwordMatch) continue; // 包含屏蔽词,不回复
|
||||
|
||||
// 评估逻辑表达式
|
||||
if (action.logicalExpression && !evaluateExpression(action.logicalExpression, context)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查冷却
|
||||
const lastExecTime = runtimeState.lastExecutionTime[action.id] || 0;
|
||||
if (!action.ignoreCooldown && now - lastExecTime < (action.actionConfig.cooldownSeconds || 0) * 1000) {
|
||||
continue; // 仍在冷却中
|
||||
}
|
||||
|
||||
// 选择回复并发送
|
||||
const template = getRandomTemplate(action.templates);
|
||||
if (template) {
|
||||
// 格式化并发送
|
||||
const formattedReply = formatTemplate(template, context);
|
||||
|
||||
// 更新冷却时间
|
||||
runtimeState.lastExecutionTime[action.id] = now;
|
||||
|
||||
// 执行延迟处理
|
||||
if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
|
||||
setTimeout(() => {
|
||||
sendLiveDanmaku(roomId.value!, formattedReply);
|
||||
}, action.actionConfig.delaySeconds * 1000);
|
||||
} else {
|
||||
sendLiveDanmaku(roomId.value!, formattedReply);
|
||||
}
|
||||
|
||||
break; // 匹配到一个规则就停止
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重置冷却时间 (用于测试)
|
||||
function resetCooldowns(runtimeState: RuntimeState, actionId?: string) {
|
||||
if (actionId) {
|
||||
delete runtimeState.lastExecutionTime[actionId];
|
||||
} else {
|
||||
Object.keys(runtimeState.lastExecutionTime).forEach(id => {
|
||||
delete runtimeState.lastExecutionTime[id];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
onDanmaku,
|
||||
resetCooldowns
|
||||
};
|
||||
}
|
||||
110
src/client/store/autoAction/modules/entryWelcome.ts
Normal file
110
src/client/store/autoAction/modules/entryWelcome.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { ref, Ref } from 'vue';
|
||||
import { EventModel, EventDataTypes } from '@/api/api-models';
|
||||
import {
|
||||
formatTemplate,
|
||||
getRandomTemplate,
|
||||
buildExecutionContext
|
||||
} from '../utils';
|
||||
import {
|
||||
AutoActionItem,
|
||||
TriggerType,
|
||||
RuntimeState
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* 入场欢迎模块
|
||||
* @param isLive 是否处于直播状态
|
||||
* @param roomId 房间ID
|
||||
* @param isTianXuanActive 是否处于天选时刻
|
||||
* @param sendLiveDanmaku 发送弹幕函数
|
||||
*/
|
||||
export function useEntryWelcome(
|
||||
isLive: Ref<boolean>,
|
||||
roomId: Ref<number | undefined>,
|
||||
isTianXuanActive: Ref<boolean>,
|
||||
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>
|
||||
) {
|
||||
// 运行时数据
|
||||
const timer = ref<NodeJS.Timeout | null>(null);
|
||||
|
||||
/**
|
||||
* 处理入场事件 - 支持新的AutoActionItem结构
|
||||
* @param event 入场事件
|
||||
* @param actions 自动操作列表
|
||||
* @param runtimeState 运行时状态
|
||||
*/
|
||||
function processEnter(
|
||||
event: EventModel,
|
||||
actions: AutoActionItem[],
|
||||
runtimeState: RuntimeState
|
||||
) {
|
||||
if (!roomId.value) return;
|
||||
|
||||
// 过滤出有效的入场欢迎操作
|
||||
const enterActions = actions.filter(action =>
|
||||
action.triggerType === TriggerType.ENTER &&
|
||||
action.enabled &&
|
||||
(!action.triggerConfig.onlyDuringLive || isLive.value) &&
|
||||
(!action.triggerConfig.ignoreTianXuan || !isTianXuanActive.value)
|
||||
);
|
||||
|
||||
if (enterActions.length === 0) return;
|
||||
|
||||
// 创建执行上下文
|
||||
const context = buildExecutionContext(event, roomId.value, TriggerType.ENTER);
|
||||
|
||||
// 处理每个符合条件的操作
|
||||
for (const action of enterActions) {
|
||||
// 跳过不符合用户过滤条件的
|
||||
if (action.triggerConfig.userFilterEnabled) {
|
||||
if (action.triggerConfig.requireMedal && !event.fans_medal_wearing_status) continue;
|
||||
if (action.triggerConfig.requireCaptain && !event.guard_level) continue;
|
||||
}
|
||||
|
||||
// 检查入场过滤条件 (可以在未来扩展更多条件)
|
||||
if (action.triggerConfig.filterMode === 'blacklist' &&
|
||||
action.triggerConfig.filterGiftNames?.includes(event.uname)) continue;
|
||||
|
||||
// 检查冷却时间
|
||||
const lastExecTime = runtimeState.lastExecutionTime[action.id] || 0;
|
||||
if (!action.ignoreCooldown &&
|
||||
Date.now() - lastExecTime < (action.actionConfig.cooldownSeconds || 0) * 1000) {
|
||||
continue; // 仍在冷却中
|
||||
}
|
||||
|
||||
// 选择并发送回复
|
||||
const template = getRandomTemplate(action.templates);
|
||||
if (template) {
|
||||
// 更新冷却时间
|
||||
runtimeState.lastExecutionTime[action.id] = Date.now();
|
||||
|
||||
// 格式化并发送
|
||||
const formattedReply = formatTemplate(template, context);
|
||||
|
||||
// 延迟发送
|
||||
if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
|
||||
setTimeout(() => {
|
||||
sendLiveDanmaku(roomId.value!, formattedReply);
|
||||
}, action.actionConfig.delaySeconds * 1000);
|
||||
} else {
|
||||
sendLiveDanmaku(roomId.value!, formattedReply);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理计时器
|
||||
*/
|
||||
function clearTimer() {
|
||||
if (timer.value) {
|
||||
clearTimeout(timer.value);
|
||||
timer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
processEnter,
|
||||
clearTimer
|
||||
};
|
||||
}
|
||||
116
src/client/store/autoAction/modules/followThank.ts
Normal file
116
src/client/store/autoAction/modules/followThank.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { ref, Ref } from 'vue';
|
||||
import { EventModel, EventDataTypes } from '@/api/api-models';
|
||||
import {
|
||||
formatTemplate,
|
||||
getRandomTemplate,
|
||||
buildExecutionContext
|
||||
} from '../utils';
|
||||
import {
|
||||
AutoActionItem,
|
||||
TriggerType,
|
||||
RuntimeState
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* 关注感谢模块
|
||||
* @param isLive 是否处于直播状态
|
||||
* @param roomId 房间ID
|
||||
* @param isTianXuanActive 是否处于天选时刻
|
||||
* @param sendLiveDanmaku 发送弹幕函数
|
||||
*/
|
||||
export function useFollowThank(
|
||||
isLive: Ref<boolean>,
|
||||
roomId: Ref<number | undefined>,
|
||||
isTianXuanActive: Ref<boolean>,
|
||||
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>
|
||||
) {
|
||||
// 运行时数据
|
||||
const aggregatedFollows = ref<{uid: number, name: string, timestamp: number}[]>([]);
|
||||
const timer = ref<NodeJS.Timeout | null>(null);
|
||||
|
||||
/**
|
||||
* 处理关注事件 - 支持新的AutoActionItem结构
|
||||
* @param event 关注事件
|
||||
* @param actions 自动操作列表
|
||||
* @param runtimeState 运行时状态
|
||||
*/
|
||||
function processFollow(
|
||||
event: EventModel,
|
||||
actions: AutoActionItem[],
|
||||
runtimeState: RuntimeState
|
||||
) {
|
||||
if (!roomId.value) return;
|
||||
|
||||
// 过滤出有效的关注感谢操作
|
||||
const followActions = actions.filter(action =>
|
||||
action.triggerType === TriggerType.FOLLOW &&
|
||||
action.enabled &&
|
||||
(!action.triggerConfig.onlyDuringLive || isLive.value) &&
|
||||
(!action.triggerConfig.ignoreTianXuan || !isTianXuanActive.value)
|
||||
);
|
||||
|
||||
if (followActions.length === 0) return;
|
||||
|
||||
// 创建执行上下文
|
||||
const context = buildExecutionContext(event, roomId.value, TriggerType.FOLLOW);
|
||||
|
||||
// 处理每个符合条件的操作
|
||||
for (const action of followActions) {
|
||||
// 跳过不符合用户过滤条件的
|
||||
if (action.triggerConfig.userFilterEnabled) {
|
||||
if (action.triggerConfig.requireMedal && !event.fans_medal_wearing_status) continue;
|
||||
if (action.triggerConfig.requireCaptain && !event.guard_level) continue;
|
||||
}
|
||||
|
||||
// 检查冷却时间
|
||||
const lastExecTime = runtimeState.lastExecutionTime[action.id] || 0;
|
||||
if (!action.ignoreCooldown &&
|
||||
Date.now() - lastExecTime < (action.actionConfig.cooldownSeconds || 0) * 1000) {
|
||||
continue; // 仍在冷却中
|
||||
}
|
||||
|
||||
// 选择并发送回复
|
||||
const template = getRandomTemplate(action.templates);
|
||||
if (template) {
|
||||
// 更新冷却时间
|
||||
runtimeState.lastExecutionTime[action.id] = Date.now();
|
||||
|
||||
// 格式化并发送
|
||||
const formattedReply = formatTemplate(template, context);
|
||||
|
||||
// 延迟发送
|
||||
if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
|
||||
setTimeout(() => {
|
||||
sendLiveDanmaku(roomId.value!, formattedReply);
|
||||
}, action.actionConfig.delaySeconds * 1000);
|
||||
} else {
|
||||
sendLiveDanmaku(roomId.value!, formattedReply);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理关注事件 - 旧方式实现,用于兼容现有代码
|
||||
*/
|
||||
function onFollow(event: EventModel) {
|
||||
// 将在useAutoAction.ts中进行迁移,此方法保留但不实现具体逻辑
|
||||
console.log('关注事件处理已迁移到新的AutoActionItem结构');
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理计时器
|
||||
*/
|
||||
function clearTimer() {
|
||||
if (timer.value) {
|
||||
clearTimeout(timer.value);
|
||||
timer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
onFollow,
|
||||
processFollow,
|
||||
clearTimer
|
||||
};
|
||||
}
|
||||
213
src/client/store/autoAction/modules/giftThank.ts
Normal file
213
src/client/store/autoAction/modules/giftThank.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { ref, Ref } from 'vue';
|
||||
import { EventModel, EventDataTypes } from '@/api/api-models';
|
||||
import {
|
||||
formatTemplate,
|
||||
getRandomTemplate,
|
||||
buildExecutionContext
|
||||
} from '../utils';
|
||||
import {
|
||||
AutoActionItem,
|
||||
TriggerType,
|
||||
ExecutionContext,
|
||||
RuntimeState
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* 礼物感谢模块
|
||||
* @param isLive 是否处于直播状态
|
||||
* @param roomId 房间ID
|
||||
* @param isTianXuanActive 是否处于天选时刻
|
||||
* @param sendLiveDanmaku 发送弹幕函数
|
||||
*/
|
||||
export function useGiftThank(
|
||||
isLive: Ref<boolean>,
|
||||
roomId: Ref<number | undefined>,
|
||||
isTianXuanActive: Ref<boolean>,
|
||||
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>
|
||||
) {
|
||||
// 测试发送功能状态
|
||||
const lastTestTime = ref(0);
|
||||
const testCooldown = 5000; // 5秒冷却时间
|
||||
const testLoading = ref(false);
|
||||
|
||||
/**
|
||||
* 处理礼物事件
|
||||
* @param event 礼物事件
|
||||
* @param actions 自动操作列表
|
||||
* @param runtimeState 运行时状态
|
||||
*/
|
||||
function processGift(
|
||||
event: EventModel,
|
||||
actions: AutoActionItem[],
|
||||
runtimeState: RuntimeState
|
||||
) {
|
||||
if (!roomId.value) return;
|
||||
|
||||
// 过滤出有效的礼物感谢操作
|
||||
const giftActions = actions.filter(action =>
|
||||
action.triggerType === TriggerType.GIFT &&
|
||||
action.enabled &&
|
||||
(!action.triggerConfig.onlyDuringLive || isLive.value) &&
|
||||
(!action.triggerConfig.ignoreTianXuan || !isTianXuanActive.value)
|
||||
);
|
||||
|
||||
if (giftActions.length === 0) return;
|
||||
|
||||
// 礼物基本信息
|
||||
const giftName = event.msg;
|
||||
const giftPrice = event.price / 1000;
|
||||
const giftCount = event.num;
|
||||
|
||||
// 创建执行上下文
|
||||
const context = buildExecutionContext(event, roomId.value, TriggerType.GIFT);
|
||||
|
||||
// 处理每个符合条件的操作
|
||||
for (const action of giftActions) {
|
||||
// 跳过不符合用户过滤条件的
|
||||
if (action.triggerConfig.userFilterEnabled) {
|
||||
if (action.triggerConfig.requireMedal && !event.fans_medal_wearing_status) continue;
|
||||
if (action.triggerConfig.requireCaptain && !event.guard_level) continue;
|
||||
}
|
||||
|
||||
// 礼物过滤逻辑
|
||||
if (action.triggerConfig.filterMode === 'blacklist' &&
|
||||
action.triggerConfig.filterGiftNames?.includes(giftName)) continue;
|
||||
|
||||
if (action.triggerConfig.filterMode === 'whitelist' &&
|
||||
!action.triggerConfig.filterGiftNames?.includes(giftName)) continue;
|
||||
|
||||
if (action.triggerConfig.minValue && giftPrice < action.triggerConfig.minValue) continue;
|
||||
|
||||
// 检查冷却时间
|
||||
const lastExecTime = runtimeState.lastExecutionTime[action.id] || 0;
|
||||
if (!action.ignoreCooldown &&
|
||||
Date.now() - lastExecTime < (action.actionConfig.cooldownSeconds || 0) * 1000) {
|
||||
continue; // 仍在冷却中
|
||||
}
|
||||
|
||||
// 选择并发送回复
|
||||
const template = getRandomTemplate(action.templates);
|
||||
if (template) {
|
||||
// 更新冷却时间
|
||||
runtimeState.lastExecutionTime[action.id] = Date.now();
|
||||
|
||||
// 格式化并发送
|
||||
const formattedReply = formatTemplate(template, context);
|
||||
|
||||
// 延迟发送
|
||||
if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
|
||||
setTimeout(() => {
|
||||
sendLiveDanmaku(roomId.value!, formattedReply);
|
||||
}, (action.actionConfig.delaySeconds || 0) * 1000);
|
||||
} else {
|
||||
sendLiveDanmaku(roomId.value!, formattedReply);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试发送礼物感谢弹幕
|
||||
*/
|
||||
async function testSendThankMessage(
|
||||
action?: AutoActionItem
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
// 检查是否在冷却期
|
||||
const now = Date.now();
|
||||
if (now - lastTestTime.value < testCooldown) {
|
||||
return {
|
||||
success: false,
|
||||
message: `请等待${Math.ceil((testCooldown - (now - lastTestTime.value)) / 1000)}秒后再次测试发送`
|
||||
};
|
||||
}
|
||||
|
||||
if (!roomId.value) {
|
||||
return {
|
||||
success: false,
|
||||
message: '未设置房间号'
|
||||
};
|
||||
}
|
||||
|
||||
if (!action) {
|
||||
return {
|
||||
success: false,
|
||||
message: '未指定要测试的操作'
|
||||
};
|
||||
}
|
||||
|
||||
if (!action.templates || action.templates.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: '请至少添加一条模板'
|
||||
};
|
||||
}
|
||||
|
||||
testLoading.value = true;
|
||||
lastTestTime.value = now;
|
||||
|
||||
try {
|
||||
// 构建测试事件对象
|
||||
const testEvent: EventModel = {
|
||||
type: EventDataTypes.Gift,
|
||||
uname: '测试用户',
|
||||
uface: 'https://i0.hdslb.com/bfs/face/member/noface.jpg',
|
||||
uid: 123456,
|
||||
open_id: '123456',
|
||||
msg: '测试礼物',
|
||||
time: Date.now(),
|
||||
num: 1,
|
||||
price: 100000, // 100元
|
||||
guard_level: 0,
|
||||
fans_medal_level: 0,
|
||||
fans_medal_name: '',
|
||||
fans_medal_wearing_status: false,
|
||||
ouid: '123456'
|
||||
};
|
||||
|
||||
// 创建测试上下文
|
||||
const context = buildExecutionContext(testEvent, roomId.value, TriggerType.GIFT);
|
||||
|
||||
// 获取模板并格式化
|
||||
const template = getRandomTemplate(action.templates);
|
||||
if (!template) {
|
||||
return {
|
||||
success: false,
|
||||
message: '无法获取模板'
|
||||
};
|
||||
}
|
||||
|
||||
const testMessage = formatTemplate(template, context);
|
||||
|
||||
// 发送测试弹幕
|
||||
const success = await sendLiveDanmaku(roomId.value, testMessage);
|
||||
|
||||
if (success) {
|
||||
return {
|
||||
success: true,
|
||||
message: '测试弹幕发送成功!'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: '测试弹幕发送失败,请检查B站登录状态和网络连接'
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('测试发送出错:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: '发送过程出错'
|
||||
};
|
||||
} finally {
|
||||
testLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
processGift,
|
||||
testSendThankMessage,
|
||||
testLoading,
|
||||
lastTestTime,
|
||||
testCooldown
|
||||
};
|
||||
}
|
||||
171
src/client/store/autoAction/modules/guardPm.ts
Normal file
171
src/client/store/autoAction/modules/guardPm.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { Ref } from 'vue';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { GuardLevel, EventModel } from '@/api/api-models';
|
||||
import {
|
||||
AutoActionItem,
|
||||
TriggerType,
|
||||
ActionType,
|
||||
RuntimeState
|
||||
} from '../types';
|
||||
import { formatTemplate, buildExecutionContext } from '../utils';
|
||||
|
||||
/**
|
||||
* 舰长私信模块
|
||||
* @param isLive 是否处于直播状态
|
||||
* @param roomId 房间ID
|
||||
* @param sendPrivateMessage 发送私信函数
|
||||
* @param sendLiveDanmaku 发送弹幕函数
|
||||
*/
|
||||
export function useGuardPm(
|
||||
isLive: Ref<boolean>,
|
||||
roomId: Ref<number | undefined>,
|
||||
sendPrivateMessage: (userId: number, message: string) => Promise<boolean>,
|
||||
sendLiveDanmaku?: (roomId: number, message: string) => Promise<boolean>
|
||||
) {
|
||||
// 保留旧配置用于兼容
|
||||
const config = useStorage<{
|
||||
enabled: boolean;
|
||||
template: string;
|
||||
sendDanmakuConfirm: boolean;
|
||||
danmakuTemplate: string;
|
||||
preventRepeat: boolean;
|
||||
giftCodeMode: boolean;
|
||||
giftCodes: { level: number; codes: string[] }[];
|
||||
onlyDuringLive: boolean;
|
||||
}>(
|
||||
'autoAction.guardPmConfig',
|
||||
{
|
||||
enabled: false,
|
||||
template: '感谢 {{user.name}} 成为 {{guard.levelName}}!欢迎加入!',
|
||||
sendDanmakuConfirm: false,
|
||||
danmakuTemplate: '已私信 {{user.name}} 舰长福利!',
|
||||
preventRepeat: true,
|
||||
giftCodeMode: false,
|
||||
giftCodes: [],
|
||||
onlyDuringLive: true
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 处理舰长事件 - 支持新的AutoActionItem结构
|
||||
* @param event 舰长事件
|
||||
* @param actions 自动操作列表
|
||||
* @param runtimeState 运行时状态
|
||||
*/
|
||||
function processGuard(
|
||||
event: EventModel,
|
||||
actions: AutoActionItem[],
|
||||
runtimeState: RuntimeState
|
||||
) {
|
||||
if (!roomId.value) return;
|
||||
|
||||
const guardLevel = event.guard_level;
|
||||
if (guardLevel === GuardLevel.None) return; // 不是上舰事件
|
||||
|
||||
// 过滤出有效的舰长私信操作
|
||||
const guardActions = actions.filter(action =>
|
||||
action.triggerType === TriggerType.GUARD &&
|
||||
action.enabled &&
|
||||
action.actionType === ActionType.SEND_PRIVATE_MSG &&
|
||||
(!action.triggerConfig.onlyDuringLive || isLive.value)
|
||||
);
|
||||
|
||||
if (guardActions.length === 0) return;
|
||||
|
||||
// 创建执行上下文
|
||||
const context = buildExecutionContext(event, roomId.value, TriggerType.GUARD);
|
||||
|
||||
// 处理礼品码
|
||||
for (const action of guardActions) {
|
||||
// 防止重复发送
|
||||
if (action.triggerConfig.preventRepeat) {
|
||||
if (runtimeState.sentGuardPms.has(event.uid)) {
|
||||
console.log(`用户 ${event.uname} (${event.uid}) 已发送过上舰私信,跳过。`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 特定舰长等级过滤
|
||||
if (action.triggerConfig.guardLevels && !action.triggerConfig.guardLevels.includes(guardLevel)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取礼品码
|
||||
let giftCode = '';
|
||||
if (action.triggerConfig.giftCodes && action.triggerConfig.giftCodes.length > 0) {
|
||||
// 查找匹配等级的礼品码
|
||||
const levelCodes = action.triggerConfig.giftCodes.find(gc => gc.level === guardLevel);
|
||||
if (levelCodes && levelCodes.codes.length > 0) {
|
||||
giftCode = levelCodes.codes.shift() || '';
|
||||
} else {
|
||||
// 查找通用码 (level 0)
|
||||
const commonCodes = action.triggerConfig.giftCodes.find(gc => gc.level === GuardLevel.None);
|
||||
if (commonCodes && commonCodes.codes.length > 0) {
|
||||
giftCode = commonCodes.codes.shift() || '';
|
||||
} else {
|
||||
console.warn(`等级 ${guardLevel} 或通用礼品码已用完,无法发送给 ${event.uname}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新上下文中的礼品码
|
||||
if (context.variables.guard) {
|
||||
context.variables.guard.giftCode = giftCode;
|
||||
}
|
||||
|
||||
// 选择模板并格式化
|
||||
if (action.templates.length > 0) {
|
||||
const template = action.templates[0]; // 对于私信,使用第一个模板
|
||||
const formattedMessage = formatTemplate(template, context);
|
||||
|
||||
// 发送私信
|
||||
sendPrivateMessage(event.uid, formattedMessage).then(success => {
|
||||
if (success) {
|
||||
console.log(`成功发送上舰私信给 ${event.uname} (${event.uid})`);
|
||||
if (action.triggerConfig.preventRepeat) {
|
||||
runtimeState.sentGuardPms.add(event.uid);
|
||||
}
|
||||
|
||||
// 发送弹幕确认
|
||||
if (roomId.value && sendLiveDanmaku) {
|
||||
// 查找确认弹幕的设置
|
||||
const confirmActions = actions.filter(a =>
|
||||
a.triggerType === TriggerType.GUARD &&
|
||||
a.enabled &&
|
||||
a.actionType === ActionType.SEND_DANMAKU
|
||||
);
|
||||
|
||||
if (confirmActions.length > 0 && confirmActions[0].templates.length > 0) {
|
||||
const confirmMsg = formatTemplate(confirmActions[0].templates[0], context);
|
||||
sendLiveDanmaku(roomId.value, confirmMsg);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(`发送上舰私信给 ${event.uname} (${event.uid}) 失败`);
|
||||
// 失败时归还礼品码
|
||||
if (giftCode && action.triggerConfig.giftCodes) {
|
||||
const levelCodes = action.triggerConfig.giftCodes.find(gc => gc.level === guardLevel);
|
||||
if (levelCodes) {
|
||||
levelCodes.codes.push(giftCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理上舰事件 - 旧方式实现,用于兼容现有代码
|
||||
*/
|
||||
function onGuard(event: EventModel) {
|
||||
// 将在useAutoAction.ts中进行迁移,此方法保留但不实现具体逻辑
|
||||
console.log('舰长事件处理已迁移到新的AutoActionItem结构');
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
onGuard,
|
||||
processGuard
|
||||
};
|
||||
}
|
||||
121
src/client/store/autoAction/modules/scheduledDanmaku.ts
Normal file
121
src/client/store/autoAction/modules/scheduledDanmaku.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { ref, watch, Ref, computed } from 'vue';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import {
|
||||
getRandomTemplate,
|
||||
formatTemplate,
|
||||
buildExecutionContext
|
||||
} from '../utils';
|
||||
import {
|
||||
AutoActionItem,
|
||||
TriggerType,
|
||||
RuntimeState
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* 定时弹幕模块
|
||||
* @param isLive 是否处于直播状态
|
||||
* @param roomId 房间ID
|
||||
* @param sendLiveDanmaku 发送弹幕函数
|
||||
*/
|
||||
export function useScheduledDanmaku(
|
||||
isLive: Ref<boolean>,
|
||||
roomId: Ref<number | undefined>,
|
||||
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>
|
||||
) {
|
||||
// 运行时数据
|
||||
const timer = ref<any | null>(null);
|
||||
const remainingSeconds = ref(0); // 倒计时剩余秒数
|
||||
const countdownTimer = ref<any | null>(null); // 倒计时定时器
|
||||
|
||||
/**
|
||||
* 处理定时任务 - 使用新的AutoActionItem结构
|
||||
* @param actions 自动操作列表
|
||||
* @param runtimeState 运行时状态
|
||||
*/
|
||||
function processScheduledActions(
|
||||
actions: AutoActionItem[],
|
||||
runtimeState: RuntimeState
|
||||
) {
|
||||
if (!roomId.value) return;
|
||||
|
||||
// 获取定时消息操作
|
||||
const scheduledActions = actions.filter(action =>
|
||||
action.triggerType === TriggerType.SCHEDULED &&
|
||||
action.enabled &&
|
||||
(!action.triggerConfig.onlyDuringLive || isLive.value)
|
||||
);
|
||||
|
||||
// 为每个定时操作设置定时器
|
||||
scheduledActions.forEach(action => {
|
||||
// 检查是否已有定时器
|
||||
if (runtimeState.scheduledTimers[action.id]) return;
|
||||
|
||||
const intervalSeconds = action.triggerConfig.intervalSeconds || 300; // 默认5分钟
|
||||
|
||||
// 创建定时器函数
|
||||
const timerFn = () => {
|
||||
// 创建执行上下文
|
||||
const context = buildExecutionContext(null, roomId.value, TriggerType.SCHEDULED);
|
||||
|
||||
// 选择并发送消息
|
||||
const template = getRandomTemplate(action.templates);
|
||||
if (template && roomId.value) {
|
||||
const formattedMessage = formatTemplate(template, context);
|
||||
sendLiveDanmaku(roomId.value, formattedMessage);
|
||||
}
|
||||
|
||||
// 设置下一次定时
|
||||
runtimeState.scheduledTimers[action.id] = setTimeout(timerFn, intervalSeconds * 1000);
|
||||
};
|
||||
|
||||
// 首次启动定时器
|
||||
runtimeState.scheduledTimers[action.id] = setTimeout(timerFn, intervalSeconds * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动定时弹幕 (旧方式)
|
||||
*/
|
||||
function startScheduledDanmaku() {
|
||||
console.log('定时弹幕已迁移到新的AutoActionItem结构');
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止定时弹幕 (旧方式)
|
||||
*/
|
||||
function stopScheduledDanmaku() {
|
||||
console.log('定时弹幕已迁移到新的AutoActionItem结构');
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化剩余时间为分:秒格式
|
||||
*/
|
||||
const formattedRemainingTime = computed(() => {
|
||||
const minutes = Math.floor(remainingSeconds.value / 60);
|
||||
const seconds = remainingSeconds.value % 60;
|
||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
});
|
||||
|
||||
/**
|
||||
* 清理计时器
|
||||
*/
|
||||
function clearTimer() {
|
||||
if (timer.value) {
|
||||
clearTimeout(timer.value);
|
||||
timer.value = null;
|
||||
}
|
||||
if (countdownTimer.value) {
|
||||
clearInterval(countdownTimer.value);
|
||||
countdownTimer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
startScheduledDanmaku,
|
||||
stopScheduledDanmaku,
|
||||
processScheduledActions,
|
||||
clearTimer,
|
||||
remainingSeconds,
|
||||
formattedRemainingTime
|
||||
};
|
||||
}
|
||||
99
src/client/store/autoAction/types.ts
Normal file
99
src/client/store/autoAction/types.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
// 统一的自动操作类型定义
|
||||
|
||||
import { EventModel } from '@/api/api-models';
|
||||
|
||||
// 触发条件类型
|
||||
export enum TriggerType {
|
||||
DANMAKU = 'danmaku', // 弹幕
|
||||
GIFT = 'gift', // 礼物
|
||||
GUARD = 'guard', // 上舰
|
||||
FOLLOW = 'follow', // 关注
|
||||
ENTER = 'enter', // 进入直播间
|
||||
SCHEDULED = 'scheduled', // 定时触发
|
||||
SUPER_CHAT = 'super_chat', // SC
|
||||
}
|
||||
|
||||
// 操作类型
|
||||
export enum ActionType {
|
||||
SEND_DANMAKU = 'send_danmaku', // 发送弹幕
|
||||
SEND_PRIVATE_MSG = 'send_private_msg', // 发送私信
|
||||
EXECUTE_COMMAND = 'execute_command', // 执行命令
|
||||
}
|
||||
|
||||
// 优先级
|
||||
export enum Priority {
|
||||
HIGHEST = 0,
|
||||
HIGH = 1,
|
||||
NORMAL = 2,
|
||||
LOW = 3,
|
||||
LOWEST = 4,
|
||||
}
|
||||
|
||||
// 统一的自动操作定义
|
||||
export type AutoActionItem = {
|
||||
id: string; // 唯一ID
|
||||
name: string; // 操作名称
|
||||
enabled: boolean; // 是否启用
|
||||
triggerType: TriggerType; // 触发类型
|
||||
actionType: ActionType; // 操作类型
|
||||
templates: string[]; // 模板列表
|
||||
priority: Priority; // 优先级
|
||||
|
||||
// 高级配置
|
||||
logicalExpression: string; // 逻辑表达式,为真时才执行此操作
|
||||
ignoreCooldown: boolean; // 是否忽略冷却时间
|
||||
executeCommand: string; // 要执行的JS代码
|
||||
|
||||
// 触发器特定配置
|
||||
triggerConfig: {
|
||||
// 通用
|
||||
userFilterEnabled?: boolean; // 是否启用用户过滤
|
||||
requireMedal?: boolean; // 要求本房间勋章
|
||||
requireCaptain?: boolean; // 要求任意舰长
|
||||
onlyDuringLive?: boolean; // 仅直播中启用
|
||||
ignoreTianXuan?: boolean; // 天选时刻忽略
|
||||
|
||||
// 弹幕触发特定
|
||||
keywords?: string[]; // 触发关键词
|
||||
blockwords?: string[]; // 屏蔽词
|
||||
|
||||
// 礼物触发特定
|
||||
filterMode?: 'none' | 'blacklist' | 'whitelist' | 'value' | 'free'; // 礼物过滤模式
|
||||
filterGiftNames?: string[]; // 礼物黑/白名单
|
||||
minValue?: number; // 最低礼物价值
|
||||
includeQuantity?: boolean; // 是否包含礼物数量
|
||||
|
||||
// 定时触发特定
|
||||
intervalSeconds?: number; // 间隔秒数
|
||||
schedulingMode?: 'random' | 'sequential'; // 定时模式
|
||||
|
||||
// 上舰特定
|
||||
guardLevels?: number[]; // 舰长等级过滤
|
||||
preventRepeat?: boolean; // 防止重复发送
|
||||
giftCodes?: {level: number, codes: string[]}[]; // 礼品码
|
||||
};
|
||||
|
||||
// 动作特定配置
|
||||
actionConfig: {
|
||||
delaySeconds?: number; // 延迟执行秒数
|
||||
maxUsersPerMsg?: number; // 每条消息最大用户数
|
||||
maxItemsPerUser?: number; // 每用户最大项目数 (礼物等)
|
||||
cooldownSeconds?: number; // 冷却时间(秒)
|
||||
};
|
||||
}
|
||||
|
||||
// 执行上下文,包含事件信息和可用变量
|
||||
export interface ExecutionContext {
|
||||
event?: EventModel; // 触发事件
|
||||
roomId?: number; // 直播间ID
|
||||
variables: Record<string, any>; // 额外变量
|
||||
timestamp: number; // 时间戳
|
||||
}
|
||||
|
||||
// 运行状态接口
|
||||
export interface RuntimeState {
|
||||
lastExecutionTime: Record<string, number>; // 上次执行时间
|
||||
aggregatedEvents: Record<string, any[]>; // 聚合的事件
|
||||
scheduledTimers: Record<string, NodeJS.Timeout | null>; // 定时器
|
||||
sentGuardPms: Set<number>; // 已发送的舰长私信
|
||||
}
|
||||
344
src/client/store/autoAction/utils.ts
Normal file
344
src/client/store/autoAction/utils.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import {
|
||||
AutoActionItem,
|
||||
TriggerType,
|
||||
ActionType,
|
||||
Priority,
|
||||
RuntimeState,
|
||||
ExecutionContext
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* 创建默认的运行时状态
|
||||
*/
|
||||
export function createDefaultRuntimeState(): RuntimeState {
|
||||
return {
|
||||
lastExecutionTime: {},
|
||||
scheduledTimers: {},
|
||||
sentGuardPms: new Set(),
|
||||
aggregatedEvents: {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建默认的自动操作项
|
||||
* @param triggerType 触发类型
|
||||
*/
|
||||
export function createDefaultAutoAction(triggerType: TriggerType): AutoActionItem {
|
||||
const id = `auto-action-${nanoid(8)}`;
|
||||
|
||||
// 根据不同触发类型设置默认模板
|
||||
const defaultTemplates: Record<TriggerType, string[]> = {
|
||||
[TriggerType.DANMAKU]: ['收到 @{user.name} 的弹幕: {event.msg}'],
|
||||
[TriggerType.GIFT]: ['感谢 @{user.name} 赠送的 {gift.summary}'],
|
||||
[TriggerType.GUARD]: ['感谢 @{user.name} 开通了{guard.levelName}!'],
|
||||
[TriggerType.FOLLOW]: ['感谢 @{user.name} 的关注!'],
|
||||
[TriggerType.ENTER]: ['欢迎 @{user.name} 进入直播间'],
|
||||
[TriggerType.SCHEDULED]: ['这是一条定时消息,当前时间: {date.formatted}'],
|
||||
[TriggerType.SUPER_CHAT]: ['感谢 @{user.name} 的SC: {sc.message}'],
|
||||
};
|
||||
|
||||
// 根据不同触发类型设置默认名称
|
||||
const defaultNames: Record<TriggerType, string> = {
|
||||
[TriggerType.DANMAKU]: '弹幕回复',
|
||||
[TriggerType.GIFT]: '礼物感谢',
|
||||
[TriggerType.GUARD]: '舰长感谢',
|
||||
[TriggerType.FOLLOW]: '关注感谢',
|
||||
[TriggerType.ENTER]: '入场欢迎',
|
||||
[TriggerType.SCHEDULED]: '定时消息',
|
||||
[TriggerType.SUPER_CHAT]: 'SC感谢',
|
||||
};
|
||||
|
||||
return {
|
||||
id,
|
||||
name: defaultNames[triggerType] || '新建自动操作',
|
||||
enabled: true,
|
||||
triggerType,
|
||||
actionType: triggerType === TriggerType.GUARD ? ActionType.SEND_PRIVATE_MSG : ActionType.SEND_DANMAKU,
|
||||
priority: Priority.NORMAL,
|
||||
templates: defaultTemplates[triggerType] || ['默认模板'],
|
||||
logicalExpression: '',
|
||||
executeCommand: '',
|
||||
ignoreCooldown: false,
|
||||
triggerConfig: {
|
||||
onlyDuringLive: true,
|
||||
ignoreTianXuan: true,
|
||||
userFilterEnabled: false,
|
||||
requireMedal: false,
|
||||
requireCaptain: false,
|
||||
preventRepeat: triggerType === TriggerType.GUARD,
|
||||
intervalSeconds: triggerType === TriggerType.SCHEDULED ? 300 : undefined,
|
||||
},
|
||||
actionConfig: {
|
||||
delaySeconds: 0,
|
||||
cooldownSeconds: 5,
|
||||
maxUsersPerMsg: 5,
|
||||
maxItemsPerUser: 3
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从模板数组中随机选择一个
|
||||
* @param templates 模板数组
|
||||
*/
|
||||
export function getRandomTemplate(templates: string[]): string | null {
|
||||
if (!templates || templates.length === 0) return null;
|
||||
const index = Math.floor(Math.random() * templates.length);
|
||||
return templates[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化模板,替换变量
|
||||
* @param template 模板字符串
|
||||
* @param context 执行上下文
|
||||
*/
|
||||
export function formatTemplate(template: string, context: ExecutionContext): string {
|
||||
if (!template) return '';
|
||||
|
||||
// 简单的模板替换
|
||||
return template.replace(/{([^}]+)}/g, (match, path) => {
|
||||
try {
|
||||
// 解析路径
|
||||
const parts = path.trim().split('.');
|
||||
let value: any = context;
|
||||
|
||||
// 特殊处理函数类型
|
||||
if (parts[0] === 'timeOfDay' && typeof context.variables.timeOfDay === 'function') {
|
||||
return context.variables.timeOfDay();
|
||||
}
|
||||
|
||||
// 特殊处理event直接访问
|
||||
if (parts[0] === 'event') {
|
||||
value = context.event;
|
||||
parts.shift();
|
||||
} else {
|
||||
// 否则从variables中获取
|
||||
value = context.variables;
|
||||
}
|
||||
|
||||
// 递归获取嵌套属性
|
||||
for (const part of parts) {
|
||||
if (value === undefined || value === null) return match;
|
||||
value = value[part];
|
||||
if (typeof value === 'function') value = value();
|
||||
}
|
||||
|
||||
return value !== undefined && value !== null ? String(value) : match;
|
||||
} catch (error) {
|
||||
console.error('模板格式化错误:', error);
|
||||
return match; // 出错时返回原始匹配项
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算逻辑表达式
|
||||
* @param expression 表达式字符串
|
||||
* @param context 执行上下文
|
||||
*/
|
||||
export function evaluateExpression(expression: string, context: ExecutionContext): boolean {
|
||||
if (!expression || expression.trim() === '') return true; // 空表达式默认为true
|
||||
|
||||
try {
|
||||
// 预定义函数和变量
|
||||
const utils = {
|
||||
// 事件相关
|
||||
inDanmaku: (keyword: string) => {
|
||||
if (!context.event?.msg) return false;
|
||||
return context.event.msg.includes(keyword);
|
||||
},
|
||||
|
||||
// 礼物相关
|
||||
giftValue: () => {
|
||||
if (!context.event) return 0;
|
||||
return (context.event.price || 0) * (context.event.num || 1) / 1000;
|
||||
},
|
||||
|
||||
giftName: () => context.event?.msg || '',
|
||||
giftCount: () => context.event?.num || 0,
|
||||
|
||||
// 用户相关
|
||||
hasMedal: () => context.event?.fans_medal_wearing_status || false,
|
||||
medalLevel: () => context.event?.fans_medal_level || 0,
|
||||
isCaptain: () => (context.event?.guard_level || 0) > 0,
|
||||
|
||||
// 时间相关
|
||||
time: {
|
||||
hour: new Date().getHours(),
|
||||
minute: new Date().getMinutes()
|
||||
},
|
||||
|
||||
// 字符串处理
|
||||
str: {
|
||||
includes: (str: string, search: string) => str.includes(search),
|
||||
startsWith: (str: string, search: string) => str.startsWith(search),
|
||||
endsWith: (str: string, search: string) => str.endsWith(search)
|
||||
}
|
||||
};
|
||||
|
||||
// 创建安全的eval环境
|
||||
const evalFunc = new Function(
|
||||
'context',
|
||||
'event',
|
||||
'utils',
|
||||
`try {
|
||||
with(utils) {
|
||||
return (${expression});
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('表达式评估错误:', e);
|
||||
return false;
|
||||
}`
|
||||
);
|
||||
|
||||
// 执行表达式
|
||||
return Boolean(evalFunc(context, context.event, utils));
|
||||
} catch (error) {
|
||||
console.error('表达式评估错误:', error);
|
||||
return false; // 出错时返回false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化消息模板,替换变量
|
||||
* @param template 模板字符串
|
||||
* @param params 参数对象
|
||||
*/
|
||||
export function formatMessage(template: string, params: Record<string, any>): string {
|
||||
if (!template) return '';
|
||||
|
||||
// 简单的模板替换
|
||||
return template.replace(/{{([^}]+)}}/g, (match, path) => {
|
||||
try {
|
||||
// 解析路径
|
||||
const parts = path.trim().split('.');
|
||||
let value: any = params;
|
||||
|
||||
// 递归获取嵌套属性
|
||||
for (const part of parts) {
|
||||
if (value === undefined || value === null) return match;
|
||||
value = value[part];
|
||||
if (typeof value === 'function') value = value();
|
||||
}
|
||||
|
||||
return value !== undefined && value !== null ? String(value) : match;
|
||||
} catch (error) {
|
||||
console.error('模板格式化错误:', error);
|
||||
return match; // 出错时返回原始匹配项
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该处理自动操作
|
||||
* @param config 配置对象,需要包含enabled和onlyDuringLive属性
|
||||
* @param isLive 当前是否为直播状态
|
||||
*/
|
||||
export function shouldProcess(config: { enabled: boolean; onlyDuringLive: boolean }, isLive: boolean): boolean {
|
||||
if (!config.enabled) return false;
|
||||
if (config.onlyDuringLive && !isLive) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否符合过滤条件
|
||||
* @param config 配置对象,需要包含userFilterEnabled、requireMedal和requireCaptain属性
|
||||
* @param event 事件对象
|
||||
*/
|
||||
export function checkUserFilter(config: { userFilterEnabled: boolean; requireMedal: boolean; requireCaptain: boolean }, event: { fans_medal_wearing_status?: boolean; guard_level?: number }): boolean {
|
||||
if (!config.userFilterEnabled) return true;
|
||||
if (config.requireMedal && !event.fans_medal_wearing_status) return false;
|
||||
if (config.requireCaptain && (!event.guard_level || event.guard_level === 0)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建执行上下文对象
|
||||
* @param event 事件对象
|
||||
* @param roomId 房间ID
|
||||
* @param triggerType 触发类型
|
||||
* @returns 标准化的执行上下文
|
||||
*/
|
||||
export function buildExecutionContext(
|
||||
event: any,
|
||||
roomId: number | undefined,
|
||||
triggerType?: TriggerType
|
||||
): ExecutionContext {
|
||||
const now = Date.now();
|
||||
const dateObj = new Date(now);
|
||||
|
||||
// 基础上下文
|
||||
const context: ExecutionContext = {
|
||||
event,
|
||||
roomId,
|
||||
timestamp: now,
|
||||
variables: {
|
||||
// 日期相关变量
|
||||
date: {
|
||||
formatted: dateObj.toLocaleString('zh-CN'),
|
||||
year: dateObj.getFullYear(),
|
||||
month: dateObj.getMonth() + 1,
|
||||
day: dateObj.getDate(),
|
||||
hour: dateObj.getHours(),
|
||||
minute: dateObj.getMinutes(),
|
||||
second: dateObj.getSeconds()
|
||||
},
|
||||
// 时段函数
|
||||
timeOfDay: () => {
|
||||
const hour = dateObj.getHours();
|
||||
if (hour < 6) return '凌晨';
|
||||
if (hour < 9) return '早上';
|
||||
if (hour < 12) return '上午';
|
||||
if (hour < 14) return '中午';
|
||||
if (hour < 18) return '下午';
|
||||
if (hour < 22) return '晚上';
|
||||
return '深夜';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 如果有事件对象,添加用户信息
|
||||
if (event) {
|
||||
context.variables.user = {
|
||||
name: event.uname,
|
||||
uid: event.uid,
|
||||
guardLevel: event.guard_level,
|
||||
hasMedal: event.fans_medal_wearing_status,
|
||||
medalLevel: event.fans_medal_level,
|
||||
medalName: event.fans_medal_name
|
||||
};
|
||||
|
||||
context.variables.message = event.msg;
|
||||
|
||||
// 根据不同触发类型添加特定变量
|
||||
if (triggerType === TriggerType.GIFT) {
|
||||
context.variables.gift = {
|
||||
name: event.msg, // 礼物名称通常存在msg字段
|
||||
count: event.num,
|
||||
price: (event.price || 0) / 1000, // B站价格单位通常是 1/1000 元
|
||||
totalPrice: ((event.price || 0) / 1000) * (event.num || 1),
|
||||
summary: `${event.num || 1}个${event.msg || '礼物'}`
|
||||
};
|
||||
} else if (triggerType === TriggerType.GUARD) {
|
||||
const guardLevelMap: Record<number, string> = {
|
||||
1: '总督',
|
||||
2: '提督',
|
||||
3: '舰长',
|
||||
0: '无舰长'
|
||||
};
|
||||
context.variables.guard = {
|
||||
level: event.guard_level || 0,
|
||||
levelName: guardLevelMap[event.guard_level || 0] || '未知舰长等级',
|
||||
giftCode: ''
|
||||
};
|
||||
} else if (triggerType === TriggerType.SUPER_CHAT) {
|
||||
context.variables.sc = {
|
||||
message: event.msg,
|
||||
price: (event.price || 0) / 1000
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,13 +2,87 @@ import { useAccount } from "@/api/account";
|
||||
import { useBiliCookie } from "./useBiliCookie";
|
||||
import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; // 引入 Body
|
||||
import { defineStore, acceptHMRUpdate } from 'pinia';
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import md5 from 'md5';
|
||||
import { QueryBiliAPI } from "../data/utils";
|
||||
import { onSendPrivateMessageFailed } from "../data/notification";
|
||||
|
||||
// WBI 混合密钥编码表
|
||||
const mixinKeyEncTab = [
|
||||
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
|
||||
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
|
||||
61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
|
||||
36, 20, 34, 44, 52
|
||||
];
|
||||
|
||||
// 对 imgKey 和 subKey 进行字符顺序打乱编码
|
||||
const getMixinKey = (orig: string): string =>
|
||||
mixinKeyEncTab.map(n => orig[n]).join('').slice(0, 32);
|
||||
|
||||
// 为请求参数进行 wbi 签名
|
||||
function encWbi(
|
||||
params: { [key: string]: string | number },
|
||||
img_key: string,
|
||||
sub_key: string
|
||||
): string {
|
||||
const mixin_key = getMixinKey(img_key + sub_key);
|
||||
const curr_time = Math.round(Date.now() / 1000);
|
||||
const chr_filter = /[!'()*]/g;
|
||||
|
||||
Object.assign(params, { wts: curr_time.toString() }); // 添加 wts 字段
|
||||
|
||||
// 按照 key 重排参数
|
||||
const query = Object.keys(params)
|
||||
.sort()
|
||||
.map(key => {
|
||||
// 过滤 value 中的 "!'()*" 字符
|
||||
const value = params[key].toString().replace(chr_filter, '');
|
||||
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
||||
})
|
||||
.join('&');
|
||||
|
||||
const wbi_sign = md5(query + mixin_key); // 计算 w_rid
|
||||
return query + '&w_rid=' + wbi_sign;
|
||||
}
|
||||
|
||||
// 获取最新的 img_key 和 sub_key
|
||||
async function getWbiKeys(cookie: string): Promise<{ img_key: string, sub_key: string }> {
|
||||
try {
|
||||
const response = await QueryBiliAPI('https://api.bilibili.com/x/web-interface/nav');
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("获取WBI密钥失败:", response.status);
|
||||
throw new Error("获取WBI密钥失败");
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const { wbi_img } = result.data;
|
||||
|
||||
console.log(`获取WBI秘钥: img_key: ${wbi_img.img_url}, sub_key: ${wbi_img.sub_url}`);
|
||||
|
||||
return {
|
||||
img_key: wbi_img.img_url.slice(
|
||||
wbi_img.img_url.lastIndexOf('/') + 1,
|
||||
wbi_img.img_url.lastIndexOf('.')
|
||||
),
|
||||
sub_key: wbi_img.sub_url.slice(
|
||||
wbi_img.sub_url.lastIndexOf('/') + 1,
|
||||
wbi_img.sub_url.lastIndexOf('.')
|
||||
)
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("获取WBI密钥时发生错误:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const useBiliFunction = defineStore('biliFunction', () => {
|
||||
const biliCookieStore = useBiliCookie();
|
||||
const account = useAccount();
|
||||
const cookie = computed(() => biliCookieStore.cookie);
|
||||
const uid = computed(() => account.value.biliId);
|
||||
// 存储WBI密钥
|
||||
const wbiKeys = ref<{ img_key: string, sub_key: string } | null>(null);
|
||||
|
||||
const csrf = computed(() => {
|
||||
if (!cookie.value) return null;
|
||||
@@ -34,6 +108,7 @@ export const useBiliFunction = defineStore('biliFunction', () => {
|
||||
console.warn("尝试发送空弹幕,已阻止。");
|
||||
return false;
|
||||
}
|
||||
roomId = 1294406; // 测试用房间号
|
||||
const url = "https://api.live.bilibili.com/msg/send";
|
||||
const rnd = Math.floor(Date.now() / 1000);
|
||||
const data = {
|
||||
@@ -47,7 +122,7 @@ export const useBiliFunction = defineStore('biliFunction', () => {
|
||||
csrf: csrf.value,
|
||||
csrf_token: csrf.value,
|
||||
};
|
||||
|
||||
const params = new URLSearchParams(data)
|
||||
try {
|
||||
// 注意: B站网页版发送弹幕是用 application/x-www-form-urlencoded
|
||||
const response = await tauriFetch(url, {
|
||||
@@ -58,7 +133,7 @@ export const useBiliFunction = defineStore('biliFunction', () => {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36",
|
||||
"Referer": `https://live.bilibili.com/${roomId}`
|
||||
},
|
||||
body: JSON.stringify(data), // 发送 JSON 数据
|
||||
body: params, // 发送 JSON 数据
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -105,6 +180,7 @@ export const useBiliFunction = defineStore('biliFunction', () => {
|
||||
};
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams(data)
|
||||
const response = await tauriFetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -113,7 +189,7 @@ export const useBiliFunction = defineStore('biliFunction', () => {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36",
|
||||
"Referer": `https://live.bilibili.com/p/html/live-room-setting/#/room-manager/black-list?room_id=${roomId}` // 模拟来源
|
||||
},
|
||||
body: JSON.stringify(data), // 发送 JSON 数据
|
||||
body: params, // 发送 URLSearchParams 数据
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.error("封禁用户失败:", response.status, await response.text());
|
||||
@@ -139,61 +215,107 @@ export const useBiliFunction = defineStore('biliFunction', () => {
|
||||
*/
|
||||
async function sendPrivateMessage(receiverId: number, message: string): Promise<boolean> {
|
||||
if (!csrf.value || !cookie.value || !uid.value) {
|
||||
console.error("发送私信失败:缺少 cookie, csrf token 或 uid");
|
||||
const error = "发送私信失败:缺少 cookie, csrf token 或 uid";
|
||||
console.error(error);
|
||||
onSendPrivateMessageFailed(receiverId, message, error);
|
||||
return false;
|
||||
}
|
||||
if (!message || message.trim().length === 0) {
|
||||
console.warn("尝试发送空私信,已阻止。");
|
||||
const error = "尝试发送空私信,已阻止。";
|
||||
console.warn(error);
|
||||
return false;
|
||||
}
|
||||
const url = "https://api.vc.bilibili.com/web_im/v1/web_im/send_msg";
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const content = JSON.stringify({ content: message });
|
||||
const dev_id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16).toUpperCase();
|
||||
});
|
||||
const data = {
|
||||
'msg[sender_uid]': uid.value.toString(),
|
||||
'msg[receiver_id]': receiverId.toString(),
|
||||
'msg[receiver_type]': '1',
|
||||
'msg[msg_type]': '1',
|
||||
'msg[msg_status]': '0',
|
||||
'msg[content]': content,
|
||||
'msg[timestamp]': timestamp.toString(),
|
||||
'msg[new_face_version]': '0',
|
||||
'msg[dev_id]': dev_id,
|
||||
'build': '0',
|
||||
'mobi_app': 'web',
|
||||
'csrf': csrf.value,
|
||||
'csrf_token': csrf.value,
|
||||
};
|
||||
|
||||
try {
|
||||
// 获取WBI密钥(如果还没有)
|
||||
if (!wbiKeys.value) {
|
||||
wbiKeys.value = await getWbiKeys(cookie.value);
|
||||
}
|
||||
if (!wbiKeys.value) {
|
||||
const error = "获取WBI密钥失败,无法发送私信";
|
||||
console.error(error);
|
||||
onSendPrivateMessageFailed(receiverId, message, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
const dev_id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16).toUpperCase();
|
||||
});
|
||||
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const content = JSON.stringify({ content: message });
|
||||
|
||||
// 准备URL参数(需要WBI签名的参数)
|
||||
const urlParams = {
|
||||
w_sender_uid: uid.value.toString(),
|
||||
w_receiver_id: receiverId.toString(),
|
||||
w_dev_id: dev_id,
|
||||
};
|
||||
|
||||
// 生成带WBI签名的URL查询字符串
|
||||
const signedQuery = encWbi(
|
||||
urlParams,
|
||||
wbiKeys.value.img_key,
|
||||
wbiKeys.value.sub_key
|
||||
);
|
||||
|
||||
// 构建最终URL
|
||||
const url = `https://api.vc.bilibili.com/web_im/v1/web_im/send_msg?${signedQuery}`;
|
||||
|
||||
// 准备表单数据
|
||||
const formData = {
|
||||
'msg[sender_uid]': uid.value.toString(),
|
||||
'msg[receiver_id]': receiverId.toString(),
|
||||
'msg[receiver_type]': '1',
|
||||
'msg[msg_type]': '1',
|
||||
'msg[msg_status]': '0',
|
||||
'msg[content]': content,
|
||||
'msg[timestamp]': timestamp.toString(),
|
||||
'msg[new_face_version]': '0',
|
||||
'msg[dev_id]': dev_id,
|
||||
'build': '0',
|
||||
'mobi_app': 'web',
|
||||
'csrf': csrf.value,
|
||||
'csrf_token': csrf.value,
|
||||
};
|
||||
|
||||
const params = new URLSearchParams(formData);
|
||||
const response = await tauriFetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Cookie": cookie.value,
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36",
|
||||
"Referer": `https://message.bilibili.com/`,
|
||||
"Origin": '',
|
||||
},
|
||||
body: JSON.stringify(data), // 发送 JSON 数据
|
||||
body: params,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("发送私信网络失败:", response.status, await response.text());
|
||||
const error = `发送私信网络失败: ${response.status}`;
|
||||
console.error(error, await response.text());
|
||||
onSendPrivateMessageFailed(receiverId, message, error);
|
||||
return false;
|
||||
}
|
||||
// 私信成功码也是 0
|
||||
if (response.data.code !== 0) {
|
||||
console.error("发送私信API失败:", response.data.code, response.data.message);
|
||||
|
||||
const json = await response.json();
|
||||
if (json.code !== 0) {
|
||||
const error = `发送私信API失败: ${json.code} - ${json.message}`;
|
||||
console.error(error);
|
||||
onSendPrivateMessageFailed(receiverId, message, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`发送私信给 ${receiverId} 成功`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("发送私信时发生错误:", error);
|
||||
// 如果是WBI密钥问题,清空密钥以便下次重新获取
|
||||
if (String(error).includes('WBI')) {
|
||||
wbiKeys.value = null;
|
||||
}
|
||||
onSendPrivateMessageFailed(receiverId, message, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export type DanmakuWindowSettings = {
|
||||
reverseOrder: boolean; // 是否倒序显示(从下往上)
|
||||
filterTypes: string[]; // 要显示的弹幕类型
|
||||
animationDuration: number; // 动画持续时间
|
||||
enableAnimation: boolean; // 是否启用动画效果
|
||||
backgroundColor: string; // 背景色
|
||||
textColor: string; // 文字颜色
|
||||
alwaysOnTop: boolean; // 是否总在最前
|
||||
@@ -182,6 +183,7 @@ export const useDanmakuWindow = defineStore('danmakuWindow', () => {
|
||||
textStyleCompact: false, // 新增:默认不使用紧凑布局
|
||||
textStyleShowType: true, // 新增:默认显示消息类型标签
|
||||
textStyleNameSeparator: ': ', // 新增:默认用户名和消息之间的分隔符为冒号+空格
|
||||
enableAnimation: true, // 新增:默认启用动画效果
|
||||
});
|
||||
const emojiData = useStorage<{
|
||||
updateAt: number,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useTauriStore } from './useTauriStore';
|
||||
|
||||
export type NotificationType = 'question-box' | 'danmaku' | 'goods-buy';
|
||||
export type NotificationType = 'question-box' | 'danmaku' | 'goods-buy' | 'message-failed' | 'live-danmaku-failed';
|
||||
export type NotificationSettings = {
|
||||
enableTypes: NotificationType[];
|
||||
};
|
||||
@@ -29,7 +29,7 @@ export const useSettings = defineStore('settings', () => {
|
||||
loginType: 'qrcode',
|
||||
enableNotification: true,
|
||||
notificationSettings: {
|
||||
enableTypes: ['question-box', 'danmaku'],
|
||||
enableTypes: ['question-box', 'danmaku', 'message-failed'],
|
||||
},
|
||||
|
||||
dev_disableDanmakuClient: false,
|
||||
@@ -39,7 +39,7 @@ export const useSettings = defineStore('settings', () => {
|
||||
async function init() {
|
||||
settings.value = (await store.get()) || Object.assign({}, defaultSettings);
|
||||
settings.value.notificationSettings ??= defaultSettings.notificationSettings;
|
||||
settings.value.notificationSettings.enableTypes ??= [ 'question-box', 'danmaku' ];
|
||||
settings.value.notificationSettings.enableTypes ??= [ 'question-box', 'danmaku', 'message-failed' ];
|
||||
}
|
||||
async function save() {
|
||||
await store.set(settings.value);
|
||||
|
||||
Reference in New Issue
Block a user