chore: format code style and update linting configuration

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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))
}

View File

@@ -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))
}

View File

@@ -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))
}

View File

@@ -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))

View File

@@ -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))