feat: 更新项目配置和组件,增强功能和用户体验

- 在 .gitignore 中添加了 .specstory 文件的忽略规则。
- 更新 tsconfig.json,修正了 vue-vine/types/macros 的引用路径。
- 在组件声明中新增了 NInput 组件的类型支持。
- 优化了 EventModel 接口,调整了 guard_level 的类型为 GuardLevel。
- 增加了 Follow 事件类型到 EventDataTypes 枚举中。
- 在 ClientAutoAction.vue 中引入了新的 store 和组件,增强了功能。
- 更新了多个设置组件,添加了关键词匹配类型和过滤模式的支持。
- 改进了模板编辑器和测试器的功能,支持更灵活的模板管理。
- 在弹幕客户端中新增了关注事件的处理逻辑,提升了事件响应能力。
This commit is contained in:
2025-04-22 02:30:09 +08:00
parent 2fc8f7fcf8
commit 77cf0c5edc
39 changed files with 3955 additions and 1959 deletions

View File

@@ -0,0 +1,287 @@
import { Ref } from 'vue';
import { EventModel } from '@/api/api-models';
import {
AutoActionItem,
TriggerType,
RuntimeState,
ActionType,
ExecutionContext
} from './types';
import { buildExecutionContext, getRandomTemplate } from './utils';
import { evaluateTemplateExpressions } from './expressionEvaluator';
import { evaluateExpression } from './utils';
import { useBiliCookie } from '../useBiliCookie';
/**
* 过滤有效的自动操作项
* @param actions 所有操作项列表
* @param triggerType 触发类型
* @param isLive 是否直播中
* @param isTianXuanActive 是否天选时刻激活
* @param options 额外过滤选项
* @returns 过滤后的操作项
*/
export function filterValidActions(
actions: AutoActionItem[],
triggerType: TriggerType,
isLive: Ref<boolean>,
isTianXuanActive?: Ref<boolean>,
options?: {
actionType?: ActionType; // 特定操作类型
customFilter?: (action: AutoActionItem) => boolean; // 自定义过滤器
}
): AutoActionItem[] {
return actions.filter(action => {
// 基本过滤条件
if (action.triggerType !== triggerType || !action.enabled) {
return false;
}
// 直播状态过滤
if (action.triggerConfig.onlyDuringLive && !isLive.value) {
return false;
}
// 天选时刻过滤
if (isTianXuanActive && action.triggerConfig.ignoreTianXuan && isTianXuanActive.value) {
return false;
}
// 操作类型过滤
if (options?.actionType && action.actionType !== options.actionType) {
return false;
}
// 自定义过滤器
if (options?.customFilter && !options.customFilter(action)) {
return false;
}
return true;
});
}
/**
* 检查用户是否满足过滤条件
* @param action 操作项
* @param event 事件数据
* @returns 是否满足条件
*/
export function checkUserFilters(action: AutoActionItem, event: EventModel): boolean {
if (!action.triggerConfig.userFilterEnabled) {
return true;
}
if (action.triggerConfig.requireMedal && !event.fans_medal_wearing_status) {
return false;
}
if (action.triggerConfig.requireCaptain && !event.guard_level) {
return false;
}
return true;
}
/**
* 检查冷却时间
* @param action 操作项
* @param runtimeState 运行时状态
* @returns 是否可以执行(已过冷却期)
*/
export function checkCooldown(action: AutoActionItem, runtimeState: RuntimeState): boolean {
if (action.ignoreCooldown) {
return true;
}
const now = Date.now();
const lastExecTime = runtimeState.lastExecutionTime[action.id] || 0;
const cooldownMs = (action.actionConfig.cooldownSeconds || 0) * 1000;
return now - lastExecTime >= cooldownMs;
}
/**
* 处理模板并返回格式化后的内容
* @param action 操作项
* @param context 执行上下文
* @param options 可选配置
* @returns 格式化后的内容如果没有有效模板则返回null
*/
export function processTemplate(
action: AutoActionItem,
context: any,
options?: {
useRandomTemplate?: boolean; // 是否随机选择模板默认true
defaultValue?: string; // 如果模板为空或格式化失败时的默认值
}
): string | null {
if (!action.template || action.template.trim() === '') {
console.warn(`跳过操作 "${action.name || '未命名'}":未设置有效模板`);
return options?.defaultValue || null;
}
try {
// 获取模板内容
let template: string;
if (options?.useRandomTemplate !== false) {
// 使用随机模板 (默认行为)
const randomTemplate = getRandomTemplate(action.template);
if (!randomTemplate) {
return options?.defaultValue || null;
}
template = randomTemplate;
} else {
// 使用整个模板字符串
template = action.template;
}
// 格式化模板
const formattedContent = evaluateTemplateExpressions(template, context);
return formattedContent;
} catch (error) {
console.error(`模板处理错误 (${action.name || action.id}):`, error);
return options?.defaultValue || null;
}
}
/**
* 执行操作的通用函数
* @param actions 过滤后的操作列表
* @param event 触发事件
* @param triggerType 触发类型
* @param roomId 房间ID
* @param runtimeState 运行时状态
* @param handlers 操作处理器
* @param options 额外选项
*/
export function executeActions(
actions: AutoActionItem[],
event: EventModel | null,
triggerType: TriggerType,
roomId: number,
runtimeState: RuntimeState,
handlers: {
sendLiveDanmaku?: (roomId: number, message: string) => Promise<boolean>;
sendPrivateMessage?: (userId: number, message: string) => Promise<boolean>;
// 可以扩展其他类型的发送处理器
},
options?: {
customContextBuilder?: (event: EventModel | null, roomId: number, triggerType: TriggerType) => ExecutionContext;
customFilters?: Array<(action: AutoActionItem, context: ExecutionContext) => boolean>;
skipUserFilters?: boolean;
skipCooldownCheck?: boolean;
onSuccess?: (action: AutoActionItem, context: ExecutionContext) => void;
}
) {
if (!roomId || actions.length === 0) return;
const biliCookie = useBiliCookie()
// 对每个操作进行处理
for (const action of actions) {
// 构建执行上下文
const context = options?.customContextBuilder
? options.customContextBuilder(event, roomId, triggerType)
: buildExecutionContext(event, roomId, triggerType);
// 应用自定义过滤器
if (options?.customFilters) {
const passesAllFilters = options.customFilters.every(filter => filter(action, context));
if (!passesAllFilters) continue;
}
// 检查用户过滤条件
if (!options?.skipUserFilters && event && !checkUserFilters(action, event)) {
continue;
}
// 检查逻辑表达式
if (action.logicalExpression && event) {
if (!evaluateExpression(action.logicalExpression, context)) {
continue;
}
}
// 检查冷却时间
if (!options?.skipCooldownCheck && !checkCooldown(action, runtimeState)) {
continue;
}
// 根据操作类型执行不同的处理逻辑
switch (action.actionType) {
case ActionType.SEND_DANMAKU:
if (!biliCookie.isCookieValid) {
continue; // 如果未登录,则跳过
}
if (handlers.sendLiveDanmaku) {
// 处理弹幕发送
const message = processTemplate(action, context);
if (message) {
// 更新冷却时间
runtimeState.lastExecutionTime[action.id] = Date.now();
// 延迟发送
if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
setTimeout(() => {
handlers.sendLiveDanmaku!(roomId, message)
.catch(err => console.error(`[AutoAction] 发送弹幕失败 (${action.name || action.id}):`, err));
}, action.actionConfig.delaySeconds * 1000);
} else {
handlers.sendLiveDanmaku(roomId, message)
.catch(err => console.error(`[AutoAction] 发送弹幕失败 (${action.name || action.id}):`, err));
}
}
} else {
console.warn(`[AutoAction] 未提供弹幕发送处理器,无法执行操作: ${action.name || action.id}`);
}
break;
case ActionType.SEND_PRIVATE_MSG:
if (!biliCookie.isCookieValid) {
continue; // 如果未登录,则跳过
}
if (handlers.sendPrivateMessage && event && event.uid) {
// 处理私信发送
const message = processTemplate(action, context);
if (message) {
// 更新冷却时间(私信也可以有冷却时间)
runtimeState.lastExecutionTime[action.id] = Date.now();
const sendPmPromise = (uid: number, msg: string) => {
return handlers.sendPrivateMessage!(uid, msg)
.then(success => {
if (success && options?.onSuccess) {
// 发送成功后调用 onSuccess 回调
options.onSuccess(action, context);
}
return success;
})
.catch(err => {
console.error(`[AutoAction] 发送私信失败 (${action.name || action.id}):`, err);
return false; // 明确返回 false 表示失败
});
};
// 私信通常不需要延迟,但我们也可以支持
if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
setTimeout(() => {
sendPmPromise(event.uid, message);
}, action.actionConfig.delaySeconds * 1000);
} else {
sendPmPromise(event.uid, message);
}
}
} else {
console.warn(`[AutoAction] 未提供私信发送处理器或事件缺少UID无法执行操作: ${action.name || action.id}`);
}
break;
case ActionType.EXECUTE_COMMAND:
// 执行自定义命令(未实现)
console.warn(`[AutoAction] 暂不支持执行自定义命令: ${action.name || action.id}`);
break;
default:
console.warn(`[AutoAction] 未知的操作类型: ${action.actionType}`);
}
}
}

