chore: format code style and update linting configuration

This commit is contained in:
Megghy
2025-10-02 10:38:23 +08:00
parent 6fd046adcd
commit 758549d29d
253 changed files with 16258 additions and 15833 deletions

View File

@@ -1,17 +1,18 @@
import { Ref } from 'vue';
import { EventModel } from '@/api/api-models';
import {
import type { Ref } from 'vue'
import type {
AutoActionItem,
TriggerType,
ExecutionContext,
RuntimeState,
TriggerType,
} from './types'
import type { EventModel } from '@/api/api-models'
import { useBiliCookie } from '../useBiliCookie'
import { evaluateTemplateExpressions } from './expressionEvaluator'
import {
ActionType,
ExecutionContext
} from './types';
import { buildExecutionContext, getRandomTemplate } from './utils';
import { evaluateTemplateExpressions } from './expressionEvaluator';
import { evaluateExpression } from './utils';
import { useBiliCookie } from '../useBiliCookie';
import { logDanmakuHistory, logPrivateMsgHistory, logCommandHistory } from './utils/historyLogger';
} from './types'
import { buildExecutionContext, evaluateExpression, getRandomTemplate } from './utils'
import { logCommandHistory, logDanmakuHistory, logPrivateMsgHistory } from './utils/historyLogger'
/**
* 过滤有效的自动操作项
@@ -28,44 +29,44 @@ export function filterValidActions(
isLive: Ref<boolean>,
isTianXuanActive?: Ref<boolean>,
options?: {
actionType?: ActionType; // 特定操作类型
customFilter?: (action: AutoActionItem) => boolean; // 自定义过滤器
actionType?: ActionType // 特定操作类型
customFilter?: (action: AutoActionItem) => boolean // 自定义过滤器
enabledTriggerTypes?: Ref<Record<TriggerType, boolean>> // 触发类型启用状态
}
},
): AutoActionItem[] {
return actions.filter(action => {
return actions.filter((action) => {
// 基本过滤条件
if (action.triggerType !== triggerType || !action.enabled) {
return false;
return false
}
// 检查触发类型是否启用
if (options?.enabledTriggerTypes && !options.enabledTriggerTypes.value[triggerType]) {
return false;
return false
}
// 直播状态过滤
if (action.triggerConfig.onlyDuringLive && !isLive.value) {
return false;
return false
}
// 天选时刻过滤
if (isTianXuanActive && action.triggerConfig.ignoreTianXuan && isTianXuanActive.value) {
return false;
return false
}
// 操作类型过滤
if (options?.actionType && action.actionType !== options.actionType) {
return false;
return false
}
// 自定义过滤器
if (options?.customFilter && !options.customFilter(action)) {
return false;
return false
}
return true;
});
return true
})
}
/**
@@ -76,18 +77,18 @@ export function filterValidActions(
*/
export function checkUserFilters(action: AutoActionItem, event: EventModel): boolean {
if (!action.triggerConfig.userFilterEnabled) {
return true;
return true
}
if (action.triggerConfig.requireMedal && !event.fans_medal_wearing_status) {
return false;
return false
}
if (action.triggerConfig.requireCaptain && !event.guard_level) {
return false;
return false
}
return true;
return true
}
/**
@@ -98,14 +99,14 @@ export function checkUserFilters(action: AutoActionItem, event: EventModel): boo
*/
export function checkCooldown(action: AutoActionItem, runtimeState: RuntimeState): boolean {
if (action.ignoreCooldown) {
return true;
return true
}
const now = Date.now();
const lastExecTime = runtimeState.lastExecutionTime[action.id] || 0;
const cooldownMs = (action.actionConfig.cooldownSeconds || 0) * 1000;
const now = Date.now()
const lastExecTime = runtimeState.lastExecutionTime[action.id] || 0
const cooldownMs = (action.actionConfig.cooldownSeconds || 0) * 1000
return now - lastExecTime >= cooldownMs;
return now - lastExecTime >= cooldownMs
}
/**
@@ -119,36 +120,36 @@ export function processTemplate(
action: AutoActionItem,
context: ExecutionContext,
options?: {
useRandomTemplate?: boolean; // 是否随机选择模板默认true
defaultValue?: string; // 如果模板为空或格式化失败时的默认值
}
useRandomTemplate?: boolean // 是否随机选择模板默认true
defaultValue?: string // 如果模板为空或格式化失败时的默认值
},
): string | null {
if (!action.template || action.template.trim() === '') {
console.warn(`跳过操作 "${action.name || '未命名'}":未设置有效模板`);
return options?.defaultValue || null;
console.warn(`跳过操作 "${action.name || '未命名'}":未设置有效模板`)
return options?.defaultValue || null
}
try {
// 获取模板内容
let template: string;
let template: string
if (options?.useRandomTemplate !== false) {
// 使用随机模板 (默认行为)
const randomTemplate = getRandomTemplate(action.template);
const randomTemplate = getRandomTemplate(action.template)
if (!randomTemplate) {
return options?.defaultValue || null;
return options?.defaultValue || null
}
template = randomTemplate;
template = randomTemplate
} else {
// 使用整个模板字符串
template = action.template;
template = action.template
}
// 格式化模板
const formattedContent = evaluateTemplateExpressions(template, context);
return formattedContent;
const formattedContent = evaluateTemplateExpressions(template, context)
return formattedContent
} catch (error) {
console.error(`模板处理错误 (${action.name || action.id}):`, error);
return options?.defaultValue || null;
console.error(`模板处理错误 (${action.name || action.id}):`, error)
return options?.defaultValue || null
}
}
@@ -157,30 +158,30 @@ async function sendAndLogDanmaku(
sendHandler: (roomId: number, message: string) => Promise<boolean>,
action: AutoActionItem,
roomId: number,
message: string
message: string,
): Promise<boolean> {
try {
const success = await sendHandler(roomId, message);
const success = await sendHandler(roomId, message)
logDanmakuHistory(
action.id,
action.name || '未命名操作',
message,
roomId,
success,
success ? undefined : '发送失败'
).catch(err => console.error('记录弹幕历史失败:', err));
return success;
success ? undefined : '发送失败',
).catch(err => console.error('记录弹幕历史失败:', err))
return success
} catch (err) {
console.error(`[AutoAction] 发送弹幕失败 (${action.name || action.id}):`, err);
console.error(`[AutoAction] 发送弹幕失败 (${action.name || action.id}):`, err)
logDanmakuHistory(
action.id,
action.name || '未命名操作',
message,
roomId,
false,
err instanceof Error ? err.toString() : String(err) // 确保err是字符串
).catch(e => console.error('记录弹幕历史失败:', e));
return false;
err instanceof Error ? err.toString() : String(err), // 确保err是字符串
).catch(e => console.error('记录弹幕历史失败:', e))
return false
}
}
@@ -201,91 +202,91 @@ export function executeActions(
roomId: number,
runtimeState: RuntimeState,
handlers: {
sendLiveDanmaku?: (roomId: number, message: string) => Promise<boolean>;
sendPrivateMessage?: (userId: number, message: string) => Promise<boolean>;
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;
}
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;
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);
: buildExecutionContext(event, roomId, triggerType)
// 应用自定义过滤器
if (options?.customFilters) {
const passesAllFilters = options.customFilters.every(filter => filter(action, context));
if (!passesAllFilters) continue;
const passesAllFilters = options.customFilters.every(filter => filter(action, context))
if (!passesAllFilters) continue
}
// 检查用户过滤条件
if (!options?.skipUserFilters && event && !checkUserFilters(action, event)) {
continue;
continue
}
// 检查逻辑表达式
if (action.logicalExpression && event) {
if (!evaluateExpression(action.logicalExpression, context)) {
continue;
continue
}
}
// 检查冷却时间
if (!options?.skipCooldownCheck && !checkCooldown(action, runtimeState)) {
continue;
continue
}
// 根据操作类型执行不同的处理逻辑
switch (action.actionType) {
case ActionType.SEND_DANMAKU:
if (!biliCookie.isCookieValid) {
continue; // 如果未登录,则跳过
continue // 如果未登录,则跳过
}
if (handlers.sendLiveDanmaku) {
// 处理弹幕发送
const message = processTemplate(action, context);
const message = processTemplate(action, context)
if (message) {
// 更新冷却时间
runtimeState.lastExecutionTime[action.id] = Date.now();
runtimeState.lastExecutionTime[action.id] = Date.now()
const sendAction = () => sendAndLogDanmaku(handlers.sendLiveDanmaku!, action, roomId, message);
const sendAction = async () => sendAndLogDanmaku(handlers.sendLiveDanmaku!, action, roomId, message)
// 延迟发送
if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
setTimeout(sendAction, action.actionConfig.delaySeconds * 1000);
setTimeout(sendAction, action.actionConfig.delaySeconds * 1000)
} else {
sendAction();
sendAction()
}
}
} else {
console.warn(`[AutoAction] 未提供弹幕发送处理器,无法执行操作: ${action.name || action.id}`);
console.warn(`[AutoAction] 未提供弹幕发送处理器,无法执行操作: ${action.name || action.id}`)
}
break;
break
case ActionType.SEND_PRIVATE_MSG:
if (!biliCookie.isCookieValid) {
continue; // 如果未登录,则跳过
continue // 如果未登录,则跳过
}
if (handlers.sendPrivateMessage && event && event.uid) {
// 处理私信发送
const message = processTemplate(action, context);
const message = processTemplate(action, context)
if (message) {
// 更新冷却时间(私信也可以有冷却时间)
runtimeState.lastExecutionTime[action.id] = Date.now();
runtimeState.lastExecutionTime[action.id] = Date.now()
const sendPmPromise = (uid: number, msg: string) => {
const sendPmPromise = async (uid: number, msg: string) => {
return handlers.sendPrivateMessage!(uid, msg)
.then(success => {
.then((success) => {
// 记录私信发送历史
logPrivateMsgHistory(
action.id,
@@ -293,17 +294,17 @@ export function executeActions(
msg,
uid,
success,
success ? undefined : '发送失败'
).catch(err => console.error('记录私信历史失败:', err));
success ? undefined : '发送失败',
).catch(err => console.error('记录私信历史失败:', err))
if (success && options?.onSuccess) {
// 发送成功后调用 onSuccess 回调
options.onSuccess(action, context);
options.onSuccess(action, context)
}
return success;
return success
})
.catch(err => {
console.error(`[AutoAction] 发送私信失败 (${action.name || action.id}):`, err);
.catch((err) => {
console.error(`[AutoAction] 发送私信失败 (${action.name || action.id}):`, err)
// 记录失败的发送
logPrivateMsgHistory(
action.id,
@@ -311,47 +312,47 @@ export function executeActions(
msg,
uid,
false,
err instanceof Error ? err.toString() : String(err) // 确保err是字符串
).catch(e => console.error('记录私信历史失败:', e));
return false; // 明确返回 false 表示失败
});
};
err instanceof Error ? err.toString() : String(err), // 确保err是字符串
).catch(e => console.error('记录私信历史失败:', e))
return false // 明确返回 false 表示失败
})
}
// 私信通常不需要延迟,但我们也可以支持
if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
setTimeout(() => {
sendPmPromise(event.uid, message);
}, action.actionConfig.delaySeconds * 1000);
sendPmPromise(event.uid, message)
}, action.actionConfig.delaySeconds * 1000)
} else {
sendPmPromise(event.uid, message);
sendPmPromise(event.uid, message)
}
}
} else {
console.warn(`[AutoAction] 未提供私信发送处理器或事件缺少UID无法执行操作: ${action.name || action.id}`);
console.warn(`[AutoAction] 未提供私信发送处理器或事件缺少UID无法执行操作: ${action.name || action.id}`)
}
break;
break
case ActionType.EXECUTE_COMMAND:
// 执行自定义命令
const command = processTemplate(action, context);
const command = processTemplate(action, context)
if (command) {
// 更新冷却时间
runtimeState.lastExecutionTime[action.id] = Date.now();
runtimeState.lastExecutionTime[action.id] = Date.now()
// 目前只记录执行历史,具体实现可在未来扩展
logCommandHistory(
action.id,
action.name || '未命名操作',
command,
true
).catch(err => console.error('记录命令执行历史失败:', err));
true,
).catch(err => console.error('记录命令执行历史失败:', err))
console.warn(`[AutoAction] 暂不支持执行自定义命令: ${action.name || action.id}`);
console.warn(`[AutoAction] 暂不支持执行自定义命令: ${action.name || action.id}`)
}
break;
break
default:
console.warn(`[AutoAction] 未知的操作类型: ${action.actionType}`);
console.warn(`[AutoAction] 未知的操作类型: ${action.actionType}`)
}
}
}
}

View File

@@ -3,13 +3,13 @@
*/
// 导入ExecutionContext类型
import { ExecutionContext } from './types';
import type { ExecutionContext } from './types'
// 表达式模式匹配
// {{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
export const JS_EXPRESSION_REGEX = /\{\{\s*(js(?:\+|-run)?):\s*(.*?)\s*\}\}/gs // 使 s
/**
* 处理模板中的表达式
@@ -20,14 +20,14 @@ export const JS_EXPRESSION_REGEX = /\{\{\s*(js(?:\+|\-run)?):\s*(.*?)\s*\}\}/gs;
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 ""; // 或者抛出错误,或者返回一个默认值
console.error('[evaluateTemplateExpressions] Error: Expected template to be a string, but received:', typeof template, template)
return '' // 或者抛出错误,或者返回一个默认值
}
if (!template) return "";
if (!template) return ''
// 获取基础变量和数据管理函数
const variables = context.variables;
const variables = context.variables
const dataFunctions = {
getData: context.getData,
setData: context.setData,
@@ -38,72 +38,71 @@ export function evaluateTemplateExpressions(template: string, context: Execution
hasStorageData: context.hasStorageData,
removeStorageData: context.removeStorageData,
clearStorageData: context.clearStorageData,
};
}
// 合并基础变量和数据管理函数的作用域
const scopeVariables = { ...variables, ...dataFunctions };
const scopeKeys = Object.keys(scopeVariables);
const scopeValues = Object.values(scopeVariables);
const scopeVariables = { ...variables, ...dataFunctions }
const scopeKeys = Object.keys(scopeVariables)
const scopeValues = Object.values(scopeVariables)
// 第一步:处理简单的文本替换 {{variable.path}}
let result = template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
const result = template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
if (path.trim().startsWith('js:') || path.trim().startsWith('js+:') || path.trim().startsWith('js-run:')) {
return match; // 跳过所有JS变体留给下一步
return match // 跳过所有JS变体留给下一步
}
try {
// 解析路径
const parts = path.trim().split('.');
let value: any = scopeVariables;
const parts = path.trim().split('.')
let value: any = scopeVariables
// 递归获取嵌套属性
for (const part of parts) {
if (value === undefined || value === null) return match;
if (value === undefined || value === null) return match
if (dataFunctions.hasOwnProperty(part) && parts.length === 1) {
value = value[part]; // 不要调用顶层函数
value = value[part] // 不要调用顶层函数
} else if (typeof value[part] === 'function') {
value = value[part]();
value = value[part]()
} else {
value = value[part];
value = value[part]
}
if (typeof value === 'function' && !dataFunctions.hasOwnProperty(part)) value = value();
if (typeof value === 'function' && !dataFunctions.hasOwnProperty(part)) value = value()
}
return value !== undefined && value !== null ? String(value) : match;
return value !== undefined && value !== null ? String(value) : match
} catch (error) {
console.error('模板格式化错误:', error);
return match; // 出错时返回原始匹配项
console.error('模板格式化错误:', error)
return match // 出错时返回原始匹配项
}
});
})
// 第二步:处理 JS 表达式和代码块 {{js: ...}}, {{js+: ...}}, {{js-run: ...}}
return result.replace(JS_EXPRESSION_REGEX, (match, type, code) => {
try {
let functionBody: string;
let functionBody: string
if (type === 'js') {
// 简单表达式: 隐式 return
functionBody = `try { return (${code}); } catch (e) { console.error("表达式[js:]执行错误:", e, "代码:", ${JSON.stringify(code)}); return \"[表达式错误: \" + e.message + \"]\"; }`;
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 + \"]\"; }`;
functionBody = `try { ${code} } catch (e) { console.error("代码块[js+/js-run:]执行错误:", e, "代码:", ${JSON.stringify(code)}); return "[代码块错误: " + e.message + "]"; }`
}
const evalInContext = new Function(...scopeKeys, functionBody);
const evalInContext = new Function(...scopeKeys, functionBody)
const evalResult = evalInContext(...scopeValues);
const evalResult = evalInContext(...scopeValues)
// 对结果进行处理,将 undefined/null 转换为空字符串,除非是错误消息
return typeof evalResult === 'string' && (evalResult.startsWith('[表达式错误:') || evalResult.startsWith('[代码块错误:'))
? evalResult
: String(evalResult ?? '');
: String(evalResult ?? '')
} catch (error) {
// 捕获 Function 构造或顶层执行错误
console.error("JS占位符处理错误:", error, "类型:", type, "代码:", code);
return `[处理错误: ${(error as Error).message}]`;
console.error('JS占位符处理错误:', error, '类型:', type, '代码:', code)
return `[处理错误: ${(error as Error).message}]`
}
});
})
}
/**
@@ -112,7 +111,7 @@ export function evaluateTemplateExpressions(template: string, context: Execution
* @returns 是否包含表达式
*/
export function containsJsExpression(template: string): boolean {
return JS_EXPRESSION_REGEX.test(template);
return JS_EXPRESSION_REGEX.test(template)
}
/**
@@ -121,7 +120,7 @@ export function containsJsExpression(template: string): boolean {
* @returns 转义后的字符串
*/
export function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
/**
@@ -132,16 +131,16 @@ export function escapeRegExp(string: string): string {
* @returns 转换后的模板
*/
export function convertToJsExpressions(template: string, placeholders: { name: string, description: string }[]): string {
let result = template;
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}}}`);
});
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;
return result
}
/**
@@ -153,11 +152,11 @@ export function convertToJsExpressions(template: string, placeholders: { name: s
*/
export function extractJsExpressions(template: string): string[] {
if (!template) {
return [];
return []
}
// 使用全局匹配来查找所有出现
const matches = template.match(JS_EXPRESSION_REGEX);
return matches || []; // match 返回 null 或字符串数组
const matches = template.match(JS_EXPRESSION_REGEX)
return matches || [] // match 返回 null 或字符串数组
}
/**
@@ -166,8 +165,7 @@ export function extractJsExpressions(template: string): string[] {
* @param gift 礼物信息
* @returns 上下文对象
*/
export function createGiftThankContext(user: { uid: number; name: string },
gift: { name: string; count: number; price: number }): Record<string, any> {
export function createGiftThankContext(user: { uid: number, name: string }, gift: { name: string, count: number, price: number }): Record<string, any> {
return {
user: {
uid: user.uid,
@@ -182,7 +180,7 @@ export function createGiftThankContext(user: { uid: number; name: string },
totalPrice: gift.count * gift.price,
// 工具方法
summary: `${gift.count}${gift.name}`,
isExpensive: gift.price >= 50
isExpensive: gift.price >= 50,
},
// 工具函数
format: {
@@ -193,7 +191,7 @@ export function createGiftThankContext(user: { uid: number; name: string },
date: {
now: new Date(),
timestamp: Date.now(),
formatted: new Intl.DateTimeFormat('zh-CN').format(new Date())
}
};
}
formatted: new Intl.DateTimeFormat('zh-CN').format(new Date()),
},
}
}

View File

@@ -1,18 +1,21 @@
import { ref, Ref } from 'vue';
import { EventModel } from '@/api/api-models';
import {
import type { Ref } from 'vue'
import type {
AutoActionItem,
TriggerType,
RuntimeState,
KeywordMatchType
} from '../types';
import {
buildExecutionContext
} from '../utils';
} from '../types'
import type { EventModel } from '@/api/api-models'
import { ref } from 'vue'
import {
executeActions,
filterValidActions,
executeActions
} from '../actionUtils';
} from '../actionUtils'
import {
KeywordMatchType,
TriggerType,
} from '../types'
import {
buildExecutionContext,
} from '../utils'
/**
* 自动回复模块
@@ -23,10 +26,10 @@ import {
export function useAutoReply(
isLive: Ref<boolean>,
roomId: Ref<number | undefined>,
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>,
) {
// 运行时数据 - 记录特定关键词的最后回复时间
const lastReplyTimestamps = ref<{ [keyword: string]: number; }>({});
const lastReplyTimestamps = ref<{ [keyword: string]: number }>({})
/**
* 检查关键词匹配
@@ -38,19 +41,19 @@ export function useAutoReply(
function isKeywordMatch(text: string, keyword: string, matchType: KeywordMatchType = KeywordMatchType.Contains): boolean {
switch (matchType) {
case KeywordMatchType.Full:
return text === keyword;
return text === keyword
case KeywordMatchType.Contains:
return text.includes(keyword);
return text.includes(keyword)
case KeywordMatchType.Regex:
try {
const regex = new RegExp(keyword);
return regex.test(text);
const regex = new RegExp(keyword)
return regex.test(text)
} catch (e) {
console.warn('无效的正则表达式:', keyword, e);
return false;
console.warn('无效的正则表达式:', keyword, e)
return false
}
default:
return text.includes(keyword); // 默认使用包含匹配
return text.includes(keyword) // 默认使用包含匹配
}
}
@@ -63,15 +66,15 @@ export function useAutoReply(
function onDanmaku(
event: EventModel,
actions: AutoActionItem[],
runtimeState: RuntimeState
runtimeState: RuntimeState,
) {
if (!roomId.value) return;
if (!roomId.value) return
// 使用通用函数过滤有效的自动回复操作
const replyActions = filterValidActions(actions, TriggerType.DANMAKU, isLive);
const replyActions = filterValidActions(actions, TriggerType.DANMAKU, isLive)
if (replyActions.length > 0 && roomId.value) {
const message = event.msg;
const message = event.msg
executeActions(
replyActions,
@@ -84,56 +87,56 @@ export function useAutoReply(
customFilters: [
// 关键词和屏蔽词检查
(action, context) => {
const keywordMatchType = action.triggerConfig.keywordMatchType || KeywordMatchType.Contains;
const keywordMatchType = action.triggerConfig.keywordMatchType || KeywordMatchType.Contains
const keywordMatch = action.triggerConfig.keywords?.some(kw =>
isKeywordMatch(message, kw, keywordMatchType)
);
if (!keywordMatch) return false;
isKeywordMatch(message, kw, keywordMatchType),
)
if (!keywordMatch) return false
const blockwordMatchType = action.triggerConfig.blockwordMatchType || KeywordMatchType.Contains;
const blockwordMatchType = action.triggerConfig.blockwordMatchType || KeywordMatchType.Contains
const blockwordMatch = action.triggerConfig.blockwords?.some(bw =>
isKeywordMatch(message, bw, blockwordMatchType)
);
return !blockwordMatch; // 如果匹配屏蔽词返回false否则返回true
}
isKeywordMatch(message, bw, blockwordMatchType),
)
return !blockwordMatch // 如果匹配屏蔽词返回false否则返回true
},
],
// 附加选项:只处理第一个匹配的自动回复
customContextBuilder: (event, roomId, triggerType) => {
const now = Date.now();
const context = buildExecutionContext(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 '深夜';
};
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;
}
}
);
return context
},
},
)
}
}
// 重置冷却时间 (用于测试)
function resetCooldowns(runtimeState: RuntimeState, actionId?: string) {
if (actionId) {
delete runtimeState.lastExecutionTime[actionId];
delete runtimeState.lastExecutionTime[actionId]
} else {
Object.keys(runtimeState.lastExecutionTime).forEach(id => {
delete runtimeState.lastExecutionTime[id];
});
Object.keys(runtimeState.lastExecutionTime).forEach((id) => {
delete runtimeState.lastExecutionTime[id]
})
}
}
return {
onDanmaku,
resetCooldowns
};
}
resetCooldowns,
}
}

View File

@@ -1,38 +1,40 @@
import { CheckInResult, EventDataTypes, EventModel } from '@/api/api-models';
import { QueryGetAPI } from '@/api/query';
import { CHECKIN_API_URL } from '@/data/constants';
import { useIDBKeyval } from '@vueuse/integrations/useIDBKeyval';
import { v4 as uuidv4 } from 'uuid';
import { Ref } from 'vue';
import { executeActions } from '../actionUtils';
import { ActionType, AutoActionItem, KeywordMatchType, Priority, RuntimeState, TriggerType } from '../types';
import { buildExecutionContext, createDefaultAutoAction } from '../utils';
import { useAccount } from '@/api/account';
import type { Ref } from 'vue'
import type { AutoActionItem, RuntimeState } from '../types'
import type { CheckInResult, EventModel } from '@/api/api-models'
import { useIDBKeyval } from '@vueuse/integrations/useIDBKeyval'
import { v4 as uuidv4 } from 'uuid'
import { useAccount } from '@/api/account'
import { EventDataTypes } from '@/api/api-models'
import { QueryGetAPI } from '@/api/query'
import { CHECKIN_API_URL } from '@/data/constants'
import { executeActions } from '../actionUtils'
import { ActionType, KeywordMatchType, Priority, TriggerType } from '../types'
import { buildExecutionContext, createDefaultAutoAction } from '../utils'
// 签到配置接口
export interface CheckInConfig {
sendReply: boolean; // 是否发送签到回复消息
successAction: AutoActionItem; // 使用 AutoActionItem 替代字符串
cooldownAction: AutoActionItem; // 使用 AutoActionItem 替代字符串
sendReply: boolean // 是否发送签到回复消息
successAction: AutoActionItem // 使用 AutoActionItem 替代字符串
cooldownAction: AutoActionItem // 使用 AutoActionItem 替代字符串
earlyBird: {
enabled: boolean;
successAction: AutoActionItem; // 使用 AutoActionItem 替代字符串
};
enabled: boolean
successAction: AutoActionItem // 使用 AutoActionItem 替代字符串
}
}
// 创建默认配置
function createDefaultCheckInConfig(): CheckInConfig {
const successAction = createDefaultAutoAction(TriggerType.DANMAKU);
successAction.name = '签到成功回复';
successAction.template = '@{{user.name}} 签到成功,获得 {{checkin.points}} 积分,连续签到 {{checkin.consecutiveDays}} 天';
const successAction = createDefaultAutoAction(TriggerType.DANMAKU)
successAction.name = '签到成功回复'
successAction.template = '@{{user.name}} 签到成功,获得 {{checkin.points}} 积分,连续签到 {{checkin.consecutiveDays}} 天'
const cooldownAction = createDefaultAutoAction(TriggerType.DANMAKU);
cooldownAction.name = '签到冷却回复';
cooldownAction.template = '{{user.name}} 你今天已经签到过了,明天再来吧~';
const cooldownAction = createDefaultAutoAction(TriggerType.DANMAKU)
cooldownAction.name = '签到冷却回复'
cooldownAction.template = '{{user.name}} 你今天已经签到过了,明天再来吧~'
const earlyBirdAction = createDefaultAutoAction(TriggerType.DANMAKU);
earlyBirdAction.name = '早鸟签到回复';
earlyBirdAction.template = '恭喜 {{user.name}} 完成早鸟签到!获得 {{checkin.points}} 积分,连续签到 {{checkin.consecutiveDays}} 天!';
const earlyBirdAction = createDefaultAutoAction(TriggerType.DANMAKU)
earlyBirdAction.name = '早鸟签到回复'
earlyBirdAction.template = '恭喜 {{user.name}} 完成早鸟签到!获得 {{checkin.points}} 积分,连续签到 {{checkin.consecutiveDays}} 天!'
return {
sendReply: true, // 默认发送回复消息
@@ -40,9 +42,9 @@ function createDefaultCheckInConfig(): CheckInConfig {
cooldownAction,
earlyBird: {
enabled: false,
successAction: earlyBirdAction
}
};
successAction: earlyBirdAction,
},
}
}
/**
@@ -53,7 +55,7 @@ export function useCheckIn(
roomId: Ref<number | undefined>,
liveStartTime: Ref<number | null>,
isTianXuanActive: Ref<boolean>,
sendDanmaku: (roomId: number, message: string) => Promise<boolean>
sendDanmaku: (roomId: number, message: string) => Promise<boolean>,
) {
// 使用 IndexedDB 持久化存储签到配置
const { data: checkInConfig, isFinished: isConfigLoaded } = useIDBKeyval<CheckInConfig>(
@@ -61,59 +63,61 @@ export function useCheckIn(
createDefaultCheckInConfig(),
{
onError: (err) => {
console.error('[CheckIn] IDB 错误 (配置):', err);
}
}
);
const accountInfo = useAccount();
console.error('[CheckIn] IDB 错误 (配置):', err)
},
},
)
const accountInfo = useAccount()
// 处理签到弹幕 - 调用服务端API
async function processCheckIn(
event: EventModel,
runtimeState: RuntimeState
runtimeState: RuntimeState,
) {
// 确保配置已加载
if (!isConfigLoaded.value) {
console.log('[CheckIn] 配置尚未加载完成,跳过处理');
return;
console.log('[CheckIn] 配置尚未加载完成,跳过处理')
return
}
if (!accountInfo.value.settings.point.enableCheckIn) {
return;
return
}
// 跳过非弹幕事件
if (event.type !== EventDataTypes.Message) {
return;
return
}
// 检查弹幕内容是否匹配签到指令
if (event.msg?.trim() !== accountInfo.value.settings.point.checkInKeyword.trim()) {
return;
return
}
const username = event.uname || event.uid || event.open_id || '用户';
const username = event.uname || event.uid || event.open_id || '用户'
try {
// 调用服务端API进行签到
const apiUrl = `${CHECKIN_API_URL}check-in-for`;
const apiUrl = `${CHECKIN_API_URL}check-in-for`
// 使用query.ts中的QueryGetAPI替代fetch
const response = await QueryGetAPI<CheckInResult>(apiUrl, event.uid ? {
uid: event.uid,
name: username
} : {
oId: event.ouid,
name: username
});
const response = await QueryGetAPI<CheckInResult>(apiUrl, event.uid
? {
uid: event.uid,
name: username,
}
: {
oId: event.ouid,
name: username,
})
const checkInResult = response.data;
const checkInResult = response.data
if (checkInResult) {
if (checkInResult.success) {
// 签到成功
if (roomId.value && checkInConfig.value.sendReply) {
const isEarlyBird = liveStartTime.value && (Date.now() - liveStartTime.value < 30 * 60 * 1000);
const isEarlyBird = liveStartTime.value && (Date.now() - liveStartTime.value < 30 * 60 * 1000)
// 构建签到数据上下文
const checkInData = {
@@ -121,15 +125,15 @@ export function useCheckIn(
points: checkInResult.points,
consecutiveDays: checkInResult.consecutiveDays,
todayRank: checkInResult.todayRank,
time: new Date()
}
};
time: new Date(),
},
}
// 执行回复动作
const successContext = buildExecutionContext(event, roomId.value, TriggerType.DANMAKU, checkInData);
const successContext = buildExecutionContext(event, roomId.value, TriggerType.DANMAKU, checkInData)
const action = isEarlyBird && checkInConfig.value.earlyBird.enabled
? checkInConfig.value.earlyBird.successAction
: checkInConfig.value.successAction;
: checkInConfig.value.successAction
executeActions(
[action],
@@ -139,21 +143,21 @@ export function useCheckIn(
runtimeState,
{ sendLiveDanmaku: sendDanmaku },
{
customContextBuilder: () => successContext
}
);
customContextBuilder: () => successContext,
},
)
}
// 显示签到成功通知
window.$notification.success({
title: '签到成功',
description: `${username} 完成签到, 获得 ${checkInResult.points} 积分, 连续签到 ${checkInResult.consecutiveDays}`,
duration: 5000
});
duration: 5000,
})
} else {
// 签到失败 - 今天已经签到过
if (roomId.value && checkInConfig.value.sendReply) {
const cooldownContext = buildExecutionContext(event, roomId.value, TriggerType.DANMAKU);
const cooldownContext = buildExecutionContext(event, roomId.value, TriggerType.DANMAKU)
executeActions(
[checkInConfig.value.cooldownAction],
@@ -163,33 +167,33 @@ export function useCheckIn(
runtimeState,
{ sendLiveDanmaku: sendDanmaku },
{
customContextBuilder: () => cooldownContext
}
);
customContextBuilder: () => cooldownContext,
},
)
}
// 显示签到失败通知
window.$notification.info({
title: '签到提示',
description: checkInResult.message || `${username} 重复签到, 已忽略`,
duration: 5000
});
duration: 5000,
})
}
}
} catch (error) {
console.error('[CheckIn] 处理签到失败:', error);
console.error('[CheckIn] 处理签到失败:', error)
window.$notification.error({
title: '签到错误',
description: `签到请求失败:${error instanceof Error ? error.message : String(error)}`,
duration: 5000
});
duration: 5000,
})
}
}
return {
checkInConfig,
processCheckIn
};
processCheckIn,
}
}
/**
@@ -212,11 +216,11 @@ export function createCheckInAutoActions(): AutoActionItem[] {
executeCommand: '',
triggerConfig: {
keywords: ['签到'],
keywordMatchType: KeywordMatchType.Full
keywordMatchType: KeywordMatchType.Full,
},
actionConfig: {
cooldownSeconds: 86400 // 24小时确保每天只能签到一次
}
cooldownSeconds: 86400, // 24小时确保每天只能签到一次
},
},
// 早鸟签到成功响应
{
@@ -232,11 +236,11 @@ export function createCheckInAutoActions(): AutoActionItem[] {
executeCommand: '',
triggerConfig: {
keywords: ['签到'],
keywordMatchType: KeywordMatchType.Full
keywordMatchType: KeywordMatchType.Full,
},
actionConfig: {
cooldownSeconds: 86400 // 24小时
}
cooldownSeconds: 86400, // 24小时
},
},
// 签到冷却期提示
{
@@ -252,9 +256,9 @@ export function createCheckInAutoActions(): AutoActionItem[] {
executeCommand: '',
triggerConfig: {
keywords: ['签到'],
keywordMatchType: KeywordMatchType.Full
keywordMatchType: KeywordMatchType.Full,
},
actionConfig: {}
}
];
}
actionConfig: {},
},
]
}

View File

@@ -1,14 +1,17 @@
import { EventModel } from '@/api/api-models';
import { ref, Ref } from 'vue';
import {
executeActions,
filterValidActions
} from '../actionUtils';
import {
import type { Ref } from 'vue'
import type {
AutoActionItem,
RuntimeState,
TriggerType
} from '../types';
} from '../types'
import type { EventModel } from '@/api/api-models'
import { ref } from 'vue'
import {
executeActions,
filterValidActions,
} from '../actionUtils'
import {
TriggerType,
} from '../types'
/**
* 入场欢迎模块
@@ -21,10 +24,10 @@ export function useEntryWelcome(
isLive: Ref<boolean>,
roomId: Ref<number | undefined>,
isTianXuanActive: Ref<boolean>,
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>,
) {
// 运行时数据
const timer = ref<any | null>(null);
const timer = ref<any | null>(null)
/**
* 处理入场事件 - 支持新的AutoActionItem结构
@@ -35,12 +38,12 @@ export function useEntryWelcome(
function processEnter(
event: EventModel,
actions: AutoActionItem[],
runtimeState: RuntimeState
runtimeState: RuntimeState,
) {
if (!roomId.value) return;
if (!roomId.value) return
// 使用通用函数过滤有效的入场欢迎操作
const enterActions = filterValidActions(actions, TriggerType.ENTER, isLive, isTianXuanActive);
const enterActions = filterValidActions(actions, TriggerType.ENTER, isLive, isTianXuanActive)
// 使用通用执行函数处理入场事件
if (enterActions.length > 0 && roomId.value) {
@@ -55,15 +58,15 @@ export function useEntryWelcome(
customFilters: [
// 检查入场过滤条件
(action, context) => {
if (action.triggerConfig.filterMode === 'blacklist' &&
action.triggerConfig.filterGiftNames?.includes(event.uname)) {
return false;
if (action.triggerConfig.filterMode === 'blacklist'
&& action.triggerConfig.filterGiftNames?.includes(event.uname)) {
return false
}
return true;
}
]
}
);
return true
},
],
},
)
}
}
@@ -72,13 +75,13 @@ export function useEntryWelcome(
*/
function clearTimer() {
if (timer.value) {
clearTimeout(timer.value);
timer.value = null;
clearTimeout(timer.value)
timer.value = null
}
}
return {
processEnter,
clearTimer
};
}
clearTimer,
}
}

View File

@@ -1,17 +1,18 @@
import { ref, Ref } from 'vue';
import { EventModel } from '@/api/api-models';
import {
buildExecutionContext
} from '../utils';
import {
import type { Ref } from 'vue'
import type {
AutoActionItem,
TriggerType,
RuntimeState
} from '../types';
RuntimeState,
} from '../types'
import type { EventModel } from '@/api/api-models'
import { ref } from 'vue'
import {
executeActions,
filterValidActions,
executeActions
} from '../actionUtils';
} from '../actionUtils'
import {
TriggerType,
} from '../types'
/**
* 关注感谢模块
@@ -24,11 +25,11 @@ export function useFollowThank(
isLive: Ref<boolean>,
roomId: Ref<number | undefined>,
isTianXuanActive: Ref<boolean>,
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>,
) {
// 运行时数据
const aggregatedFollows = ref<{uid: number, name: string, timestamp: number}[]>([]);
const timer = ref<any | null>(null);
const aggregatedFollows = ref<{ uid: number, name: string, timestamp: number }[]>([])
const timer = ref<any | null>(null)
/**
* 处理关注事件 - 支持新的AutoActionItem结构
@@ -39,12 +40,12 @@ export function useFollowThank(
function processFollow(
event: EventModel,
actions: AutoActionItem[],
runtimeState: RuntimeState
runtimeState: RuntimeState,
) {
if (!roomId.value) return;
if (!roomId.value) return
// 使用通用函数过滤有效的关注感谢操作
const followActions = filterValidActions(actions, TriggerType.FOLLOW, isLive, isTianXuanActive);
const followActions = filterValidActions(actions, TriggerType.FOLLOW, isLive, isTianXuanActive)
// 使用通用执行函数处理关注事件
if (followActions.length > 0 && roomId.value) {
@@ -54,8 +55,8 @@ export function useFollowThank(
TriggerType.FOLLOW,
roomId.value,
runtimeState,
{ sendLiveDanmaku }
);
{ sendLiveDanmaku },
)
}
}
@@ -64,13 +65,13 @@ export function useFollowThank(
*/
function clearTimer() {
if (timer.value) {
clearTimeout(timer.value);
timer.value = null;
clearTimeout(timer.value)
timer.value = null
}
}
return {
processFollow,
clearTimer
};
}
clearTimer,
}
}

View File

@@ -1,19 +1,17 @@
import { ref, Ref } from 'vue';
import { EventModel, EventDataTypes } from '@/api/api-models';
import {
getRandomTemplate,
buildExecutionContext
} from '../utils';
import { evaluateTemplateExpressions } from '../expressionEvaluator';
import {
import type { Ref } from 'vue'
import type {
AutoActionItem,
TriggerType,
RuntimeState
} from '../types';
RuntimeState,
} from '../types'
import type { EventModel } from '@/api/api-models'
import {
executeActions,
filterValidActions,
executeActions
} from '../actionUtils';
} from '../actionUtils'
import {
TriggerType,
} from '../types'
/**
* 礼物感谢模块
@@ -26,9 +24,8 @@ export function useGiftThank(
isLive: Ref<boolean>,
roomId: Ref<number | undefined>,
isTianXuanActive: Ref<boolean>,
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>,
) {
/**
* 处理礼物事件
* @param event 礼物事件
@@ -38,18 +35,18 @@ export function useGiftThank(
function processGift(
event: EventModel,
actions: AutoActionItem[],
runtimeState: RuntimeState
runtimeState: RuntimeState,
) {
if (!roomId.value) return;
if (!roomId.value) return
// 使用通用函数过滤有效的礼物感谢操作
const giftActions = filterValidActions(actions, TriggerType.GIFT, isLive, isTianXuanActive);
const giftActions = filterValidActions(actions, TriggerType.GIFT, isLive, isTianXuanActive)
// 使用通用执行函数处理礼物事件
if (giftActions.length > 0 && roomId.value) {
// 礼物基本信息
const giftName = event.msg;
const giftPrice = event.price / 1000;
const giftName = event.msg
const giftPrice = event.price / 1000
executeActions(
giftActions,
@@ -63,31 +60,31 @@ export function useGiftThank(
// 礼物过滤逻辑
(action, context) => {
// 黑名单模式
if (action.triggerConfig.filterMode === 'blacklist' &&
action.triggerConfig.filterGiftNames?.includes(giftName)) {
return false;
if (action.triggerConfig.filterMode === 'blacklist'
&& action.triggerConfig.filterGiftNames?.includes(giftName)) {
return false
}
// 白名单模式
if (action.triggerConfig.filterMode === 'whitelist' &&
!action.triggerConfig.filterGiftNames?.includes(giftName)) {
return false;
if (action.triggerConfig.filterMode === 'whitelist'
&& !action.triggerConfig.filterGiftNames?.includes(giftName)) {
return false
}
// 礼物价值过滤
if (action.triggerConfig.minValue && giftPrice < action.triggerConfig.minValue) {
return false;
return false
}
return true;
}
]
}
);
return true
},
],
},
)
}
}
return {
processGift,
};
}
}
}

View File

@@ -1,17 +1,19 @@
import { computed, Ref } from 'vue';
import { GuardLevel, EventModel } from '@/api/api-models';
import {
import type { Ref } from 'vue'
import type {
AutoActionItem,
TriggerType,
RuntimeState,
ExecutionContext,
ActionType
} from '../types';
RuntimeState,
} from '../types'
import { computed } from 'vue'
import {
executeActions,
filterValidActions,
executeActions
} from '../actionUtils';
import { buildExecutionContext } from '../utils';
} from '../actionUtils'
import {
ActionType,
TriggerType,
} from '../types'
import { buildExecutionContext } from '../utils'
/**
* 舰长私信模块
@@ -22,7 +24,7 @@ import { buildExecutionContext } from '../utils';
export function useGuardPm(
roomId: Ref<number | undefined>,
sendPrivateMessage: (uid: number, message: string) => Promise<boolean>,
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>,
) {
/**
* 处理舰长购买事件
@@ -33,13 +35,13 @@ export function useGuardPm(
function handleGuardBuy(
actions: AutoActionItem[],
event: any,
runtimeState: RuntimeState
runtimeState: RuntimeState,
) {
if (!roomId.value) return;
if (!roomId.value) return
// 使用通用函数过滤舰长事件的操作
const isLiveRef = computed(() => true);
const guardActions = filterValidActions(actions, TriggerType.GUARD, isLiveRef);
const isLiveRef = computed(() => true)
const guardActions = filterValidActions(actions, TriggerType.GUARD, isLiveRef)
// 使用通用执行函数处理舰长事件
if (guardActions.length > 0 && roomId.value) {
@@ -56,99 +58,99 @@ export function useGuardPm(
(action, context) => {
if (action.triggerConfig.preventRepeat && event && event.uid) {
// 确保 uid 是数字类型
const uid = typeof event.uid === 'number' ? event.uid : parseInt(event.uid, 10);
const uid = typeof event.uid === 'number' ? event.uid : Number.parseInt(event.uid, 10)
// 检查是否已经发送过
if (runtimeState.sentGuardPms.has(uid)) {
return false;
return false
}
// 添加到已发送集合
runtimeState.sentGuardPms.add(uid);
runtimeState.sentGuardPms.add(uid)
}
return true;
}
return true
},
],
customContextBuilder: (eventData, roomId, triggerType): ExecutionContext => {
// 使用标准上下文构建方法
const context = buildExecutionContext(eventData, roomId, triggerType);
const context = buildExecutionContext(eventData, roomId, triggerType)
// 如果是舰长事件且有事件数据,处理礼品码
if (triggerType === TriggerType.GUARD && eventData && eventData.guard_level !== undefined) {
const guardLevel = eventData.guard_level;
const guardLevel = eventData.guard_level
// 查找包含礼品码的操作
guardActions.forEach(action => {
guardActions.forEach((action) => {
// 找到对应等级的礼品码
if (action.triggerConfig.giftCodes && action.triggerConfig.giftCodes.length > 0) {
// 优先查找特定等级的礼品码
let levelCodesEntry = action.triggerConfig.giftCodes.find(gc => gc.level === guardLevel);
let levelCodesEntry = action.triggerConfig.giftCodes.find(gc => gc.level === guardLevel)
// 如果没有找到特定等级的礼品码尝试查找通用礼品码level为0
if (!levelCodesEntry) {
levelCodesEntry = action.triggerConfig.giftCodes.find(gc => gc.level === 0);
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];
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.giftCode = randomCode
// 在上下文中存储选中的礼品码信息以供后续消耗
context.variables.guard.selectedGiftCode = {
code: randomCode,
level: levelCodesEntry.level
};
level: levelCodesEntry.level,
}
}
}
}
});
})
}
return context;
return context
},
onSuccess: (action: AutoActionItem, context: ExecutionContext) => {
// 检查是否需要消耗礼品码
if (
action.actionType === ActionType.SEND_PRIVATE_MSG &&
action.triggerConfig.consumeGiftCode &&
context.variables.guard?.selectedGiftCode
action.actionType === ActionType.SEND_PRIVATE_MSG
&& action.triggerConfig.consumeGiftCode
&& context.variables.guard?.selectedGiftCode
) {
const { code: selectedCode, level: selectedLevel } = context.variables.guard.selectedGiftCode;
const { code: selectedCode, level: selectedLevel } = context.variables.guard.selectedGiftCode
console.log(`[AutoAction] 尝试消耗礼品码: ActionID=${action.id}, Level=${selectedLevel}, Code=${selectedCode}`);
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);
const levelCodesEntry = action.triggerConfig.giftCodes.find(gc => gc.level === selectedLevel)
if (levelCodesEntry && Array.isArray(levelCodesEntry.codes)) {
// 找到要删除的礼品码的索引
const codeIndex = levelCodesEntry.codes.indexOf(selectedCode);
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} 个。`);
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}`);
console.warn(`[AutoAction] 未能在等级 ${selectedLevel} 中找到要消耗的礼品码: ${selectedCode}, ActionID=${action.id}`)
}
} else {
console.warn(`[AutoAction] 未找到等级 ${selectedLevel} 的礼品码列表或列表格式不正确, ActionID=${action.id}`);
console.warn(`[AutoAction] 未找到等级 ${selectedLevel} 的礼品码列表或列表格式不正确, ActionID=${action.id}`)
}
} else {
console.warn(`[AutoAction] Action ${action.id} 的 giftCodes 配置不存在或不是数组。`);
console.warn(`[AutoAction] Action ${action.id} 的 giftCodes 配置不存在或不是数组。`)
}
}
}
}
);
},
},
)
}
}
@@ -159,14 +161,14 @@ export function useGuardPm(
*/
function getGuardLevelName(level: number): string {
switch (level) {
case 1: return '总督';
case 2: return '提督';
case 3: return '舰长';
default: return '未知等级';
case 1: return '总督'
case 2: return '提督'
case 3: return '舰长'
default: return '未知等级'
}
}
return {
handleGuardBuy
};
}
handleGuardBuy,
}
}

View File

@@ -1,18 +1,16 @@
import { ref, watch, Ref, computed } from 'vue';
import { useStorage } from '@vueuse/core';
import {
buildExecutionContext
} from '../utils';
import {
import type { Ref } from 'vue'
import type {
AutoActionItem,
TriggerType,
RuntimeState,
ExecutionContext
} from '../types';
} from '../types'
import { computed, ref } from 'vue'
import {
executeActions,
filterValidActions,
executeActions
} from '../actionUtils';
} from '../actionUtils'
import {
TriggerType,
} from '../types'
/**
* 定时弹幕模块
@@ -23,12 +21,12 @@ import {
export function useScheduledDanmaku(
isLive: Ref<boolean>,
roomId: Ref<number | undefined>,
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>,
) {
// 运行时数据
const timer = ref<any | null>(null);
const remainingSeconds = ref(0); // 倒计时剩余秒数
const countdownTimer = ref<any | null>(null); // 倒计时定时器
const timer = ref<any | null>(null)
const remainingSeconds = ref(0) // 倒计时剩余秒数
const countdownTimer = ref<any | null>(null) // 倒计时定时器
/**
* 处理定时任务 - 使用新的AutoActionItem结构
@@ -37,19 +35,19 @@ export function useScheduledDanmaku(
*/
function processScheduledActions(
actions: AutoActionItem[],
runtimeState: RuntimeState
runtimeState: RuntimeState,
) {
if (!roomId.value) return;
if (!roomId.value) return
// 使用通用函数过滤有效的定时弹幕操作
const scheduledActions = filterValidActions(actions, TriggerType.SCHEDULED, isLive);
const scheduledActions = filterValidActions(actions, TriggerType.SCHEDULED, isLive)
// 为每个定时操作设置定时器
scheduledActions.forEach(action => {
scheduledActions.forEach((action) => {
// 检查是否已有定时器
if (runtimeState.scheduledTimers[action.id]) return;
if (runtimeState.scheduledTimers[action.id]) return
const intervalSeconds = action.triggerConfig.intervalSeconds || 300; // 默认5分钟
const intervalSeconds = action.triggerConfig.intervalSeconds || 300 // 默认5分钟
// 创建定时器函数
const timerFn = () => {
@@ -64,42 +62,42 @@ export function useScheduledDanmaku(
{ sendLiveDanmaku },
{
skipUserFilters: true, // 定时任务不需要用户过滤
skipCooldownCheck: false // 可以保留冷却检查
}
);
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() // 更新定时器启动时间
}
// 首次启动定时器
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() // 记录定时器启动时间
})
}
/**
* 格式化剩余时间为分:秒格式
*/
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')}`;
});
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;
clearTimeout(timer.value)
timer.value = null
}
if (countdownTimer.value) {
clearInterval(countdownTimer.value);
countdownTimer.value = null;
clearInterval(countdownTimer.value)
countdownTimer.value = null
}
}
@@ -107,6 +105,6 @@ export function useScheduledDanmaku(
processScheduledActions,
clearTimer,
remainingSeconds,
formattedRemainingTime
};
}
formattedRemainingTime,
}
}

