mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-11 21:06:56 +08:00
chore: format code style and update linting configuration
This commit is contained in:
@@ -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}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 // 标记这是一个确认消息
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,72 +1,71 @@
|
||||
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
|
||||
import { useTauriStore } from './useTauriStore';
|
||||
import { error, info, warn, debug } from '@tauri-apps/plugin-log';
|
||||
import { AES, enc, MD5 } from 'crypto-js';
|
||||
import { QueryBiliAPI } from '../data/utils';
|
||||
import { BiliUserProfile } from '../data/models';
|
||||
import { defineStore, acceptHMRUpdate } from 'pinia';
|
||||
import { ref, computed, shallowRef } from 'vue';
|
||||
import { StorageSerializers } from '@vueuse/core';
|
||||
import type { BiliUserProfile } from '../data/models'
|
||||
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
|
||||
import { debug, error, info, warn } from '@tauri-apps/plugin-log'
|
||||
import { AES, enc, MD5 } from 'crypto-js'
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia'
|
||||
import { computed, ref, shallowRef } from 'vue'
|
||||
import { QueryBiliAPI } from '../data/utils'
|
||||
import { useTauriStore } from './useTauriStore'
|
||||
|
||||
// --- 常量定义 ---
|
||||
// Tauri Store 存储键名
|
||||
export const BILI_COOKIE_KEY = 'user.bilibili.cookie';
|
||||
export const COOKIE_CLOUD_KEY = 'user.bilibili.cookie_cloud';
|
||||
export const USER_INFO_CACHE_KEY = 'cache.bilibili.userInfo';
|
||||
export const BILI_COOKIE_KEY = 'user.bilibili.cookie'
|
||||
export const COOKIE_CLOUD_KEY = 'user.bilibili.cookie_cloud'
|
||||
export const USER_INFO_CACHE_KEY = 'cache.bilibili.userInfo'
|
||||
|
||||
// 检查周期 (毫秒)
|
||||
const REGULAR_CHECK_INTERVAL = 60 * 1000; // 每分钟检查一次 Cookie 有效性
|
||||
const CLOUD_SYNC_INTERVAL_CHECKS = 30; // 每 30 次常规检查后 (约 30 分钟) 同步一次 CookieCloud
|
||||
const REGULAR_CHECK_INTERVAL = 60 * 1000 // 每分钟检查一次 Cookie 有效性
|
||||
const CLOUD_SYNC_INTERVAL_CHECKS = 30 // 每 30 次常规检查后 (约 30 分钟) 同步一次 CookieCloud
|
||||
|
||||
// 用户信息缓存有效期 (毫秒)
|
||||
const USER_INFO_CACHE_DURATION = 5 * 60 * 1000; // 缓存 5 分钟
|
||||
const USER_INFO_CACHE_DURATION = 5 * 60 * 1000 // 缓存 5 分钟
|
||||
|
||||
// --- 类型定义 ---
|
||||
|
||||
// Bilibili Cookie 存储数据结构
|
||||
type BiliCookieStoreData = {
|
||||
cookie: string;
|
||||
refreshToken?: string; // refreshToken 似乎未使用,设为可选
|
||||
lastRefresh?: Date; // 上次刷新时间,似乎未使用,设为可选
|
||||
};
|
||||
interface BiliCookieStoreData {
|
||||
cookie: string
|
||||
refreshToken?: string // refreshToken 似乎未使用,设为可选
|
||||
lastRefresh?: Date // 上次刷新时间,似乎未使用,设为可选
|
||||
}
|
||||
|
||||
// Cookie Cloud 配置数据结构
|
||||
export type CookieCloudConfig = {
|
||||
key: string;
|
||||
password: string;
|
||||
host?: string; // CookieCloud 服务地址,可选,有默认值
|
||||
};
|
||||
export interface CookieCloudConfig {
|
||||
key: string
|
||||
password: string
|
||||
host?: string // CookieCloud 服务地址,可选,有默认值
|
||||
}
|
||||
|
||||
// CookieCloud 导出的 Cookie 单项结构
|
||||
export interface CookieCloudCookie {
|
||||
domain: string;
|
||||
expirationDate: number;
|
||||
hostOnly: boolean;
|
||||
httpOnly: boolean;
|
||||
name: string;
|
||||
path: string;
|
||||
sameSite: string;
|
||||
secure: boolean;
|
||||
session: boolean;
|
||||
storeId: string;
|
||||
value: string;
|
||||
domain: string
|
||||
expirationDate: number
|
||||
hostOnly: boolean
|
||||
httpOnly: boolean
|
||||
name: string
|
||||
path: string
|
||||
sameSite: string
|
||||
secure: boolean
|
||||
session: boolean
|
||||
storeId: string
|
||||
value: string
|
||||
}
|
||||
|
||||
// CookieCloud 导出的完整数据结构
|
||||
interface CookieCloudExportData {
|
||||
cookie_data: Record<string, CookieCloudCookie[]>; // 按域名分组的 Cookie 数组
|
||||
local_storage_data?: Record<string, any>; // 本地存储数据 (可选)
|
||||
update_time: string; // 更新时间 ISO 8601 字符串
|
||||
cookie_data: Record<string, CookieCloudCookie[]> // 按域名分组的 Cookie 数组
|
||||
local_storage_data?: Record<string, any> // 本地存储数据 (可选)
|
||||
update_time: string // 更新时间 ISO 8601 字符串
|
||||
}
|
||||
|
||||
// 用户信息缓存结构
|
||||
type UserInfoCache = {
|
||||
userInfo: BiliUserProfile;
|
||||
accessedAt: number; // 使用时间戳 (Date.now()) 以方便比较
|
||||
};
|
||||
interface UserInfoCache {
|
||||
userInfo: BiliUserProfile
|
||||
accessedAt: number // 使用时间戳 (Date.now()) 以方便比较
|
||||
}
|
||||
|
||||
// CookieCloud 状态类型
|
||||
type CookieCloudState = 'unset' | 'valid' | 'invalid' | 'syncing';
|
||||
type CookieCloudState = 'unset' | 'valid' | 'invalid' | 'syncing'
|
||||
|
||||
// --- Store 定义 ---
|
||||
|
||||
@@ -77,30 +76,30 @@ export const useBiliCookie = defineStore('biliCookie', () => {
|
||||
cookie: '',
|
||||
refreshToken: undefined, // 可选,未使用
|
||||
lastRefresh: new Date(0), // 默认值
|
||||
}); // 为保持响应性
|
||||
const cookieCloudStore = useTauriStore().getTarget<CookieCloudConfig>(COOKIE_CLOUD_KEY);
|
||||
const userInfoCacheStore = useTauriStore().getTarget<UserInfoCache>(USER_INFO_CACHE_KEY);
|
||||
}) // 为保持响应性
|
||||
const cookieCloudStore = useTauriStore().getTarget<CookieCloudConfig>(COOKIE_CLOUD_KEY)
|
||||
const userInfoCacheStore = useTauriStore().getTarget<UserInfoCache>(USER_INFO_CACHE_KEY)
|
||||
|
||||
// --- 核心状态 ---
|
||||
// 使用 shallowRef 存储用户信息对象,避免不必要的深度侦听,提高性能
|
||||
const _cachedUserInfo = shallowRef<UserInfoCache | undefined>();
|
||||
const _cachedUserInfo = shallowRef<UserInfoCache | undefined>()
|
||||
// 是否已从存储加载了 Cookie (不代表有效)
|
||||
const hasBiliCookie = ref(false);
|
||||
const hasBiliCookie = ref(false)
|
||||
// 当前 Cookie 是否通过 Bilibili API 验证有效
|
||||
const isCookieValid = ref(false);
|
||||
const isCookieValid = ref(false)
|
||||
// CookieCloud 配置及同步状态
|
||||
const cookieCloudState = ref<CookieCloudState>('unset');
|
||||
const cookieCloudState = ref<CookieCloudState>('unset')
|
||||
// Bilibili 用户 ID
|
||||
const uId = ref<number | undefined>();
|
||||
const uId = ref<number | undefined>()
|
||||
|
||||
// --- 计算属性 ---
|
||||
// 公开的用户信息,只读
|
||||
const userInfo = computed(() => _cachedUserInfo.value?.userInfo);
|
||||
const userInfo = computed(() => _cachedUserInfo.value?.userInfo)
|
||||
|
||||
// --- 内部状态和变量 ---
|
||||
let _isInitialized = false; // 初始化标志,防止重复执行
|
||||
let _checkIntervalId: ReturnType<typeof setInterval> | null = null; // 定时检查器 ID
|
||||
let _checkCounter = 0; // 常规检查计数器,用于触发 CookieCloud 同步
|
||||
let _isInitialized = false // 初始化标志,防止重复执行
|
||||
let _checkIntervalId: ReturnType<typeof setInterval> | null = null // 定时检查器 ID
|
||||
let _checkCounter = 0 // 常规检查计数器,用于触发 CookieCloud 同步
|
||||
|
||||
// --- 私有辅助函数 ---
|
||||
|
||||
@@ -109,30 +108,30 @@ export const useBiliCookie = defineStore('biliCookie', () => {
|
||||
* @param data Bilibili 用户信息
|
||||
*/
|
||||
const _updateUserInfoCache = async (data: BiliUserProfile): Promise<void> => {
|
||||
const cacheData: UserInfoCache = { userInfo: data, accessedAt: Date.now() };
|
||||
_cachedUserInfo.value = cacheData; // 更新内存缓存
|
||||
uId.value = data.mid; // 更新 uId
|
||||
const cacheData: UserInfoCache = { userInfo: data, accessedAt: Date.now() }
|
||||
_cachedUserInfo.value = cacheData // 更新内存缓存
|
||||
uId.value = data.mid // 更新 uId
|
||||
try {
|
||||
await userInfoCacheStore.set(cacheData); // 持久化缓存
|
||||
debug('[BiliCookie] 用户信息缓存已更新并持久化');
|
||||
await userInfoCacheStore.set(cacheData) // 持久化缓存
|
||||
debug('[BiliCookie] 用户信息缓存已更新并持久化')
|
||||
} catch (err) {
|
||||
error('[BiliCookie] 持久化用户信息缓存失败: ' + String(err));
|
||||
error(`[BiliCookie] 持久化用户信息缓存失败: ${String(err)}`)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 清除用户信息缓存 (内存和持久化)
|
||||
*/
|
||||
const _clearUserInfoCache = async (): Promise<void> => {
|
||||
_cachedUserInfo.value = undefined; // 清除内存缓存
|
||||
uId.value = undefined; // 清除 uId
|
||||
_cachedUserInfo.value = undefined // 清除内存缓存
|
||||
uId.value = undefined // 清除 uId
|
||||
try {
|
||||
await userInfoCacheStore.delete(); // 删除持久化缓存
|
||||
debug('[BiliCookie] 用户信息缓存已清除');
|
||||
await userInfoCacheStore.delete() // 删除持久化缓存
|
||||
debug('[BiliCookie] 用户信息缓存已清除')
|
||||
} catch (err) {
|
||||
error('[BiliCookie] 清除持久化用户信息缓存失败: ' + String(err));
|
||||
error(`[BiliCookie] 清除持久化用户信息缓存失败: ${String(err)}`)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 更新 Cookie 存在状态和有效状态
|
||||
@@ -140,45 +139,44 @@ export const useBiliCookie = defineStore('biliCookie', () => {
|
||||
* @param isValid Cookie 是否有效
|
||||
*/
|
||||
const _updateCookieState = (hasCookie: boolean, isValid: boolean): void => {
|
||||
hasBiliCookie.value = hasCookie;
|
||||
isCookieValid.value = isValid;
|
||||
hasBiliCookie.value = hasCookie
|
||||
isCookieValid.value = isValid
|
||||
if (!hasCookie || !isValid) {
|
||||
// 如果 Cookie 不存在或无效,清除可能过时的用户信息缓存
|
||||
// 注意:这里采取了更严格的策略,无效则清除缓存,避免显示旧信息
|
||||
// _clearUserInfoCache(); // 考虑是否在无效时立即清除缓存
|
||||
debug(`[BiliCookie] Cookie 状态更新: hasCookie=${hasCookie}, isValid=${isValid}`);
|
||||
debug(`[BiliCookie] Cookie 状态更新: hasCookie=${hasCookie}, isValid=${isValid}`)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 检查提供的 Bilibili Cookie 是否有效
|
||||
* @param cookie 要验证的 Cookie 字符串
|
||||
* @returns Promise<{ valid: boolean; data?: BiliUserProfile }> 验证结果和用户信息 (如果有效)
|
||||
*/
|
||||
const _checkCookieValidity = async (cookie: string): Promise<{ valid: boolean; data?: BiliUserProfile; }> => {
|
||||
const _checkCookieValidity = async (cookie: string): Promise<{ valid: boolean, data?: BiliUserProfile }> => {
|
||||
if (!cookie) {
|
||||
return { valid: false };
|
||||
return { valid: false }
|
||||
}
|
||||
try {
|
||||
// 使用传入的 cookie 调用 Bilibili API
|
||||
const resp = await QueryBiliAPI('https://api.bilibili.com/x/space/myinfo', 'GET', cookie);
|
||||
const resp = await QueryBiliAPI('https://api.bilibili.com/x/space/myinfo', 'GET', cookie)
|
||||
|
||||
const json = await resp.json();
|
||||
const json = await resp.json()
|
||||
if (json.code === 0 && json.data) {
|
||||
debug('[BiliCookie] Cookie 验证成功, 用户:', json.data.name);
|
||||
debug('[BiliCookie] Cookie 验证成功, 用户:', json.data.name)
|
||||
// 验证成功,更新用户信息缓存
|
||||
await _updateUserInfoCache(json.data);
|
||||
return { valid: true, data: json.data };
|
||||
await _updateUserInfoCache(json.data)
|
||||
return { valid: true, data: json.data }
|
||||
} else {
|
||||
warn(`[BiliCookie] Cookie 验证失败 (API 返回): ${json.message || `code: ${json.code}`}`);
|
||||
return { valid: false };
|
||||
warn(`[BiliCookie] Cookie 验证失败 (API 返回): ${json.message || `code: ${json.code}`}`)
|
||||
return { valid: false }
|
||||
}
|
||||
} catch (err) {
|
||||
error('[BiliCookie] 验证 Cookie 时请求 Bilibili API 出错: ' + String(err));
|
||||
return { valid: false };
|
||||
error(`[BiliCookie] 验证 Cookie 时请求 Bilibili API 出错: ${String(err)}`)
|
||||
return { valid: false }
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 从 CookieCloud 服务获取并解密 Bilibili Cookie
|
||||
@@ -187,17 +185,17 @@ export const useBiliCookie = defineStore('biliCookie', () => {
|
||||
* @throws 如果配置缺失、网络请求失败、解密失败或未找到 Bilibili Cookie,则抛出错误
|
||||
*/
|
||||
const _fetchAndDecryptFromCloud = async (config?: CookieCloudConfig): Promise<string> => {
|
||||
const cloudConfig = config ?? await cookieCloudStore.get(); // 获取配置
|
||||
const cloudConfig = config ?? await cookieCloudStore.get() // 获取配置
|
||||
|
||||
if (!cloudConfig?.key || !cloudConfig?.password) {
|
||||
throw new Error("CookieCloud 配置不完整 (缺少 Key 或 Password)");
|
||||
throw new Error('CookieCloud 配置不完整 (缺少 Key 或 Password)')
|
||||
}
|
||||
|
||||
const host = cloudConfig.host || "https://cookie.vtsuru.live"; // 默认 Host
|
||||
const url = new URL(host);
|
||||
url.pathname = `/get/${cloudConfig.key}`;
|
||||
const host = cloudConfig.host || 'https://cookie.vtsuru.live' // 默认 Host
|
||||
const url = new URL(host)
|
||||
url.pathname = `/get/${cloudConfig.key}`
|
||||
|
||||
info(`[BiliCookie] 正在从 CookieCloud (${url.hostname}) 获取 Cookie...`);
|
||||
info(`[BiliCookie] 正在从 CookieCloud (${url.hostname}) 获取 Cookie...`)
|
||||
|
||||
try {
|
||||
// 注意: 浏览器环境通常无法直接设置 User-Agent
|
||||
@@ -205,114 +203,111 @@ export const useBiliCookie = defineStore('biliCookie', () => {
|
||||
const response = await tauriFetch(url.toString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json' // 根据 CookieCloud API 要求可能需要调整
|
||||
}
|
||||
});
|
||||
'Content-Type': 'application/json', // 根据 CookieCloud API 要求可能需要调整
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`CookieCloud 请求失败: ${response.status} ${response.statusText}. ${errorText}`);
|
||||
const errorText = await response.text()
|
||||
throw new Error(`CookieCloud 请求失败: ${response.status} ${response.statusText}. ${errorText}`)
|
||||
}
|
||||
|
||||
const json = await response.json() as any; // 类型断言需要谨慎
|
||||
const json = await response.json() // 类型断言需要谨慎
|
||||
|
||||
if (json.encrypted) {
|
||||
// 执行解密
|
||||
try {
|
||||
const keyMaterial = MD5(cloudConfig.key + '-' + cloudConfig.password).toString();
|
||||
const decryptionKey = keyMaterial.substring(0, 16); // 取前16位作为 AES 密钥
|
||||
const decrypted = AES.decrypt(json.encrypted, decryptionKey).toString(enc.Utf8);
|
||||
const keyMaterial = MD5(`${cloudConfig.key}-${cloudConfig.password}`).toString()
|
||||
const decryptionKey = keyMaterial.substring(0, 16) // 取前16位作为 AES 密钥
|
||||
const decrypted = AES.decrypt(json.encrypted, decryptionKey).toString(enc.Utf8)
|
||||
|
||||
if (!decrypted) {
|
||||
throw new Error("解密结果为空,可能是密钥不匹配");
|
||||
throw new Error('解密结果为空,可能是密钥不匹配')
|
||||
}
|
||||
|
||||
const cookieData = JSON.parse(decrypted) as CookieCloudExportData;
|
||||
const cookieData = JSON.parse(decrypted) as CookieCloudExportData
|
||||
|
||||
// 提取 bilibili.com 的 Cookie
|
||||
const biliCookies = cookieData.cookie_data?.['bilibili.com'];
|
||||
const biliCookies = cookieData.cookie_data?.['bilibili.com']
|
||||
if (!biliCookies || biliCookies.length === 0) {
|
||||
throw new Error("在 CookieCloud 数据中未找到 'bilibili.com' 的 Cookie");
|
||||
throw new Error('在 CookieCloud 数据中未找到 \'bilibili.com\' 的 Cookie')
|
||||
}
|
||||
|
||||
// 拼接 Cookie 字符串
|
||||
const cookieString = biliCookies
|
||||
.map(c => `${c.name}=${c.value}`)
|
||||
.join('; ');
|
||||
|
||||
info('[BiliCookie] CookieCloud Cookie 获取并解密成功');
|
||||
return cookieString;
|
||||
.join('; ')
|
||||
|
||||
info('[BiliCookie] CookieCloud Cookie 获取并解密成功')
|
||||
return cookieString
|
||||
} catch (decryptErr) {
|
||||
error('[BiliCookie] CookieCloud Cookie 解密失败: ' + String(decryptErr));
|
||||
throw new Error(`Cookie 解密失败: ${decryptErr instanceof Error ? decryptErr.message : String(decryptErr)}`);
|
||||
error(`[BiliCookie] CookieCloud Cookie 解密失败: ${String(decryptErr)}`)
|
||||
throw new Error(`Cookie 解密失败: ${decryptErr instanceof Error ? decryptErr.message : String(decryptErr)}`)
|
||||
}
|
||||
} else if (json.cookie_data) {
|
||||
// 处理未加密的情况 (如果 CookieCloud 支持)
|
||||
warn('[BiliCookie] 从 CookieCloud 收到未加密的 Cookie 数据');
|
||||
const biliCookies = (json as CookieCloudExportData).cookie_data?.['bilibili.com'];
|
||||
warn('[BiliCookie] 从 CookieCloud 收到未加密的 Cookie 数据')
|
||||
const biliCookies = (json as CookieCloudExportData).cookie_data?.['bilibili.com']
|
||||
if (!biliCookies || biliCookies.length === 0) {
|
||||
throw new Error("在 CookieCloud 数据中未找到 'bilibili.com' 的 Cookie");
|
||||
throw new Error('在 CookieCloud 数据中未找到 \'bilibili.com\' 的 Cookie')
|
||||
}
|
||||
const cookieString = biliCookies
|
||||
.map(c => `${c.name}=${c.value}`)
|
||||
.join('; ');
|
||||
return cookieString;
|
||||
}
|
||||
else {
|
||||
.join('; ')
|
||||
return cookieString
|
||||
} else {
|
||||
// API 返回了非预期的数据结构
|
||||
throw new Error(json.message || "从 CookieCloud 获取 Cookie 失败,响应格式不正确");
|
||||
throw new Error(json.message || '从 CookieCloud 获取 Cookie 失败,响应格式不正确')
|
||||
}
|
||||
} catch (networkErr) {
|
||||
error('[BiliCookie] 请求 CookieCloud 时出错: ' + String(networkErr));
|
||||
throw new Error(`请求 CookieCloud 时出错: ${networkErr instanceof Error ? networkErr.message : String(networkErr)}`);
|
||||
error(`[BiliCookie] 请求 CookieCloud 时出错: ${String(networkErr)}`)
|
||||
throw new Error(`请求 CookieCloud 时出错: ${networkErr instanceof Error ? networkErr.message : String(networkErr)}`)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 从已配置的 CookieCloud 同步 Cookie,并更新本地状态
|
||||
* @returns Promise<boolean> 是否同步并验证成功
|
||||
*/
|
||||
const _syncFromCookieCloud = async (): Promise<boolean> => {
|
||||
const config = await cookieCloudStore.get();
|
||||
const config = await cookieCloudStore.get()
|
||||
if (!config?.key) {
|
||||
debug('[BiliCookie] 未配置 CookieCloud 或缺少 key,跳过同步');
|
||||
debug('[BiliCookie] 未配置 CookieCloud 或缺少 key,跳过同步')
|
||||
// 如果从未设置过,保持 unset;如果之前设置过但现在无效,标记为 invalid
|
||||
if (cookieCloudState.value !== 'unset') {
|
||||
cookieCloudState.value = 'invalid'; // 假设配置被清空意味着无效
|
||||
cookieCloudState.value = 'invalid' // 假设配置被清空意味着无效
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
cookieCloudState.value = 'syncing'; // 标记为同步中
|
||||
cookieCloudState.value = 'syncing' // 标记为同步中
|
||||
try {
|
||||
const cookieString = await _fetchAndDecryptFromCloud(config);
|
||||
const cookieString = await _fetchAndDecryptFromCloud(config)
|
||||
// 验证从 Cloud 获取的 Cookie
|
||||
const validationResult = await _checkCookieValidity(cookieString);
|
||||
const validationResult = await _checkCookieValidity(cookieString)
|
||||
|
||||
if (validationResult.valid) {
|
||||
// 验证成功,保存 Cookie
|
||||
await setBiliCookie(cookieString); // setBiliCookie 内部会处理状态更新和持久化
|
||||
cookieCloudState.value = 'valid'; // 标记为有效
|
||||
info('[BiliCookie] 从 CookieCloud 同步并验证 Cookie 成功');
|
||||
return true;
|
||||
await setBiliCookie(cookieString) // setBiliCookie 内部会处理状态更新和持久化
|
||||
cookieCloudState.value = 'valid' // 标记为有效
|
||||
info('[BiliCookie] 从 CookieCloud 同步并验证 Cookie 成功')
|
||||
return true
|
||||
} else {
|
||||
// 从 Cloud 获取的 Cookie 无效
|
||||
warn('[BiliCookie] 从 CookieCloud 获取的 Cookie 无效');
|
||||
cookieCloudState.value = 'invalid'; // 标记为无效
|
||||
warn('[BiliCookie] 从 CookieCloud 获取的 Cookie 无效')
|
||||
cookieCloudState.value = 'invalid' // 标记为无效
|
||||
// 不更新本地 Cookie,保留当前有效的或无效的状态
|
||||
_updateCookieState(hasBiliCookie.value, false); // 显式标记当前cookie状态可能因云端无效而变为无效
|
||||
return false;
|
||||
_updateCookieState(hasBiliCookie.value, false) // 显式标记当前cookie状态可能因云端无效而变为无效
|
||||
return false
|
||||
}
|
||||
} catch (err) {
|
||||
error('[BiliCookie] CookieCloud 同步失败: ' + String(err));
|
||||
cookieCloudState.value = 'invalid'; // 同步出错,标记为无效
|
||||
error(`[BiliCookie] CookieCloud 同步失败: ${String(err)}`)
|
||||
cookieCloudState.value = 'invalid' // 同步出错,标记为无效
|
||||
// 同步失败不应影响当前的 isCookieValid 状态,除非需要强制失效
|
||||
// _updateCookieState(hasBiliCookie.value, false); // 可选:同步失败时强制本地cookie失效
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// --- 公开方法 ---
|
||||
|
||||
@@ -325,117 +320,115 @@ export const useBiliCookie = defineStore('biliCookie', () => {
|
||||
*/
|
||||
const init = async (): Promise<void> => {
|
||||
if (_isInitialized) {
|
||||
debug('[BiliCookie] Store 已初始化,跳过');
|
||||
return;
|
||||
debug('[BiliCookie] Store 已初始化,跳过')
|
||||
return
|
||||
}
|
||||
_isInitialized = true;
|
||||
info('[BiliCookie] Store 初始化开始...');
|
||||
_isInitialized = true
|
||||
info('[BiliCookie] Store 初始化开始...')
|
||||
|
||||
// 1. 加载持久化数据
|
||||
const [storedCookieData, storedCloudConfig, storedUserInfo] = await Promise.all([
|
||||
biliCookieStore.value,
|
||||
cookieCloudStore.get(),
|
||||
userInfoCacheStore.get(),
|
||||
]);
|
||||
])
|
||||
|
||||
// 2. 处理 CookieCloud 配置
|
||||
if (storedCloudConfig?.key && storedCloudConfig?.password) {
|
||||
// 这里仅设置初始状态,有效性将在后续检查或同步中确认
|
||||
cookieCloudState.value = 'valid'; // 假设配置存在即可能有效,待验证
|
||||
info('[BiliCookie] 检测到已配置 CookieCloud');
|
||||
cookieCloudState.value = 'valid' // 假设配置存在即可能有效,待验证
|
||||
info('[BiliCookie] 检测到已配置 CookieCloud')
|
||||
} else {
|
||||
cookieCloudState.value = 'unset';
|
||||
info('[BiliCookie] 未配置 CookieCloud');
|
||||
cookieCloudState.value = 'unset'
|
||||
info('[BiliCookie] 未配置 CookieCloud')
|
||||
}
|
||||
|
||||
// 3. 处理用户信息缓存
|
||||
if (storedUserInfo && (Date.now() - storedUserInfo.accessedAt < USER_INFO_CACHE_DURATION)) {
|
||||
_cachedUserInfo.value = storedUserInfo;
|
||||
uId.value = storedUserInfo.userInfo.mid;
|
||||
info(`[BiliCookie] 从缓存加载有效用户信息: UID=${uId.value}`);
|
||||
_cachedUserInfo.value = storedUserInfo
|
||||
uId.value = storedUserInfo.userInfo.mid
|
||||
info(`[BiliCookie] 从缓存加载有效用户信息: UID=${uId.value}`)
|
||||
// 如果缓存有效,可以初步认为 Cookie 是有效的 (至少在缓存有效期内是)
|
||||
_updateCookieState(!!storedCookieData?.cookie, true);
|
||||
_updateCookieState(!!storedCookieData?.cookie, true)
|
||||
} else {
|
||||
info('[BiliCookie] 无有效用户信息缓存');
|
||||
_updateCookieState(!!storedCookieData?.cookie, false); // 默认无效,待检查
|
||||
info('[BiliCookie] 无有效用户信息缓存')
|
||||
_updateCookieState(!!storedCookieData?.cookie, false) // 默认无效,待检查
|
||||
if (storedUserInfo) {
|
||||
// 如果有缓存但已过期,清除它
|
||||
await _clearUserInfoCache();
|
||||
await _clearUserInfoCache()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 4. 处理 Bilibili Cookie
|
||||
if (storedCookieData?.cookie) {
|
||||
hasBiliCookie.value = true; // 标记存在 Cookie
|
||||
info('[BiliCookie] 检测到已存储的 Bilibili Cookie');
|
||||
hasBiliCookie.value = true // 标记存在 Cookie
|
||||
info('[BiliCookie] 检测到已存储的 Bilibili Cookie')
|
||||
// 检查 Cookie 有效性,除非用户信息缓存有效且未过期
|
||||
if (!_cachedUserInfo.value) { // 只有在没有有效缓存时才立即检查
|
||||
info('[BiliCookie] 无有效缓存,立即检查 Cookie 有效性...');
|
||||
const { valid } = await _checkCookieValidity(storedCookieData.cookie);
|
||||
_updateCookieState(true, valid); // 更新状态
|
||||
info('[BiliCookie] 无有效缓存,立即检查 Cookie 有效性...')
|
||||
const { valid } = await _checkCookieValidity(storedCookieData.cookie)
|
||||
_updateCookieState(true, valid) // 更新状态
|
||||
}
|
||||
} else {
|
||||
_updateCookieState(false, false); // 没有 Cookie,自然无效
|
||||
info('[BiliCookie] 未找到存储的 Bilibili Cookie');
|
||||
_updateCookieState(false, false) // 没有 Cookie,自然无效
|
||||
info('[BiliCookie] 未找到存储的 Bilibili Cookie')
|
||||
}
|
||||
|
||||
|
||||
// 5. 启动定时检查器
|
||||
if (_checkIntervalId) {
|
||||
clearInterval(_checkIntervalId); // 清除旧的定时器 (理论上不应存在)
|
||||
clearInterval(_checkIntervalId) // 清除旧的定时器 (理论上不应存在)
|
||||
}
|
||||
_checkIntervalId = setInterval(check, REGULAR_CHECK_INTERVAL);
|
||||
info(`[BiliCookie] 定时检查已启动,周期: ${REGULAR_CHECK_INTERVAL / 1000} 秒`);
|
||||
_checkIntervalId = setInterval(check, REGULAR_CHECK_INTERVAL)
|
||||
info(`[BiliCookie] 定时检查已启动,周期: ${REGULAR_CHECK_INTERVAL / 1000} 秒`)
|
||||
|
||||
info('[BiliCookie] Store 初始化完成');
|
||||
};
|
||||
info('[BiliCookie] Store 初始化完成')
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 定期检查 Cookie 有效性,并按需从 CookieCloud 同步
|
||||
* @param forceCheckCloud 是否强制立即尝试从 CookieCloud 同步 (通常由 init 调用)
|
||||
*/
|
||||
const check = async (forceCheckCloud: boolean = false): Promise<void> => {
|
||||
debug('[BiliCookie] 开始周期性检查...');
|
||||
_checkCounter++;
|
||||
debug('[BiliCookie] 开始周期性检查...')
|
||||
_checkCounter++
|
||||
|
||||
let cloudSyncAttempted = false;
|
||||
let cloudSyncSuccess = false;
|
||||
let cloudSyncAttempted = false
|
||||
let cloudSyncSuccess = false
|
||||
|
||||
// 检查是否需要从 CookieCloud 同步
|
||||
const shouldSyncCloud = forceCheckCloud || (_checkCounter % CLOUD_SYNC_INTERVAL_CHECKS === 0);
|
||||
const shouldSyncCloud = forceCheckCloud || (_checkCounter % CLOUD_SYNC_INTERVAL_CHECKS === 0)
|
||||
|
||||
if (shouldSyncCloud && cookieCloudState.value !== 'unset' && cookieCloudState.value !== 'syncing') {
|
||||
info(`[BiliCookie] 触发 CookieCloud 同步 (计数: ${_checkCounter}, 强制: ${forceCheckCloud})`);
|
||||
cloudSyncAttempted = true;
|
||||
cloudSyncSuccess = await _syncFromCookieCloud();
|
||||
info(`[BiliCookie] 触发 CookieCloud 同步 (计数: ${_checkCounter}, 强制: ${forceCheckCloud})`)
|
||||
cloudSyncAttempted = true
|
||||
cloudSyncSuccess = await _syncFromCookieCloud()
|
||||
// 同步后重置计数器,避免连续同步
|
||||
_checkCounter = 0;
|
||||
_checkCounter = 0
|
||||
}
|
||||
|
||||
// 如果没有尝试云同步,或者云同步失败,则检查本地 Cookie
|
||||
if (!cloudSyncAttempted || !cloudSyncSuccess) {
|
||||
debug('[BiliCookie] 检查本地存储的 Cookie 有效性...');
|
||||
const storedCookie = biliCookieStore.value?.cookie;
|
||||
debug('[BiliCookie] 检查本地存储的 Cookie 有效性...')
|
||||
const storedCookie = biliCookieStore.value?.cookie
|
||||
if (storedCookie) {
|
||||
const { valid } = await _checkCookieValidity(storedCookie);
|
||||
const { valid } = await _checkCookieValidity(storedCookie)
|
||||
// 只有在云同步未成功时才更新状态,避免覆盖云同步设置的状态
|
||||
if (!cloudSyncSuccess) {
|
||||
_updateCookieState(true, valid);
|
||||
_updateCookieState(true, valid)
|
||||
}
|
||||
} else {
|
||||
// 本地没有 Cookie
|
||||
_updateCookieState(false, false);
|
||||
_updateCookieState(false, false)
|
||||
// 如果本地没 cookie 但 cookieCloud 配置存在且非 syncing, 尝试一次同步
|
||||
if (!cloudSyncAttempted && cookieCloudState.value !== 'unset' && cookieCloudState.value !== 'syncing') {
|
||||
info('[BiliCookie] 本地无 Cookie,尝试从 CookieCloud 获取...');
|
||||
await _syncFromCookieCloud(); // 尝试获取一次
|
||||
_checkCounter = 0; // 同步后重置计数器
|
||||
info('[BiliCookie] 本地无 Cookie,尝试从 CookieCloud 获取...')
|
||||
await _syncFromCookieCloud() // 尝试获取一次
|
||||
_checkCounter = 0 // 同步后重置计数器
|
||||
}
|
||||
}
|
||||
}
|
||||
debug('[BiliCookie] 周期性检查结束');
|
||||
};
|
||||
debug('[BiliCookie] 周期性检查结束')
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 设置新的 Bilibili Cookie
|
||||
@@ -443,69 +436,69 @@ export const useBiliCookie = defineStore('biliCookie', () => {
|
||||
* @param refreshToken (可选) Bilibili refresh token
|
||||
*/
|
||||
const setBiliCookie = async (cookie: string, refreshToken?: string): Promise<void> => {
|
||||
info('[BiliCookie] 正在设置新的 Bilibili Cookie...');
|
||||
info('[BiliCookie] 正在设置新的 Bilibili Cookie...')
|
||||
// 1. 验证新 Cookie 的有效性
|
||||
const { valid } = await _checkCookieValidity(cookie);
|
||||
const { valid } = await _checkCookieValidity(cookie)
|
||||
|
||||
if (valid) {
|
||||
// 2. 如果有效,则持久化存储
|
||||
const dataToStore: BiliCookieStoreData = {
|
||||
cookie,
|
||||
...(refreshToken && { refreshToken }), // 仅在提供时添加 refreshToken
|
||||
lastRefresh: new Date() // 更新刷新时间戳
|
||||
};
|
||||
lastRefresh: new Date(), // 更新刷新时间戳
|
||||
}
|
||||
try {
|
||||
biliCookieStore.value = dataToStore; // 使用响应式存储
|
||||
info('[BiliCookie] 新 Bilibili Cookie 已验证并保存');
|
||||
_updateCookieState(true, true); // 更新状态为存在且有效
|
||||
biliCookieStore.value = dataToStore // 使用响应式存储
|
||||
info('[BiliCookie] 新 Bilibili Cookie 已验证并保存')
|
||||
_updateCookieState(true, true) // 更新状态为存在且有效
|
||||
} catch (err) {
|
||||
error('[BiliCookie] 保存 Bilibili Cookie 失败: ' + String(err));
|
||||
error(`[BiliCookie] 保存 Bilibili Cookie 失败: ${String(err)}`)
|
||||
// 保存失败,状态回滚或标记为错误?暂时保持验证结果
|
||||
_updateCookieState(true, false); // Cookie 存在但保存失败,标记无效可能更安全
|
||||
throw new Error("保存 Bilibili Cookie 失败"); // 向上抛出错误
|
||||
_updateCookieState(true, false) // Cookie 存在但保存失败,标记无效可能更安全
|
||||
throw new Error('保存 Bilibili Cookie 失败') // 向上抛出错误
|
||||
}
|
||||
} else {
|
||||
// 新 Cookie 无效,不保存,并标记状态
|
||||
_updateCookieState(hasBiliCookie.value, false); // 保持 hasBiliCookie 原样或设为 false?取决于策略
|
||||
warn('[BiliCookie] 尝试设置的 Bilibili Cookie 无效,未保存');
|
||||
_updateCookieState(hasBiliCookie.value, false) // 保持 hasBiliCookie 原样或设为 false?取决于策略
|
||||
warn('[BiliCookie] 尝试设置的 Bilibili Cookie 无效,未保存')
|
||||
// 可以选择抛出错误,让调用者知道设置失败
|
||||
// throw new Error("设置的 Bilibili Cookie 无效");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取当前存储的 Bilibili Cookie (不保证有效性)
|
||||
* @returns Promise<string | undefined> Cookie 字符串或 undefined
|
||||
*/
|
||||
const getBiliCookie = async (): Promise<string | undefined> => {
|
||||
const data = biliCookieStore.value;
|
||||
return data?.cookie;
|
||||
};
|
||||
const data = biliCookieStore.value
|
||||
return data?.cookie
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 退出登录,清除 Bilibili Cookie 及相关状态和缓存
|
||||
*/
|
||||
const logout = async (): Promise<void> => {
|
||||
info('[BiliCookie] 用户请求退出登录...');
|
||||
info('[BiliCookie] 用户请求退出登录...')
|
||||
// 停止定时检查器
|
||||
if (_checkIntervalId) {
|
||||
clearInterval(_checkIntervalId);
|
||||
_checkIntervalId = null;
|
||||
debug('[BiliCookie] 定时检查已停止');
|
||||
clearInterval(_checkIntervalId)
|
||||
_checkIntervalId = null
|
||||
debug('[BiliCookie] 定时检查已停止')
|
||||
}
|
||||
// 清除 Cookie 存储
|
||||
biliCookieStore.value = undefined; // 清除持久化存储
|
||||
biliCookieStore.value = undefined // 清除持久化存储
|
||||
// 清除用户信息缓存
|
||||
await _clearUserInfoCache();
|
||||
await _clearUserInfoCache()
|
||||
// 重置状态变量
|
||||
_updateCookieState(false, false);
|
||||
_updateCookieState(false, false)
|
||||
// Cookie Cloud 状态是否重置?取决于产品逻辑,暂时保留
|
||||
// cookieCloudState.value = 'unset';
|
||||
// 重置初始化标志,允许重新 init
|
||||
_isInitialized = false;
|
||||
_checkCounter = 0; // 重置计数器
|
||||
info('[BiliCookie] 退出登录完成,状态已重置');
|
||||
};
|
||||
_isInitialized = false
|
||||
_checkCounter = 0 // 重置计数器
|
||||
info('[BiliCookie] 退出登录完成,状态已重置')
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 设置并验证 CookieCloud 配置
|
||||
@@ -513,46 +506,46 @@ export const useBiliCookie = defineStore('biliCookie', () => {
|
||||
* @throws 如果配置无效或从 CookieCloud 获取/验证 Cookie 失败
|
||||
*/
|
||||
const setCookieCloudConfig = async (config: CookieCloudConfig): Promise<void> => {
|
||||
info('[BiliCookie] 正在设置新的 CookieCloud 配置...');
|
||||
cookieCloudState.value = 'syncing'; // 标记为尝试同步/验证中
|
||||
info('[BiliCookie] 正在设置新的 CookieCloud 配置...')
|
||||
cookieCloudState.value = 'syncing' // 标记为尝试同步/验证中
|
||||
|
||||
try {
|
||||
// 1. 使用新配置尝试从 Cloud 获取 Cookie
|
||||
const cookieString = await _fetchAndDecryptFromCloud(config);
|
||||
const cookieString = await _fetchAndDecryptFromCloud(config)
|
||||
// 2. 验证获取到的 Cookie
|
||||
const validationResult = await _checkCookieValidity(cookieString);
|
||||
const validationResult = await _checkCookieValidity(cookieString)
|
||||
|
||||
if (validationResult.valid && validationResult.data) {
|
||||
// 3. 如果验证成功,保存 CookieCloud 配置
|
||||
await cookieCloudStore.set(config);
|
||||
info('[BiliCookie] CookieCloud 配置验证成功并已保存. 用户:' + validationResult.data.name);
|
||||
cookieCloudState.value = 'valid'; // 标记为有效
|
||||
await cookieCloudStore.set(config)
|
||||
info(`[BiliCookie] CookieCloud 配置验证成功并已保存. 用户:${validationResult.data.name}`)
|
||||
cookieCloudState.value = 'valid' // 标记为有效
|
||||
|
||||
// 4. 使用从 Cloud 获取的有效 Cookie 更新本地 Cookie
|
||||
// 注意:这里直接调用 setBiliCookie 会再次进行验证,但确保状态一致性
|
||||
await setBiliCookie(cookieString);
|
||||
await setBiliCookie(cookieString)
|
||||
// 重置检查计数器,以便下次正常检查
|
||||
_checkCounter = 0;
|
||||
_checkCounter = 0
|
||||
} else {
|
||||
// 从 Cloud 获取的 Cookie 无效
|
||||
cookieCloudState.value = 'invalid';
|
||||
warn('[BiliCookie] 使用新 CookieCloud 配置获取的 Cookie 无效');
|
||||
throw new Error('CookieCloud 配置无效:获取到的 Bilibili Cookie 无法通过验证');
|
||||
cookieCloudState.value = 'invalid'
|
||||
warn('[BiliCookie] 使用新 CookieCloud 配置获取的 Cookie 无效')
|
||||
throw new Error('CookieCloud 配置无效:获取到的 Bilibili Cookie 无法通过验证')
|
||||
}
|
||||
} catch (err) {
|
||||
error('[BiliCookie] 设置 CookieCloud 配置失败: ' + String(err));
|
||||
cookieCloudState.value = 'invalid'; // 出错则标记为无效
|
||||
error(`[BiliCookie] 设置 CookieCloud 配置失败: ${String(err)}`)
|
||||
cookieCloudState.value = 'invalid' // 出错则标记为无效
|
||||
// 向上抛出错误,通知调用者失败
|
||||
throw err; // err 已经是 Error 类型或被包装过
|
||||
throw err // err 已经是 Error 类型或被包装过
|
||||
}
|
||||
};
|
||||
}
|
||||
async function clearCookieCloudConfig() {
|
||||
info('[BiliCookie] 清除 CookieCloud 配置...');
|
||||
cookieCloudState.value = 'unset';
|
||||
info('[BiliCookie] 清除 CookieCloud 配置...')
|
||||
cookieCloudState.value = 'unset'
|
||||
// 清除持久化存储
|
||||
await cookieCloudStore.delete().catch(err => {
|
||||
error('[BiliCookie] 清除 CookieCloud 配置失败: ' + String(err));
|
||||
});
|
||||
await cookieCloudStore.delete().catch((err) => {
|
||||
error(`[BiliCookie] 清除 CookieCloud 配置失败: ${String(err)}`)
|
||||
})
|
||||
}
|
||||
|
||||
// --- 返回 Store 的公开接口 ---
|
||||
@@ -575,10 +568,10 @@ export const useBiliCookie = defineStore('biliCookie', () => {
|
||||
setCookieCloudConfig,
|
||||
clearCookieCloudConfig,
|
||||
// 注意:不再直接暴露 fetchBiliCookieFromCloud,其逻辑已整合到内部同步和设置流程中
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
// --- HMR 支持 ---
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useBiliCookie, import.meta.hot));
|
||||
}
|
||||
import.meta.hot.accept(acceptHMRUpdate(useBiliCookie, import.meta.hot))
|
||||
}
|
||||
|
||||
@@ -1,95 +1,156 @@
|
||||
import { useAccount } from "@/api/account";
|
||||
import { useBiliCookie } from "./useBiliCookie";
|
||||
import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; // 引入 Body
|
||||
import { defineStore, acceptHMRUpdate } from 'pinia';
|
||||
import { computed, ref, onUnmounted, h } from 'vue';
|
||||
import md5 from 'md5';
|
||||
import { QueryBiliAPI } from "../data/utils";
|
||||
import { onSendPrivateMessageFailed } from "../data/notification";
|
||||
import { useSettings } from "./useSettings";
|
||||
import { isDev } from "@/data/constants";
|
||||
import { fetch as tauriFetch } from '@tauri-apps/plugin-http' // 引入 Body
|
||||
import md5 from 'md5'
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia'
|
||||
import { computed, h, onUnmounted, ref } from 'vue'
|
||||
import { useAccount } from '@/api/account'
|
||||
import { isDev } from '@/data/constants'
|
||||
import { onSendPrivateMessageFailed } from '../data/notification'
|
||||
import { QueryBiliAPI } from '../data/utils'
|
||||
import { useBiliCookie } from './useBiliCookie'
|
||||
import { useSettings } from './useSettings'
|
||||
|
||||
// WBI 混合密钥编码表
|
||||
const mixinKeyEncTab = [
|
||||
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
|
||||
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
|
||||
61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
|
||||
36, 20, 34, 44, 52
|
||||
];
|
||||
46,
|
||||
47,
|
||||
18,
|
||||
2,
|
||||
53,
|
||||
8,
|
||||
23,
|
||||
32,
|
||||
15,
|
||||
50,
|
||||
10,
|
||||
31,
|
||||
58,
|
||||
3,
|
||||
45,
|
||||
35,
|
||||
27,
|
||||
43,
|
||||
5,
|
||||
49,
|
||||
33,
|
||||
9,
|
||||
42,
|
||||
19,
|
||||
29,
|
||||
28,
|
||||
14,
|
||||
39,
|
||||
12,
|
||||
38,
|
||||
41,
|
||||
13,
|
||||
37,
|
||||
48,
|
||||
7,
|
||||
16,
|
||||
24,
|
||||
55,
|
||||
40,
|
||||
61,
|
||||
26,
|
||||
17,
|
||||
0,
|
||||
1,
|
||||
60,
|
||||
51,
|
||||
30,
|
||||
4,
|
||||
22,
|
||||
25,
|
||||
54,
|
||||
21,
|
||||
56,
|
||||
59,
|
||||
6,
|
||||
63,
|
||||
57,
|
||||
62,
|
||||
11,
|
||||
36,
|
||||
20,
|
||||
34,
|
||||
44,
|
||||
52,
|
||||
]
|
||||
|
||||
// 对 imgKey 和 subKey 进行字符顺序打乱编码
|
||||
const getMixinKey = (orig: string): string =>
|
||||
mixinKeyEncTab.map(n => orig[n]).join('').slice(0, 32);
|
||||
function getMixinKey(orig: string): string {
|
||||
return mixinKeyEncTab.map(n => orig[n]).join('').slice(0, 32)
|
||||
}
|
||||
|
||||
export const useBiliFunction = defineStore('biliFunction', () => {
|
||||
const biliCookieStore = useBiliCookie();
|
||||
const account = useAccount();
|
||||
const settingsStore = useSettings();
|
||||
const cookie = computed(() => biliCookieStore.cookie);
|
||||
const uid = computed(() => account.value.biliId);
|
||||
const biliCookieStore = useBiliCookie()
|
||||
const account = useAccount()
|
||||
const settingsStore = useSettings()
|
||||
const cookie = computed(() => biliCookieStore.cookie)
|
||||
const uid = computed(() => account.value.biliId)
|
||||
// 存储WBI密钥
|
||||
const wbiKeys = ref<{ img_key: string, sub_key: string } | null>(null);
|
||||
const wbiKeysTimestamp = ref<number | null>(null);
|
||||
const wbiKeys = ref<{ img_key: string, sub_key: string } | null>(null)
|
||||
const wbiKeysTimestamp = ref<number | null>(null)
|
||||
|
||||
// 队列相关状态
|
||||
const danmakuQueue = ref<{ roomId: number, message: string, color?: string, fontsize?: number, mode?: number }[]>([]);
|
||||
const pmQueue = ref<{ receiverId: number, message: string }[]>([]);
|
||||
const isDanmakuProcessing = ref(false);
|
||||
const isPmProcessing = ref(false);
|
||||
const danmakuTimer = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||
const pmTimer = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||
const danmakuQueue = ref<{ roomId: number, message: string, color?: string, fontsize?: number, mode?: number }[]>([])
|
||||
const pmQueue = ref<{ receiverId: number, message: string }[]>([])
|
||||
const isDanmakuProcessing = ref(false)
|
||||
const isPmProcessing = ref(false)
|
||||
const danmakuTimer = ref<ReturnType<typeof setTimeout> | null>(null)
|
||||
const pmTimer = ref<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
// 使用computed获取设置中的间隔值
|
||||
const danmakuInterval = computed(() => settingsStore.settings.danmakuInterval);
|
||||
const pmInterval = computed(() => settingsStore.settings.pmInterval);
|
||||
const danmakuInterval = computed(() => settingsStore.settings.danmakuInterval)
|
||||
const pmInterval = computed(() => settingsStore.settings.pmInterval)
|
||||
|
||||
const csrf = computed(() => {
|
||||
if (!cookie.value) return null;
|
||||
const match = cookie.value.match(/bili_jct=([^;]+)/);
|
||||
return match ? match[1] : null;
|
||||
});
|
||||
if (!cookie.value) return null
|
||||
const match = cookie.value.match(/bili_jct=([^;]+)/)
|
||||
return match ? match[1] : null
|
||||
})
|
||||
|
||||
// 设置间隔的方法
|
||||
async function setDanmakuInterval(interval: number) {
|
||||
settingsStore.settings.danmakuInterval = interval;
|
||||
await settingsStore.save();
|
||||
settingsStore.settings.danmakuInterval = interval
|
||||
await settingsStore.save()
|
||||
}
|
||||
|
||||
async function setPmInterval(interval: number) {
|
||||
settingsStore.settings.pmInterval = interval;
|
||||
await settingsStore.save();
|
||||
settingsStore.settings.pmInterval = interval
|
||||
await settingsStore.save()
|
||||
}
|
||||
|
||||
// 处理弹幕队列
|
||||
async function processDanmakuQueue() {
|
||||
if (isDanmakuProcessing.value || danmakuQueue.value.length === 0) return;
|
||||
isDanmakuProcessing.value = true;
|
||||
console.log('[BiliFunction] 处理弹幕队列', danmakuQueue.value);
|
||||
if (isDanmakuProcessing.value || danmakuQueue.value.length === 0) return
|
||||
isDanmakuProcessing.value = true
|
||||
console.log('[BiliFunction] 处理弹幕队列', danmakuQueue.value)
|
||||
try {
|
||||
const item = danmakuQueue.value[0];
|
||||
await _sendLiveDanmaku(item.roomId, item.message, item.color, item.fontsize, item.mode);
|
||||
danmakuQueue.value.shift();
|
||||
const item = danmakuQueue.value[0]
|
||||
await _sendLiveDanmaku(item.roomId, item.message, item.color, item.fontsize, item.mode)
|
||||
danmakuQueue.value.shift()
|
||||
} finally {
|
||||
isDanmakuProcessing.value = false;
|
||||
isDanmakuProcessing.value = false
|
||||
if (danmakuQueue.value.length > 0) {
|
||||
danmakuTimer.value = setTimeout(() => processDanmakuQueue(), danmakuInterval.value);
|
||||
danmakuTimer.value = setTimeout(async () => processDanmakuQueue(), danmakuInterval.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理私信队列
|
||||
async function processPmQueue() {
|
||||
if (isPmProcessing.value || pmQueue.value.length === 0) return;
|
||||
isPmProcessing.value = true;
|
||||
if (isPmProcessing.value || pmQueue.value.length === 0) return
|
||||
isPmProcessing.value = true
|
||||
|
||||
try {
|
||||
const item = pmQueue.value[0];
|
||||
await _sendPrivateMessage(item.receiverId, item.message);
|
||||
pmQueue.value.shift();
|
||||
const item = pmQueue.value[0]
|
||||
await _sendPrivateMessage(item.receiverId, item.message)
|
||||
pmQueue.value.shift()
|
||||
} finally {
|
||||
isPmProcessing.value = false;
|
||||
isPmProcessing.value = false
|
||||
if (pmQueue.value.length > 0) {
|
||||
pmTimer.value = setTimeout(() => processPmQueue(), pmInterval.value);
|
||||
pmTimer.value = setTimeout(async () => processPmQueue(), pmInterval.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,12 +158,12 @@ export const useBiliFunction = defineStore('biliFunction', () => {
|
||||
// 清理定时器
|
||||
function clearTimers() {
|
||||
if (danmakuTimer.value) {
|
||||
clearTimeout(danmakuTimer.value);
|
||||
danmakuTimer.value = null;
|
||||
clearTimeout(danmakuTimer.value)
|
||||
danmakuTimer.value = null
|
||||
}
|
||||
if (pmTimer.value) {
|
||||
clearTimeout(pmTimer.value);
|
||||
pmTimer.value = null;
|
||||
clearTimeout(pmTimer.value)
|
||||
pmTimer.value = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,71 +171,71 @@ export const useBiliFunction = defineStore('biliFunction', () => {
|
||||
async function init() {
|
||||
// 确保设置已经初始化
|
||||
if (!settingsStore.settings.danmakuInterval) {
|
||||
await settingsStore.init();
|
||||
await settingsStore.init()
|
||||
}
|
||||
// 启动队列处理
|
||||
processDanmakuQueue();
|
||||
processPmQueue();
|
||||
console.log('[BiliFunction] 队列初始化完成');
|
||||
processDanmakuQueue()
|
||||
processPmQueue()
|
||||
console.log('[BiliFunction] 队列初始化完成')
|
||||
// 清理定时器
|
||||
onUnmounted(() => {
|
||||
clearTimers();
|
||||
});
|
||||
clearTimers()
|
||||
})
|
||||
}
|
||||
|
||||
// 原始发送弹幕方法(重命名为_sendLiveDanmaku)
|
||||
async function _sendLiveDanmaku(roomId: number, message: string, color: string = 'ffffff', fontsize: number = 25, mode: number = 1): Promise<boolean> {
|
||||
if (!csrf.value || !cookie.value) {
|
||||
console.error("发送弹幕失败:缺少 cookie 或 csrf token");
|
||||
return false;
|
||||
console.error('发送弹幕失败:缺少 cookie 或 csrf token')
|
||||
return false
|
||||
}
|
||||
if (!message || message.trim().length === 0) {
|
||||
console.warn("尝试发送空弹幕,已阻止。");
|
||||
return false;
|
||||
console.warn('尝试发送空弹幕,已阻止。')
|
||||
return false
|
||||
}
|
||||
|
||||
// 开发环境下只显示通知,不实际发送
|
||||
if (isDev) {
|
||||
console.log(`[开发环境] 模拟发送弹幕到房间 ${roomId}: ${message}`);
|
||||
console.log(`[开发环境] 模拟发送弹幕到房间 ${roomId}: ${message}`)
|
||||
window.$notification.info({
|
||||
title: '开发环境 - 弹幕未实际发送',
|
||||
description: `房间: ${roomId}, 内容: ${message}`,
|
||||
duration: 10000,
|
||||
});
|
||||
return true;
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
const url = "https://api.live.bilibili.com/msg/send";
|
||||
const rnd = Math.floor(Date.now() / 1000);
|
||||
const url = 'https://api.live.bilibili.com/msg/send'
|
||||
const rnd = Math.floor(Date.now() / 1000)
|
||||
const data = {
|
||||
bubble: '0',
|
||||
msg: message,
|
||||
color: parseInt(color, 16).toString(),
|
||||
color: Number.parseInt(color, 16).toString(),
|
||||
fontsize: fontsize.toString(),
|
||||
mode: mode.toString(),
|
||||
roomid: roomId.toString(),
|
||||
rnd: rnd.toString(),
|
||||
csrf: csrf.value,
|
||||
csrf_token: csrf.value,
|
||||
};
|
||||
}
|
||||
const params = new URLSearchParams(data)
|
||||
try {
|
||||
const response = await tauriFetch(url, {
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Cookie": cookie.value,
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36",
|
||||
"Referer": `https://live.bilibili.com/${roomId}`
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Cookie': cookie.value,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36',
|
||||
'Referer': `https://live.bilibili.com/${roomId}`,
|
||||
},
|
||||
body: params,
|
||||
});
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("发送弹幕网络失败:", response.status, await response.text());
|
||||
return false;
|
||||
console.error('发送弹幕网络失败:', response.status, await response.text())
|
||||
return false
|
||||
}
|
||||
const json = await response.json();
|
||||
const json = await response.json()
|
||||
if (json.code !== 0) {
|
||||
window.$notification.error({
|
||||
title: '发送弹幕失败',
|
||||
@@ -188,16 +249,16 @@ export const useBiliFunction = defineStore('biliFunction', () => {
|
||||
},
|
||||
}, `错误: ${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.error(`发送弹幕API失败 to: ${roomId} ${uid.value} [${message}] - ${json.code} - ${json.message || json.msg}`)
|
||||
return false
|
||||
}
|
||||
|
||||
console.log("发送弹幕成功:", message);
|
||||
return true;
|
||||
console.log('发送弹幕成功:', message)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error("发送弹幕时发生错误:", error);
|
||||
return false;
|
||||
console.error('发送弹幕时发生错误:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,124 +266,124 @@ export const useBiliFunction = defineStore('biliFunction', () => {
|
||||
async function encWbi(
|
||||
params: { [key: string]: string | number },
|
||||
): Promise<string> {
|
||||
const keys = await getWbiKeys();
|
||||
const { img_key, sub_key } = keys;
|
||||
const mixin_key = getMixinKey(img_key + sub_key);
|
||||
const curr_time = Math.round(Date.now() / 1000);
|
||||
const chr_filter = /[!'()*]/g;
|
||||
const keys = await getWbiKeys()
|
||||
const { img_key, sub_key } = keys
|
||||
const mixin_key = getMixinKey(img_key + sub_key)
|
||||
const curr_time = Math.round(Date.now() / 1000)
|
||||
const chr_filter = /[!'()*]/g
|
||||
|
||||
Object.assign(params, { wts: curr_time.toString() }); // 添加 wts 字段
|
||||
Object.assign(params, { wts: curr_time.toString() }) // 添加 wts 字段
|
||||
|
||||
// 按照 key 重排参数
|
||||
const query = Object.keys(params)
|
||||
.sort()
|
||||
.map(key => {
|
||||
.map((key) => {
|
||||
// 过滤 value 中的 "!'()*" 字符
|
||||
const value = params[key].toString().replace(chr_filter, '');
|
||||
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
||||
const value = params[key].toString().replace(chr_filter, '')
|
||||
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
|
||||
})
|
||||
.join('&');
|
||||
.join('&')
|
||||
|
||||
const wbi_sign = md5(query + mixin_key); // 计算 w_rid
|
||||
return query + '&w_rid=' + wbi_sign;
|
||||
const wbi_sign = md5(query + mixin_key) // 计算 w_rid
|
||||
return `${query}&w_rid=${wbi_sign}`
|
||||
}
|
||||
|
||||
// 获取最新的 img_key 和 sub_key
|
||||
async function _fetchWbiKeys(): Promise<{ img_key: string, sub_key: string }> {
|
||||
try {
|
||||
const response = await QueryBiliAPI('https://api.bilibili.com/x/web-interface/nav');
|
||||
const response = await QueryBiliAPI('https://api.bilibili.com/x/web-interface/nav')
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("获取WBI密钥失败:", response.status);
|
||||
throw new Error("获取WBI密钥失败");
|
||||
console.error('获取WBI密钥失败:', response.status)
|
||||
throw new Error('获取WBI密钥失败')
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const { wbi_img } = result.data;
|
||||
const result = await response.json()
|
||||
const { wbi_img } = result.data
|
||||
|
||||
console.log(`获取WBI秘钥: img_key: ${wbi_img.img_url}, sub_key: ${wbi_img.sub_url}`);
|
||||
console.log(`获取WBI秘钥: img_key: ${wbi_img.img_url}, sub_key: ${wbi_img.sub_url}`)
|
||||
|
||||
return {
|
||||
img_key: wbi_img.img_url.slice(
|
||||
wbi_img.img_url.lastIndexOf('/') + 1,
|
||||
wbi_img.img_url.lastIndexOf('.')
|
||||
wbi_img.img_url.lastIndexOf('.'),
|
||||
),
|
||||
sub_key: wbi_img.sub_url.slice(
|
||||
wbi_img.sub_url.lastIndexOf('/') + 1,
|
||||
wbi_img.sub_url.lastIndexOf('.')
|
||||
)
|
||||
};
|
||||
wbi_img.sub_url.lastIndexOf('.'),
|
||||
),
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取WBI密钥时发生错误:", error);
|
||||
throw error;
|
||||
console.error('获取WBI密钥时发生错误:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function getWbiKeys(): Promise<{ img_key: string, sub_key: string }> {
|
||||
const now = Date.now();
|
||||
const now = Date.now()
|
||||
if (wbiKeys.value && wbiKeysTimestamp.value && (now - wbiKeysTimestamp.value < 10 * 60 * 1000)) {
|
||||
console.log("使用缓存的WBI密钥");
|
||||
return wbiKeys.value;
|
||||
console.log('使用缓存的WBI密钥')
|
||||
return wbiKeys.value
|
||||
}
|
||||
|
||||
console.log("缓存不存在或已过期,获取新的WBI密钥");
|
||||
const newKeys = await _fetchWbiKeys();
|
||||
wbiKeys.value = newKeys;
|
||||
wbiKeysTimestamp.value = now;
|
||||
return newKeys;
|
||||
console.log('缓存不存在或已过期,获取新的WBI密钥')
|
||||
const newKeys = await _fetchWbiKeys()
|
||||
wbiKeys.value = newKeys
|
||||
wbiKeysTimestamp.value = now
|
||||
return newKeys
|
||||
}
|
||||
|
||||
// 原始发送私信方法(重命名为_sendPrivateMessage)
|
||||
async function _sendPrivateMessage(receiverId: number, message: string): Promise<boolean> {
|
||||
if (!csrf.value || !cookie.value || !uid.value) {
|
||||
const error = "发送私信失败:缺少 cookie, csrf token 或 uid";
|
||||
console.error(error);
|
||||
onSendPrivateMessageFailed(receiverId, message, error);
|
||||
return false;
|
||||
const error = '发送私信失败:缺少 cookie, csrf token 或 uid'
|
||||
console.error(error)
|
||||
onSendPrivateMessageFailed(receiverId, message, error)
|
||||
return false
|
||||
}
|
||||
if (!message || message.trim().length === 0) {
|
||||
const error = "尝试发送空私信,已阻止。";
|
||||
console.warn(error);
|
||||
const error = '尝试发送空私信,已阻止。'
|
||||
console.warn(error)
|
||||
window.$notification.error({
|
||||
title: '发送私信失败',
|
||||
description: `尝试发送空私信给 ${receiverId}, 已阻止`,
|
||||
duration: 0,
|
||||
});
|
||||
return false;
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
// 开发环境下只显示通知,不实际发送
|
||||
if (isDev) {
|
||||
console.log(`[开发环境] 模拟发送私信到用户 ${receiverId}: ${message}`);
|
||||
console.log(`[开发环境] 模拟发送私信到用户 ${receiverId}: ${message}`)
|
||||
window.$notification.info({
|
||||
title: '开发环境 - 私信未实际发送',
|
||||
description: `接收者: ${receiverId}, 内容: ${message}`,
|
||||
duration: 10000,
|
||||
});
|
||||
return true;
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
const dev_id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16).toUpperCase();
|
||||
});
|
||||
const dev_id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = Math.random() * 16 | 0; const v = c == 'x' ? r : (r & 0x3 | 0x8)
|
||||
return v.toString(16).toUpperCase()
|
||||
})
|
||||
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const content = JSON.stringify({ content: message });
|
||||
const timestamp = Math.floor(Date.now() / 1000)
|
||||
const content = JSON.stringify({ content: message })
|
||||
|
||||
// 准备URL参数(需要WBI签名的参数)
|
||||
const urlParams = {
|
||||
w_sender_uid: uid.value.toString(),
|
||||
w_receiver_id: receiverId.toString(),
|
||||
w_dev_id: dev_id,
|
||||
};
|
||||
}
|
||||
|
||||
// 生成带WBI签名的URL查询字符串
|
||||
const signedQuery = await encWbi(urlParams);
|
||||
const signedQuery = await encWbi(urlParams)
|
||||
|
||||
// 构建最终URL
|
||||
const url = `https://api.vc.bilibili.com/web_im/v1/web_im/send_msg?${signedQuery}`;
|
||||
const url = `https://api.vc.bilibili.com/web_im/v1/web_im/send_msg?${signedQuery}`
|
||||
|
||||
// 准备表单数据
|
||||
const formData = {
|
||||
@@ -339,59 +400,59 @@ export const useBiliFunction = defineStore('biliFunction', () => {
|
||||
'mobi_app': 'web',
|
||||
'csrf': csrf.value,
|
||||
'csrf_token': csrf.value,
|
||||
};
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(formData);
|
||||
const params = new URLSearchParams(formData)
|
||||
const response = await tauriFetch(url, {
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Cookie": cookie.value,
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36",
|
||||
"Origin": '',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Cookie': cookie.value,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36',
|
||||
'Origin': '',
|
||||
},
|
||||
body: params,
|
||||
});
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = `发送私信网络失败: ${response.status}`;
|
||||
console.error(error, await response.text());
|
||||
onSendPrivateMessageFailed(receiverId, message, error);
|
||||
return false;
|
||||
const error = `发送私信网络失败: ${response.status}`
|
||||
console.error(error, await response.text())
|
||||
onSendPrivateMessageFailed(receiverId, message, error)
|
||||
return false
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
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} 成功`);
|
||||
return true;
|
||||
console.log(`发送私信给 ${receiverId} 成功`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error("发送私信时发生错误:", error);
|
||||
console.error('发送私信时发生错误:', error)
|
||||
// 如果是WBI密钥问题,清空密钥以便下次重新获取
|
||||
if (String(error).includes('WBI')) {
|
||||
wbiKeys.value = null;
|
||||
wbiKeys.value = null
|
||||
}
|
||||
onSendPrivateMessageFailed(receiverId, message, error);
|
||||
return false;
|
||||
onSendPrivateMessageFailed(receiverId, message, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 新的队列发送方法
|
||||
async function sendLiveDanmaku(roomId: number, message: string, color: string = 'ffffff', fontsize: number = 25, mode: number = 1): Promise<boolean> {
|
||||
danmakuQueue.value.push({ roomId, message, color, fontsize, mode });
|
||||
processDanmakuQueue();
|
||||
return true;
|
||||
danmakuQueue.value.push({ roomId, message, color, fontsize, mode })
|
||||
processDanmakuQueue()
|
||||
return true
|
||||
}
|
||||
|
||||
async function sendPrivateMessage(receiverId: number, message: string): Promise<boolean> {
|
||||
pmQueue.value.push({ receiverId, message });
|
||||
processPmQueue();
|
||||
return true;
|
||||
pmQueue.value.push({ receiverId, message })
|
||||
processPmQueue()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -403,46 +464,46 @@ export const useBiliFunction = defineStore('biliFunction', () => {
|
||||
async function banLiveUser(roomId: number, userId: number, hours: number = 1) {
|
||||
// 使用 csrf.value
|
||||
if (!csrf.value || !cookie.value) {
|
||||
console.error("封禁用户失败:缺少 cookie 或 csrf token");
|
||||
return;
|
||||
console.error('封禁用户失败:缺少 cookie 或 csrf token')
|
||||
return
|
||||
}
|
||||
// 确保 hours 在 1 到 720 之间
|
||||
const validHours = Math.max(1, Math.min(hours, 720));
|
||||
const url = "https://api.live.bilibili.com/banned_service/v2/Silent/add_user";
|
||||
const validHours = Math.max(1, Math.min(hours, 720))
|
||||
const url = 'https://api.live.bilibili.com/banned_service/v2/Silent/add_user'
|
||||
const data = {
|
||||
room_id: roomId.toString(),
|
||||
block_uid: userId.toString(),
|
||||
hour: validHours.toString(),
|
||||
csrf: csrf.value, // 使用计算属性的值
|
||||
csrf_token: csrf.value, // 使用计算属性的值
|
||||
visit_id: "", // 通常可以为空
|
||||
};
|
||||
visit_id: '', // 通常可以为空
|
||||
}
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams(data)
|
||||
const response = await tauriFetch(url, {
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Cookie": cookie.value, // 使用计算属性的值
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36",
|
||||
"Referer": `https://live.bilibili.com/p/html/live-room-setting/#/room-manager/black-list?room_id=${roomId}` // 模拟来源
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Cookie': cookie.value, // 使用计算属性的值
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36',
|
||||
'Referer': `https://live.bilibili.com/p/html/live-room-setting/#/room-manager/black-list?room_id=${roomId}`, // 模拟来源
|
||||
},
|
||||
body: params, // 发送 URLSearchParams 数据
|
||||
});
|
||||
})
|
||||
if (!response.ok) {
|
||||
console.error("封禁用户失败:", response.status, await response.text());
|
||||
return response.statusText;
|
||||
console.error('封禁用户失败:', response.status, await response.text())
|
||||
return response.statusText
|
||||
}
|
||||
const json = await response.json();
|
||||
const json = await response.json()
|
||||
if (json.code !== 0) {
|
||||
console.error("封禁用户API失败:", json.code, json.message || json.msg);
|
||||
return json.data;
|
||||
console.error('封禁用户API失败:', json.code, json.message || json.msg)
|
||||
return json.data
|
||||
}
|
||||
console.log("封禁用户成功:", json.data);
|
||||
return json.data;
|
||||
console.log('封禁用户成功:', json.data)
|
||||
return json.data
|
||||
} catch (error) {
|
||||
console.error("封禁用户时发生错误:", error);
|
||||
console.error('封禁用户时发生错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,10 +518,10 @@ export const useBiliFunction = defineStore('biliFunction', () => {
|
||||
pmInterval,
|
||||
setDanmakuInterval,
|
||||
setPmInterval,
|
||||
encWbi
|
||||
};
|
||||
});
|
||||
encWbi,
|
||||
}
|
||||
})
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useBiliFunction, import.meta.hot));
|
||||
}
|
||||
import.meta.hot.accept(acceptHMRUpdate(useBiliFunction, import.meta.hot))
|
||||
}
|
||||
|
||||
@@ -1,56 +1,58 @@
|
||||
import { EventDataTypes, EventModel, GuardLevel } from "@/api/api-models";
|
||||
import { QueryGetAPI } from "@/api/query";
|
||||
import { VTSURU_API_URL } from "@/data/constants";
|
||||
import { useDanmakuClient } from "@/store/useDanmakuClient";
|
||||
import { PhysicalPosition, PhysicalSize } from "@tauri-apps/api/dpi";
|
||||
import { getAllWebviewWindows, WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import type { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import type { EventModel } from '@/api/api-models'
|
||||
import { PhysicalPosition, PhysicalSize } from '@tauri-apps/api/dpi'
|
||||
import { getAllWebviewWindows } from '@tauri-apps/api/webviewWindow'
|
||||
import { EventDataTypes, GuardLevel } from '@/api/api-models'
|
||||
import { QueryGetAPI } from '@/api/query'
|
||||
import { VTSURU_API_URL } from '@/data/constants'
|
||||
import { useDanmakuClient } from '@/store/useDanmakuClient'
|
||||
|
||||
export type DanmakuWindowSettings = {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
opacity: number; // 窗口透明度
|
||||
showAvatar: boolean; // 是否显示头像
|
||||
showUsername: boolean; // 是否显示用户名
|
||||
showFansMedal: boolean; // 是否显示粉丝牌
|
||||
showGuardIcon: boolean; // 是否显示舰长图标
|
||||
fontSize: number; // 弹幕字体大小
|
||||
maxDanmakuCount: number; // 最大显示的弹幕数量
|
||||
reverseOrder: boolean; // 是否倒序显示(从下往上)
|
||||
filterTypes: string[]; // 要显示的弹幕类型
|
||||
animationDuration: number; // 动画持续时间
|
||||
enableAnimation: boolean; // 是否启用动画效果
|
||||
backgroundColor: string; // 背景色
|
||||
textColor: string; // 文字颜色
|
||||
alwaysOnTop: boolean; // 是否总在最前
|
||||
interactive: boolean; // 是否可交互(穿透鼠标点击)
|
||||
borderRadius: number; // 边框圆角
|
||||
itemSpacing: number; // 项目间距
|
||||
enableShadow: boolean; // 是否启用阴影
|
||||
shadowColor: string; // 阴影颜色
|
||||
autoDisappearTime: number; // 单位:秒,0表示不自动消失
|
||||
displayStyle: string; // 新增:显示风格,可选值:'card'(卡片风格), 'text'(纯文本风格)
|
||||
textStyleCompact: boolean; // 新增:纯文本模式下是否使用紧凑布局
|
||||
textStyleShowType: boolean; // 新增:纯文本模式下是否显示消息类型标签
|
||||
textStyleNameSeparator: string; // 新增:纯文本模式下用户名和消息之间的分隔符
|
||||
};
|
||||
export interface DanmakuWindowSettings {
|
||||
width: number
|
||||
height: number
|
||||
x: number
|
||||
y: number
|
||||
opacity: number // 窗口透明度
|
||||
showAvatar: boolean // 是否显示头像
|
||||
showUsername: boolean // 是否显示用户名
|
||||
showFansMedal: boolean // 是否显示粉丝牌
|
||||
showGuardIcon: boolean // 是否显示舰长图标
|
||||
fontSize: number // 弹幕字体大小
|
||||
maxDanmakuCount: number // 最大显示的弹幕数量
|
||||
reverseOrder: boolean // 是否倒序显示(从下往上)
|
||||
filterTypes: string[] // 要显示的弹幕类型
|
||||
animationDuration: number // 动画持续时间
|
||||
enableAnimation: boolean // 是否启用动画效果
|
||||
backgroundColor: string // 背景色
|
||||
textColor: string // 文字颜色
|
||||
alwaysOnTop: boolean // 是否总在最前
|
||||
interactive: boolean // 是否可交互(穿透鼠标点击)
|
||||
borderRadius: number // 边框圆角
|
||||
itemSpacing: number // 项目间距
|
||||
enableShadow: boolean // 是否启用阴影
|
||||
shadowColor: string // 阴影颜色
|
||||
autoDisappearTime: number // 单位:秒,0表示不自动消失
|
||||
displayStyle: string // 新增:显示风格,可选值:'card'(卡片风格), 'text'(纯文本风格)
|
||||
textStyleCompact: boolean // 新增:纯文本模式下是否使用紧凑布局
|
||||
textStyleShowType: boolean // 新增:纯文本模式下是否显示消息类型标签
|
||||
textStyleNameSeparator: string // 新增:纯文本模式下用户名和消息之间的分隔符
|
||||
}
|
||||
|
||||
export const DANMAKU_WINDOW_BROADCAST_CHANNEL = 'channel.danmaku.window';
|
||||
export const DANMAKU_WINDOW_BROADCAST_CHANNEL = 'channel.danmaku.window'
|
||||
export type DanmakuWindowBCData = {
|
||||
type: 'danmaku',
|
||||
data: EventModel;
|
||||
type: 'danmaku'
|
||||
data: EventModel
|
||||
} | {
|
||||
type: 'update-setting',
|
||||
data: DanmakuWindowSettings;
|
||||
type: 'update-setting'
|
||||
data: DanmakuWindowSettings
|
||||
} | {
|
||||
type: 'window-ready';
|
||||
type: 'window-ready'
|
||||
} | {
|
||||
type: 'clear-danmaku'; // 新增:清空弹幕消息
|
||||
type: 'clear-danmaku' // 新增:清空弹幕消息
|
||||
} | {
|
||||
type: 'test-danmaku', // 新增:测试弹幕消息
|
||||
data: EventModel;
|
||||
};
|
||||
type: 'test-danmaku' // 新增:测试弹幕消息
|
||||
data: EventModel
|
||||
}
|
||||
|
||||
// Helper function to generate random test data
|
||||
function generateTestDanmaku(): EventModel {
|
||||
@@ -60,19 +62,19 @@ function generateTestDanmaku(): EventModel {
|
||||
EventDataTypes.SC,
|
||||
EventDataTypes.Guard,
|
||||
EventDataTypes.Enter,
|
||||
];
|
||||
const randomType = types[Math.floor(Math.random() * types.length)];
|
||||
const randomUid = Math.floor(Math.random() * 1000000);
|
||||
const randomName = `测试用户${randomUid % 100}`;
|
||||
const randomTime = Date.now();
|
||||
const randomOuid = `oid_${randomUid}`;
|
||||
]
|
||||
const randomType = types[Math.floor(Math.random() * types.length)]
|
||||
const randomUid = Math.floor(Math.random() * 1000000)
|
||||
const randomName = `测试用户${randomUid % 100}`
|
||||
const randomTime = Date.now()
|
||||
const randomOuid = `oid_${randomUid}`
|
||||
|
||||
// 扩展粉丝勋章相关的随机数据
|
||||
const hasMedal = Math.random() > 0.3; // 70% 概率拥有粉丝勋章
|
||||
const isWearingMedal = hasMedal && Math.random() > 0.2; // 佩戴粉丝勋章的概率
|
||||
const medalNames = ['鸽子团', '鲨鱼牌', '椰奶', '饼干', '猫猫头', '南极', '狗妈', '可爱', '团子', '喵'];
|
||||
const randomMedalName = medalNames[Math.floor(Math.random() * medalNames.length)];
|
||||
const randomMedalLevel = isWearingMedal ? Math.floor(Math.random() * 40) + 1 : 0;
|
||||
const hasMedal = Math.random() > 0.3 // 70% 概率拥有粉丝勋章
|
||||
const isWearingMedal = hasMedal && Math.random() > 0.2 // 佩戴粉丝勋章的概率
|
||||
const medalNames = ['鸽子团', '鲨鱼牌', '椰奶', '饼干', '猫猫头', '南极', '狗妈', '可爱', '团子', '喵']
|
||||
const randomMedalName = medalNames[Math.floor(Math.random() * medalNames.length)]
|
||||
const randomMedalLevel = isWearingMedal ? Math.floor(Math.random() * 40) + 1 : 0
|
||||
|
||||
const baseEvent: Partial<EventModel> = {
|
||||
uname: randomName,
|
||||
@@ -85,7 +87,7 @@ function generateTestDanmaku(): EventModel {
|
||||
fans_medal_name: randomMedalName,
|
||||
fans_medal_wearing_status: isWearingMedal,
|
||||
ouid: randomOuid,
|
||||
};
|
||||
}
|
||||
|
||||
switch (randomType) {
|
||||
case EventDataTypes.Message:
|
||||
@@ -96,36 +98,36 @@ function generateTestDanmaku(): EventModel {
|
||||
num: 0, // Not applicable
|
||||
price: 0, // Not applicable
|
||||
emoji: Math.random() > 0.8 ? '😀' : undefined, // Randomly add emoji
|
||||
} as EventModel;
|
||||
} as EventModel
|
||||
case EventDataTypes.Gift:
|
||||
const giftNames = ['小花花', '辣条', '能量饮料', '小星星'];
|
||||
const giftNums = [1, 5, 10];
|
||||
const giftPrices = [100, 1000, 5000]; // Price in copper coins (100 = 0.1 yuan)
|
||||
const giftNames = ['小花花', '辣条', '能量饮料', '小星星']
|
||||
const giftNums = [1, 5, 10]
|
||||
const giftPrices = [100, 1000, 5000] // Price in copper coins (100 = 0.1 yuan)
|
||||
return {
|
||||
...baseEvent,
|
||||
type: EventDataTypes.Gift,
|
||||
msg: giftNames[Math.floor(Math.random() * giftNames.length)],
|
||||
num: giftNums[Math.floor(Math.random() * giftNums.length)],
|
||||
price: giftPrices[Math.floor(Math.random() * giftPrices.length)],
|
||||
} as EventModel;
|
||||
} as EventModel
|
||||
case EventDataTypes.SC:
|
||||
const scPrices = [30, 50, 100, 500, 1000, 2000]; // Price in yuan
|
||||
const scPrices = [30, 50, 100, 500, 1000, 2000] // Price in yuan
|
||||
return {
|
||||
...baseEvent,
|
||||
type: EventDataTypes.SC,
|
||||
msg: `这是一条测试SC消息!感谢老板!`,
|
||||
num: 1, // Not applicable
|
||||
price: scPrices[Math.floor(Math.random() * scPrices.length)],
|
||||
} as EventModel;
|
||||
} as EventModel
|
||||
case EventDataTypes.Guard:
|
||||
const guardLevels = [GuardLevel.Jianzhang, GuardLevel.Tidu, GuardLevel.Zongdu];
|
||||
const guardLevels = [GuardLevel.Jianzhang, GuardLevel.Tidu, GuardLevel.Zongdu]
|
||||
const guardPrices = {
|
||||
[GuardLevel.Jianzhang]: 198,
|
||||
[GuardLevel.Tidu]: 1998,
|
||||
[GuardLevel.Zongdu]: 19998,
|
||||
[GuardLevel.None]: 0, // Add missing GuardLevel.None case
|
||||
};
|
||||
const selectedGuardLevel = guardLevels[Math.floor(Math.random() * guardLevels.length)];
|
||||
}
|
||||
const selectedGuardLevel = guardLevels[Math.floor(Math.random() * guardLevels.length)]
|
||||
return {
|
||||
...baseEvent,
|
||||
type: EventDataTypes.Guard,
|
||||
@@ -133,7 +135,7 @@ function generateTestDanmaku(): EventModel {
|
||||
num: 1, // Represents 1 month usually
|
||||
price: guardPrices[selectedGuardLevel],
|
||||
guard_level: selectedGuardLevel, // Ensure guard level matches
|
||||
} as EventModel;
|
||||
} as EventModel
|
||||
case EventDataTypes.Enter:
|
||||
return {
|
||||
...baseEvent,
|
||||
@@ -141,7 +143,7 @@ function generateTestDanmaku(): EventModel {
|
||||
msg: '进入了直播间',
|
||||
num: 0, // Not applicable
|
||||
price: 0, // Not applicable
|
||||
} as EventModel;
|
||||
} as EventModel
|
||||
default: // Fallback to Message
|
||||
return {
|
||||
...baseEvent,
|
||||
@@ -149,12 +151,12 @@ function generateTestDanmaku(): EventModel {
|
||||
msg: `默认测试弹幕`,
|
||||
num: 0,
|
||||
price: 0,
|
||||
} as EventModel;
|
||||
} as EventModel
|
||||
}
|
||||
}
|
||||
|
||||
export const useDanmakuWindow = defineStore('danmakuWindow', () => {
|
||||
const danmakuWindow = ref<WebviewWindow>();
|
||||
const danmakuWindow = ref<WebviewWindow>()
|
||||
const danmakuWindowSetting = useStorage<DanmakuWindowSettings>('Setting.DanmakuWindow', {
|
||||
width: 400,
|
||||
height: 600,
|
||||
@@ -168,7 +170,7 @@ export const useDanmakuWindow = defineStore('danmakuWindow', () => {
|
||||
fontSize: 14,
|
||||
maxDanmakuCount: 30,
|
||||
reverseOrder: false,
|
||||
filterTypes: ["Message", "Gift", "SC", "Guard"],
|
||||
filterTypes: ['Message', 'Gift', 'SC', 'Guard'],
|
||||
animationDuration: 300,
|
||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
textColor: '#ffffff',
|
||||
@@ -184,91 +186,91 @@ export const useDanmakuWindow = defineStore('danmakuWindow', () => {
|
||||
textStyleShowType: true, // 新增:默认显示消息类型标签
|
||||
textStyleNameSeparator: ': ', // 新增:默认用户名和消息之间的分隔符为冒号+空格
|
||||
enableAnimation: true, // 新增:默认启用动画效果
|
||||
});
|
||||
})
|
||||
const emojiData = useStorage<{
|
||||
updateAt: number,
|
||||
updateAt: number
|
||||
data: {
|
||||
inline: { [key: string]: string; },
|
||||
plain: { [key: string]: string; },
|
||||
};
|
||||
inline: { [key: string]: string }
|
||||
plain: { [key: string]: string }
|
||||
}
|
||||
}>('Data.Emoji', {
|
||||
updateAt: 0,
|
||||
data: {
|
||||
inline: {},
|
||||
plain: {},
|
||||
}
|
||||
});
|
||||
const danmakuClient = useDanmakuClient();
|
||||
const isWindowOpened = ref(false);
|
||||
let bc: BroadcastChannel | undefined = undefined;
|
||||
},
|
||||
})
|
||||
const danmakuClient = useDanmakuClient()
|
||||
const isWindowOpened = ref(false)
|
||||
let bc: BroadcastChannel | undefined
|
||||
|
||||
function closeWindow() {
|
||||
danmakuWindow.value?.hide();
|
||||
isWindowOpened.value = false;
|
||||
danmakuWindow.value?.hide()
|
||||
isWindowOpened.value = false
|
||||
}
|
||||
function openWindow() {
|
||||
if (!isInited) {
|
||||
init();
|
||||
init()
|
||||
}
|
||||
checkAndUseSetting(danmakuWindowSetting.value);
|
||||
danmakuWindow.value?.show();
|
||||
isWindowOpened.value = true;
|
||||
checkAndUseSetting(danmakuWindowSetting.value)
|
||||
danmakuWindow.value?.show()
|
||||
isWindowOpened.value = true
|
||||
}
|
||||
|
||||
function setDanmakuWindowSize(width: number, height: number) {
|
||||
danmakuWindowSetting.value.width = width;
|
||||
danmakuWindowSetting.value.height = height;
|
||||
danmakuWindow.value?.setSize(new PhysicalSize(width, height));
|
||||
danmakuWindowSetting.value.width = width
|
||||
danmakuWindowSetting.value.height = height
|
||||
danmakuWindow.value?.setSize(new PhysicalSize(width, height))
|
||||
}
|
||||
|
||||
function setDanmakuWindowPosition(x: number, y: number) {
|
||||
danmakuWindowSetting.value.x = x;
|
||||
danmakuWindowSetting.value.y = y;
|
||||
danmakuWindow.value?.setPosition(new PhysicalPosition(x, y));
|
||||
danmakuWindowSetting.value.x = x
|
||||
danmakuWindowSetting.value.y = y
|
||||
danmakuWindow.value?.setPosition(new PhysicalPosition(x, y))
|
||||
}
|
||||
function updateWindowPosition() {
|
||||
danmakuWindow.value?.setPosition(new PhysicalPosition(danmakuWindowSetting.value.x, danmakuWindowSetting.value.y));
|
||||
danmakuWindow.value?.setPosition(new PhysicalPosition(danmakuWindowSetting.value.x, danmakuWindowSetting.value.y))
|
||||
}
|
||||
let isInited = false;
|
||||
let isInited = false
|
||||
|
||||
async function init() {
|
||||
if (isInited) return;
|
||||
danmakuWindow.value = (await getAllWebviewWindows()).find((win) => win.label === 'danmaku-window');
|
||||
if (isInited) return
|
||||
danmakuWindow.value = (await getAllWebviewWindows()).find(win => win.label === 'danmaku-window')
|
||||
if (!danmakuWindow.value) {
|
||||
window.$message.error('弹幕窗口不存在,请先打开弹幕窗口。');
|
||||
return;
|
||||
window.$message.error('弹幕窗口不存在,请先打开弹幕窗口。')
|
||||
return
|
||||
}
|
||||
console.log('打开弹幕窗口', danmakuWindow.value.label, danmakuWindowSetting.value);
|
||||
console.log('打开弹幕窗口', danmakuWindow.value.label, danmakuWindowSetting.value)
|
||||
|
||||
danmakuWindow.value.onCloseRequested((event) => {
|
||||
event.preventDefault(); // 阻止默认关闭行为
|
||||
closeWindow();
|
||||
console.log('弹幕窗口关闭');
|
||||
});
|
||||
event.preventDefault() // 阻止默认关闭行为
|
||||
closeWindow()
|
||||
console.log('弹幕窗口关闭')
|
||||
})
|
||||
danmakuWindow.value.onMoved(({
|
||||
payload: position
|
||||
payload: position,
|
||||
}) => {
|
||||
danmakuWindowSetting.value.x = position.x;
|
||||
danmakuWindowSetting.value.y = position.y;
|
||||
});
|
||||
danmakuWindowSetting.value.x = position.x
|
||||
danmakuWindowSetting.value.y = position.y
|
||||
})
|
||||
|
||||
bc = new BroadcastChannel(DANMAKU_WINDOW_BROADCAST_CHANNEL);
|
||||
bc = new BroadcastChannel(DANMAKU_WINDOW_BROADCAST_CHANNEL)
|
||||
bc.onmessage = (event: MessageEvent<DanmakuWindowBCData>) => {
|
||||
if (event.data.type === 'window-ready') {
|
||||
console.log(`[danmaku-window] 窗口已就绪`);
|
||||
console.log(`[danmaku-window] 窗口已就绪`)
|
||||
bc?.postMessage({
|
||||
type: 'update-setting',
|
||||
data: toRaw(danmakuWindowSetting.value),
|
||||
});
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
bc.postMessage({
|
||||
type: 'window-ready',
|
||||
});
|
||||
})
|
||||
bc.postMessage({
|
||||
type: 'update-setting',
|
||||
data: toRaw(danmakuWindowSetting.value),
|
||||
});
|
||||
})
|
||||
|
||||
bc?.postMessage({
|
||||
type: 'danmaku',
|
||||
@@ -276,96 +278,94 @@ export const useDanmakuWindow = defineStore('danmakuWindow', () => {
|
||||
type: EventDataTypes.Message,
|
||||
msg: '弹幕窗口已打开',
|
||||
} as Partial<EventModel>,
|
||||
});
|
||||
})
|
||||
|
||||
danmakuClient.onEvent('danmaku', (event) => onGetDanmakus(event));
|
||||
danmakuClient.onEvent('gift', (event) => onGetDanmakus(event));
|
||||
danmakuClient.onEvent('sc', (event) => onGetDanmakus(event));
|
||||
danmakuClient.onEvent('guard', (event) => onGetDanmakus(event));
|
||||
danmakuClient.onEvent('enter', (event) => onGetDanmakus(event));
|
||||
danmakuClient.onEvent('scDel', (event) => onGetDanmakus(event));
|
||||
danmakuClient.onEvent('danmaku', event => onGetDanmakus(event))
|
||||
danmakuClient.onEvent('gift', event => onGetDanmakus(event))
|
||||
danmakuClient.onEvent('sc', event => onGetDanmakus(event))
|
||||
danmakuClient.onEvent('guard', event => onGetDanmakus(event))
|
||||
danmakuClient.onEvent('enter', event => onGetDanmakus(event))
|
||||
danmakuClient.onEvent('scDel', event => onGetDanmakus(event))
|
||||
|
||||
watch(() => danmakuWindowSetting, async (newValue) => {
|
||||
if (danmakuWindow.value) {
|
||||
bc?.postMessage({
|
||||
type: 'update-setting',
|
||||
data: toRaw(newValue.value),
|
||||
});
|
||||
await checkAndUseSetting(newValue.value);
|
||||
})
|
||||
await checkAndUseSetting(newValue.value)
|
||||
}
|
||||
}, { deep: true });
|
||||
}, { deep: true })
|
||||
|
||||
console.log('[danmaku-window] 初始化完成');
|
||||
console.log('[danmaku-window] 初始化完成')
|
||||
|
||||
isInited = true;
|
||||
isInited = true
|
||||
}
|
||||
async function checkAndUseSetting(setting: DanmakuWindowSettings) {
|
||||
if (setting.alwaysOnTop) {
|
||||
await danmakuWindow.value?.setAlwaysOnTop(true);
|
||||
}
|
||||
else {
|
||||
await danmakuWindow.value?.setAlwaysOnTop(false);
|
||||
await danmakuWindow.value?.setAlwaysOnTop(true)
|
||||
} else {
|
||||
await danmakuWindow.value?.setAlwaysOnTop(false)
|
||||
}
|
||||
if (setting.interactive) {
|
||||
await danmakuWindow.value?.setIgnoreCursorEvents(true);
|
||||
await danmakuWindow.value?.setIgnoreCursorEvents(true)
|
||||
} else {
|
||||
await danmakuWindow.value?.setIgnoreCursorEvents(false);
|
||||
await danmakuWindow.value?.setIgnoreCursorEvents(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function getEmojiData() {
|
||||
try {
|
||||
const resp = await QueryGetAPI<{
|
||||
inline: { [key: string]: string; },
|
||||
plain: { [key: string]: string; },
|
||||
}>(VTSURU_API_URL + 'client/live-emoji');
|
||||
inline: { [key: string]: string }
|
||||
plain: { [key: string]: string }
|
||||
}>(`${VTSURU_API_URL}client/live-emoji`)
|
||||
if (resp.code == 200) {
|
||||
emojiData.value = {
|
||||
updateAt: Date.now(),
|
||||
data: resp.data,
|
||||
};
|
||||
console.log(`已获取表情数据, 共 ${Object.keys(resp.data.inline).length + Object.keys(resp.data.plain).length} 条`, resp.data);
|
||||
}
|
||||
else {
|
||||
console.error('获取表情数据失败:', resp.message);
|
||||
}
|
||||
console.log(`已获取表情数据, 共 ${Object.keys(resp.data.inline).length + Object.keys(resp.data.plain).length} 条`, resp.data)
|
||||
} else {
|
||||
console.error('获取表情数据失败:', resp.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('无法获取表情数据:', error);
|
||||
console.error('无法获取表情数据:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function onGetDanmakus(data: EventModel) {
|
||||
if (!isWindowOpened.value || !bc) return;
|
||||
if (!isWindowOpened.value || !bc) return
|
||||
bc.postMessage({
|
||||
type: 'danmaku',
|
||||
data,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// 新增:清空弹幕函数
|
||||
function clearAllDanmaku() {
|
||||
if (!isWindowOpened.value || !bc) {
|
||||
console.warn('[danmaku-window] 窗口未打开或 BC 未初始化,无法清空弹幕');
|
||||
return;
|
||||
console.warn('[danmaku-window] 窗口未打开或 BC 未初始化,无法清空弹幕')
|
||||
return
|
||||
}
|
||||
bc.postMessage({
|
||||
type: 'clear-danmaku',
|
||||
});
|
||||
console.log('[danmaku-window] 发送清空弹幕指令');
|
||||
})
|
||||
console.log('[danmaku-window] 发送清空弹幕指令')
|
||||
}
|
||||
|
||||
// 新增:发送测试弹幕函数
|
||||
function sendTestDanmaku() {
|
||||
if (!isWindowOpened.value || !bc) {
|
||||
console.warn('[danmaku-window] 窗口未打开或 BroadcastChannel 未初始化,无法发送测试弹幕');
|
||||
return;
|
||||
console.warn('[danmaku-window] 窗口未打开或 BroadcastChannel 未初始化,无法发送测试弹幕')
|
||||
return
|
||||
}
|
||||
const testData = generateTestDanmaku();
|
||||
const testData = generateTestDanmaku()
|
||||
bc.postMessage({
|
||||
type: 'test-danmaku',
|
||||
data: testData,
|
||||
});
|
||||
console.log('[danmaku-window] 发送测试弹幕指令:', testData);
|
||||
})
|
||||
console.log('[danmaku-window] 发送测试弹幕指令:', testData)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -382,9 +382,9 @@ export const useDanmakuWindow = defineStore('danmakuWindow', () => {
|
||||
init,
|
||||
clearAllDanmaku, // 导出新函数
|
||||
sendTestDanmaku, // 导出新函数
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useDanmakuWindow, import.meta.hot));
|
||||
}
|
||||
import.meta.hot.accept(acceptHMRUpdate(useDanmakuWindow, import.meta.hot))
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import { useTauriStore } from './useTauriStore';
|
||||
import { useTauriStore } from './useTauriStore'
|
||||
|
||||
export type NotificationType = 'question-box' | 'danmaku' | 'goods-buy' | 'message-failed' | 'live-danmaku-failed';
|
||||
export type NotificationSettings = {
|
||||
enableTypes: NotificationType[];
|
||||
};
|
||||
export type VTsuruClientSettings = {
|
||||
useDanmakuClientType: 'openlive' | 'direct';
|
||||
fallbackToOpenLive: boolean;
|
||||
bootAsMinimized: boolean;
|
||||
export type NotificationType = 'question-box' | 'danmaku' | 'goods-buy' | 'message-failed' | 'live-danmaku-failed'
|
||||
export interface NotificationSettings {
|
||||
enableTypes: NotificationType[]
|
||||
}
|
||||
export interface VTsuruClientSettings {
|
||||
useDanmakuClientType: 'openlive' | 'direct'
|
||||
fallbackToOpenLive: boolean
|
||||
bootAsMinimized: boolean
|
||||
|
||||
danmakuHistorySize: number;
|
||||
danmakuHistorySize: number
|
||||
loginType: 'qrcode' | 'cookiecloud'
|
||||
|
||||
enableNotification: boolean;
|
||||
notificationSettings: NotificationSettings;
|
||||
enableNotification: boolean
|
||||
notificationSettings: NotificationSettings
|
||||
|
||||
// 消息队列间隔设置
|
||||
danmakuInterval: number;
|
||||
pmInterval: number;
|
||||
danmakuInterval: number
|
||||
pmInterval: number
|
||||
|
||||
dev_disableDanmakuClient: boolean;
|
||||
};
|
||||
dev_disableDanmakuClient: boolean
|
||||
}
|
||||
|
||||
export const useSettings = defineStore('settings', () => {
|
||||
const store = useTauriStore().getTarget<VTsuruClientSettings>('settings');
|
||||
const store = useTauriStore().getTarget<VTsuruClientSettings>('settings')
|
||||
const defaultSettings: VTsuruClientSettings = {
|
||||
useDanmakuClientType: 'openlive',
|
||||
fallbackToOpenLive: true,
|
||||
@@ -41,22 +41,22 @@ export const useSettings = defineStore('settings', () => {
|
||||
pmInterval: 2000,
|
||||
|
||||
dev_disableDanmakuClient: false,
|
||||
};
|
||||
const settings = ref<VTsuruClientSettings>(Object.assign({}, defaultSettings));
|
||||
}
|
||||
const settings = ref<VTsuruClientSettings>(Object.assign({}, defaultSettings))
|
||||
|
||||
async function init() {
|
||||
settings.value = (await store.get()) || Object.assign({}, defaultSettings);
|
||||
settings.value.notificationSettings ??= defaultSettings.notificationSettings;
|
||||
settings.value.notificationSettings.enableTypes ??= [ 'question-box', 'danmaku', 'message-failed' ];
|
||||
settings.value = (await store.get()) || Object.assign({}, defaultSettings)
|
||||
settings.value.notificationSettings ??= defaultSettings.notificationSettings
|
||||
settings.value.notificationSettings.enableTypes ??= ['question-box', 'danmaku', 'message-failed']
|
||||
// 初始化消息队列间隔设置
|
||||
settings.value.danmakuInterval ??= defaultSettings.danmakuInterval;
|
||||
settings.value.pmInterval ??= defaultSettings.pmInterval;
|
||||
settings.value.danmakuInterval ??= defaultSettings.danmakuInterval
|
||||
settings.value.pmInterval ??= defaultSettings.pmInterval
|
||||
}
|
||||
async function save() {
|
||||
await store.set(settings.value);
|
||||
await store.set(settings.value)
|
||||
}
|
||||
|
||||
return { init, save, settings };
|
||||
});
|
||||
return { init, save, settings }
|
||||
})
|
||||
|
||||
if (import.meta.hot) import.meta.hot.accept(acceptHMRUpdate(useSettings, import.meta.hot));
|
||||
if (import.meta.hot) import.meta.hot.accept(acceptHMRUpdate(useSettings, import.meta.hot))
|
||||
|
||||
@@ -1,53 +1,60 @@
|
||||
import { LazyStore } from '@tauri-apps/plugin-store';
|
||||
import { LazyStore } from '@tauri-apps/plugin-store'
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia'
|
||||
|
||||
export class StoreTarget<T> {
|
||||
constructor(key: string, target: LazyStore, defaultValue?: T) {
|
||||
this.target = target;
|
||||
this.key = key;
|
||||
this.defaultValue = defaultValue;
|
||||
this.target = target
|
||||
this.key = key
|
||||
this.defaultValue = defaultValue
|
||||
}
|
||||
protected target: LazyStore;
|
||||
protected defaultValue: T | undefined;
|
||||
|
||||
protected key: string;
|
||||
protected target: LazyStore
|
||||
protected defaultValue: T | undefined
|
||||
|
||||
protected key: string
|
||||
async set(value: T) {
|
||||
return await this.target.set(this.key, value);
|
||||
return this.target.set(this.key, value)
|
||||
}
|
||||
|
||||
async get(): Promise<T | undefined> {
|
||||
const result = await this.target.get<T>(this.key);
|
||||
const result = await this.target.get<T>(this.key)
|
||||
|
||||
if (result === undefined && this.defaultValue !== undefined) {
|
||||
await this.set(this.defaultValue);
|
||||
return this.defaultValue as T;
|
||||
await this.set(this.defaultValue)
|
||||
return this.defaultValue as T
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
|
||||
async delete() {
|
||||
return await this.target.delete(this.key);
|
||||
return this.target.delete(this.key)
|
||||
}
|
||||
}
|
||||
|
||||
export const useTauriStore = defineStore('tauri', () => {
|
||||
const store = new LazyStore('vtsuru.data.json', {
|
||||
autoSave: true,
|
||||
});
|
||||
async function set(key: string, value: any) {
|
||||
await store.set(key, value);
|
||||
defaults: {},
|
||||
})
|
||||
|
||||
async function set(key: string, value: unknown) {
|
||||
await store.set(key, value)
|
||||
}
|
||||
|
||||
async function get<T>(key: string) {
|
||||
return await store.get<T>(key);
|
||||
return store.get<T>(key)
|
||||
}
|
||||
|
||||
function getTarget<T>(key: string, defaultValue?: T) {
|
||||
return new StoreTarget<T>(key, store, defaultValue);
|
||||
return new StoreTarget<T>(key, store, defaultValue)
|
||||
}
|
||||
|
||||
return {
|
||||
store,
|
||||
set,
|
||||
get,
|
||||
getTarget,
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
if (import.meta.hot) import.meta.hot.accept(acceptHMRUpdate(useTauriStore, import.meta.hot));
|
||||
if (import.meta.hot) import.meta.hot.accept(acceptHMRUpdate(useTauriStore, import.meta.hot))
|
||||
|
||||
Reference in New Issue
Block a user