View File

@@ -2,36 +2,106 @@
* 表达式求值工具 - 用于在自动操作模板中支持简单的JavaScript表达式
*/
// 导入ExecutionContext类型
import { ExecutionContext } from './types';
// 表达式模式匹配
// {{js: expression}} - 完整的JavaScript表达式
const JS_EXPRESSION_REGEX = /\{\{\s*js:\s*(.*?)\s*\}\}/g;
// {{js: expression}} - 简单的JavaScript表达式 (隐式return)
// {{js+: code block}} - JavaScript代码块 (需要显式return)
// {{js-run: code block}} - JavaScript代码块 (需要显式return)
export const JS_EXPRESSION_REGEX = /\{\{\s*(js(?:\+|\-run)?):\s*(.*?)\s*\}\}/gs; // 使 s
/**
* 处理模板中的表达式
* @param template 包含表达式的模板字符串
* @param context 上下文对象,包含可在表达式中访问的变量
* @param context 执行上下文对象
* @returns 处理后的字符串
*/
export function evaluateTemplateExpressions(template: string, context: Record<string, any>): string {
export function evaluateTemplateExpressions(template: string, context: ExecutionContext): string {
// 增加严格的类型检查
if (typeof template !== 'string') {
console.error('[evaluateTemplateExpressions] Error: Expected template to be a string, but received:', typeof template, template);
return ""; // 或者抛出错误,或者返回一个默认值
}
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 variables = context.variables;
const dataFunctions = {
getData: context.getData,
setData: context.setData,
containsData: context.containsData,
removeData: context.removeData,
getStorageData: context.getStorageData,
setStorageData: context.setStorageData,
hasStorageData: context.hasStorageData,
removeStorageData: context.removeStorageData,
clearStorageData: context.clearStorageData,
};
// 执行表达式并返回结果
const result = evalInContext(...Object.values(context));
return result !== undefined ? String(result) : "";
// 合并基础变量和数据管理函数的作用域
const scopeVariables = { ...variables, ...dataFunctions };
const scopeKeys = Object.keys(scopeVariables);
const scopeValues = Object.values(scopeVariables);
// 第一步:处理简单的文本替换 {{variable.path}}
let result = template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
if (path.trim().startsWith('js:') || path.trim().startsWith('js+:') || path.trim().startsWith('js-run:')) {
return match; // 跳过所有JS变体留给下一步
}
try {
// 解析路径
const parts = path.trim().split('.');
let value: any = scopeVariables;
// 递归获取嵌套属性
for (const part of parts) {
if (value === undefined || value === null) return match;
if (dataFunctions.hasOwnProperty(part) && parts.length === 1) {
value = value[part]; // 不要调用顶层函数
} else if (typeof value[part] === 'function') {
value = value[part]();
} else {
value = value[part];
}
if (typeof value === 'function' && !dataFunctions.hasOwnProperty(part)) value = value();
}
return value !== undefined && value !== null ? String(value) : match;
} catch (error) {
console.error("表达式求值错误:", error);
return `[表达式错误: ${(error as Error).message}]`;
console.error('模板格式化错误:', error);
return match; // 出错时返回原始匹配项
}
});
// 第二步:处理 JS 表达式和代码块 {{js: ...}}, {{js+: ...}}, {{js-run: ...}}
return result.replace(JS_EXPRESSION_REGEX, (match, type, code) => {
try {
let functionBody: string;
if (type === 'js') {
// 简单表达式: 隐式 return
functionBody = `try { return (${code}); } catch (e) { console.error("表达式[js:]执行错误:", e, "代码:", ${JSON.stringify(code)}); return \"[表达式错误: \" + e.message + \"]\"; }`;
} else { // js+ 或 js-run
// 代码块: 需要显式 return
functionBody = `try { ${code} } catch (e) { console.error("代码块[js+/js-run:]执行错误:", e, "代码:", ${JSON.stringify(code)}); return \"[代码块错误: \" + e.message + \"]\"; }`;
}
const evalInContext = new Function(...scopeKeys, functionBody);
const evalResult = evalInContext(...scopeValues);
// 对结果进行处理,将 undefined/null 转换为空字符串,除非是错误消息
return typeof evalResult === 'string' && (evalResult.startsWith('[表达式错误:') || evalResult.startsWith('[代码块错误:'))
? evalResult
: String(evalResult ?? '');
} catch (error) {
// 捕获 Function 构造或顶层执行错误
console.error("JS占位符处理错误:", error, "类型:", type, "代码:", code);
return `[处理错误: ${(error as Error).message}]`;
}
});
}
@@ -61,7 +131,7 @@ export function escapeRegExp(string: string): string {
* @param placeholders 占位符列表
* @returns 转换后的模板
*/
export function convertToJsExpressions(template: string, placeholders: {name: string, description: string}[]): string {
export function convertToJsExpressions(template: string, placeholders: { name: string, description: string }[]): string {
let result = template;
placeholders.forEach(p => {
@@ -74,6 +144,22 @@ export function convertToJsExpressions(template: string, placeholders: {name: st
return result;
}
/**
* 从模板字符串中提取所有 JS 表达式占位符。
* 例如,从 'Hello {{js: user.name}}, time: {{js: Date.now()}}' 提取出
* ['{{js: user.name}}', '{{js: Date.now()}}']
* @param template 模板字符串
* @returns 包含所有匹配的 JS 表达式字符串的数组
*/
export function extractJsExpressions(template: string): string[] {
if (!template) {
return [];
}
// 使用全局匹配来查找所有出现
const matches = template.match(JS_EXPRESSION_REGEX);
return matches || []; // match 返回 null 或字符串数组
}
/**
* 为礼物感谢模块创建上下文对象
* @param user 用户信息
@@ -81,7 +167,7 @@ export function convertToJsExpressions(template: string, placeholders: {name: st
* @returns 上下文对象
*/
export function createGiftThankContext(user: { uid: number; name: string },
gift: { name: string; count: number; price: number }): Record<string, any> {
gift: { name: string; count: number; price: number }): Record<string, any> {
return {
user: {
uid: user.uid,
@@ -110,66 +196,4 @@ export function createGiftThankContext(user: { uid: number; name: string },
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())
}
};
}

View File

@@ -3,15 +3,16 @@ import { EventModel } from '@/api/api-models';
import {
AutoActionItem,
TriggerType,
ExecutionContext,
RuntimeState
RuntimeState,
KeywordMatchType
} from '../types';
import {
formatTemplate,
getRandomTemplate,
shouldProcess,
evaluateExpression
buildExecutionContext
} from '../utils';
import {
filterValidActions,
executeActions
} from '../actionUtils';
/**
* 自动回复模块
@@ -27,6 +28,32 @@ export function useAutoReply(
// 运行时数据 - 记录特定关键词的最后回复时间
const lastReplyTimestamps = ref<{ [keyword: string]: number; }>({});
/**
* 检查关键词匹配
* @param text 要检查的文本
* @param keyword 关键词
* @param matchType 匹配类型
* @returns 是否匹配
*/
function isKeywordMatch(text: string, keyword: string, matchType: KeywordMatchType = KeywordMatchType.Contains): boolean {
switch (matchType) {
case KeywordMatchType.Full:
return text === keyword;
case KeywordMatchType.Contains:
return text.includes(keyword);
case KeywordMatchType.Regex:
try {
const regex = new RegExp(keyword);
return regex.test(text);
} catch (e) {
console.warn('无效的正则表达式:', keyword, e);
return false;
}
default:
return text.includes(keyword); // 默认使用包含匹配
}
}
/**
* 处理弹幕事件
* @param event 弹幕事件
@@ -40,95 +67,57 @@ export function useAutoReply(
) {
if (!roomId.value) return;
// 过滤有效的自动回复操作
const replyActions = actions.filter(action =>
action.triggerType === TriggerType.DANMAKU &&
action.enabled &&
(!action.triggerConfig.onlyDuringLive || isLive.value)
);
// 使用通用函数过滤有效的自动回复操作
const replyActions = filterValidActions(actions, TriggerType.DANMAKU, isLive);
if (replyActions.length === 0) return;
if (replyActions.length > 0 && roomId.value) {
const message = event.msg;
const message = event.msg;
const now = Date.now();
executeActions(
replyActions,
event,
TriggerType.DANMAKU,
roomId.value,
runtimeState,
{ sendLiveDanmaku },
{
customFilters: [
// 关键词和屏蔽词检查
(action, context) => {
const keywordMatchType = action.triggerConfig.keywordMatchType || KeywordMatchType.Contains;
const keywordMatch = action.triggerConfig.keywords?.some(kw =>
isKeywordMatch(message, kw, keywordMatchType)
);
if (!keywordMatch) return false;
// 准备执行上下文
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')
const blockwordMatchType = action.triggerConfig.blockwordMatchType || KeywordMatchType.Contains;
const blockwordMatch = action.triggerConfig.blockwords?.some(bw =>
isKeywordMatch(message, bw, blockwordMatchType)
);
return !blockwordMatch; // 如果匹配屏蔽词返回false否则返回true
}
],
// 附加选项:只处理第一个匹配的自动回复
customContextBuilder: (event, roomId, triggerType) => {
const now = Date.now();
const context = buildExecutionContext(event, roomId, triggerType);
// 添加时间段判断变量
context.variables.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 '深夜';
};
return context;
}
}
},
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; // 匹配到一个规则就停止
}
);
}
}

View File

@@ -1,14 +1,13 @@
import { EventModel } from '@/api/api-models';
import { ref, Ref } from 'vue';
import { EventModel, EventDataTypes } from '@/api/api-models';
import {
formatTemplate,
getRandomTemplate,
buildExecutionContext
} from '../utils';
executeActions,
filterValidActions
} from '../actionUtils';
import {
AutoActionItem,
TriggerType,
RuntimeState
RuntimeState,
TriggerType
} from '../types';
/**
@@ -25,7 +24,7 @@ export function useEntryWelcome(
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>
) {
// 运行时数据
const timer = ref<NodeJS.Timeout | null>(null);
const timer = ref<any | null>(null);
/**
* 处理入场事件 - 支持新的AutoActionItem结构
@@ -40,56 +39,31 @@ export function useEntryWelcome(
) {
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)
);
// 使用通用函数过滤有效的入场欢迎操作
const enterActions = filterValidActions(actions, TriggerType.ENTER, isLive, isTianXuanActive);
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);
// 使用通用执行函数处理入场事件
if (enterActions.length > 0 && roomId.value) {
executeActions(
enterActions,
event,
TriggerType.ENTER,
roomId.value,
runtimeState,
{ sendLiveDanmaku },
{
customFilters: [
// 检查入场过滤条件
(action, context) => {
if (action.triggerConfig.filterMode === 'blacklist' &&
action.triggerConfig.filterGiftNames?.includes(event.uname)) {
return false;
}
return true;
}
]
}
}
);
}
}

View File

@@ -1,8 +1,6 @@
import { ref, Ref } from 'vue';
import { EventModel, EventDataTypes } from '@/api/api-models';
import { EventModel } from '@/api/api-models';
import {
formatTemplate,
getRandomTemplate,
buildExecutionContext
} from '../utils';
import {
@@ -10,6 +8,10 @@ import {
TriggerType,
RuntimeState
} from '../types';
import {
filterValidActions,
executeActions
} from '../actionUtils';
/**
* 关注感谢模块
@@ -26,7 +28,7 @@ export function useFollowThank(
) {
// 运行时数据
const aggregatedFollows = ref<{uid: number, name: string, timestamp: number}[]>([]);
const timer = ref<NodeJS.Timeout | null>(null);
const timer = ref<any | null>(null);
/**
* 处理关注事件 - 支持新的AutoActionItem结构
@@ -41,52 +43,19 @@ export function useFollowThank(
) {
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)
);
// 使用通用函数过滤有效的关注感谢操作
const followActions = filterValidActions(actions, TriggerType.FOLLOW, isLive, isTianXuanActive);
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);
}
}
// 使用通用执行函数处理关注事件
if (followActions.length > 0 && roomId.value) {
executeActions(
followActions,
event,
TriggerType.FOLLOW,
roomId.value,
runtimeState,
{ sendLiveDanmaku }
);
}
}

View File

@@ -1,16 +1,19 @@
import { ref, Ref } from 'vue';
import { EventModel, EventDataTypes } from '@/api/api-models';
import {
formatTemplate,
getRandomTemplate,
buildExecutionContext
} from '../utils';
import { evaluateTemplateExpressions } from '../expressionEvaluator';
import {
AutoActionItem,
TriggerType,
ExecutionContext,
RuntimeState
} from '../types';
import {
filterValidActions,
executeActions
} from '../actionUtils';
/**
* 礼物感谢模块
@@ -25,10 +28,6 @@ export function useGiftThank(
isTianXuanActive: Ref<boolean>,
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>
) {
// 测试发送功能状态
const lastTestTime = ref(0);
const testCooldown = 5000; // 5秒冷却时间
const testLoading = ref(false);
/**
* 处理礼物事件
@@ -43,171 +42,52 @@ export function useGiftThank(
) {
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)
);
// 使用通用函数过滤有效的礼物感谢操作
const giftActions = filterValidActions(actions, TriggerType.GIFT, isLive, isTianXuanActive);
if (giftActions.length === 0) return;
// 使用通用执行函数处理礼物事件
if (giftActions.length > 0 && roomId.value) {
// 礼物基本信息
const giftName = event.msg;
const giftPrice = event.price / 1000;
// 礼物基本信息
const giftName = event.msg;
const giftPrice = event.price / 1000;
const giftCount = event.num;
executeActions(
giftActions,
event,
TriggerType.GIFT,
roomId.value,
runtimeState,
{ sendLiveDanmaku },
{
customFilters: [
// 礼物过滤逻辑
(action, context) => {
// 黑名单模式
if (action.triggerConfig.filterMode === 'blacklist' &&
action.triggerConfig.filterGiftNames?.includes(giftName)) {
return false;
}
// 创建执行上下文
const context = buildExecutionContext(event, roomId.value, TriggerType.GIFT);
// 白名单模式
if (action.triggerConfig.filterMode === 'whitelist' &&
!action.triggerConfig.filterGiftNames?.includes(giftName)) {
return false;
}
// 处理每个符合条件的操作
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.minValue && giftPrice < action.triggerConfig.minValue) {
return false;
}
// 礼物过滤逻辑
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);
return true;
}
]
}
}
}
}
/**
* 测试发送礼物感谢弹幕
*/
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
};
}

View File

@@ -1,162 +1,172 @@
import { Ref } from 'vue';
import { useStorage } from '@vueuse/core';
import { computed, Ref } from 'vue';
import { GuardLevel, EventModel } from '@/api/api-models';
import {
AutoActionItem,
TriggerType,
ActionType,
RuntimeState
RuntimeState,
ExecutionContext,
ActionType
} from '../types';
import { formatTemplate, buildExecutionContext } from '../utils';
import {
filterValidActions,
executeActions
} from '../actionUtils';
import { 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>
sendPrivateMessage: (uid: 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 event 舰长购买事件
* @param runtimeState 运行时状态
*/
function processGuard(
event: EventModel,
function handleGuardBuy(
actions: AutoActionItem[],
event: any,
runtimeState: RuntimeState
) {
if (!roomId.value) return;
const guardLevel = event.guard_level;
if (guardLevel === GuardLevel.None) return; // 不是上舰事件
// 使用通用函数过滤舰长事件的操作
const isLiveRef = computed(() => true);
const guardActions = filterValidActions(actions, TriggerType.GUARD, isLiveRef);
// 过滤出有效的舰长私信操作
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 && roomId.value) {
executeActions(
guardActions,
event,
TriggerType.GUARD,
roomId.value,
runtimeState,
{ sendPrivateMessage, sendLiveDanmaku },
{
customFilters: [
// 防止重复发送检查
(action, context) => {
if (action.triggerConfig.preventRepeat && event && event.uid) {
// 确保 uid 是数字类型
const uid = typeof event.uid === 'number' ? event.uid : parseInt(event.uid, 10);
if (guardActions.length === 0) return;
// 检查是否已经发送过
if (runtimeState.sentGuardPms.has(uid)) {
return false;
}
// 创建执行上下文
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);
// 添加到已发送集合
runtimeState.sentGuardPms.add(uid);
}
return true;
}
} 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);
],
customContextBuilder: (eventData, roomId, triggerType): ExecutionContext => {
// 使用标准上下文构建方法
const context = buildExecutionContext(eventData, roomId, triggerType);
// 如果是舰长事件且有事件数据,处理礼品码
if (triggerType === TriggerType.GUARD && eventData && eventData.guard_level !== undefined) {
const guardLevel = eventData.guard_level;
// 查找包含礼品码的操作
guardActions.forEach(action => {
// 找到对应等级的礼品码
if (action.triggerConfig.giftCodes && action.triggerConfig.giftCodes.length > 0) {
// 优先查找特定等级的礼品码
let levelCodesEntry = action.triggerConfig.giftCodes.find(gc => gc.level === guardLevel);
// 如果没有找到特定等级的礼品码尝试查找通用礼品码level为0
if (!levelCodesEntry) {
levelCodesEntry = action.triggerConfig.giftCodes.find(gc => gc.level === 0);
}
if (levelCodesEntry && levelCodesEntry.codes && levelCodesEntry.codes.length > 0) {
// 随机选择一个礼品码
const randomIndex = Math.floor(Math.random() * levelCodesEntry.codes.length);
const randomCode = levelCodesEntry.codes[randomIndex];
// 确保guard变量存在并设置礼品码
if (context.variables.guard) {
context.variables.guard.giftCode = randomCode;
// 在上下文中存储选中的礼品码信息以供后续消耗
context.variables.guard.selectedGiftCode = {
code: randomCode,
level: levelCodesEntry.level
};
}
}
}
});
}
return context;
},
onSuccess: (action: AutoActionItem, context: ExecutionContext) => {
// 检查是否需要消耗礼品码
if (
action.actionType === ActionType.SEND_PRIVATE_MSG &&
action.triggerConfig.consumeGiftCode &&
context.variables.guard?.selectedGiftCode
) {
const { code: selectedCode, level: selectedLevel } = context.variables.guard.selectedGiftCode;
console.log(`[AutoAction] 尝试消耗礼品码: ActionID=${action.id}, Level=${selectedLevel}, Code=${selectedCode}`);
// 确保 giftCodes 存在且为数组
if (Array.isArray(action.triggerConfig.giftCodes)) {
// 找到对应等级的礼品码条目
const levelCodesEntry = action.triggerConfig.giftCodes.find(gc => gc.level === selectedLevel);
if (levelCodesEntry && Array.isArray(levelCodesEntry.codes)) {
// 找到要删除的礼品码的索引
const codeIndex = levelCodesEntry.codes.indexOf(selectedCode);
if (codeIndex > -1) {
// 从数组中移除礼品码
levelCodesEntry.codes.splice(codeIndex, 1);
console.log(`[AutoAction] 成功消耗礼品码: ActionID=${action.id}, Level=${selectedLevel}, Code=${selectedCode}. 剩余 ${levelCodesEntry.codes.length} 个。`);
// !!! 重要提示: 此处直接修改了 action 对象。
// !!! 请确保你的状态管理允许这种修改,或者调用 store action 来持久化更新。
// 例如: store.updateActionGiftCodes(action.id, selectedLevel, levelCodesEntry.codes);
} else {
console.warn(`[AutoAction] 未能在等级 ${selectedLevel} 中找到要消耗的礼品码: ${selectedCode}, ActionID=${action.id}`);
}
} else {
console.warn(`[AutoAction] 未找到等级 ${selectedLevel} 的礼品码列表或列表格式不正确, ActionID=${action.id}`);
}
} else {
console.warn(`[AutoAction] Action ${action.id} 的 giftCodes 配置不存在或不是数组。`);
}
}
}
});
}
}
);
}
}
/**
* 获取舰长等级名称
* @param level 舰长等级
* @returns 舰长等级名称
*/
function getGuardLevelName(level: number): string {
switch (level) {
case 1: return '总督';
case 2: return '提督';
case 3: return '舰长';
default: return '未知等级';
}
}
return {
config,
processGuard
handleGuardBuy
};
}