View File

@@ -1,14 +1,16 @@
import { EventModel } from '@/api/api-models';
import { Ref } from 'vue';
import {
executeActions,
filterValidActions
} from '../actionUtils';
import {
import type { Ref } from 'vue'
import type {
AutoActionItem,
RuntimeState,
TriggerType
} from '../types';
} from '../types'
import type { EventModel } from '@/api/api-models'
import {
executeActions,
filterValidActions,
} from '../actionUtils'
import {
TriggerType,
} from '../types'
/**
* 醒目留言感谢模块
@@ -21,9 +23,8 @@ export function useSuperChatThank(
isLive: Ref<boolean>,
roomId: Ref<number | undefined>,
isTianXuanActive: Ref<boolean>,
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>
sendLiveDanmaku: (roomId: number, message: string) => Promise<boolean>,
) {
/**
* 处理醒目留言事件
* @param event 醒目留言事件
@@ -33,12 +34,12 @@ export function useSuperChatThank(
function processSuperChat(
event: EventModel,
actions: AutoActionItem[],
runtimeState: RuntimeState
runtimeState: RuntimeState,
) {
if (!roomId.value) return;
if (!roomId.value) return
// 使用通用函数过滤有效的SC感谢操作
const scActions = filterValidActions(actions, TriggerType.SUPER_CHAT, isLive, isTianXuanActive);
const scActions = filterValidActions(actions, TriggerType.SUPER_CHAT, isLive, isTianXuanActive)
// 使用通用执行函数处理SC事件
if (scActions.length > 0 && roomId.value) {
@@ -55,25 +56,25 @@ export function useSuperChatThank(
(action, context) => {
// 如果未设置SC过滤或选择了不过滤模式
if (!action.triggerConfig.scFilterMode || action.triggerConfig.scFilterMode === 'none') {
return true;
return true
}
// 价格过滤模式
if (action.triggerConfig.scFilterMode === 'price' &&
action.triggerConfig.scMinPrice &&
event.price < action.triggerConfig.scMinPrice * 1000) {
return false;
if (action.triggerConfig.scFilterMode === 'price'
&& action.triggerConfig.scMinPrice
&& event.price < action.triggerConfig.scMinPrice * 1000) {
return false
}
return true;
}
]
}
);
return true
},
],
},
)
}
}
return {
processSuperChat,
};
}
}
}

View File

@@ -1,30 +1,30 @@
// 统一的自动操作类型定义
import { EventModel, GuardLevel } from '@/api/api-models';
import type { EventModel, GuardLevel } from '@/api/api-models'
// 触发条件类型
export enum TriggerType {
DANMAKU = 'danmaku', // 弹幕
GIFT = 'gift', // 礼物
GUARD = 'guard', // 上舰
FOLLOW = 'follow', // 关注
ENTER = 'enter', // 进入直播间
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_DANMAKU = 'send_danmaku', // 发送弹幕
SEND_PRIVATE_MSG = 'send_private_msg', // 发送私信
EXECUTE_COMMAND = 'execute_command', // 执行命令
EXECUTE_COMMAND = 'execute_command', // 执行命令
}
// 关键词匹配类型
export enum KeywordMatchType {
Full = 'full', // 完全匹配
Full = 'full', // 完全匹配
Contains = 'contains', // 包含匹配
Regex = 'regex', // 正则匹配
Regex = 'regex', // 正则匹配
}
// 优先级
@@ -37,110 +37,110 @@ export enum Priority {
}
// 统一的自动操作定义
export type AutoActionItem = {
id: string; // 唯一ID
name: string; // 操作名称
enabled: boolean; // 是否启用
triggerType: TriggerType; // 触发类型
actionType: ActionType; // 操作类型
template: string; // 模板
priority: Priority; // 优先级
export interface AutoActionItem {
id: string // 唯一ID
name: string // 操作名称
enabled: boolean // 是否启用
triggerType: TriggerType // 触发类型
actionType: ActionType // 操作类型
template: string // 模板
priority: Priority // 优先级
// 高级配置
logicalExpression: string; // 逻辑表达式,为真时才执行此操作
ignoreCooldown: boolean; // 是否忽略冷却时间
executeCommand: string; // 要执行的JS代码
logicalExpression: string // 逻辑表达式,为真时才执行此操作
ignoreCooldown: boolean // 是否忽略冷却时间
executeCommand: string // 要执行的JS代码
// 触发器特定配置
triggerConfig: TriggerConfig;
triggerConfig: TriggerConfig
// 动作特定配置
actionConfig: {
delaySeconds?: number; // 延迟执行秒数
maxUsersPerMsg?: number; // 每条消息最大用户数
maxItemsPerUser?: number; // 每用户最大项目数 (礼物等)
cooldownSeconds?: number; // 冷却时间(秒)
};
delaySeconds?: number // 延迟执行秒数
maxUsersPerMsg?: number // 每条消息最大用户数
maxItemsPerUser?: number // 每用户最大项目数 (礼物等)
cooldownSeconds?: number // 冷却时间(秒)
}
}
// 执行上下文,包含事件信息和可用变量
export interface ExecutionContext {
event?: EventModel; // 触发事件
roomId?: number; // 直播间ID
variables: Record<string, any>; // 额外变量
timestamp: number; // 时间戳
event?: EventModel // 触发事件
roomId?: number // 直播间ID
variables: Record<string, any> // 额外变量
timestamp: number // 时间戳
// --- 新增运行时数据管理函数 ---
/** 获取运行时数据 */
getData: <T>(key: string, defaultValue?: T) => T | undefined;
getData: <T>(key: string, defaultValue?: T) => T | undefined
/** 设置运行时数据 */
setData: <T>(key: string, value: T) => void;
setData: <T>(key: string, value: T) => void
/** 检查运行时数据是否存在 */
containsData: (key: string) => boolean;
containsData: (key: string) => boolean
/** 移除运行时数据 */
removeData: (key: string) => void;
removeData: (key: string) => void
// --- 新增持久化数据管理函数 ---
/** 获取持久化存储的数据 */
getStorageData: <T>(key: string, defaultValue?: T) => Promise<T | undefined>;
getStorageData: <T>(key: string, defaultValue?: T) => Promise<T | undefined>
/** 设置持久化存储的数据 */
setStorageData: <T>(key: string, value: T) => Promise<void>;
setStorageData: <T>(key: string, value: T) => Promise<void>
/** 检查持久化存储中是否存在指定的键 */
hasStorageData: (key: string) => Promise<boolean>;
hasStorageData: (key: string) => Promise<boolean>
/** 从持久化存储中删除数据 */
removeStorageData: (key: string) => Promise<void>;
removeStorageData: (key: string) => Promise<void>
/** 清除所有持久化存储的数据 */
clearStorageData: () => Promise<void>;
clearStorageData: () => Promise<void>
}
// 运行状态接口
export interface RuntimeState {
lastExecutionTime: Record<string, number>; // 上次执行时间
aggregatedEvents: Record<string, any[]>; // 聚合的事件
scheduledTimers: Record<string, any | null>; // 定时器 ID
timerStartTimes: Record<string, number>; // <--- 新增:独立定时器启动时间戳
globalTimerStartTime: number | null; // <--- 新增:全局定时器启动时间戳
sentGuardPms: Set<number>; // 已发送的舰长私信
lastExecutionTime: Record<string, number> // 上次执行时间
aggregatedEvents: Record<string, any[]> // 聚合的事件
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;
userFilterEnabled?: boolean
requireMedal?: boolean
requireCaptain?: boolean
// Common conditions
onlyDuringLive?: boolean;
ignoreTianXuan?: boolean;
onlyDuringLive?: boolean
ignoreTianXuan?: boolean
// Keywords for autoReply
keywords?: string[];
keywordMatchType?: KeywordMatchType;
blockwords?: string[];
blockwordMatchType?: KeywordMatchType;
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; // 是否包含礼物数量
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最低价格(元)
scFilterMode?: 'none' | 'price' // SC过滤模式
scMinPrice?: number // SC最低价格(元)
// Scheduled options
useGlobalTimer?: boolean;
intervalSeconds?: number;
schedulingMode?: 'random' | 'sequential';
useGlobalTimer?: boolean
intervalSeconds?: number
schedulingMode?: 'random' | 'sequential'
// Guard related
guardLevels?: GuardLevel[];
preventRepeat?: boolean;
giftCodes?: { level: number; codes: string[] }[];
consumeGiftCode?: boolean; // 是否消耗礼品码
guardLevels?: GuardLevel[]
preventRepeat?: boolean
giftCodes?: { level: number, codes: string[] }[]
consumeGiftCode?: boolean // 是否消耗礼品码
// Confirm message options
sendDanmakuConfirm?: boolean; // 是否发送弹幕确认
isConfirmMessage?: boolean; // 标记这是一个确认消息
}
sendDanmakuConfirm?: boolean // 是否发送弹幕确认
isConfirmMessage?: boolean // 标记这是一个确认消息
}

View File

@@ -1,22 +1,24 @@
import { nanoid } from 'nanoid';
import {
import type {
AutoActionItem,
TriggerType,
ExecutionContext,
RuntimeState,
} from './types'
import { clear, createStore, del, get, set } from 'idb-keyval' // 导入 useIDBKeyval
import { nanoid } from 'nanoid'
import {
ActionType,
Priority,
RuntimeState,
ExecutionContext
} from './types';
import { get, set, del, clear, keys as idbKeys, createStore } from 'idb-keyval'; // 导入 useIDBKeyval
TriggerType,
} from './types'
// --- 定义用户持久化数据的自定义存储区 ---
const USER_DATA_DB_NAME = 'AutoActionUserDataDB';
const USER_DATA_STORE_NAME = 'userData';
const userDataStore = createStore(USER_DATA_DB_NAME, USER_DATA_STORE_NAME);
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_';
const RUNTIME_STORAGE_PREFIX = 'autoaction_runtime_'
/**
* 创建默认的运行时状态
@@ -28,8 +30,8 @@ export function createDefaultRuntimeState(): RuntimeState {
timerStartTimes: {},
globalTimerStartTime: null,
sentGuardPms: new Set(),
aggregatedEvents: {}
};
aggregatedEvents: {},
}
}
/**
@@ -37,7 +39,7 @@ export function createDefaultRuntimeState(): RuntimeState {
* @param triggerType 触发类型
*/
export function createDefaultAutoAction(triggerType: TriggerType): AutoActionItem {
const id = `auto-action-${nanoid(8)}`;
const id = `auto-action-${nanoid(8)}`
// 根据不同触发类型设置默认模板
const defaultTemplates: Record<TriggerType, string> = {
@@ -48,7 +50,7 @@ export function createDefaultAutoAction(triggerType: TriggerType): AutoActionIte
[TriggerType.ENTER]: '欢迎 {{user.name}} 进入直播间',
[TriggerType.SCHEDULED]: '这是一条定时消息,当前时间: {{date.formatted}}',
[TriggerType.SUPER_CHAT]: '感谢 {{user.name}} 的SC!',
};
}
// 根据不同触发类型设置默认名称
const defaultNames: Record<TriggerType, string> = {
@@ -59,7 +61,7 @@ export function createDefaultAutoAction(triggerType: TriggerType): AutoActionIte
[TriggerType.ENTER]: '入场欢迎',
[TriggerType.SCHEDULED]: '定时消息',
[TriggerType.SUPER_CHAT]: 'SC感谢',
};
}
return {
id,
@@ -85,9 +87,9 @@ export function createDefaultAutoAction(triggerType: TriggerType): AutoActionIte
delaySeconds: 0,
cooldownSeconds: 5,
maxUsersPerMsg: 5,
maxItemsPerUser: 3
}
};
maxItemsPerUser: 3,
},
}
}
/**
@@ -95,8 +97,8 @@ export function createDefaultAutoAction(triggerType: TriggerType): AutoActionIte
* @param template 模板字符串
*/
export function getRandomTemplate(template: string): string | null {
if (!template) return null;
return template;
if (!template) return null
return template
}
/**
@@ -105,42 +107,42 @@ export function getRandomTemplate(template: string): string | null {
* @param context 执行上下文
*/
export function formatTemplate(template: string, context: ExecutionContext): string {
if (!template) return '';
if (!template) return ''
// 简单的模板替换
return template.replace(/{([^}]+)}/g, (match, path) => {
return template.replace(/\{([^}]+)\}/g, (match, path) => {
try {
// 解析路径
const parts = path.trim().split('.');
let value: any = context;
const parts = path.trim().split('.')
let value: any = context
// 特殊处理函数类型
if (parts[0] === 'timeOfDay' && typeof context.variables.timeOfDay === 'function') {
return context.variables.timeOfDay();
return context.variables.timeOfDay()
}
// 特殊处理event直接访问
if (parts[0] === 'event') {
value = context.event;
parts.shift();
value = context.event
parts.shift()
} else {
// 否则从variables中获取
value = context.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();
if (value === undefined || value === null) return match
value = value[part]
if (typeof value === 'function') value = value()
}
return value !== undefined && value !== null ? String(value) : match;
return value !== undefined && value !== null ? String(value) : match
} catch (error) {
console.error('模板格式化错误:', error);
return match; // 出错时返回原始匹配项
console.error('模板格式化错误:', error)
return match // 出错时返回原始匹配项
}
});
})
}
/**
@@ -149,21 +151,21 @@ export function formatTemplate(template: string, context: ExecutionContext): str
* @param context 执行上下文
*/
export function evaluateExpression(expression: string, context: ExecutionContext): boolean {
if (!expression || expression.trim() === '') return true; // 空表达式默认为true
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);
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;
if (!context.event) return 0
return (context.event.price || 0) * (context.event.num || 1) / 1000
},
giftName: () => context.event?.msg || '',
@@ -177,16 +179,16 @@ export function evaluateExpression(expression: string, context: ExecutionContext
// 时间相关
time: {
hour: new Date().getHours(),
minute: new Date().getMinutes()
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)
}
};
endsWith: (str: string, search: string) => str.endsWith(search),
},
}
// 创建安全的eval环境
const evalFunc = new Function(
@@ -200,14 +202,14 @@ export function evaluateExpression(expression: string, context: ExecutionContext
} catch(e) {
console.error('表达式评估错误:', e);
return false;
}`
);
}`,
)
// 执行表达式
return Boolean(evalFunc(context, context.event, utils));
return Boolean(evalFunc(context, context.event, utils))
} catch (error) {
console.error('表达式评估错误:', error);
return false; // 出错时返回false
console.error('表达式评估错误:', error)
return false // 出错时返回false
}
}
@@ -217,28 +219,28 @@ export function evaluateExpression(expression: string, context: ExecutionContext
* @param params 参数对象
*/
export function formatMessage(template: string, params: Record<string, any>): string {
if (!template) return '';
if (!template) return ''
// 简单的模板替换
return template.replace(/{{([^}]+)}}/g, (match, path) => {
return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
try {
// 解析路径
const parts = path.trim().split('.');
let value: any = params;
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();
if (value === undefined || value === null) return match
value = value[part]
if (typeof value === 'function') value = value()
}
return value !== undefined && value !== null ? String(value) : match;
return value !== undefined && value !== null ? String(value) : match
} catch (error) {
console.error('模板格式化错误:', error);
return match; // 出错时返回原始匹配项
console.error('模板格式化错误:', error)
return match // 出错时返回原始匹配项
}
});
})
}
/**
@@ -246,10 +248,10 @@ export function formatMessage(template: string, params: Record<string, any>): st
* @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;
export function shouldProcess(config: { enabled: boolean, onlyDuringLive: boolean }, isLive: boolean): boolean {
if (!config.enabled) return false
if (config.onlyDuringLive && !isLive) return false
return true
}
/**
@@ -257,11 +259,11 @@ export function shouldProcess(config: { enabled: boolean; onlyDuringLive: boolea
* @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;
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
}
/**
@@ -276,10 +278,10 @@ export function buildExecutionContext(
event: any,
roomId: number | undefined,
triggerType?: TriggerType,
additionalContext?: Record<string, any>
additionalContext?: Record<string, any>,
): ExecutionContext {
const now = Date.now();
const dateObj = new Date(now);
const now = Date.now()
const dateObj = new Date(now)
const context: ExecutionContext = {
event,
@@ -294,103 +296,103 @@ export function buildExecutionContext(
day: dateObj.getDate(),
hour: dateObj.getHours(),
minute: dateObj.getMinutes(),
second: dateObj.getSeconds()
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 '深夜';
}
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 '深夜'
},
},
// --- 实现运行时数据管理函数 (使用 sessionStorage) ---
getData: <T>(key: string, defaultValue?: T): T | undefined => {
const prefixedKey = RUNTIME_STORAGE_PREFIX + key;
const prefixedKey = RUNTIME_STORAGE_PREFIX + key
try {
const storedValue = sessionStorage.getItem(prefixedKey);
const storedValue = sessionStorage.getItem(prefixedKey)
if (storedValue === null) {
return defaultValue;
return defaultValue
}
return JSON.parse(storedValue) as T;
return JSON.parse(storedValue) as T
} catch (error) {
console.error(`[Runtime SessionStorage] Error getting/parsing key '${key}':`, error);
return defaultValue;
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;
const prefixedKey = RUNTIME_STORAGE_PREFIX + key
try {
// 不存储 undefined
if (value === undefined) {
sessionStorage.removeItem(prefixedKey);
return;
sessionStorage.removeItem(prefixedKey)
return
}
sessionStorage.setItem(prefixedKey, JSON.stringify(value));
sessionStorage.setItem(prefixedKey, JSON.stringify(value))
} catch (error) {
console.error(`[Runtime SessionStorage] Error setting key '${key}':`, error);
console.error(`[Runtime SessionStorage] Error setting key '${key}':`, error)
// 如果序列化失败,可以选择移除旧键或保留
sessionStorage.removeItem(prefixedKey);
sessionStorage.removeItem(prefixedKey)
}
},
containsData: (key: string): boolean => {
const prefixedKey = RUNTIME_STORAGE_PREFIX + key;
return sessionStorage.getItem(prefixedKey) !== null;
const prefixedKey = RUNTIME_STORAGE_PREFIX + key
return sessionStorage.getItem(prefixedKey) !== null
},
removeData: (key: string): void => {
const prefixedKey = RUNTIME_STORAGE_PREFIX + key;
sessionStorage.removeItem(prefixedKey);
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;
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;
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);
await set(key, value, userDataStore)
} catch (error) {
console.error(`[UserData IDB] setStorageData error for key '${key}':`, 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;
const value = await get(key, userDataStore)
return value !== undefined
} catch (error) {
console.error(`[UserData IDB] hasStorageData error for key '${key}':`, error);
return false;
console.error(`[UserData IDB] hasStorageData error for key '${key}':`, error)
return false
}
},
removeStorageData: async (key: string): Promise<void> => {
try {
// 使用 userDataStore
await del(key, userDataStore);
await del(key, userDataStore)
} catch (error) {
console.error(`[UserData IDB] removeStorageData error for key '${key}':`, error);
console.error(`[UserData IDB] removeStorageData error for key '${key}':`, error)
}
},
clearStorageData: async (): Promise<void> => {
try {
// 使用 userDataStore
await clear(userDataStore);
await clear(userDataStore)
} catch (error) {
console.error('[UserData IDB] clearStorageData error:', error);
console.error('[UserData IDB] clearStorageData error:', error)
}
}
};
},
}
// 如果有事件对象,添加用户信息
if (event) {
@@ -400,10 +402,10 @@ export function buildExecutionContext(
guardLevel: event.guard_level,
hasMedal: event.fans_medal_wearing_status,
medalLevel: event.fans_medal_level,
medalName: event.fans_medal_name
};
context.variables.danmaku = event;
context.variables.message = event.msg;
medalName: event.fans_medal_name,
}
context.variables.danmaku = event
context.variables.message = event.msg
// 根据不同触发类型添加特定变量
if (triggerType === TriggerType.GIFT) {
@@ -412,25 +414,25 @@ export function buildExecutionContext(
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 || '礼物'}`
};
summary: `${event.num || 1}${event.msg || '礼物'}`,
}
} else if (triggerType === TriggerType.GUARD) {
const guardLevelMap: Record<number, string> = {
1: '总督',
2: '提督',
3: '舰长',
0: '无舰长'
};
0: '无舰长',
}
context.variables.guard = {
level: event.guard_level || 0,
levelName: guardLevelMap[event.guard_level || 0] || '未知舰长等级',
giftCode: ''
};
giftCode: '',
}
} else if (triggerType === TriggerType.SUPER_CHAT) {
context.variables.sc = {
message: event.msg,
price: (event.price || 0) / 1000
};
price: (event.price || 0) / 1000,
}
}
}
@@ -438,9 +440,9 @@ export function buildExecutionContext(
if (additionalContext) {
context.variables = {
...context.variables,
...additionalContext
};
...additionalContext,
}
}
return context;
return context
}

View File

@@ -1,5 +1,5 @@
import { get, set, del, update } from 'idb-keyval';
import { ActionType } from '../types';
import { del, get, update } from 'idb-keyval'
import { ActionType } from '../types'
// 历史记录类型常量
export enum HistoryType {
@@ -10,39 +10,39 @@ export enum HistoryType {
// 历史记录项结构
export interface HistoryItem {
id: string; // 唯一ID
actionId: string; // 操作ID
actionName: string; // 操作名称
actionType: ActionType; // 操作类型
timestamp: number; // 执行时间戳
content: string; // 发送的内容
target?: string; // 目标如UID或房间ID
success: boolean; // 是否成功
error?: string; // 错误信息(如果有)
id: string // 唯一ID
actionId: string // 操作ID
actionName: string // 操作名称
actionType: ActionType // 操作类型
timestamp: number // 执行时间戳
content: string // 发送的内容
target?: string // 目标如UID或房间ID
success: boolean // 是否成功
error?: string // 错误信息(如果有)
}
// 每种类型的历史记录容量
const HISTORY_CAPACITY = 1000;
const HISTORY_CAPACITY = 1000
// 使用IDB存储的键名
const HISTORY_KEYS = {
[HistoryType.DANMAKU]: 'autoAction_history_danmaku',
[HistoryType.PRIVATE_MSG]: 'autoAction_history_privateMsg',
[HistoryType.COMMAND]: 'autoAction_history_command',
};
}
// 环形队列添加记录
async function addToCircularQueue(key: string, item: HistoryItem, capacity: number): Promise<void> {
await update<HistoryItem[]>(key, (history = []) => {
// 添加到队列末尾
history.push(item);
history.push(item)
// 如果超出容量,移除最旧的记录
if (history.length > capacity) {
history.splice(0, history.length - capacity);
history.splice(0, history.length - capacity)
}
return history;
});
return history
})
}
/**
@@ -54,7 +54,7 @@ export async function logDanmakuHistory(
content: string,
roomId: number,
success: boolean,
error?: string
error?: string,
): Promise<void> {
const historyItem: HistoryItem = {
id: `d_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
@@ -65,10 +65,10 @@ export async function logDanmakuHistory(
content,
target: roomId.toString(),
success,
error
};
error,
}
await addToCircularQueue(HISTORY_KEYS[HistoryType.DANMAKU], historyItem, HISTORY_CAPACITY);
await addToCircularQueue(HISTORY_KEYS[HistoryType.DANMAKU], historyItem, HISTORY_CAPACITY)
}
/**
@@ -80,7 +80,7 @@ export async function logPrivateMsgHistory(
content: string,
userId: number,
success: boolean,
error?: string
error?: string,
): Promise<void> {
const historyItem: HistoryItem = {
id: `p_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
@@ -91,10 +91,10 @@ export async function logPrivateMsgHistory(
content,
target: userId.toString(),
success,
error
};
error,
}
await addToCircularQueue(HISTORY_KEYS[HistoryType.PRIVATE_MSG], historyItem, HISTORY_CAPACITY);
await addToCircularQueue(HISTORY_KEYS[HistoryType.PRIVATE_MSG], historyItem, HISTORY_CAPACITY)
}
/**
@@ -105,7 +105,7 @@ export async function logCommandHistory(
actionName: string,
content: string,
success: boolean,
error?: string
error?: string,
): Promise<void> {
const historyItem: HistoryItem = {
id: `c_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
@@ -115,24 +115,24 @@ export async function logCommandHistory(
timestamp: Date.now(),
content,
success,
error
};
error,
}
await addToCircularQueue(HISTORY_KEYS[HistoryType.COMMAND], historyItem, HISTORY_CAPACITY);
await addToCircularQueue(HISTORY_KEYS[HistoryType.COMMAND], historyItem, HISTORY_CAPACITY)
}
/**
* 获取历史记录
*/
export async function getHistoryByType(type: HistoryType): Promise<HistoryItem[]> {
return await get<HistoryItem[]>(HISTORY_KEYS[type]) || [];
return await get<HistoryItem[]>(HISTORY_KEYS[type]) || []
}
/**
* 清除历史记录
*/
export async function clearHistory(type: HistoryType): Promise<void> {
await del(HISTORY_KEYS[type]);
await del(HISTORY_KEYS[type])
}
/**
@@ -140,6 +140,6 @@ export async function clearHistory(type: HistoryType): Promise<void> {
*/
export async function clearAllHistory(): Promise<void> {
await Promise.all(
Object.values(HistoryType).map(type => clearHistory(type as HistoryType))
);
}
Object.values(HistoryType).map(async type => clearHistory(type as HistoryType)),
)
}