View File

@@ -1,15 +1,18 @@
import { ref, watch, Ref, computed } from 'vue';
import { useStorage } from '@vueuse/core';
import {
getRandomTemplate,
formatTemplate,
buildExecutionContext
} from '../utils';
import {
AutoActionItem,
TriggerType,
RuntimeState
RuntimeState,
ExecutionContext
} from '../types';
import {
filterValidActions,
executeActions
} from '../actionUtils';
/**
* 定时弹幕模块
@@ -38,12 +41,8 @@ export function useScheduledDanmaku(
) {
if (!roomId.value) return;
// 获取定时消息操作
const scheduledActions = actions.filter(action =>
action.triggerType === TriggerType.SCHEDULED &&
action.enabled &&
(!action.triggerConfig.onlyDuringLive || isLive.value)
);
// 使用通用函数过滤有效的定时弹幕操作
const scheduledActions = filterValidActions(actions, TriggerType.SCHEDULED, isLive);
// 为每个定时操作设置定时器
scheduledActions.forEach(action => {
@@ -54,22 +53,30 @@ export function useScheduledDanmaku(
// 创建定时器函数
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);
// 使用通用执行函数处理定时操作
if (roomId.value) {
executeActions(
[action], // 只处理单个操作
null, // 定时操作没有触发事件
TriggerType.SCHEDULED,
roomId.value,
runtimeState,
{ sendLiveDanmaku },
{
skipUserFilters: true, // 定时任务不需要用户过滤
skipCooldownCheck: false // 可以保留冷却检查
}
);
}
// 设置下一次定时
runtimeState.scheduledTimers[action.id] = setTimeout(timerFn, intervalSeconds * 1000);
runtimeState.timerStartTimes[action.id] = Date.now(); // 更新定时器启动时间
};
// 首次启动定时器
runtimeState.scheduledTimers[action.id] = setTimeout(timerFn, intervalSeconds * 1000);
runtimeState.timerStartTimes[action.id] = Date.now(); // 记录定时器启动时间
});
}

View File

@@ -0,0 +1,79 @@
import { EventModel } from '@/api/api-models';
import { Ref } from 'vue';
import {
executeActions,
filterValidActions
} from '../actionUtils';
import {
AutoActionItem,
RuntimeState,
TriggerType
} from '../types';
/**
* 醒目留言感谢模块
* @param isLive 是否处于直播状态
* @param roomId 房间ID
* @param isTianXuanActive 是否处于天选时刻
* @param sendLiveDanmaku 发送弹幕函数
*/
export function useSuperChatThank(
isLive: Ref<boolean>,
roomId: Ref<number | undefined>,
isTianXuanActive: Ref<boolean>,
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>
) {
/**
* 处理醒目留言事件
* @param event 醒目留言事件
* @param actions 自动操作列表
* @param runtimeState 运行时状态
*/
function processSuperChat(
event: EventModel,
actions: AutoActionItem[],
runtimeState: RuntimeState
) {
if (!roomId.value) return;
// 使用通用函数过滤有效的SC感谢操作
const scActions = filterValidActions(actions, TriggerType.SUPER_CHAT, isLive, isTianXuanActive);
// 使用通用执行函数处理SC事件
if (scActions.length > 0 && roomId.value) {
executeActions(
scActions,
event,
TriggerType.SUPER_CHAT,
roomId.value,
runtimeState,
{ sendLiveDanmaku },
{
customFilters: [
// SC价格过滤
(action, context) => {
// 如果未设置SC过滤或选择了不过滤模式
if (!action.triggerConfig.scFilterMode || action.triggerConfig.scFilterMode === 'none') {
return true;
}
// 价格过滤模式
if (action.triggerConfig.scFilterMode === 'price' &&
action.triggerConfig.scMinPrice &&
event.price < action.triggerConfig.scMinPrice * 1000) {
return false;
}
return true;
}
]
}
);
}
}
return {
processSuperChat,
};
}

View File

@@ -1,6 +1,6 @@
// 统一的自动操作类型定义
import { EventModel } from '@/api/api-models';
import { EventModel, GuardLevel } from '@/api/api-models';
// 触发条件类型
export enum TriggerType {
@@ -20,6 +20,13 @@ export enum ActionType {
EXECUTE_COMMAND = 'execute_command', // 执行命令
}
// 关键词匹配类型
export enum KeywordMatchType {
Full = 'full', // 完全匹配
Contains = 'contains', // 包含匹配
Regex = 'regex', // 正则匹配
}
// 优先级
export enum Priority {
HIGHEST = 0,
@@ -36,7 +43,7 @@ export type AutoActionItem = {
enabled: boolean; // 是否启用
triggerType: TriggerType; // 触发类型
actionType: ActionType; // 操作类型
templates: string[]; // 模板列表
template: string; // 模板
priority: Priority; // 优先级
// 高级配置
@@ -45,33 +52,7 @@ export type AutoActionItem = {
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[]}[]; // 礼品码
};
triggerConfig: TriggerConfig;
// 动作特定配置
actionConfig: {
@@ -88,12 +69,78 @@ export interface ExecutionContext {
roomId?: number; // 直播间ID
variables: Record<string, any>; // 额外变量
timestamp: number; // 时间戳
// --- 新增运行时数据管理函数 ---
/** 获取运行时数据 */
getData: <T>(key: string, defaultValue?: T) => T | undefined;
/** 设置运行时数据 */
setData: <T>(key: string, value: T) => void;
/** 检查运行时数据是否存在 */
containsData: (key: string) => boolean;
/** 移除运行时数据 */
removeData: (key: string) => void;
// --- 新增持久化数据管理函数 ---
/** 获取持久化存储的数据 */
getStorageData: <T>(key: string, defaultValue?: T) => Promise<T | undefined>;
/** 设置持久化存储的数据 */
setStorageData: <T>(key: string, value: T) => Promise<void>;
/** 检查持久化存储中是否存在指定的键 */
hasStorageData: (key: string) => Promise<boolean>;
/** 从持久化存储中删除数据 */
removeStorageData: (key: string) => Promise<void>;
/** 清除所有持久化存储的数据 */
clearStorageData: () => Promise<void>;
}
// 运行状态接口
export interface RuntimeState {
lastExecutionTime: Record<string, number>; // 上次执行时间
aggregatedEvents: Record<string, any[]>; // 聚合的事件
scheduledTimers: Record<string, NodeJS.Timeout | null>; // 定时器
scheduledTimers: Record<string, any | null>; // 定时器 ID
timerStartTimes: Record<string, number>; // <--- 新增:独立定时器启动时间戳
globalTimerStartTime: number | null; // <--- 新增:全局定时器启动时间戳
sentGuardPms: Set<number>; // 已发送的舰长私信
}
export interface TriggerConfig {
// User filters
userFilterEnabled?: boolean;
requireMedal?: boolean;
requireCaptain?: boolean;
// Common conditions
onlyDuringLive?: boolean;
ignoreTianXuan?: boolean;
// Keywords for autoReply
keywords?: string[];
keywordMatchType?: KeywordMatchType;
blockwords?: string[];
blockwordMatchType?: KeywordMatchType;
// Gift filters
filterMode?: 'blacklist' | 'whitelist' | 'value' | 'none' | 'free';
filterGiftNames?: string[];
minValue?: number; // For gift and SC minimum value (元)
includeQuantity?: boolean; // 是否包含礼物数量
// SC相关配置
scFilterMode?: 'none' | 'price'; // SC过滤模式
scMinPrice?: number; // SC最低价格(元)
// Scheduled options
useGlobalTimer?: boolean;
intervalSeconds?: number;
schedulingMode?: 'random' | 'sequential';
// Guard related
guardLevels?: GuardLevel[];
preventRepeat?: boolean;
giftCodes?: { level: number; codes: string[] }[];
consumeGiftCode?: boolean; // 是否消耗礼品码
// Confirm message options
sendDanmakuConfirm?: boolean; // 是否发送弹幕确认
isConfirmMessage?: boolean; // 标记这是一个确认消息
}

View File

@@ -7,6 +7,16 @@ import {
RuntimeState,
ExecutionContext
} from './types';
import { get, set, del, clear, keys as idbKeys, createStore } from 'idb-keyval'; // 导入 useIDBKeyval
// --- 定义用户持久化数据的自定义存储区 ---
const USER_DATA_DB_NAME = 'AutoActionUserDataDB';
const USER_DATA_STORE_NAME = 'userData';
const userDataStore = createStore(USER_DATA_DB_NAME, USER_DATA_STORE_NAME);
// ----------------------------------------
// --- 定义运行时数据的前缀 (避免与页面其他 sessionStorage 冲突) ---
const RUNTIME_STORAGE_PREFIX = 'autoaction_runtime_';
/**
* 创建默认的运行时状态
@@ -15,6 +25,8 @@ export function createDefaultRuntimeState(): RuntimeState {
return {
lastExecutionTime: {},
scheduledTimers: {},
timerStartTimes: {},
globalTimerStartTime: null,
sentGuardPms: new Set(),
aggregatedEvents: {}
};
@@ -28,14 +40,14 @@ export function createDefaultAutoAction(triggerType: TriggerType): AutoActionIte
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 defaultTemplates: Record<TriggerType, string> = {
[TriggerType.DANMAKU]: '收到 {{user.name}} 的弹幕: {{danmaku.msg}}',
[TriggerType.GIFT]: '感谢 {{user.name}} 赠送的 {{gift.summary}}',
[TriggerType.GUARD]: '感谢 {{user.name}} 开通了{{danmaku.msg}}',
[TriggerType.FOLLOW]: '感谢 {{user.name}} 的关注!',
[TriggerType.ENTER]: '欢迎 {{user.name}} 进入直播间',
[TriggerType.SCHEDULED]: '这是一条定时消息,当前时间: {{date.formatted}}',
[TriggerType.SUPER_CHAT]: '感谢 {{user.name}} 的SC!',
};
// 根据不同触发类型设置默认名称
@@ -56,7 +68,7 @@ export function createDefaultAutoAction(triggerType: TriggerType): AutoActionIte
triggerType,
actionType: triggerType === TriggerType.GUARD ? ActionType.SEND_PRIVATE_MSG : ActionType.SEND_DANMAKU,
priority: Priority.NORMAL,
templates: defaultTemplates[triggerType] || ['默认模板'],
template: defaultTemplates[triggerType] || '默认模板',
logicalExpression: '',
executeCommand: '',
ignoreCooldown: false,
@@ -79,13 +91,12 @@ export function createDefaultAutoAction(triggerType: TriggerType): AutoActionIte
}
/**
* 从模板数组中随机选择一个
* @param templates 模板数组
* 处理模板字符串
* @param template 模板字符串
*/
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];
export function getRandomTemplate(template: string): string | null {
if (!template) return null;
return template;
}
/**
@@ -268,7 +279,6 @@ export function buildExecutionContext(
const now = Date.now();
const dateObj = new Date(now);
// 基础上下文
const context: ExecutionContext = {
event,
roomId,
@@ -295,6 +305,88 @@ export function buildExecutionContext(
if (hour < 22) return '晚上';
return '深夜';
}
},
// --- 实现运行时数据管理函数 (使用 sessionStorage) ---
getData: <T>(key: string, defaultValue?: T): T | undefined => {
const prefixedKey = RUNTIME_STORAGE_PREFIX + key;
try {
const storedValue = sessionStorage.getItem(prefixedKey);
if (storedValue === null) {
return defaultValue;
}
return JSON.parse(storedValue) as T;
} catch (error) {
console.error(`[Runtime SessionStorage] Error getting/parsing key '${key}':`, error);
return defaultValue;
}
},
setData: <T>(key: string, value: T): void => {
const prefixedKey = RUNTIME_STORAGE_PREFIX + key;
try {
// 不存储 undefined
if (value === undefined) {
sessionStorage.removeItem(prefixedKey);
return;
}
sessionStorage.setItem(prefixedKey, JSON.stringify(value));
} catch (error) {
console.error(`[Runtime SessionStorage] Error setting key '${key}':`, error);
// 如果序列化失败,可以选择移除旧键或保留
sessionStorage.removeItem(prefixedKey);
}
},
containsData: (key: string): boolean => {
const prefixedKey = RUNTIME_STORAGE_PREFIX + key;
return sessionStorage.getItem(prefixedKey) !== null;
},
removeData: (key: string): void => {
const prefixedKey = RUNTIME_STORAGE_PREFIX + key;
sessionStorage.removeItem(prefixedKey);
},
// --- 持久化数据管理函数 (不变,继续使用 userDataStore) ---
getStorageData: async <T>(key: string, defaultValue?: T): Promise<T | undefined> => {
try {
// 使用 userDataStore
const value = await get<T>(key, userDataStore);
return value === undefined ? defaultValue : value;
} catch (error) {
console.error(`[UserData IDB] getStorageData error for key '${key}':`, error);
return defaultValue;
}
},
setStorageData: async <T>(key: string, value: T): Promise<void> => {
try {
// 使用 userDataStore
await set(key, value, userDataStore);
} catch (error) {
console.error(`[UserData IDB] setStorageData error for key '${key}':`, error);
}
},
hasStorageData: async (key: string): Promise<boolean> => {
try {
// 使用 userDataStore
const value = await get(key, userDataStore);
return value !== undefined;
} catch (error) {
console.error(`[UserData IDB] hasStorageData error for key '${key}':`, error);
return false;
}
},
removeStorageData: async (key: string): Promise<void> => {
try {
// 使用 userDataStore
await del(key, userDataStore);
} catch (error) {
console.error(`[UserData IDB] removeStorageData error for key '${key}':`, error);
}
},
clearStorageData: async (): Promise<void> => {
try {
// 使用 userDataStore
await clear(userDataStore);
} catch (error) {
console.error('[UserData IDB] clearStorageData error:', error);
}
}
};
@@ -308,7 +400,7 @@ export function buildExecutionContext(
medalLevel: event.fans_medal_level,
medalName: event.fans_medal_name
};
context.variables.danmaku = event;
context.variables.message = event.msg;
// 根据不同触发类型添加特定变量

File diff suppressed because it is too large Load Diff

View File

@@ -105,8 +105,8 @@ export const useBiliFunction = defineStore('biliFunction', () => {
return false;
}
if (!message || message.trim().length === 0) {
console.warn("尝试发送空弹幕,已阻止。");
return false;
console.warn("尝试发送空弹幕,已阻止。");
return false;
}
roomId = 1294406; // 测试用房间号
const url = "https://api.live.bilibili.com/msg/send";
@@ -143,8 +143,21 @@ export const useBiliFunction = defineStore('biliFunction', () => {
const json = await response.json();
// B站成功码通常是 0
if (json.code !== 0) {
console.error("发送弹幕API失败:", json.code, json.message || json.msg);
return false;
window.$notification.error({
title: '发送弹幕失败',
description: `内容: ${message}`,
meta: () => h('div', {
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
},
}, () => `错误: ${json.code} - ${json.message || json.msg}`),
duration: 0,
});
console.error(`发送弹幕API失败 to: ${roomId} ${uid.value} [${message}] - ${json.code} - ${json.message || json.msg}`);
return false;
}
console.log("发送弹幕成功:", message);
@@ -221,9 +234,14 @@ export const useBiliFunction = defineStore('biliFunction', () => {
return false;
}
if (!message || message.trim().length === 0) {
const error = "尝试发送空私信,已阻止。";
console.warn(error);
return false;
const error = "尝试发送空私信,已阻止。";
console.warn(error);
window.$notification.error({
title: '发送私信失败',
description: `尝试发送空私信给 ${receiverId}, 已阻止`,
duration: 0,
});
return false;
}
try {
@@ -239,8 +257,8 @@ export const useBiliFunction = defineStore('biliFunction', () => {
}
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();
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);
@@ -301,10 +319,10 @@ export const useBiliFunction = defineStore('biliFunction', () => {
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;
const error = `发送私信API失败: ${json.code} - ${json.message}`;
console.error(error);
onSendPrivateMessageFailed(receiverId, message, error);
return false;
}
console.log(`发送私信给 ${receiverId} 成功`);