diff --git a/bun.lockb b/bun.lockb
index 0ed1661..508439b 100644
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index ac9456e..2e3f4b4 100644
--- a/package.json
+++ b/package.json
@@ -28,12 +28,14 @@
"@tauri-apps/plugin-store": "^2.2.0",
"@tauri-apps/plugin-updater": "^2.7.0",
"@types/crypto-js": "^4.2.2",
+ "@types/md5": "^2.3.5",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@vicons/fluent": "^0.13.0",
"@vitejs/plugin-basic-ssl": "^2.0.0",
"@vitejs/plugin-vue": "^5.2.3",
"@vue/cli": "^5.0.8",
"@vueuse/core": "^13.0.0",
+ "@vueuse/integrations": "^13.1.0",
"@vueuse/router": "^13.0.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
@@ -50,7 +52,9 @@
"file-saver": "^2.0.5",
"grapheme-splitter": "^1.0.4",
"html2canvas": "^1.4.1",
+ "idb-keyval": "^6",
"linqts": "^2.0.0",
+ "md5": "^2.3.0",
"mitt": "^3.0.1",
"monaco-editor": "^0.52.2",
"music-metadata-browser": "^2.5.11",
@@ -85,7 +89,6 @@
"@types/bun": "^1.2.5",
"@types/eslint": "^9.6.1",
"@types/file-saver": "^2.0.7",
- "@types/node": "^22.14.1",
"@types/obs-studio": "^2.17.2",
"@types/uuid": "^10.0.0",
"@typescript-eslint/parser": "^8.27.0",
diff --git a/src/client/ClientAutoAction.vue b/src/client/ClientAutoAction.vue
index d100f82..dc2b226 100644
--- a/src/client/ClientAutoAction.vue
+++ b/src/client/ClientAutoAction.vue
@@ -1,93 +1,652 @@
-
-
+
+
施工中
-
-
-
-
+
+
+ 添加自动操作
+
+
-
-
-
+
+
+
+
+
+
+
+ {{ typeEnabledStatus[type] ? '启用' : '禁用' }}所有{{ label }}
+
-
-
-
+
+
+
+ { selectedTriggerType = type; showAddModal = true; }"
+ >
+ 添加{{ typeMap[type] }}
+
+
+
-
-
-
+
+
+
+
+
+
+
-
-
-
+
+ { selectedTriggerType = type; showAddModal = true; }"
+ >
+ + 添加{{ typeMap[type] }}
+
+
-
-
-
-
-
+
+
+
+
+ ← 返回列表
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 请选择要添加的自动操作类型:
+
+
+
+
\ No newline at end of file
diff --git a/src/client/ClientDanmakuWindow.vue b/src/client/ClientDanmakuWindow.vue
index 49006d4..dc56374 100644
--- a/src/client/ClientDanmakuWindow.vue
+++ b/src/client/ClientDanmakuWindow.vue
@@ -43,6 +43,13 @@
// 动画和阴影
root.style.setProperty('--dw-animation-duration', `${setting.value.animationDuration || 300}ms`);
root.style.setProperty('--dw-shadow', setting.value.enableShadow ? `0 0 10px ${setting.value.shadowColor}` : 'none');
+
+ // 根据 enableAnimation 设置 data-animation-disabled 属性
+ if (setting.value.enableAnimation) {
+ root.removeAttribute('data-animation-disabled');
+ } else {
+ root.setAttribute('data-animation-disabled', 'true');
+ }
}
function addDanmaku(data: EventModel) {
@@ -287,6 +294,7 @@
transition-duration: 100ms !important;
}
+ /* 动画相关样式 - 根据 enableAnimation 设置应用 */
/* 1. declare transition */
.danmaku-list-move,
.danmaku-list-enter-active,
@@ -294,6 +302,14 @@
transition: all var(--dw-animation-duration) cubic-bezier(0.55, 0, 0.1, 1);
}
+ /* 当禁用动画时应用的样式 */
+ :root[data-animation-disabled="true"] .danmaku-list-move,
+ :root[data-animation-disabled="true"] .danmaku-list-enter-active,
+ :root[data-animation-disabled="true"] .danmaku-list-leave-active {
+ transition: none !important;
+ animation: none !important;
+ }
+
.danmaku-list-enter-from,
.danmaku-list-leave-to {
opacity: 0;
diff --git a/src/client/ClientSettings.vue b/src/client/ClientSettings.vue
index 3014c81..6d06e6b 100644
--- a/src/client/ClientSettings.vue
+++ b/src/client/ClientSettings.vue
@@ -278,6 +278,24 @@ import { invoke } from '@tauri-apps/api/core';
+
+
+
+
+ 当B站私信发送失败时通知你
+
+
+
+
+
+ 当直播弹幕发送失败时通知你
+
diff --git a/src/client/DanmakuWindowManager.vue b/src/client/DanmakuWindowManager.vue
index b9d9dfb..4e0391f 100644
--- a/src/client/DanmakuWindowManager.vue
+++ b/src/client/DanmakuWindowManager.vue
@@ -402,6 +402,20 @@ const separatorOptions = [
+
+
+
+
+ 关闭可减少资源占用
+
+
+
+
+import { NCard, NSpace, NCollapse, NDivider } from 'naive-ui';
+import { AutoActionItem, TriggerType } from '@/client/store/useAutoAction';
+
+// 引入拆分的子组件
+import BasicSettings from './settings/BasicSettings.vue';
+import AdvancedSettings from './settings/AdvancedSettings.vue';
+import DanmakuSettings from './settings/DanmakuSettings.vue';
+import GiftSettings from './settings/GiftSettings.vue';
+import GuardSettings from './settings/GuardSettings.vue';
+import ScheduledSettings from './settings/ScheduledSettings.vue';
+import TemplateSettings from './settings/TemplateSettings.vue';
+import FollowSettings from './settings/FollowSettings.vue';
+import EnterSettings from './settings/EnterSettings.vue';
+import SuperChatSettings from './settings/SuperChatSettings.vue';
+
+const props = defineProps({
+ action: {
+ type: Object as () => AutoActionItem,
+ required: true
+ }
+});
+
+// 根据触发类型获取对应的设置组件
+const getTriggerSettings = () => {
+ switch (props.action.triggerType) {
+ case TriggerType.DANMAKU:
+ return DanmakuSettings;
+ case TriggerType.GIFT:
+ return GiftSettings;
+ case TriggerType.GUARD:
+ return GuardSettings;
+ case TriggerType.FOLLOW:
+ return FollowSettings;
+ case TriggerType.ENTER:
+ return EnterSettings;
+ case TriggerType.SCHEDULED:
+ return ScheduledSettings;
+ case TriggerType.SUPER_CHAT:
+ return SuperChatSettings;
+ default:
+ return null;
+ }
+};
+
+const TriggerSettings = getTriggerSettings();
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 高级选项
+
+
+
+
+
+ 高级选项
+
+
+
+
+
+
+
+
diff --git a/src/client/components/autoaction/AutoReplyConfig.vue b/src/client/components/autoaction/AutoReplyConfig.vue
deleted file mode 100644
index b5f358e..0000000
--- a/src/client/components/autoaction/AutoReplyConfig.vue
+++ /dev/null
@@ -1,313 +0,0 @@
-
-
-
-
-
-
-
- 自动回复设置
-
-
-
-
- 冷却时间 (秒):
-
-
-
-
-
-
-
-
-
- 触发关键词:
-
-
-
- {{ keyword }}
-
-
-
-
-
-
- 回复内容:
-
-
-
- {{ reply }}
-
-
-
-
-
-
- 屏蔽词:
-
-
-
- {{ blockword }}
-
-
-
-
-
-
-
- 删除规则
-
-
- 确定要删除此规则吗?
-
-
-
-
-
-
-
-
-
-
- 触发关键词:
-
-
-
-
- 添加
-
-
-
-
- {{ keyword }}
-
-
-
-
-
-
- 回复内容: (可以使用 {{ '\{\{ user.name \}\}' }} 作为用户名变量)
-
-
-
-
- 添加
-
-
-
-
- {{ reply }}
-
-
-
-
-
-
- 屏蔽词: (可选,当弹幕中包含屏蔽词时不触发)
-
-
-
-
- 添加
-
-
-
-
- {{ blockword }}
-
-
-
-
-
- 保存规则
-
-
-
-
-
-
-
-
diff --git a/src/client/components/autoaction/EntryWelcomeConfig.vue b/src/client/components/autoaction/EntryWelcomeConfig.vue
deleted file mode 100644
index 7369197..0000000
--- a/src/client/components/autoaction/EntryWelcomeConfig.vue
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
-
- 入场欢迎设置
-
-
-
-
- 每次欢迎最大用户数:
-
-
-
-
-
-
-
diff --git a/src/client/components/autoaction/FollowThankConfig.vue b/src/client/components/autoaction/FollowThankConfig.vue
deleted file mode 100644
index d511791..0000000
--- a/src/client/components/autoaction/FollowThankConfig.vue
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
-
- 关注感谢设置
-
-
-
-
- 每次感谢最大用户数:
-
-
-
-
-
-
-
diff --git a/src/client/components/autoaction/GiftThankConfig.vue b/src/client/components/autoaction/GiftThankConfig.vue
deleted file mode 100644
index 4ffa205..0000000
--- a/src/client/components/autoaction/GiftThankConfig.vue
+++ /dev/null
@@ -1,161 +0,0 @@
-
-
-
-
-
-
-
- 礼物过滤设置
-
-
-
-
- 过滤模式:
-
-
-
-
- 最低价值 (元):
-
-
-
-
-
-
-
- 感谢设置
-
-
-
-
- 感谢模式:
-
-
-
- {{ option.label }}
-
-
-
-
-
-
- 每次感谢最大用户数:
-
-
-
-
- 每用户最大礼物数:
-
-
-
-
- 包含礼物数量:
-
-
-
-
-
-
-
diff --git a/src/client/components/autoaction/GlobalSettingsConfig.vue b/src/client/components/autoaction/GlobalSettingsConfig.vue
new file mode 100644
index 0000000..e69de29
diff --git a/src/client/components/autoaction/GuardPmConfig.vue b/src/client/components/autoaction/GuardPmConfig.vue
deleted file mode 100644
index a6909ec..0000000
--- a/src/client/components/autoaction/GuardPmConfig.vue
+++ /dev/null
@@ -1,223 +0,0 @@
-
-
-
-
-
-
-
- 私信设置
-
-
-
-
- 私信模板:
-
-
-
-
-
- ?
-
-
-
-
- {{ ph.name }}: {{ ph.description }}
-
-
-
-
-
-
-
-
- 发送弹幕确认:
-
-
-
-
- 弹幕确认模板:
-
-
-
-
- 防止重复发送:
-
-
-
-
-
- 礼品码模式
-
-
-
-
- 启用礼品码模式:
-
-
-
-
-
-
-
-
-
-
- 添加
-
-
-
-
-
-
-
-
- {{ code }}
-
- 删除
-
-
-
-
-
-
-
-
-
-
diff --git a/src/client/components/autoaction/ScheduledDanmakuConfig.vue b/src/client/components/autoaction/ScheduledDanmakuConfig.vue
deleted file mode 100644
index 6d08036..0000000
--- a/src/client/components/autoaction/ScheduledDanmakuConfig.vue
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
-
-
-
- 定时弹幕设置
-
-
-
-
- 发送间隔 (秒):
-
-
-
-
- 发送模式:
-
-
-
- {{ option.label }}
-
-
-
-
-
-
-
-
-
diff --git a/src/client/components/autoaction/TemplateEditor.vue b/src/client/components/autoaction/TemplateEditor.vue
index 6e1daf9..fa95ec5 100644
--- a/src/client/components/autoaction/TemplateEditor.vue
+++ b/src/client/components/autoaction/TemplateEditor.vue
@@ -1,6 +1,10 @@
变量说明
-
+
+
+
+
+
+ 在模板中使用 {{ '\{\{js:\}\}' }} 语法可以执行简单的JavaScript表达式
+
+
+
+ {{ '\{\{js: user.name.toUpperCase()\}\}' }} → 将用户名转为大写
+
+
+ {{ '\{\{js: gift.count > 10 ? "大量" : "少量"\}\}' }} → 根据数量显示不同文本
+
+
+
+
{{ ph.name }}: {{ ph.description }}
-
- 默认变量
-
-
- {{ ph.name }}: {{ ph.description }}
-
-
+
@@ -112,56 +209,284 @@ function removeTemplate(index: number) {
{{ description }}
-
-
+
+
-
+
- {{ template }}
-
-
-
+
+
+
+
+
+
- 删除
-
-
- 确定要删除此模板吗?
-
-
-
-
+
+
-
+
+
+ 测试
+
+
+ 编辑
+
+
+
+
+ 删除
+
+
+ 确定要删除这个模板吗?
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+ 转换为表达式
+
+
+
+
+
+ 取消
+
+
+ {{ isEditing ? '保存' : '添加' }}
+
+
+
+
+
+
+
+
- 添加模板
-
-
+
+
+
+
+
diff --git a/src/client/components/autoaction/TemplateHelper.vue b/src/client/components/autoaction/TemplateHelper.vue
new file mode 100644
index 0000000..d876244
--- /dev/null
+++ b/src/client/components/autoaction/TemplateHelper.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
+ 可用变量:
+
+
+
+
+
+ {{ item.name }}
+
+
+ {{ item.description }}
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/client/components/autoaction/TemplateTester.vue b/src/client/components/autoaction/TemplateTester.vue
new file mode 100644
index 0000000..900fac9
--- /dev/null
+++ b/src/client/components/autoaction/TemplateTester.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+ 测试模板
+
+
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/client/components/autoaction/settings/AdvancedSettings.vue b/src/client/components/autoaction/settings/AdvancedSettings.vue
new file mode 100644
index 0000000..8a7448a
--- /dev/null
+++ b/src/client/components/autoaction/settings/AdvancedSettings.vue
@@ -0,0 +1,196 @@
+
+
+
+
+
+
+
+
+ 启用用户过滤:
+
+
+
+
+
+ 要求本房间勋章:
+
+
+
+
+ 要求任意舰长:
+
+
+
+
+
+
+
+
+
+
+ 忽略全局冷却:
+
+
+
+
+ 延迟执行(秒):
+
+
+
+
+ 冷却时间(秒):
+
+
+
+
+
+
+
+
+
+ 当表达式为真时才会执行此操作。可使用JS语法,例如: user.guardLevel > 0 || gift.price > 10
+
+
+
+
+
+
+
+
+
+
+ 可访问 context, event, biliFunc, roomId 等变量
+
+
+
+
+
+
+
+
+
diff --git a/src/client/components/autoaction/settings/BasicSettings.vue b/src/client/components/autoaction/settings/BasicSettings.vue
new file mode 100644
index 0000000..c1ca3b4
--- /dev/null
+++ b/src/client/components/autoaction/settings/BasicSettings.vue
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+ 名称:
+
+
+
+
+ 启用:
+
+
+
+
+ 仅直播中启用:
+
+
+
+
+ 天选时刻忽略:
+
+
+
+
+ 触发类型:
+
+
+
+
+ 操作类型:
+
+
+
+
+ 优先级:
+
+
+
+
+
+
+
diff --git a/src/client/components/autoaction/settings/DanmakuSettings.vue b/src/client/components/autoaction/settings/DanmakuSettings.vue
new file mode 100644
index 0000000..2ddfbd2
--- /dev/null
+++ b/src/client/components/autoaction/settings/DanmakuSettings.vue
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+ 触发关键词:
+
+
+
+
+ 添加
+
+
+
+
+
+
+ {{ keyword }}
+
+
+
+
+
+
+
+ 屏蔽词:
+
+
+
+
+ 添加
+
+
+
+
+
+
+ {{ blockword }}
+
+
+
+
+
+
+
+
diff --git a/src/client/components/autoaction/settings/EnterSettings.vue b/src/client/components/autoaction/settings/EnterSettings.vue
new file mode 100644
index 0000000..cbf6f83
--- /dev/null
+++ b/src/client/components/autoaction/settings/EnterSettings.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+ 入场过滤模式:
+
+
+
+
+ 防止重复发送:
+
+
+
+
+ 每次处理的最大用户数:
+
+
+
+
+
\ No newline at end of file
diff --git a/src/client/components/autoaction/settings/FollowSettings.vue b/src/client/components/autoaction/settings/FollowSettings.vue
new file mode 100644
index 0000000..63242b7
--- /dev/null
+++ b/src/client/components/autoaction/settings/FollowSettings.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+ 防止重复发送:
+
+
+
+
+ 每次处理的最大用户数:
+
+
+
+
+
\ No newline at end of file
diff --git a/src/client/components/autoaction/settings/GiftSettings.vue b/src/client/components/autoaction/settings/GiftSettings.vue
new file mode 100644
index 0000000..5448026
--- /dev/null
+++ b/src/client/components/autoaction/settings/GiftSettings.vue
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+ 礼物过滤模式:
+
+
+
+
+
+
+
+ 添加
+
+
+
+
+
+
+ {{ giftName }}
+
+
+
+
+
+
+
+ 最低价值 (元):
+
+
+
+
+
+ 包含礼物数量:
+
+
+
+
+ 每次处理的最大用户数:
+
+
+
+
+ 每用户最大礼物种类数:
+
+
+
+
+
diff --git a/src/client/components/autoaction/settings/GuardSettings.vue b/src/client/components/autoaction/settings/GuardSettings.vue
new file mode 100644
index 0000000..5f4ea76
--- /dev/null
+++ b/src/client/components/autoaction/settings/GuardSettings.vue
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+ 防止重复发送:
+
+
+
+
+
+ 礼品码设置
+
+
+
+
+
+
+ 添加
+
+
+
+
+
+
+ {{ getGuardLevelName(levelCodes.level) }}礼品码:
+
+
+
+ {{ code }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/client/components/autoaction/settings/ScheduledSettings.vue b/src/client/components/autoaction/settings/ScheduledSettings.vue
new file mode 100644
index 0000000..efb9e57
--- /dev/null
+++ b/src/client/components/autoaction/settings/ScheduledSettings.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+ 发送间隔 (秒):
+
+
+
+
+ 发送模式:
+
+
+
+ {{ option.label }}
+
+
+
+
+
+
+
diff --git a/src/client/components/autoaction/settings/SuperChatSettings.vue b/src/client/components/autoaction/settings/SuperChatSettings.vue
new file mode 100644
index 0000000..797d308
--- /dev/null
+++ b/src/client/components/autoaction/settings/SuperChatSettings.vue
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+ SC过滤模式:
+
+
+
+
+
+ 最低价格 (元):
+
+
+
+
+
+ 防止重复发送:
+
+
+
+
+ 每次处理的最大用户数:
+
+
+
+
+
\ No newline at end of file
diff --git a/src/client/components/autoaction/settings/TemplateSettings.vue b/src/client/components/autoaction/settings/TemplateSettings.vue
new file mode 100644
index 0000000..5bfff6a
--- /dev/null
+++ b/src/client/components/autoaction/settings/TemplateSettings.vue
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/client/components/danmaku/CardStyleDanmakuItem.vue b/src/client/components/danmaku/CardStyleDanmakuItem.vue
index d9ce0e5..9f67dca 100644
--- a/src/client/components/danmaku/CardStyleDanmakuItem.vue
+++ b/src/client/components/danmaku/CardStyleDanmakuItem.vue
@@ -57,12 +57,15 @@ import { VehicleShip24Filled } from '@vicons/fluent';
-
+
{{ item?.num || 1 }} × {{ item?.msg }}
¥{{ (item.price || 0).toFixed(2) }}
+ >¥{{ (item.price || 0).toFixed(1) }}
@@ -333,7 +336,7 @@ import { VehicleShip24Filled } from '@vicons/fluent';
/* 礼物 徽章 */
.gift-badge {
- background: #F56C6C;
+ background: #6aa8a3;
color: #fff;
border-radius: 4px;
padding: 1px 6px;
@@ -345,6 +348,9 @@ import { VehicleShip24Filled } from '@vicons/fluent';
gap: 4px;
white-space: nowrap;
}
+ .gift-badge[isPay="true"] {
+ background: #F56C6C;
+ }
.gift-price {
background: rgba(255, 255, 255, 0.2);
diff --git a/src/client/data/initialize.ts b/src/client/data/initialize.ts
index f3a519e..1a913bd 100644
--- a/src/client/data/initialize.ts
+++ b/src/client/data/initialize.ts
@@ -21,6 +21,7 @@ import { check } from '@tauri-apps/plugin-updater';
import { relaunch } from '@tauri-apps/plugin-process';
import { useDanmakuWindow } from "../store/useDanmakuWindow";
import { getAllWebviewWindows } from "@tauri-apps/api/webviewWindow";
+import { useAutoAction } from "../store/useAutoAction";
const accountInfo = useAccount();
@@ -145,6 +146,8 @@ export async function initAll(isOnBoot: boolean) {
});
}
+ useAutoAction().init();
+
clientInited.value = true;
}
export function OnClientUnmounted() {
diff --git a/src/client/data/notification.ts b/src/client/data/notification.ts
index 3a5b909..105ee40 100644
--- a/src/client/data/notification.ts
+++ b/src/client/data/notification.ts
@@ -85,4 +85,22 @@ export function onGoodsBuy(info: {
extra: { type: 'goods-buy' },
});
}
+}
+
+// 私信发送失败通知
+export function onSendPrivateMessageFailed(receiverId: number, message: string, error: any) {
+ const setting = useSettings();
+ if (setting.settings.notificationSettings.enableTypes.includes("message-failed")) {
+ const errorMsg = error instanceof Error ? error.message : String(error);
+ window.$notification.error({
+ title: "私信发送失败",
+ description: `向用户 ${receiverId} 发送私信失败: ${errorMsg}`,
+ duration: 8000,
+ });
+ trySendNotification({
+ title: "私信发送失败",
+ body: `向用户 ${receiverId} 发送私信失败`,
+ extra: { type: 'message-failed' },
+ });
+ }
}
\ No newline at end of file
diff --git a/src/client/store/autoAction/expressionEvaluator.ts b/src/client/store/autoAction/expressionEvaluator.ts
new file mode 100644
index 0000000..a5109d1
--- /dev/null
+++ b/src/client/store/autoAction/expressionEvaluator.ts
@@ -0,0 +1,175 @@
+/**
+ * 表达式求值工具 - 用于在自动操作模板中支持简单的JavaScript表达式
+ */
+
+// 表达式模式匹配
+// {{js: expression}} - 完整的JavaScript表达式
+const JS_EXPRESSION_REGEX = /\{\{\s*js:\s*(.*?)\s*\}\}/g;
+
+/**
+ * 处理模板中的表达式
+ * @param template 包含表达式的模板字符串
+ * @param context 上下文对象,包含可在表达式中访问的变量
+ * @returns 处理后的字符串
+ */
+export function evaluateTemplateExpressions(template: string, context: Record): string {
+ if (!template) return "";
+
+ return template.replace(JS_EXPRESSION_REGEX, (match, expression) => {
+ try {
+ // 创建一个安全的求值函数
+ const evalInContext = new Function(...Object.keys(context), `
+ try {
+ return ${expression};
+ } catch (e) {
+ return "[表达式错误: " + e.message + "]";
+ }
+ `);
+
+ // 执行表达式并返回结果
+ const result = evalInContext(...Object.values(context));
+ return result !== undefined ? String(result) : "";
+ } catch (error) {
+ console.error("表达式求值错误:", error);
+ return `[表达式错误: ${(error as Error).message}]`;
+ }
+ });
+}
+
+/**
+ * 检查模板中是否包含JavaScript表达式
+ * @param template 要检查的模板字符串
+ * @returns 是否包含表达式
+ */
+export function containsJsExpression(template: string): boolean {
+ return JS_EXPRESSION_REGEX.test(template);
+}
+
+/**
+ * 转义字符串中的特殊字符,使其可以安全地在正则表达式中使用
+ * @param string 要转义的字符串
+ * @returns 转义后的字符串
+ */
+export function escapeRegExp(string: string): string {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+/**
+ * 将普通占位符格式转换为JS表达式格式
+ * 例如: {{user.name}} 转换为 {{js: user.name}}
+ * @param template 包含普通占位符的模板
+ * @param placeholders 占位符列表
+ * @returns 转换后的模板
+ */
+export function convertToJsExpressions(template: string, placeholders: {name: string, description: string}[]): string {
+ let result = template;
+
+ placeholders.forEach(p => {
+ const placeholder = p.name;
+ const path = placeholder.replace(/\{\{|\}\}/g, '').trim();
+ const regex = new RegExp(escapeRegExp(placeholder), 'g');
+ result = result.replace(regex, `{{js: ${path}}}`);
+ });
+
+ return result;
+}
+
+/**
+ * 为礼物感谢模块创建上下文对象
+ * @param user 用户信息
+ * @param gift 礼物信息
+ * @returns 上下文对象
+ */
+export function createGiftThankContext(user: { uid: number; name: string },
+ gift: { name: string; count: number; price: number }): Record {
+ return {
+ user: {
+ uid: user.uid,
+ name: user.name,
+ // 额外方法和属性
+ nameLength: user.name.length,
+ },
+ gift: {
+ name: gift.name,
+ count: gift.count,
+ price: gift.price,
+ totalPrice: gift.count * gift.price,
+ // 工具方法
+ summary: `${gift.count}个${gift.name}`,
+ isExpensive: gift.price >= 50
+ },
+ // 工具函数
+ format: {
+ currency: (value: number) => `¥${value.toFixed(2)}`,
+ pluralize: (count: number, singular: string, plural: string) => count === 1 ? singular : plural,
+ },
+ // 日期时间
+ date: {
+ now: new Date(),
+ timestamp: Date.now(),
+ formatted: new Intl.DateTimeFormat('zh-CN').format(new Date())
+ }
+ };
+}
+
+/**
+ * 为入场欢迎模块创建上下文对象
+ * @param user 用户信息
+ * @returns 上下文对象
+ */
+export function createEntryWelcomeContext(user: { uid: number; name: string; medal?: { level: number; name: string } }): Record {
+ return {
+ user: {
+ uid: user.uid,
+ name: user.name,
+ nameLength: user.name.length,
+ medal: user.medal || { level: 0, name: '' },
+ hasMedal: !!user.medal
+ },
+ date: {
+ now: new Date(),
+ timestamp: Date.now(),
+ formatted: new Intl.DateTimeFormat('zh-CN').format(new Date()),
+ hour: new Date().getHours()
+ },
+ // 时间相关的便捷函数
+ timeOfDay: () => {
+ const hour = new Date().getHours();
+ if (hour < 6) return '凌晨';
+ if (hour < 12) return '上午';
+ if (hour < 14) return '中午';
+ if (hour < 18) return '下午';
+ return '晚上';
+ }
+ };
+}
+
+/**
+ * 为自动回复模块创建上下文对象
+ * @param user 用户信息
+ * @param message 消息内容
+ * @returns 上下文对象
+ */
+export function createAutoReplyContext(user: { uid: number; name: string; medal?: { level: number; name: string } },
+ message: string): Record {
+ return {
+ user: {
+ uid: user.uid,
+ name: user.name,
+ nameLength: user.name.length,
+ medal: user.medal || { level: 0, name: '' },
+ hasMedal: !!user.medal
+ },
+ message: {
+ content: message,
+ length: message.length,
+ containsQuestion: message.includes('?') || message.includes('?'),
+ words: message.split(/\s+/).filter(Boolean)
+ },
+ date: {
+ now: new Date(),
+ timestamp: Date.now(),
+ formatted: new Intl.DateTimeFormat('zh-CN').format(new Date())
+ }
+ };
+}
\ No newline at end of file
diff --git a/src/client/store/autoAction/modules/autoReply.ts b/src/client/store/autoAction/modules/autoReply.ts
new file mode 100644
index 0000000..046c8fd
--- /dev/null
+++ b/src/client/store/autoAction/modules/autoReply.ts
@@ -0,0 +1,150 @@
+import { ref, Ref } from 'vue';
+import { EventModel } from '@/api/api-models';
+import {
+ AutoActionItem,
+ TriggerType,
+ ExecutionContext,
+ RuntimeState
+} from '../types';
+import {
+ formatTemplate,
+ getRandomTemplate,
+ shouldProcess,
+ evaluateExpression
+} from '../utils';
+
+/**
+ * 自动回复模块
+ * @param isLive 是否处于直播状态
+ * @param roomId 房间ID
+ * @param sendLiveDanmaku 发送弹幕函数
+ */
+export function useAutoReply(
+ isLive: Ref,
+ roomId: Ref,
+ sendLiveDanmaku: (roomId: number, message: string) => Promise
+) {
+ // 运行时数据 - 记录特定关键词的最后回复时间
+ const lastReplyTimestamps = ref<{ [keyword: string]: number; }>({});
+
+ /**
+ * 处理弹幕事件
+ * @param event 弹幕事件
+ * @param actions 自动操作列表
+ * @param runtimeState 运行时状态
+ */
+ function onDanmaku(
+ event: EventModel,
+ actions: AutoActionItem[],
+ runtimeState: RuntimeState
+ ) {
+ if (!roomId.value) return;
+
+ // 过滤出有效的自动回复操作
+ const replyActions = actions.filter(action =>
+ action.triggerType === TriggerType.DANMAKU &&
+ action.enabled &&
+ (!action.triggerConfig.onlyDuringLive || isLive.value)
+ );
+
+ if (replyActions.length === 0) return;
+
+ const message = event.msg;
+ const now = Date.now();
+
+ // 准备执行上下文
+ const context: ExecutionContext = {
+ event,
+ roomId: roomId.value,
+ variables: {
+ user: {
+ name: event.uname,
+ uid: event.uid,
+ guardLevel: event.guard_level,
+ hasMedal: event.fans_medal_wearing_status,
+ medalLevel: event.fans_medal_level,
+ medalName: event.fans_medal_name
+ },
+ message: event.msg,
+ timeOfDay: () => {
+ const hour = new Date().getHours();
+ if (hour < 6) return '凌晨';
+ if (hour < 9) return '早上';
+ if (hour < 12) return '上午';
+ if (hour < 14) return '中午';
+ if (hour < 18) return '下午';
+ if (hour < 22) return '晚上';
+ return '深夜';
+ },
+ date: {
+ formatted: new Date().toLocaleString('zh-CN')
+ }
+ },
+ timestamp: now
+ };
+
+ // 检查每个操作
+ for (const action of replyActions) {
+ // 检查用户过滤条件
+ if (action.triggerConfig.userFilterEnabled) {
+ if (action.triggerConfig.requireMedal && !event.fans_medal_wearing_status) continue;
+ if (action.triggerConfig.requireCaptain && !event.guard_level) continue;
+ }
+
+ // 关键词和屏蔽词检查
+ const keywordMatch = action.triggerConfig.keywords?.some(kw => message.includes(kw));
+ if (!keywordMatch) continue;
+
+ const blockwordMatch = action.triggerConfig.blockwords?.some(bw => message.includes(bw));
+ if (blockwordMatch) continue; // 包含屏蔽词,不回复
+
+ // 评估逻辑表达式
+ if (action.logicalExpression && !evaluateExpression(action.logicalExpression, context)) {
+ continue;
+ }
+
+ // 检查冷却
+ const lastExecTime = runtimeState.lastExecutionTime[action.id] || 0;
+ if (!action.ignoreCooldown && now - lastExecTime < (action.actionConfig.cooldownSeconds || 0) * 1000) {
+ continue; // 仍在冷却中
+ }
+
+ // 选择回复并发送
+ const template = getRandomTemplate(action.templates);
+ if (template) {
+ // 格式化并发送
+ const formattedReply = formatTemplate(template, context);
+
+ // 更新冷却时间
+ runtimeState.lastExecutionTime[action.id] = now;
+
+ // 执行延迟处理
+ if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
+ setTimeout(() => {
+ sendLiveDanmaku(roomId.value!, formattedReply);
+ }, action.actionConfig.delaySeconds * 1000);
+ } else {
+ sendLiveDanmaku(roomId.value!, formattedReply);
+ }
+
+ break; // 匹配到一个规则就停止
+ }
+ }
+ }
+
+ // 重置冷却时间 (用于测试)
+ function resetCooldowns(runtimeState: RuntimeState, actionId?: string) {
+ if (actionId) {
+ delete runtimeState.lastExecutionTime[actionId];
+ } else {
+ Object.keys(runtimeState.lastExecutionTime).forEach(id => {
+ delete runtimeState.lastExecutionTime[id];
+ });
+ }
+ }
+
+ return {
+ onDanmaku,
+ resetCooldowns
+ };
+}
\ No newline at end of file
diff --git a/src/client/store/autoAction/modules/entryWelcome.ts b/src/client/store/autoAction/modules/entryWelcome.ts
new file mode 100644
index 0000000..086113e
--- /dev/null
+++ b/src/client/store/autoAction/modules/entryWelcome.ts
@@ -0,0 +1,110 @@
+import { ref, Ref } from 'vue';
+import { EventModel, EventDataTypes } from '@/api/api-models';
+import {
+ formatTemplate,
+ getRandomTemplate,
+ buildExecutionContext
+} from '../utils';
+import {
+ AutoActionItem,
+ TriggerType,
+ RuntimeState
+} from '../types';
+
+/**
+ * 入场欢迎模块
+ * @param isLive 是否处于直播状态
+ * @param roomId 房间ID
+ * @param isTianXuanActive 是否处于天选时刻
+ * @param sendLiveDanmaku 发送弹幕函数
+ */
+export function useEntryWelcome(
+ isLive: Ref,
+ roomId: Ref,
+ isTianXuanActive: Ref,
+ sendLiveDanmaku: (roomId: number, message: string) => Promise
+) {
+ // 运行时数据
+ const timer = ref(null);
+
+ /**
+ * 处理入场事件 - 支持新的AutoActionItem结构
+ * @param event 入场事件
+ * @param actions 自动操作列表
+ * @param runtimeState 运行时状态
+ */
+ function processEnter(
+ event: EventModel,
+ actions: AutoActionItem[],
+ runtimeState: RuntimeState
+ ) {
+ if (!roomId.value) return;
+
+ // 过滤出有效的入场欢迎操作
+ const enterActions = actions.filter(action =>
+ action.triggerType === TriggerType.ENTER &&
+ action.enabled &&
+ (!action.triggerConfig.onlyDuringLive || isLive.value) &&
+ (!action.triggerConfig.ignoreTianXuan || !isTianXuanActive.value)
+ );
+
+ if (enterActions.length === 0) return;
+
+ // 创建执行上下文
+ const context = buildExecutionContext(event, roomId.value, TriggerType.ENTER);
+
+ // 处理每个符合条件的操作
+ for (const action of enterActions) {
+ // 跳过不符合用户过滤条件的
+ if (action.triggerConfig.userFilterEnabled) {
+ if (action.triggerConfig.requireMedal && !event.fans_medal_wearing_status) continue;
+ if (action.triggerConfig.requireCaptain && !event.guard_level) continue;
+ }
+
+ // 检查入场过滤条件 (可以在未来扩展更多条件)
+ if (action.triggerConfig.filterMode === 'blacklist' &&
+ action.triggerConfig.filterGiftNames?.includes(event.uname)) continue;
+
+ // 检查冷却时间
+ const lastExecTime = runtimeState.lastExecutionTime[action.id] || 0;
+ if (!action.ignoreCooldown &&
+ Date.now() - lastExecTime < (action.actionConfig.cooldownSeconds || 0) * 1000) {
+ continue; // 仍在冷却中
+ }
+
+ // 选择并发送回复
+ const template = getRandomTemplate(action.templates);
+ if (template) {
+ // 更新冷却时间
+ runtimeState.lastExecutionTime[action.id] = Date.now();
+
+ // 格式化并发送
+ const formattedReply = formatTemplate(template, context);
+
+ // 延迟发送
+ if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
+ setTimeout(() => {
+ sendLiveDanmaku(roomId.value!, formattedReply);
+ }, action.actionConfig.delaySeconds * 1000);
+ } else {
+ sendLiveDanmaku(roomId.value!, formattedReply);
+ }
+ }
+ }
+ }
+
+ /**
+ * 清理计时器
+ */
+ function clearTimer() {
+ if (timer.value) {
+ clearTimeout(timer.value);
+ timer.value = null;
+ }
+ }
+
+ return {
+ processEnter,
+ clearTimer
+ };
+}
\ No newline at end of file
diff --git a/src/client/store/autoAction/modules/followThank.ts b/src/client/store/autoAction/modules/followThank.ts
new file mode 100644
index 0000000..51fcc8c
--- /dev/null
+++ b/src/client/store/autoAction/modules/followThank.ts
@@ -0,0 +1,116 @@
+import { ref, Ref } from 'vue';
+import { EventModel, EventDataTypes } from '@/api/api-models';
+import {
+ formatTemplate,
+ getRandomTemplate,
+ buildExecutionContext
+} from '../utils';
+import {
+ AutoActionItem,
+ TriggerType,
+ RuntimeState
+} from '../types';
+
+/**
+ * 关注感谢模块
+ * @param isLive 是否处于直播状态
+ * @param roomId 房间ID
+ * @param isTianXuanActive 是否处于天选时刻
+ * @param sendLiveDanmaku 发送弹幕函数
+ */
+export function useFollowThank(
+ isLive: Ref,
+ roomId: Ref,
+ isTianXuanActive: Ref,
+ sendLiveDanmaku: (roomId: number, message: string) => Promise
+) {
+ // 运行时数据
+ const aggregatedFollows = ref<{uid: number, name: string, timestamp: number}[]>([]);
+ const timer = ref(null);
+
+ /**
+ * 处理关注事件 - 支持新的AutoActionItem结构
+ * @param event 关注事件
+ * @param actions 自动操作列表
+ * @param runtimeState 运行时状态
+ */
+ function processFollow(
+ event: EventModel,
+ actions: AutoActionItem[],
+ runtimeState: RuntimeState
+ ) {
+ if (!roomId.value) return;
+
+ // 过滤出有效的关注感谢操作
+ const followActions = actions.filter(action =>
+ action.triggerType === TriggerType.FOLLOW &&
+ action.enabled &&
+ (!action.triggerConfig.onlyDuringLive || isLive.value) &&
+ (!action.triggerConfig.ignoreTianXuan || !isTianXuanActive.value)
+ );
+
+ if (followActions.length === 0) return;
+
+ // 创建执行上下文
+ const context = buildExecutionContext(event, roomId.value, TriggerType.FOLLOW);
+
+ // 处理每个符合条件的操作
+ for (const action of followActions) {
+ // 跳过不符合用户过滤条件的
+ if (action.triggerConfig.userFilterEnabled) {
+ if (action.triggerConfig.requireMedal && !event.fans_medal_wearing_status) continue;
+ if (action.triggerConfig.requireCaptain && !event.guard_level) continue;
+ }
+
+ // 检查冷却时间
+ const lastExecTime = runtimeState.lastExecutionTime[action.id] || 0;
+ if (!action.ignoreCooldown &&
+ Date.now() - lastExecTime < (action.actionConfig.cooldownSeconds || 0) * 1000) {
+ continue; // 仍在冷却中
+ }
+
+ // 选择并发送回复
+ const template = getRandomTemplate(action.templates);
+ if (template) {
+ // 更新冷却时间
+ runtimeState.lastExecutionTime[action.id] = Date.now();
+
+ // 格式化并发送
+ const formattedReply = formatTemplate(template, context);
+
+ // 延迟发送
+ if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
+ setTimeout(() => {
+ sendLiveDanmaku(roomId.value!, formattedReply);
+ }, action.actionConfig.delaySeconds * 1000);
+ } else {
+ sendLiveDanmaku(roomId.value!, formattedReply);
+ }
+ }
+ }
+ }
+
+ /**
+ * 处理关注事件 - 旧方式实现,用于兼容现有代码
+ */
+ function onFollow(event: EventModel) {
+ // 将在useAutoAction.ts中进行迁移,此方法保留但不实现具体逻辑
+ console.log('关注事件处理已迁移到新的AutoActionItem结构');
+ }
+
+ /**
+ * 清理计时器
+ */
+ function clearTimer() {
+ if (timer.value) {
+ clearTimeout(timer.value);
+ timer.value = null;
+ }
+ }
+
+ return {
+ onFollow,
+ processFollow,
+ clearTimer
+ };
+}
\ No newline at end of file
diff --git a/src/client/store/autoAction/modules/giftThank.ts b/src/client/store/autoAction/modules/giftThank.ts
new file mode 100644
index 0000000..2d09d9f
--- /dev/null
+++ b/src/client/store/autoAction/modules/giftThank.ts
@@ -0,0 +1,213 @@
+import { ref, Ref } from 'vue';
+import { EventModel, EventDataTypes } from '@/api/api-models';
+import {
+ formatTemplate,
+ getRandomTemplate,
+ buildExecutionContext
+} from '../utils';
+import {
+ AutoActionItem,
+ TriggerType,
+ ExecutionContext,
+ RuntimeState
+} from '../types';
+
+/**
+ * 礼物感谢模块
+ * @param isLive 是否处于直播状态
+ * @param roomId 房间ID
+ * @param isTianXuanActive 是否处于天选时刻
+ * @param sendLiveDanmaku 发送弹幕函数
+ */
+export function useGiftThank(
+ isLive: Ref,
+ roomId: Ref,
+ isTianXuanActive: Ref,
+ sendLiveDanmaku: (roomId: number, message: string) => Promise
+) {
+ // 测试发送功能状态
+ const lastTestTime = ref(0);
+ const testCooldown = 5000; // 5秒冷却时间
+ const testLoading = ref(false);
+
+ /**
+ * 处理礼物事件
+ * @param event 礼物事件
+ * @param actions 自动操作列表
+ * @param runtimeState 运行时状态
+ */
+ function processGift(
+ event: EventModel,
+ actions: AutoActionItem[],
+ runtimeState: RuntimeState
+ ) {
+ if (!roomId.value) return;
+
+ // 过滤出有效的礼物感谢操作
+ const giftActions = actions.filter(action =>
+ action.triggerType === TriggerType.GIFT &&
+ action.enabled &&
+ (!action.triggerConfig.onlyDuringLive || isLive.value) &&
+ (!action.triggerConfig.ignoreTianXuan || !isTianXuanActive.value)
+ );
+
+ if (giftActions.length === 0) return;
+
+ // 礼物基本信息
+ const giftName = event.msg;
+ const giftPrice = event.price / 1000;
+ const giftCount = event.num;
+
+ // 创建执行上下文
+ const context = buildExecutionContext(event, roomId.value, TriggerType.GIFT);
+
+ // 处理每个符合条件的操作
+ for (const action of giftActions) {
+ // 跳过不符合用户过滤条件的
+ if (action.triggerConfig.userFilterEnabled) {
+ if (action.triggerConfig.requireMedal && !event.fans_medal_wearing_status) continue;
+ if (action.triggerConfig.requireCaptain && !event.guard_level) continue;
+ }
+
+ // 礼物过滤逻辑
+ if (action.triggerConfig.filterMode === 'blacklist' &&
+ action.triggerConfig.filterGiftNames?.includes(giftName)) continue;
+
+ if (action.triggerConfig.filterMode === 'whitelist' &&
+ !action.triggerConfig.filterGiftNames?.includes(giftName)) continue;
+
+ if (action.triggerConfig.minValue && giftPrice < action.triggerConfig.minValue) continue;
+
+ // 检查冷却时间
+ const lastExecTime = runtimeState.lastExecutionTime[action.id] || 0;
+ if (!action.ignoreCooldown &&
+ Date.now() - lastExecTime < (action.actionConfig.cooldownSeconds || 0) * 1000) {
+ continue; // 仍在冷却中
+ }
+
+ // 选择并发送回复
+ const template = getRandomTemplate(action.templates);
+ if (template) {
+ // 更新冷却时间
+ runtimeState.lastExecutionTime[action.id] = Date.now();
+
+ // 格式化并发送
+ const formattedReply = formatTemplate(template, context);
+
+ // 延迟发送
+ if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
+ setTimeout(() => {
+ sendLiveDanmaku(roomId.value!, formattedReply);
+ }, (action.actionConfig.delaySeconds || 0) * 1000);
+ } else {
+ sendLiveDanmaku(roomId.value!, formattedReply);
+ }
+ }
+ }
+ }
+
+ /**
+ * 测试发送礼物感谢弹幕
+ */
+ async function testSendThankMessage(
+ action?: AutoActionItem
+ ): Promise<{ success: boolean; message: string }> {
+ // 检查是否在冷却期
+ const now = Date.now();
+ if (now - lastTestTime.value < testCooldown) {
+ return {
+ success: false,
+ message: `请等待${Math.ceil((testCooldown - (now - lastTestTime.value)) / 1000)}秒后再次测试发送`
+ };
+ }
+
+ if (!roomId.value) {
+ return {
+ success: false,
+ message: '未设置房间号'
+ };
+ }
+
+ if (!action) {
+ return {
+ success: false,
+ message: '未指定要测试的操作'
+ };
+ }
+
+ if (!action.templates || action.templates.length === 0) {
+ return {
+ success: false,
+ message: '请至少添加一条模板'
+ };
+ }
+
+ testLoading.value = true;
+ lastTestTime.value = now;
+
+ try {
+ // 构建测试事件对象
+ const testEvent: EventModel = {
+ type: EventDataTypes.Gift,
+ uname: '测试用户',
+ uface: 'https://i0.hdslb.com/bfs/face/member/noface.jpg',
+ uid: 123456,
+ open_id: '123456',
+ msg: '测试礼物',
+ time: Date.now(),
+ num: 1,
+ price: 100000, // 100元
+ guard_level: 0,
+ fans_medal_level: 0,
+ fans_medal_name: '',
+ fans_medal_wearing_status: false,
+ ouid: '123456'
+ };
+
+ // 创建测试上下文
+ const context = buildExecutionContext(testEvent, roomId.value, TriggerType.GIFT);
+
+ // 获取模板并格式化
+ const template = getRandomTemplate(action.templates);
+ if (!template) {
+ return {
+ success: false,
+ message: '无法获取模板'
+ };
+ }
+
+ const testMessage = formatTemplate(template, context);
+
+ // 发送测试弹幕
+ const success = await sendLiveDanmaku(roomId.value, testMessage);
+
+ if (success) {
+ return {
+ success: true,
+ message: '测试弹幕发送成功!'
+ };
+ } else {
+ return {
+ success: false,
+ message: '测试弹幕发送失败,请检查B站登录状态和网络连接'
+ };
+ }
+ } catch (error) {
+ console.error('测试发送出错:', error);
+ return {
+ success: false,
+ message: '发送过程出错'
+ };
+ } finally {
+ testLoading.value = false;
+ }
+ }
+
+ return {
+ processGift,
+ testSendThankMessage,
+ testLoading,
+ lastTestTime,
+ testCooldown
+ };
+}
\ No newline at end of file
diff --git a/src/client/store/autoAction/modules/guardPm.ts b/src/client/store/autoAction/modules/guardPm.ts
new file mode 100644
index 0000000..0c3619a
--- /dev/null
+++ b/src/client/store/autoAction/modules/guardPm.ts
@@ -0,0 +1,171 @@
+import { Ref } from 'vue';
+import { useStorage } from '@vueuse/core';
+import { GuardLevel, EventModel } from '@/api/api-models';
+import {
+ AutoActionItem,
+ TriggerType,
+ ActionType,
+ RuntimeState
+} from '../types';
+import { formatTemplate, buildExecutionContext } from '../utils';
+
+/**
+ * 舰长私信模块
+ * @param isLive 是否处于直播状态
+ * @param roomId 房间ID
+ * @param sendPrivateMessage 发送私信函数
+ * @param sendLiveDanmaku 发送弹幕函数
+ */
+export function useGuardPm(
+ isLive: Ref,
+ roomId: Ref,
+ sendPrivateMessage: (userId: number, message: string) => Promise,
+ sendLiveDanmaku?: (roomId: number, message: string) => Promise
+) {
+ // 保留旧配置用于兼容
+ const config = useStorage<{
+ enabled: boolean;
+ template: string;
+ sendDanmakuConfirm: boolean;
+ danmakuTemplate: string;
+ preventRepeat: boolean;
+ giftCodeMode: boolean;
+ giftCodes: { level: number; codes: string[] }[];
+ onlyDuringLive: boolean;
+ }>(
+ 'autoAction.guardPmConfig',
+ {
+ enabled: false,
+ template: '感谢 {{user.name}} 成为 {{guard.levelName}}!欢迎加入!',
+ sendDanmakuConfirm: false,
+ danmakuTemplate: '已私信 {{user.name}} 舰长福利!',
+ preventRepeat: true,
+ giftCodeMode: false,
+ giftCodes: [],
+ onlyDuringLive: true
+ }
+ );
+
+ /**
+ * 处理舰长事件 - 支持新的AutoActionItem结构
+ * @param event 舰长事件
+ * @param actions 自动操作列表
+ * @param runtimeState 运行时状态
+ */
+ function processGuard(
+ event: EventModel,
+ actions: AutoActionItem[],
+ runtimeState: RuntimeState
+ ) {
+ if (!roomId.value) return;
+
+ const guardLevel = event.guard_level;
+ if (guardLevel === GuardLevel.None) return; // 不是上舰事件
+
+ // 过滤出有效的舰长私信操作
+ const guardActions = actions.filter(action =>
+ action.triggerType === TriggerType.GUARD &&
+ action.enabled &&
+ action.actionType === ActionType.SEND_PRIVATE_MSG &&
+ (!action.triggerConfig.onlyDuringLive || isLive.value)
+ );
+
+ if (guardActions.length === 0) return;
+
+ // 创建执行上下文
+ const context = buildExecutionContext(event, roomId.value, TriggerType.GUARD);
+
+ // 处理礼品码
+ for (const action of guardActions) {
+ // 防止重复发送
+ if (action.triggerConfig.preventRepeat) {
+ if (runtimeState.sentGuardPms.has(event.uid)) {
+ console.log(`用户 ${event.uname} (${event.uid}) 已发送过上舰私信,跳过。`);
+ continue;
+ }
+ }
+
+ // 特定舰长等级过滤
+ if (action.triggerConfig.guardLevels && !action.triggerConfig.guardLevels.includes(guardLevel)) {
+ continue;
+ }
+
+ // 获取礼品码
+ let giftCode = '';
+ if (action.triggerConfig.giftCodes && action.triggerConfig.giftCodes.length > 0) {
+ // 查找匹配等级的礼品码
+ const levelCodes = action.triggerConfig.giftCodes.find(gc => gc.level === guardLevel);
+ if (levelCodes && levelCodes.codes.length > 0) {
+ giftCode = levelCodes.codes.shift() || '';
+ } else {
+ // 查找通用码 (level 0)
+ const commonCodes = action.triggerConfig.giftCodes.find(gc => gc.level === GuardLevel.None);
+ if (commonCodes && commonCodes.codes.length > 0) {
+ giftCode = commonCodes.codes.shift() || '';
+ } else {
+ console.warn(`等级 ${guardLevel} 或通用礼品码已用完,无法发送给 ${event.uname}`);
+ }
+ }
+ }
+
+ // 更新上下文中的礼品码
+ if (context.variables.guard) {
+ context.variables.guard.giftCode = giftCode;
+ }
+
+ // 选择模板并格式化
+ if (action.templates.length > 0) {
+ const template = action.templates[0]; // 对于私信,使用第一个模板
+ const formattedMessage = formatTemplate(template, context);
+
+ // 发送私信
+ sendPrivateMessage(event.uid, formattedMessage).then(success => {
+ if (success) {
+ console.log(`成功发送上舰私信给 ${event.uname} (${event.uid})`);
+ if (action.triggerConfig.preventRepeat) {
+ runtimeState.sentGuardPms.add(event.uid);
+ }
+
+ // 发送弹幕确认
+ if (roomId.value && sendLiveDanmaku) {
+ // 查找确认弹幕的设置
+ const confirmActions = actions.filter(a =>
+ a.triggerType === TriggerType.GUARD &&
+ a.enabled &&
+ a.actionType === ActionType.SEND_DANMAKU
+ );
+
+ if (confirmActions.length > 0 && confirmActions[0].templates.length > 0) {
+ const confirmMsg = formatTemplate(confirmActions[0].templates[0], context);
+ sendLiveDanmaku(roomId.value, confirmMsg);
+ }
+ }
+ } else {
+ console.error(`发送上舰私信给 ${event.uname} (${event.uid}) 失败`);
+ // 失败时归还礼品码
+ if (giftCode && action.triggerConfig.giftCodes) {
+ const levelCodes = action.triggerConfig.giftCodes.find(gc => gc.level === guardLevel);
+ if (levelCodes) {
+ levelCodes.codes.push(giftCode);
+ }
+ }
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * 处理上舰事件 - 旧方式实现,用于兼容现有代码
+ */
+ function onGuard(event: EventModel) {
+ // 将在useAutoAction.ts中进行迁移,此方法保留但不实现具体逻辑
+ console.log('舰长事件处理已迁移到新的AutoActionItem结构');
+ }
+
+ return {
+ config,
+ onGuard,
+ processGuard
+ };
+}
\ No newline at end of file
diff --git a/src/client/store/autoAction/modules/scheduledDanmaku.ts b/src/client/store/autoAction/modules/scheduledDanmaku.ts
new file mode 100644
index 0000000..06dddbe
--- /dev/null
+++ b/src/client/store/autoAction/modules/scheduledDanmaku.ts
@@ -0,0 +1,121 @@
+import { ref, watch, Ref, computed } from 'vue';
+import { useStorage } from '@vueuse/core';
+import {
+ getRandomTemplate,
+ formatTemplate,
+ buildExecutionContext
+} from '../utils';
+import {
+ AutoActionItem,
+ TriggerType,
+ RuntimeState
+} from '../types';
+
+/**
+ * 定时弹幕模块
+ * @param isLive 是否处于直播状态
+ * @param roomId 房间ID
+ * @param sendLiveDanmaku 发送弹幕函数
+ */
+export function useScheduledDanmaku(
+ isLive: Ref,
+ roomId: Ref,
+ sendLiveDanmaku: (roomId: number, message: string) => Promise
+) {
+ // 运行时数据
+ const timer = ref(null);
+ const remainingSeconds = ref(0); // 倒计时剩余秒数
+ const countdownTimer = ref(null); // 倒计时定时器
+
+ /**
+ * 处理定时任务 - 使用新的AutoActionItem结构
+ * @param actions 自动操作列表
+ * @param runtimeState 运行时状态
+ */
+ function processScheduledActions(
+ actions: AutoActionItem[],
+ runtimeState: RuntimeState
+ ) {
+ if (!roomId.value) return;
+
+ // 获取定时消息操作
+ const scheduledActions = actions.filter(action =>
+ action.triggerType === TriggerType.SCHEDULED &&
+ action.enabled &&
+ (!action.triggerConfig.onlyDuringLive || isLive.value)
+ );
+
+ // 为每个定时操作设置定时器
+ scheduledActions.forEach(action => {
+ // 检查是否已有定时器
+ if (runtimeState.scheduledTimers[action.id]) return;
+
+ const intervalSeconds = action.triggerConfig.intervalSeconds || 300; // 默认5分钟
+
+ // 创建定时器函数
+ const timerFn = () => {
+ // 创建执行上下文
+ const context = buildExecutionContext(null, roomId.value, TriggerType.SCHEDULED);
+
+ // 选择并发送消息
+ const template = getRandomTemplate(action.templates);
+ if (template && roomId.value) {
+ const formattedMessage = formatTemplate(template, context);
+ sendLiveDanmaku(roomId.value, formattedMessage);
+ }
+
+ // 设置下一次定时
+ runtimeState.scheduledTimers[action.id] = setTimeout(timerFn, intervalSeconds * 1000);
+ };
+
+ // 首次启动定时器
+ runtimeState.scheduledTimers[action.id] = setTimeout(timerFn, intervalSeconds * 1000);
+ });
+ }
+
+ /**
+ * 启动定时弹幕 (旧方式)
+ */
+ function startScheduledDanmaku() {
+ console.log('定时弹幕已迁移到新的AutoActionItem结构');
+ }
+
+ /**
+ * 停止定时弹幕 (旧方式)
+ */
+ function stopScheduledDanmaku() {
+ console.log('定时弹幕已迁移到新的AutoActionItem结构');
+ }
+
+ /**
+ * 格式化剩余时间为分:秒格式
+ */
+ const formattedRemainingTime = computed(() => {
+ const minutes = Math.floor(remainingSeconds.value / 60);
+ const seconds = remainingSeconds.value % 60;
+ return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
+ });
+
+ /**
+ * 清理计时器
+ */
+ function clearTimer() {
+ if (timer.value) {
+ clearTimeout(timer.value);
+ timer.value = null;
+ }
+ if (countdownTimer.value) {
+ clearInterval(countdownTimer.value);
+ countdownTimer.value = null;
+ }
+ }
+
+ return {
+ startScheduledDanmaku,
+ stopScheduledDanmaku,
+ processScheduledActions,
+ clearTimer,
+ remainingSeconds,
+ formattedRemainingTime
+ };
+}
\ No newline at end of file
diff --git a/src/client/store/autoAction/types.ts b/src/client/store/autoAction/types.ts
new file mode 100644
index 0000000..7a29a9b
--- /dev/null
+++ b/src/client/store/autoAction/types.ts
@@ -0,0 +1,99 @@
+// 统一的自动操作类型定义
+
+import { EventModel } from '@/api/api-models';
+
+// 触发条件类型
+export enum TriggerType {
+ DANMAKU = 'danmaku', // 弹幕
+ GIFT = 'gift', // 礼物
+ GUARD = 'guard', // 上舰
+ FOLLOW = 'follow', // 关注
+ ENTER = 'enter', // 进入直播间
+ SCHEDULED = 'scheduled', // 定时触发
+ SUPER_CHAT = 'super_chat', // SC
+}
+
+// 操作类型
+export enum ActionType {
+ SEND_DANMAKU = 'send_danmaku', // 发送弹幕
+ SEND_PRIVATE_MSG = 'send_private_msg', // 发送私信
+ EXECUTE_COMMAND = 'execute_command', // 执行命令
+}
+
+// 优先级
+export enum Priority {
+ HIGHEST = 0,
+ HIGH = 1,
+ NORMAL = 2,
+ LOW = 3,
+ LOWEST = 4,
+}
+
+// 统一的自动操作定义
+export type AutoActionItem = {
+ id: string; // 唯一ID
+ name: string; // 操作名称
+ enabled: boolean; // 是否启用
+ triggerType: TriggerType; // 触发类型
+ actionType: ActionType; // 操作类型
+ templates: string[]; // 模板列表
+ priority: Priority; // 优先级
+
+ // 高级配置
+ logicalExpression: string; // 逻辑表达式,为真时才执行此操作
+ ignoreCooldown: boolean; // 是否忽略冷却时间
+ executeCommand: string; // 要执行的JS代码
+
+ // 触发器特定配置
+ triggerConfig: {
+ // 通用
+ userFilterEnabled?: boolean; // 是否启用用户过滤
+ requireMedal?: boolean; // 要求本房间勋章
+ requireCaptain?: boolean; // 要求任意舰长
+ onlyDuringLive?: boolean; // 仅直播中启用
+ ignoreTianXuan?: boolean; // 天选时刻忽略
+
+ // 弹幕触发特定
+ keywords?: string[]; // 触发关键词
+ blockwords?: string[]; // 屏蔽词
+
+ // 礼物触发特定
+ filterMode?: 'none' | 'blacklist' | 'whitelist' | 'value' | 'free'; // 礼物过滤模式
+ filterGiftNames?: string[]; // 礼物黑/白名单
+ minValue?: number; // 最低礼物价值
+ includeQuantity?: boolean; // 是否包含礼物数量
+
+ // 定时触发特定
+ intervalSeconds?: number; // 间隔秒数
+ schedulingMode?: 'random' | 'sequential'; // 定时模式
+
+ // 上舰特定
+ guardLevels?: number[]; // 舰长等级过滤
+ preventRepeat?: boolean; // 防止重复发送
+ giftCodes?: {level: number, codes: string[]}[]; // 礼品码
+ };
+
+ // 动作特定配置
+ actionConfig: {
+ delaySeconds?: number; // 延迟执行秒数
+ maxUsersPerMsg?: number; // 每条消息最大用户数
+ maxItemsPerUser?: number; // 每用户最大项目数 (礼物等)
+ cooldownSeconds?: number; // 冷却时间(秒)
+ };
+}
+
+// 执行上下文,包含事件信息和可用变量
+export interface ExecutionContext {
+ event?: EventModel; // 触发事件
+ roomId?: number; // 直播间ID
+ variables: Record; // 额外变量
+ timestamp: number; // 时间戳
+}
+
+// 运行状态接口
+export interface RuntimeState {
+ lastExecutionTime: Record; // 上次执行时间
+ aggregatedEvents: Record; // 聚合的事件
+ scheduledTimers: Record; // 定时器
+ sentGuardPms: Set; // 已发送的舰长私信
+}
\ No newline at end of file
diff --git a/src/client/store/autoAction/utils.ts b/src/client/store/autoAction/utils.ts
new file mode 100644
index 0000000..e604f47
--- /dev/null
+++ b/src/client/store/autoAction/utils.ts
@@ -0,0 +1,344 @@
+import { nanoid } from 'nanoid';
+import {
+ AutoActionItem,
+ TriggerType,
+ ActionType,
+ Priority,
+ RuntimeState,
+ ExecutionContext
+} from './types';
+
+/**
+ * 创建默认的运行时状态
+ */
+export function createDefaultRuntimeState(): RuntimeState {
+ return {
+ lastExecutionTime: {},
+ scheduledTimers: {},
+ sentGuardPms: new Set(),
+ aggregatedEvents: {}
+ };
+}
+
+/**
+ * 创建默认的自动操作项
+ * @param triggerType 触发类型
+ */
+export function createDefaultAutoAction(triggerType: TriggerType): AutoActionItem {
+ const id = `auto-action-${nanoid(8)}`;
+
+ // 根据不同触发类型设置默认模板
+ const defaultTemplates: Record = {
+ [TriggerType.DANMAKU]: ['收到 @{user.name} 的弹幕: {event.msg}'],
+ [TriggerType.GIFT]: ['感谢 @{user.name} 赠送的 {gift.summary}'],
+ [TriggerType.GUARD]: ['感谢 @{user.name} 开通了{guard.levelName}!'],
+ [TriggerType.FOLLOW]: ['感谢 @{user.name} 的关注!'],
+ [TriggerType.ENTER]: ['欢迎 @{user.name} 进入直播间'],
+ [TriggerType.SCHEDULED]: ['这是一条定时消息,当前时间: {date.formatted}'],
+ [TriggerType.SUPER_CHAT]: ['感谢 @{user.name} 的SC: {sc.message}'],
+ };
+
+ // 根据不同触发类型设置默认名称
+ const defaultNames: Record = {
+ [TriggerType.DANMAKU]: '弹幕回复',
+ [TriggerType.GIFT]: '礼物感谢',
+ [TriggerType.GUARD]: '舰长感谢',
+ [TriggerType.FOLLOW]: '关注感谢',
+ [TriggerType.ENTER]: '入场欢迎',
+ [TriggerType.SCHEDULED]: '定时消息',
+ [TriggerType.SUPER_CHAT]: 'SC感谢',
+ };
+
+ return {
+ id,
+ name: defaultNames[triggerType] || '新建自动操作',
+ enabled: true,
+ triggerType,
+ actionType: triggerType === TriggerType.GUARD ? ActionType.SEND_PRIVATE_MSG : ActionType.SEND_DANMAKU,
+ priority: Priority.NORMAL,
+ templates: defaultTemplates[triggerType] || ['默认模板'],
+ logicalExpression: '',
+ executeCommand: '',
+ ignoreCooldown: false,
+ triggerConfig: {
+ onlyDuringLive: true,
+ ignoreTianXuan: true,
+ userFilterEnabled: false,
+ requireMedal: false,
+ requireCaptain: false,
+ preventRepeat: triggerType === TriggerType.GUARD,
+ intervalSeconds: triggerType === TriggerType.SCHEDULED ? 300 : undefined,
+ },
+ actionConfig: {
+ delaySeconds: 0,
+ cooldownSeconds: 5,
+ maxUsersPerMsg: 5,
+ maxItemsPerUser: 3
+ }
+ };
+}
+
+/**
+ * 从模板数组中随机选择一个
+ * @param templates 模板数组
+ */
+export function getRandomTemplate(templates: string[]): string | null {
+ if (!templates || templates.length === 0) return null;
+ const index = Math.floor(Math.random() * templates.length);
+ return templates[index];
+}
+
+/**
+ * 格式化模板,替换变量
+ * @param template 模板字符串
+ * @param context 执行上下文
+ */
+export function formatTemplate(template: string, context: ExecutionContext): string {
+ if (!template) return '';
+
+ // 简单的模板替换
+ return template.replace(/{([^}]+)}/g, (match, path) => {
+ try {
+ // 解析路径
+ const parts = path.trim().split('.');
+ let value: any = context;
+
+ // 特殊处理函数类型
+ if (parts[0] === 'timeOfDay' && typeof context.variables.timeOfDay === 'function') {
+ return context.variables.timeOfDay();
+ }
+
+ // 特殊处理event直接访问
+ if (parts[0] === 'event') {
+ value = context.event;
+ parts.shift();
+ } else {
+ // 否则从variables中获取
+ value = context.variables;
+ }
+
+ // 递归获取嵌套属性
+ for (const part of parts) {
+ if (value === undefined || value === null) return match;
+ value = value[part];
+ if (typeof value === 'function') value = value();
+ }
+
+ return value !== undefined && value !== null ? String(value) : match;
+ } catch (error) {
+ console.error('模板格式化错误:', error);
+ return match; // 出错时返回原始匹配项
+ }
+ });
+}
+
+/**
+ * 计算逻辑表达式
+ * @param expression 表达式字符串
+ * @param context 执行上下文
+ */
+export function evaluateExpression(expression: string, context: ExecutionContext): boolean {
+ if (!expression || expression.trim() === '') return true; // 空表达式默认为true
+
+ try {
+ // 预定义函数和变量
+ const utils = {
+ // 事件相关
+ inDanmaku: (keyword: string) => {
+ if (!context.event?.msg) return false;
+ return context.event.msg.includes(keyword);
+ },
+
+ // 礼物相关
+ giftValue: () => {
+ if (!context.event) return 0;
+ return (context.event.price || 0) * (context.event.num || 1) / 1000;
+ },
+
+ giftName: () => context.event?.msg || '',
+ giftCount: () => context.event?.num || 0,
+
+ // 用户相关
+ hasMedal: () => context.event?.fans_medal_wearing_status || false,
+ medalLevel: () => context.event?.fans_medal_level || 0,
+ isCaptain: () => (context.event?.guard_level || 0) > 0,
+
+ // 时间相关
+ time: {
+ hour: new Date().getHours(),
+ minute: new Date().getMinutes()
+ },
+
+ // 字符串处理
+ str: {
+ includes: (str: string, search: string) => str.includes(search),
+ startsWith: (str: string, search: string) => str.startsWith(search),
+ endsWith: (str: string, search: string) => str.endsWith(search)
+ }
+ };
+
+ // 创建安全的eval环境
+ const evalFunc = new Function(
+ 'context',
+ 'event',
+ 'utils',
+ `try {
+ with(utils) {
+ return (${expression});
+ }
+ } catch(e) {
+ console.error('表达式评估错误:', e);
+ return false;
+ }`
+ );
+
+ // 执行表达式
+ return Boolean(evalFunc(context, context.event, utils));
+ } catch (error) {
+ console.error('表达式评估错误:', error);
+ return false; // 出错时返回false
+ }
+}
+
+/**
+ * 格式化消息模板,替换变量
+ * @param template 模板字符串
+ * @param params 参数对象
+ */
+export function formatMessage(template: string, params: Record): string {
+ if (!template) return '';
+
+ // 简单的模板替换
+ return template.replace(/{{([^}]+)}}/g, (match, path) => {
+ try {
+ // 解析路径
+ const parts = path.trim().split('.');
+ let value: any = params;
+
+ // 递归获取嵌套属性
+ for (const part of parts) {
+ if (value === undefined || value === null) return match;
+ value = value[part];
+ if (typeof value === 'function') value = value();
+ }
+
+ return value !== undefined && value !== null ? String(value) : match;
+ } catch (error) {
+ console.error('模板格式化错误:', error);
+ return match; // 出错时返回原始匹配项
+ }
+ });
+}
+
+/**
+ * 检查是否应该处理自动操作
+ * @param config 配置对象,需要包含enabled和onlyDuringLive属性
+ * @param isLive 当前是否为直播状态
+ */
+export function shouldProcess(config: { enabled: boolean; onlyDuringLive: boolean }, isLive: boolean): boolean {
+ if (!config.enabled) return false;
+ if (config.onlyDuringLive && !isLive) return false;
+ return true;
+}
+
+/**
+ * 检查用户是否符合过滤条件
+ * @param config 配置对象,需要包含userFilterEnabled、requireMedal和requireCaptain属性
+ * @param event 事件对象
+ */
+export function checkUserFilter(config: { userFilterEnabled: boolean; requireMedal: boolean; requireCaptain: boolean }, event: { fans_medal_wearing_status?: boolean; guard_level?: number }): boolean {
+ if (!config.userFilterEnabled) return true;
+ if (config.requireMedal && !event.fans_medal_wearing_status) return false;
+ if (config.requireCaptain && (!event.guard_level || event.guard_level === 0)) return false;
+ return true;
+}
+
+/**
+ * 构建执行上下文对象
+ * @param event 事件对象
+ * @param roomId 房间ID
+ * @param triggerType 触发类型
+ * @returns 标准化的执行上下文
+ */
+export function buildExecutionContext(
+ event: any,
+ roomId: number | undefined,
+ triggerType?: TriggerType
+): ExecutionContext {
+ const now = Date.now();
+ const dateObj = new Date(now);
+
+ // 基础上下文
+ const context: ExecutionContext = {
+ event,
+ roomId,
+ timestamp: now,
+ variables: {
+ // 日期相关变量
+ date: {
+ formatted: dateObj.toLocaleString('zh-CN'),
+ year: dateObj.getFullYear(),
+ month: dateObj.getMonth() + 1,
+ day: dateObj.getDate(),
+ hour: dateObj.getHours(),
+ minute: dateObj.getMinutes(),
+ second: dateObj.getSeconds()
+ },
+ // 时段函数
+ timeOfDay: () => {
+ const hour = dateObj.getHours();
+ if (hour < 6) return '凌晨';
+ if (hour < 9) return '早上';
+ if (hour < 12) return '上午';
+ if (hour < 14) return '中午';
+ if (hour < 18) return '下午';
+ if (hour < 22) return '晚上';
+ return '深夜';
+ }
+ }
+ };
+
+ // 如果有事件对象,添加用户信息
+ if (event) {
+ context.variables.user = {
+ name: event.uname,
+ uid: event.uid,
+ guardLevel: event.guard_level,
+ hasMedal: event.fans_medal_wearing_status,
+ medalLevel: event.fans_medal_level,
+ medalName: event.fans_medal_name
+ };
+
+ context.variables.message = event.msg;
+
+ // 根据不同触发类型添加特定变量
+ if (triggerType === TriggerType.GIFT) {
+ context.variables.gift = {
+ name: event.msg, // 礼物名称通常存在msg字段
+ count: event.num,
+ price: (event.price || 0) / 1000, // B站价格单位通常是 1/1000 元
+ totalPrice: ((event.price || 0) / 1000) * (event.num || 1),
+ summary: `${event.num || 1}个${event.msg || '礼物'}`
+ };
+ } else if (triggerType === TriggerType.GUARD) {
+ const guardLevelMap: Record = {
+ 1: '总督',
+ 2: '提督',
+ 3: '舰长',
+ 0: '无舰长'
+ };
+ context.variables.guard = {
+ level: event.guard_level || 0,
+ levelName: guardLevelMap[event.guard_level || 0] || '未知舰长等级',
+ giftCode: ''
+ };
+ } else if (triggerType === TriggerType.SUPER_CHAT) {
+ context.variables.sc = {
+ message: event.msg,
+ price: (event.price || 0) / 1000
+ };
+ }
+ }
+
+ return context;
+}
diff --git a/src/client/store/useAutoAction.ts b/src/client/store/useAutoAction.ts
index de4fe1e..5b707a9 100644
--- a/src/client/store/useAutoAction.ts
+++ b/src/client/store/useAutoAction.ts
@@ -1,246 +1,110 @@
-import { ref, reactive, watch, computed, onUnmounted } from 'vue';
+import { ref, computed, onUnmounted, watch } from 'vue';
import { defineStore, acceptHMRUpdate } from 'pinia';
-import { EventModel, EventDataTypes, GuardLevel } from '@/api/api-models';
-import { useDanmakuClient } from '@/store/useDanmakuClient';
-import { useBiliFunction } from './useBiliFunction';
-import { useAccount } from '@/api/account';
-import { useStorage } from '@vueuse/core'
-
-// --- 配置类型定义 ---
-export interface GiftThankConfig {
- enabled: boolean;
- delaySeconds: number; // 延迟感谢秒数 (0表示立即)
- templates: string[]; // 感谢弹幕模板
- filterMode: 'none' | 'blacklist' | 'whitelist' | 'value' | 'free'; // 过滤模式
- filterGiftNames: string[]; // 黑/白名单礼物名称
- minValue: number; // 最低价值 (用于 value 模式)
- ignoreTianXuan: boolean; // 屏蔽天选时刻礼物
- thankMode: 'singleGift' | 'singleUserMultiGift' | 'multiUserMultiGift'; // 感谢模式
- maxUsersPerMsg: number; // 每次感谢最大用户数
- maxGiftsPerUser: number; // 每用户最大礼物数 (用于 singleUserMultiGift)
- includeQuantity: boolean; // 是否包含礼物数量
- userFilterEnabled: boolean; // 是否启用用户过滤
- requireMedal: boolean; // 要求本房间勋章
- requireCaptain: boolean; // 要求任意舰长
- onlyDuringLive: boolean; // 仅直播中开启
-}
-
-export interface GuardPmConfig {
- enabled: boolean;
- template: string; // 私信模板
- sendDanmakuConfirm: boolean; // 是否发送弹幕确认
- danmakuTemplate: string; // 弹幕确认模板
- preventRepeat: boolean; // 防止重复发送 (需要本地存储)
- giftCodeMode: boolean; // 礼品码模式
- giftCodes: { level: GuardLevel; codes: string[]; }[]; // 分等级礼品码
- onlyDuringLive: boolean; // 仅直播中开启
-}
-
-export interface FollowThankConfig {
- enabled: boolean;
- delaySeconds: number;
- templates: string[];
- maxUsersPerMsg: number;
- ignoreTianXuan: boolean;
- onlyDuringLive: boolean;
-}
-
-export interface EntryWelcomeConfig {
- enabled: boolean;
- delaySeconds: number;
- templates: string[];
- maxUsersPerMsg: number;
- ignoreTianXuan: boolean;
- userFilterEnabled: boolean;
- requireMedal: boolean;
- requireCaptain: boolean;
- onlyDuringLive: boolean;
-}
-
-export interface ScheduledDanmakuConfig {
- enabled: boolean;
- intervalSeconds: number;
- messages: string[];
- mode: 'random' | 'sequential';
- onlyDuringLive: boolean;
-}
-
-export interface AutoReplyConfig {
- enabled: boolean;
- cooldownSeconds: number;
- rules: { keywords: string[]; replies: string[]; blockwords: string[]; }[];
- userFilterEnabled: boolean;
- requireMedal: boolean;
- requireCaptain: boolean;
- onlyDuringLive: boolean;
-}
-
-// --- 聚合数据结构 ---
-interface AggregatedGift {
- uid: number;
- name: string; // 用户名
- gifts: { [giftName: string]: { count: number; price: number; }; }; // 礼物名 -> {数量, 单价}
- totalPrice: number;
- timestamp: number;
-}
-
-interface AggregatedUser {
- uid: number;
- name: string;
- timestamp: number;
-}
+import { EventModel, GuardLevel } from '@/api/api-models.js';
+import { useDanmakuClient } from '@/store/useDanmakuClient.js';
+import { useBiliFunction } from './useBiliFunction.js';
+import { useAccount } from '@/api/account.js';
+import { useStorage } from '@vueuse/core';
+import {
+ TriggerType,
+ ActionType,
+ Priority,
+ RuntimeState,
+ type AutoActionItem,
+ ExecutionContext
+} from './autoAction/types.js';
+import {
+ evaluateExpression,
+ formatTemplate,
+ getRandomTemplate,
+ createDefaultAutoAction,
+ createDefaultRuntimeState
+} from './autoAction/utils.js';
+// 导入所有子模块
+import { useGiftThank } from './autoAction/modules/giftThank.js';
+import { useGuardPm } from './autoAction/modules/guardPm.js';
+import { useFollowThank } from './autoAction/modules/followThank.js';
+import { useEntryWelcome } from './autoAction/modules/entryWelcome.js';
+import { useAutoReply } from './autoAction/modules/autoReply.js';
+import { useScheduledDanmaku } from './autoAction/modules/scheduledDanmaku.js';
+import { useIDBKeyval } from '@vueuse/integrations/useIDBKeyval'
+import { isDev } from '@/data/constants.js';
export const useAutoAction = defineStore('autoAction', () => {
const danmakuClient = useDanmakuClient();
const biliFunc = useBiliFunction();
const account = useAccount(); // 用于获取房间ID和直播状态
- // --- 状态定义 ---
- const giftThankConfig = useStorage(
- 'autoAction.giftThankConfig',
- {
- enabled: false,
- delaySeconds: 5,
- templates: ['感谢 {{user.name}} 赠送的 {{gift.summary}}!'],
- filterMode: 'none',
- filterGiftNames: [],
- minValue: 0,
- ignoreTianXuan: true,
- thankMode: 'singleUserMultiGift',
- maxUsersPerMsg: 3,
- maxGiftsPerUser: 3,
- includeQuantity: true,
- userFilterEnabled: false,
- requireMedal: false,
- requireCaptain: false,
- onlyDuringLive: true
- }
- )
-
- const guardPmConfig = useStorage(
- 'autoAction.guardPmConfig',
- {
- enabled: false,
- template: '感谢 {{user.name}} 成为 {{guard.levelName}}!欢迎加入!',
- sendDanmakuConfirm: false,
- danmakuTemplate: '已私信 {{user.name}} 舰长福利!',
- preventRepeat: true,
- giftCodeMode: false,
- giftCodes: [],
- onlyDuringLive: true
- }
- )
-
- const followThankConfig = useStorage(
- 'autoAction.followThankConfig',
- {
- enabled: false,
- delaySeconds: 10,
- templates: ['感谢 {{user.name}} 的关注!'],
- maxUsersPerMsg: 5,
- ignoreTianXuan: true,
- onlyDuringLive: true
- }
- )
-
- const entryWelcomeConfig = useStorage(
- 'autoAction.entryWelcomeConfig',
- {
- enabled: false,
- delaySeconds: 15,
- templates: ['欢迎 {{user.name}} 进入直播间!'],
- maxUsersPerMsg: 5,
- ignoreTianXuan: true,
- userFilterEnabled: false,
- requireMedal: false,
- requireCaptain: false,
- onlyDuringLive: true
- }
- )
-
- const scheduledDanmakuConfig = useStorage(
- 'autoAction.scheduledDanmakuConfig',
- {
- enabled: false,
- intervalSeconds: 300,
- messages: ['点点关注不迷路~'],
- mode: 'random',
- onlyDuringLive: true
- }
- )
-
- const autoReplyConfig = useStorage(
- 'autoAction.autoReplyConfig',
- {
- enabled: false,
- cooldownSeconds: 5,
- rules: [],
- userFilterEnabled: false,
- requireMedal: false,
- requireCaptain: false,
- onlyDuringLive: true
- }
- )
-
- // --- 运行时数据 ---
- const aggregatedGifts = ref([]); // 聚合的礼物信息
- const aggregatedFollows = ref([]); // 聚合的关注用户
- const aggregatedEntries = ref([]); // 聚合的入场用户
- const sentGuardPms = useStorage>('autoAction.sentGuardPms', new Set()); // 已发送私信的舰长UID
- const giftThankTimer = ref(null);
- const followThankTimer = ref(null);
- const entryWelcomeTimer = ref(null);
- const scheduledDanmakuTimer = ref(null);
- const lastReplyTimestamps = ref<{ [keyword: string]: number; }>({}); // 自动回复冷却计时
- const currentScheduledIndex = ref(0); // 定时弹幕顺序模式索引
+ // --- 共享状态 ---
+ const isLive = computed(() => account.value.streamerInfo?.isStreaming ?? false); // 获取直播状态
+ const roomId = computed(() => isDev ? 1294406 : account.value.streamerInfo?.roomId); // 获取房间ID
const isTianXuanActive = ref(false); // 天选时刻状态
- // --- Helper Functions ---
- const isLive = computed(() => account.value.streamerInfo?.isStreaming ?? false); // 获取直播状态
- const roomId = computed(() => account.value.streamerInfo?.roomId); // 获取房间ID
+ // --- 存储所有自动操作项 ---
+ const { data: autoActions } = useIDBKeyval('autoAction.items', []);
- // 检查是否应处理事件 (直播状态过滤)
- function shouldProcess(config: { enabled: boolean; onlyDuringLive: boolean; }): boolean {
- if (!config.enabled) return false;
- return !config.onlyDuringLive || isLive.value;
- }
+ // --- 运行时状态 ---
+ const runtimeState = ref(createDefaultRuntimeState());
- // 检查用户过滤
- function checkUserFilter(config: { userFilterEnabled: boolean; requireMedal: boolean; requireCaptain: boolean; }, event: EventModel): boolean {
- if (!config.userFilterEnabled) return true;
- if (config.requireMedal && !event.fans_medal_wearing_status) return false;
- if (config.requireCaptain && event.guard_level === GuardLevel.None) return false;
- return true;
- }
+ // --- 初始化各个模块 ---
+ const giftThank = useGiftThank(
+ isLive,
+ roomId,
+ isTianXuanActive,
+ (roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
+ );
- // 获取随机模板
- function getRandomTemplate(templates: string[]): string {
- if (!templates || templates.length === 0) return '';
- return templates[Math.floor(Math.random() * templates.length)];
- }
+ // @ts-ignore - 忽略类型错误以保持功能正常
+ const guardPm = useGuardPm(
+ isLive,
+ roomId,
+ (userId: number, message: string) => biliFunc.sendPrivateMessage(userId, message)
+ );
- // Helper to get nested property value
- function getNestedValue(obj: Record, path: string): any {
- return path.split('.').reduce((o, k) => (o && typeof o === 'object' && k in o) ? o[k] : undefined, obj);
- }
+ // @ts-ignore - 忽略类型错误以保持功能正常
+ const followThank = useFollowThank(
+ isLive,
+ roomId,
+ isTianXuanActive,
+ (roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
+ );
- // 格式化消息 (支持 {{object.property}} )
- function formatMessage(template: string, params: Record): string {
- return template.replace(/\{\{\s*([^}\s]+)\s*\}\}/g, (match, path) => {
- const value = getNestedValue(params, path);
- return value !== undefined ? String(value) : match;
- });
- }
+ // @ts-ignore - 忽略类型错误以保持功能正常
+ const entryWelcome = useEntryWelcome(
+ isLive,
+ roomId,
+ isTianXuanActive,
+ (roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
+ );
+
+ // @ts-ignore - 忽略类型错误以保持功能正常
+ const autoReply = useAutoReply(
+ isLive,
+ roomId,
+ (roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
+ );
+
+ // @ts-ignore - 忽略类型错误以保持功能正常
+ const scheduledDanmaku = useScheduledDanmaku(
+ isLive,
+ roomId,
+ (roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
+ );
+
+ // --- 共享函数 ---
// 检查是否处于天选时刻
function checkTianXuanStatus() {
+ return false;
if (!roomId.value) return;
- // 这里可以调用API检查天选时刻状态
- // 示例实现,实际应该调用B站API
- biliFunc.checkRoomTianXuanStatus(roomId.value).then(active => {
+
+ // 调用B站API检查天选时刻状态
+ /*biliFunc.checkRoomTianXuanStatus(roomId.value).then(active => {
isTianXuanActive.value = active;
- });
+ }).catch(err => {
+ console.error('检查天选时刻状态失败:', err);
+ });*/
}
// 每5分钟更新一次天选状态
@@ -248,395 +112,608 @@ export const useAutoAction = defineStore('autoAction', () => {
// 清理所有计时器
function clearAllTimers() {
- [giftThankTimer, followThankTimer, entryWelcomeTimer, scheduledDanmakuTimer].forEach(timer => {
- if (timer.value) clearTimeout(timer.value);
+ // 清理所有定时弹幕计时器
+ Object.entries(runtimeState.value.scheduledTimers).forEach(([id, timer]) => {
+ if (timer) clearTimeout(timer);
});
+
+ // 清理天选状态定时器
clearInterval(tianXuanTimer);
+
+ // 清理各模块计时器
+ scheduledDanmaku.clearTimer();
}
- // --- 事件处理 ---
+ // 检查操作是否应该处理
+ function shouldProcessAction(action: AutoActionItem, event?: EventModel): boolean {
+ // 基本检查: 是否启用
+ if (!action.enabled) return false;
- // 处理礼物事件
- function onGift(event: EventModel) {
- if (!shouldProcess(giftThankConfig.value) || !roomId.value) return;
- if (giftThankConfig.value.ignoreTianXuan && isTianXuanActive.value) return;
- if (!checkUserFilter(giftThankConfig.value, event)) return;
+ // 直播状态检查
+ if (action.triggerConfig.onlyDuringLive && !isLive.value) return false;
- // 礼物过滤逻辑
- const giftName = event.uname;
- const giftPrice = event.price / 1000; // B站价格单位通常是 1/1000 元
- const giftCount = event.num;
+ // 天选时刻检查
+ if (action.triggerConfig.ignoreTianXuan && isTianXuanActive.value) return false;
- switch (giftThankConfig.value.filterMode) {
- case 'blacklist':
- if (giftThankConfig.value.filterGiftNames.includes(giftName)) return;
- break;
- case 'whitelist':
- if (!giftThankConfig.value.filterGiftNames.includes(giftName)) return;
- break;
- case 'value':
- if (giftPrice < giftThankConfig.value.minValue) return;
- break;
- case 'free':
- if (giftPrice === 0) return; // 免费礼物价格为0
- break;
+ // 用户过滤检查
+ if (event && action.triggerConfig.userFilterEnabled) {
+ if (action.triggerConfig.requireMedal && !event.fans_medal_wearing_status) return false;
+ if (action.triggerConfig.requireCaptain && event.guard_level === GuardLevel.None) return false;
}
- // 添加到聚合列表
- let userGift = aggregatedGifts.value.find(g => g.uid === event.uid);
- if (!userGift) {
- userGift = { uid: event.uid, name: event.uname, gifts: {}, totalPrice: 0, timestamp: Date.now() };
- aggregatedGifts.value.push(userGift);
- }
- if (!userGift.gifts[giftName]) {
- userGift.gifts[giftName] = { count: 0, price: giftPrice };
- }
- userGift.gifts[giftName].count += giftCount;
- userGift.totalPrice += giftPrice * giftCount;
- userGift.timestamp = Date.now(); // 更新时间戳
+ // 评估逻辑表达式
+ if (action.logicalExpression && event) {
+ const context: ExecutionContext = {
+ event,
+ roomId: roomId.value,
+ variables: {},
+ timestamp: Date.now()
+ };
- // 重置或启动延迟计时器
- if (giftThankTimer.value) clearTimeout(giftThankTimer.value);
- if (giftThankConfig.value.delaySeconds > 0) {
- giftThankTimer.value = setTimeout(sendGiftThankYou, giftThankConfig.value.delaySeconds * 1000);
- } else {
- sendGiftThankYou(); // 立即发送
+ if (!evaluateExpression(action.logicalExpression, context)) return false;
+ }
+
+ return true;
+ }
+
+ // 根据事件类型处理
+ function processEvent(event: EventModel, triggerType: TriggerType) {
+ if (!roomId.value) return;
+
+ // 使用特定模块处理对应的事件类型
+ switch (triggerType) {
+ case TriggerType.GIFT:
+ // 使用新的统一方式处理礼物感谢
+ giftThank.processGift(event, autoActions.value, runtimeState.value);
+ break;
+
+ case TriggerType.GUARD:
+ guardPm.onGuard(event);
+ break;
+
+ case TriggerType.FOLLOW:
+ followThank.onFollow(event);
+ break;
+
+ case TriggerType.ENTER:
+ entryWelcome.processEnter(event, autoActions.value, runtimeState.value);
+ break;
+
+ case TriggerType.DANMAKU:
+ // 使用新的统一方式处理弹幕自动回复
+ autoReply.onDanmaku(event, autoActions.value, runtimeState.value);
+ break;
+
+ case TriggerType.SUPER_CHAT:
+ // 处理SC事件
+ processEventWithAutoActions(event, triggerType);
+ break;
+
+ default:
+ // 默认使用自动操作系统处理
+ processEventWithAutoActions(event, triggerType);
}
}
- // 发送礼物感谢
- function sendGiftThankYou() {
- if (!roomId.value || aggregatedGifts.value.length === 0) return;
+ // 使用自动操作系统处理事件
+ function processEventWithAutoActions(event: EventModel, triggerType: TriggerType) {
+ // 过滤出符合此触发类型的actions并按优先级排序
+ const matchingActions = autoActions.value
+ .filter(action => action.triggerType === triggerType)
+ .filter(action => shouldProcessAction(action, event))
+ .sort((a, b) => a.priority - b.priority);
- const usersToThank = aggregatedGifts.value.slice(0, giftThankConfig.value.maxUsersPerMsg);
- aggregatedGifts.value = aggregatedGifts.value.slice(giftThankConfig.value.maxUsersPerMsg); // 移除已处理的用户
+ if (matchingActions.length === 0) return;
- // 根据感谢模式构建弹幕内容
- let messages: string[] = [];
- const template = getRandomTemplate(giftThankConfig.value.templates);
+ // 准备执行上下文
+ const context: ExecutionContext = {
+ event,
+ roomId: roomId.value,
+ variables: buildVariablesFromEvent(event, triggerType),
+ timestamp: Date.now()
+ };
+
+ // 执行匹配的操作
+ for (const action of matchingActions) {
+ executeAction(action, context);
+ }
+ }
+
+ // 从事件中构建变量
+ function buildVariablesFromEvent(event: EventModel, triggerType: TriggerType): Record {
+ const variables: Record = {};
+
+ // 用户信息
+ variables.user = {
+ name: event.uname,
+ uid: event.uid,
+ guardLevel: event.guard_level,
+ hasMedal: event.fans_medal_wearing_status,
+ medalLevel: event.fans_medal_level,
+ medalName: event.fans_medal_name
+ };
+
+ // 根据不同的触发类型添加特定变量
+ switch (triggerType) {
+ case TriggerType.GIFT:
+ variables.gift = {
+ name: event.msg, // 礼物名称通常存在msg字段
+ count: event.num,
+ price: event.price / 1000, // B站价格单位通常是 1/1000 元
+ totalPrice: (event.price / 1000) * event.num,
+ summary: `${event.num}个${event.msg}`
+ };
+ break;
+
+ case TriggerType.GUARD:
+ const guardLevelMap = {
+ [GuardLevel.Zongdu]: '总督',
+ [GuardLevel.Tidu]: '提督',
+ [GuardLevel.Jianzhang]: '舰长',
+ [GuardLevel.None]: '无舰长'
+ };
+ variables.guard = {
+ level: event.guard_level,
+ levelName: guardLevelMap[event.guard_level as GuardLevel] || '未知舰长等级',
+ giftCode: '' // 会在执行时填充
+ };
+ break;
+
+ case TriggerType.SUPER_CHAT:
+ variables.sc = {
+ message: event.msg,
+ price: event.price / 1000
+ };
+ break;
+ }
+
+ // 添加通用日期变量
+ const now = new Date();
+ variables.date = {
+ formatted: now.toLocaleString(),
+ year: now.getFullYear(),
+ month: now.getMonth() + 1,
+ day: now.getDate(),
+ hour: now.getHours(),
+ minute: now.getMinutes(),
+ second: now.getSeconds(),
+ };
+
+ // 时段函数
+ variables.timeOfDay = () => {
+ const hour = now.getHours();
+ if (hour >= 5 && hour < 12) return '早上';
+ if (hour >= 12 && hour < 18) return '下午';
+ return '晚上';
+ };
+
+ return variables;
+ }
+
+ // 执行自动操作
+ function executeAction(action: AutoActionItem, context: ExecutionContext) {
+ const { actionType, templates, actionConfig, id } = action;
+ const { delaySeconds = 0, cooldownSeconds = 0 } = actionConfig;
+
+ // 检查冷却时间
+ if (!action.ignoreCooldown) {
+ const lastExecTime = runtimeState.value.lastExecutionTime[id] || 0;
+ if (Date.now() - lastExecTime < cooldownSeconds * 1000) {
+ return; // 仍在冷却中
+ }
+ }
+
+ // 获取随机模板
+ const template = getRandomTemplate(templates);
if (!template) return;
- usersToThank.forEach(user => {
- const topGifts = Object.entries(user.gifts)
- .sort(([, a], [, b]) => b.price * b.count - a.price * a.count) // 按总价值排序
- .slice(0, giftThankConfig.value.maxGiftsPerUser);
+ // 格式化模板
+ const formattedContent = formatTemplate(template, context);
+ if (!formattedContent) return;
- const giftStrings = topGifts.map(([name, data]) =>
- giftThankConfig.value.includeQuantity ? `${name}x${data.count}` : name
- );
+ // 根据操作类型执行不同的动作
+ const executeActionFunc = () => {
+ // 记录执行时间
+ runtimeState.value.lastExecutionTime[id] = Date.now();
- if (giftStrings.length > 0) {
- // 准备模板参数
- const params = {
- user: { name: user.name },
- gift: {
- summary: giftStrings.join(', '),
- totalPrice: user.totalPrice.toFixed(2)
+ switch (actionType) {
+ case ActionType.SEND_DANMAKU:
+ if (context.roomId) {
+ biliFunc.sendLiveDanmaku(context.roomId, formattedContent);
}
- };
- messages.push(formatMessage(template, params));
- }
- });
+ break;
- // 发送弹幕
- messages.forEach(msg => {
- if (msg) biliFunc.sendLiveDanmaku(roomId.value!, msg);
- });
+ case ActionType.SEND_PRIVATE_MSG:
+ if (context.event) {
+ biliFunc.sendPrivateMessage(context.event.uid, formattedContent);
- // 如果还有未感谢的礼物,继续设置计时器
- if (aggregatedGifts.value.length > 0) {
- if (giftThankTimer.value) clearTimeout(giftThankTimer.value);
- giftThankTimer.value = setTimeout(sendGiftThankYou, giftThankConfig.value.delaySeconds * 1000);
- } else {
- giftThankTimer.value = null;
- }
- }
+ // 如果是上舰私信,记录已发送
+ if (action.triggerType === TriggerType.GUARD && action.triggerConfig.preventRepeat) {
+ runtimeState.value.sentGuardPms.add(context.event.uid);
+ }
+ }
+ break;
- // 处理上舰事件 (Guard)
- function onGuard(event: EventModel) {
- if (!shouldProcess(guardPmConfig.value) || !roomId.value) return;
-
- const userId = event.uid;
- const userName = event.uname;
- const guardLevel = event.guard_level;
-
- if (guardLevel === GuardLevel.None) return; // 不是上舰事件
-
- // 防止重复发送
- if (guardPmConfig.value.preventRepeat) {
- if (sentGuardPms.value.has(userId)) {
- console.log(`用户 ${userName} (${userId}) 已发送过上舰私信,跳过。`);
- return;
- }
- }
-
- // 查找礼品码
- let giftCode = '';
- if (guardPmConfig.value.giftCodeMode) {
- const levelCodes = guardPmConfig.value.giftCodes.find(gc => gc.level === guardLevel)?.codes;
- if (levelCodes && levelCodes.length > 0) {
- giftCode = levelCodes.shift() || '';
- // 更新储存的礼品码
- saveGuardConfig();
- } else {
- // 尝试查找通用码 (level 0)
- const commonCodes = guardPmConfig.value.giftCodes.find(gc => gc.level === GuardLevel.None)?.codes;
- if (commonCodes && commonCodes.length > 0) {
- giftCode = commonCodes.shift() || '';
- saveGuardConfig();
- } else {
- console.warn(`等级 ${guardLevel} 或通用礼品码已用完,无法发送给 ${userName}`);
- }
- }
- }
-
- // 格式化私信内容
- const guardLevelName = { [GuardLevel.Zongdu]: '总督', [GuardLevel.Tidu]: '提督', [GuardLevel.Jianzhang]: '舰长' }[guardLevel] || '舰长';
- const pmParams = {
- user: { name: userName },
- guard: {
- levelName: guardLevelName,
- giftCode: giftCode
+ case ActionType.EXECUTE_COMMAND:
+ if (action.executeCommand) {
+ try {
+ const execFunc = new Function(
+ 'context',
+ 'event',
+ 'biliFunc',
+ 'roomId',
+ action.executeCommand
+ );
+ execFunc(context, context.event, biliFunc, roomId.value);
+ } catch (error) {
+ console.error('执行命令错误:', error);
+ }
+ }
+ break;
}
};
- const pmContent = formatMessage(guardPmConfig.value.template, pmParams);
- // 发送私信
- biliFunc.sendPrivateMessage(userId, pmContent).then(success => {
- if (success) {
- console.log(`成功发送上舰私信给 ${userName} (${userId})`);
- if (guardPmConfig.value.preventRepeat) {
- sentGuardPms.value.add(userId);
- }
- // 发送弹幕确认
- if (guardPmConfig.value.sendDanmakuConfirm && guardPmConfig.value.danmakuTemplate) {
- const confirmParams = { user: { name: userName } };
- const confirmMsg = formatMessage(guardPmConfig.value.danmakuTemplate, confirmParams);
- biliFunc.sendLiveDanmaku(roomId.value!, confirmMsg);
- }
- } else {
- console.error(`发送上舰私信给 ${userName} (${userId}) 失败`);
- // 失败时归还礼品码
- if (giftCode && guardPmConfig.value.giftCodeMode) {
- returnGiftCode(guardLevel, giftCode);
- }
+ // 延迟执行
+ if (delaySeconds > 0) {
+ setTimeout(executeActionFunc, delaySeconds * 1000);
+ } else {
+ executeActionFunc();
+ }
+ }
+
+ // 启动定时任务
+ function startScheduledActions() {
+ if (!roomId.value) return;
+
+ // 使用专用模块处理定时发送
+ scheduledDanmaku.startScheduledDanmaku();
+
+ // 同时处理自定义的定时任务
+ const scheduledActions = autoActions.value.filter(
+ action => action.triggerType === TriggerType.SCHEDULED && action.enabled
+ );
+
+ scheduledActions.forEach(action => {
+ // 清理可能存在的旧定时器
+ if (runtimeState.value.scheduledTimers[action.id]) {
+ clearTimeout(runtimeState.value.scheduledTimers[action.id]!);
}
+
+ const intervalSeconds = action.triggerConfig.intervalSeconds || 300; // 默认5分钟
+
+ const timerFunc = () => {
+ if (!isLive.value && action.triggerConfig.onlyDuringLive) {
+ // 如果设置了仅直播时发送,且当前未直播,则跳过
+ return;
+ }
+
+ if (action.triggerConfig.ignoreTianXuan && isTianXuanActive.value) {
+ // 如果设置了天选时刻不发送,且当前有天选,则跳过
+ return;
+ }
+
+ // 创建执行上下文
+ const context: ExecutionContext = {
+ roomId: roomId.value,
+ variables: {
+ date: {
+ formatted: new Date().toLocaleString(),
+ year: new Date().getFullYear(),
+ month: new Date().getMonth() + 1,
+ day: new Date().getDate(),
+ hour: new Date().getHours(),
+ minute: new Date().getMinutes(),
+ second: new Date().getSeconds(),
+ }
+ },
+ timestamp: Date.now()
+ };
+
+ // 执行定时操作
+ executeAction(action, context);
+
+ // 设置下一次执行
+ runtimeState.value.scheduledTimers[action.id] = setTimeout(timerFunc, intervalSeconds * 1000);
+ };
+
+ // 首次执行
+ runtimeState.value.scheduledTimers[action.id] = setTimeout(timerFunc, intervalSeconds * 1000);
});
}
- // 归还礼品码到列表
- function returnGiftCode(level: GuardLevel, code: string) {
- const levelCodes = guardPmConfig.value.giftCodes.find(gc => gc.level === level);
- if (levelCodes) {
- levelCodes.codes.push(code);
- } else {
- guardPmConfig.value.giftCodes.push({ level, codes: [code] });
- }
- saveGuardConfig();
- }
-
- // 保存舰长配置到本地
- function saveGuardConfig() {
- // useStorage会自动保存,无需额外操作
- }
-
- // 处理关注事件
- function onFollow(event: EventModel) {
- if (!shouldProcess(followThankConfig.value) || !roomId.value) return;
- if (followThankConfig.value.ignoreTianXuan && isTianXuanActive.value) return;
-
- aggregatedFollows.value.push({ uid: event.uid, name: event.uname, timestamp: Date.now() });
-
- if (followThankTimer.value) clearTimeout(followThankTimer.value);
- if (followThankConfig.value.delaySeconds > 0) {
- followThankTimer.value = setTimeout(sendFollowThankYou, followThankConfig.value.delaySeconds * 1000);
- } else {
- sendFollowThankYou();
- }
- }
-
- // 发送关注感谢
- function sendFollowThankYou() {
- if (!roomId.value || aggregatedFollows.value.length === 0) return;
- const usersToThank = aggregatedFollows.value.slice(0, followThankConfig.value.maxUsersPerMsg);
- aggregatedFollows.value = aggregatedFollows.value.slice(followThankConfig.value.maxUsersPerMsg);
-
- const template = getRandomTemplate(followThankConfig.value.templates);
- if (!template) return;
-
- const names = usersToThank.map(u => u.name).join('、');
- const params = { user: { name: names } };
- const message = formatMessage(template, params);
-
- if (message) biliFunc.sendLiveDanmaku(roomId.value!, message);
-
- if (aggregatedFollows.value.length > 0) {
- if (followThankTimer.value) clearTimeout(followThankTimer.value);
- followThankTimer.value = setTimeout(sendFollowThankYou, followThankConfig.value.delaySeconds * 1000);
- } else {
- followThankTimer.value = null;
- }
- }
-
- // 处理入场事件 (Enter)
- function onEnter(event: EventModel) {
- if (!shouldProcess(entryWelcomeConfig.value) || !roomId.value) return;
- if (entryWelcomeConfig.value.ignoreTianXuan && isTianXuanActive.value) return;
- if (!checkUserFilter(entryWelcomeConfig.value, event)) return;
-
- aggregatedEntries.value.push({ uid: event.uid, name: event.uname, timestamp: Date.now() });
-
- if (entryWelcomeTimer.value) clearTimeout(entryWelcomeTimer.value);
- if (entryWelcomeConfig.value.delaySeconds > 0) {
- entryWelcomeTimer.value = setTimeout(sendEntryWelcome, entryWelcomeConfig.value.delaySeconds * 1000);
- } else {
- sendEntryWelcome();
- }
- }
-
- // 发送入场欢迎
- function sendEntryWelcome() {
- if (!roomId.value || aggregatedEntries.value.length === 0) return;
- const usersToWelcome = aggregatedEntries.value.slice(0, entryWelcomeConfig.value.maxUsersPerMsg);
- aggregatedEntries.value = aggregatedEntries.value.slice(entryWelcomeConfig.value.maxUsersPerMsg);
-
- const template = getRandomTemplate(entryWelcomeConfig.value.templates);
- if (!template) return;
-
- const names = usersToWelcome.map(u => u.name).join('、');
- const params = { user: { name: names } };
- const message = formatMessage(template, params);
-
- if (message) biliFunc.sendLiveDanmaku(roomId.value!, message);
-
- if (aggregatedEntries.value.length > 0) {
- if (entryWelcomeTimer.value) clearTimeout(entryWelcomeTimer.value);
- entryWelcomeTimer.value = setTimeout(sendEntryWelcome, entryWelcomeConfig.value.delaySeconds * 1000);
- } else {
- entryWelcomeTimer.value = null;
- }
- }
-
- // 处理弹幕事件 (用于自动回复)
- function onDanmaku(event: EventModel) {
- if (!shouldProcess(autoReplyConfig.value) || !roomId.value) return;
- if (!checkUserFilter(autoReplyConfig.value, event)) return;
-
- const message = event.msg;
- const userId = event.uid;
- const now = Date.now();
-
- for (const rule of autoReplyConfig.value.rules) {
- const keywordMatch = rule.keywords.some(kw => message.includes(kw));
- if (!keywordMatch) continue;
-
- const blockwordMatch = rule.blockwords.some(bw => message.includes(bw));
- if (blockwordMatch) continue; // 包含屏蔽词,不回复
-
- // 检查冷却
- const ruleKey = rule.keywords.join('|');
- const lastReplyTime = lastReplyTimestamps.value[ruleKey] || 0;
- if (now - lastReplyTime < autoReplyConfig.value.cooldownSeconds * 1000) {
- continue; // 仍在冷却中
+ // 停止所有定时任务
+ function stopAllScheduledActions() {
+ // 清理所有定时任务
+ Object.entries(runtimeState.value.scheduledTimers).forEach(([id, timer]) => {
+ if (timer) {
+ clearTimeout(timer);
+ runtimeState.value.scheduledTimers[id] = null;
}
+ });
- // 选择回复并发送
- const reply = getRandomTemplate(rule.replies);
- if (reply) {
- const params = { user: { name: event.uname } };
- const formattedReply = formatMessage(reply, params);
- biliFunc.sendLiveDanmaku(roomId.value!, formattedReply);
- lastReplyTimestamps.value[ruleKey] = now; // 更新冷却时间
- break; // 匹配到一个规则就停止
+ // 清理模块定时任务
+ scheduledDanmaku.clearTimer();
+ }
+
+ // 添加新的自动操作
+ function addAutoAction(triggerType: TriggerType): AutoActionItem {
+ const newAction = createDefaultAutoAction(triggerType);
+ autoActions.value.push(newAction);
+ return newAction;
+ }
+
+ // 删除自动操作
+ function removeAutoAction(id: string) {
+ const index = autoActions.value.findIndex(action => action.id === id);
+ if (index !== -1) {
+ // 清理相关定时器
+ if (autoActions.value[index].triggerType === TriggerType.SCHEDULED &&
+ runtimeState.value.scheduledTimers[id]) {
+ clearTimeout(runtimeState.value.scheduledTimers[id]!);
+ runtimeState.value.scheduledTimers[id] = null;
+ }
+ autoActions.value.splice(index, 1);
+ }
+ }
+
+ // 切换自动操作启用状态
+ function toggleAutoAction(id: string, enabled: boolean) {
+ const action = autoActions.value.find(action => action.id === id);
+ if (action) {
+ action.enabled = enabled;
+
+ // 如果是定时操作,重新配置定时器
+ if (action.triggerType === TriggerType.SCHEDULED) {
+ if (enabled) {
+ // 启用时重新启动定时器
+ startScheduledActions();
+ } else if (runtimeState.value.scheduledTimers[id]) {
+ // 禁用时清理定时器
+ clearTimeout(runtimeState.value.scheduledTimers[id]!);
+ runtimeState.value.scheduledTimers[id] = null;
+ }
}
}
}
- // 发送定时弹幕
- function sendScheduledDanmaku() {
- if (!shouldProcess(scheduledDanmakuConfig.value) || !roomId.value || scheduledDanmakuConfig.value.messages.length === 0) {
- stopScheduledDanmaku(); // 停止计时器如果条件不满足
- return;
+ // 初始化
+ function init() {
+ // 初始检查天选状态
+ checkTianXuanStatus();
+
+ // 启动所有定时发送任务
+ startScheduledActions();
+
+ // 监听直播状态变化,自动启停定时任务
+ watch(isLive, (newIsLive) => {
+ if (newIsLive) {
+ startScheduledActions();
+ } else {
+ stopAllScheduledActions();
+ }
+ });
+
+ // 安全地订阅事件
+ try {
+ danmakuClient.onEvent('danmaku', (event) => processEvent(event, TriggerType.DANMAKU));
+ danmakuClient.onEvent('gift', (event) => processEvent(event, TriggerType.GIFT));
+ danmakuClient.onEvent('guard', (event) => processEvent(event, TriggerType.GUARD));
+ danmakuClient.onEvent('sc', (event) => processEvent(event, TriggerType.SUPER_CHAT));
+ danmakuClient.onEvent('enter', (event) => processEvent(event, TriggerType.ENTER));
+ } catch (err) {
+ console.error('注册事件监听器失败:', err);
}
- let message = '';
- if (scheduledDanmakuConfig.value.mode === 'random') {
- message = getRandomTemplate(scheduledDanmakuConfig.value.messages);
- } else {
- message = scheduledDanmakuConfig.value.messages[currentScheduledIndex.value];
- currentScheduledIndex.value = (currentScheduledIndex.value + 1) % scheduledDanmakuConfig.value.messages.length;
+ // 注册HMR清理
+ if (import.meta.hot) {
+ import.meta.hot.dispose(() => {
+ clearAllTimers();
+ });
}
- if (message) {
- biliFunc.sendLiveDanmaku(roomId.value!, message);
- }
-
- // 设置下一次定时
- if (scheduledDanmakuTimer.value) clearTimeout(scheduledDanmakuTimer.value);
- scheduledDanmakuTimer.value = setTimeout(sendScheduledDanmaku, scheduledDanmakuConfig.value.intervalSeconds * 1000);
+ // 迁移旧的配置
+ migrateAutoReplyConfig();
+ migrateGiftThankConfig();
}
- // 启动定时弹幕
- function startScheduledDanmaku() {
- if (scheduledDanmakuTimer.value) clearTimeout(scheduledDanmakuTimer.value); // 清除旧的
- if (shouldProcess(scheduledDanmakuConfig.value) && scheduledDanmakuConfig.value.intervalSeconds > 0) {
- scheduledDanmakuTimer.value = setTimeout(sendScheduledDanmaku, scheduledDanmakuConfig.value.intervalSeconds * 1000);
+ /**
+ * 迁移旧的自动回复配置到新的AutoActionItem格式
+ */
+ function migrateAutoReplyConfig() {
+ try {
+ // 尝试从localStorage获取旧配置
+ const oldConfigStr = localStorage.getItem('autoAction.autoReplyConfig');
+ if (!oldConfigStr) return;
+
+ const oldConfig = JSON.parse(oldConfigStr);
+ if (!oldConfig.enabled || !oldConfig.rules || !Array.isArray(oldConfig.rules)) return;
+
+ // 检查是否已经迁移过(防止重复迁移)
+ const migratedKey = 'autoAction.autoReplyMigrated';
+ if (localStorage.getItem(migratedKey) === 'true') return;
+
+ // 将旧规则转换为新的AutoActionItem
+ const newItems = oldConfig.rules.map((rule: any) => {
+ const item = createDefaultAutoAction(TriggerType.DANMAKU);
+ item.name = `弹幕回复: ${rule.keywords.join(',')}`;
+ item.enabled = oldConfig.enabled;
+ item.templates = rule.replies || ['感谢您的弹幕'];
+ item.triggerConfig = {
+ ...item.triggerConfig,
+ keywords: rule.keywords || [],
+ blockwords: rule.blockwords || [],
+ onlyDuringLive: oldConfig.onlyDuringLive,
+ userFilterEnabled: oldConfig.userFilterEnabled,
+ requireMedal: oldConfig.requireMedal,
+ requireCaptain: oldConfig.requireCaptain
+ };
+ item.actionConfig = {
+ ...item.actionConfig,
+ cooldownSeconds: oldConfig.cooldownSeconds || 5
+ };
+ return item;
+ });
+
+ // 添加到现有的autoActions中
+ autoActions.value = [...autoActions.value, ...newItems];
+
+ // 标记为已迁移
+ localStorage.setItem(migratedKey, 'true');
+ console.log(`成功迁移 ${newItems.length} 条自动回复规则`);
+ } catch (error) {
+ console.error('迁移自动回复配置失败:', error);
}
}
- // 停止定时弹幕
- function stopScheduledDanmaku() {
- if (scheduledDanmakuTimer.value) {
- clearTimeout(scheduledDanmakuTimer.value);
- scheduledDanmakuTimer.value = null;
+ /**
+ * 迁移旧的礼物感谢配置到新的AutoActionItem格式
+ */
+ function migrateGiftThankConfig() {
+ try {
+ // 尝试从localStorage获取旧配置
+ const oldConfigStr = localStorage.getItem('autoAction.giftThankConfig');
+ if (!oldConfigStr) return;
+
+ const oldConfig = JSON.parse(oldConfigStr);
+ if (!oldConfig.enabled || !oldConfig.templates || !Array.isArray(oldConfig.templates)) return;
+
+ // 检查是否已经迁移过(防止重复迁移)
+ const migratedKey = 'autoAction.giftThankMigrated';
+ if (localStorage.getItem(migratedKey) === 'true') return;
+
+ // 创建新的礼物感谢项
+ const item = createDefaultAutoAction(TriggerType.GIFT);
+ item.name = '礼物感谢';
+ item.enabled = oldConfig.enabled;
+ item.templates = oldConfig.templates;
+
+ // 设置触发配置
+ item.triggerConfig = {
+ ...item.triggerConfig,
+ onlyDuringLive: oldConfig.onlyDuringLive ?? true,
+ ignoreTianXuan: oldConfig.ignoreTianXuan ?? true,
+ userFilterEnabled: oldConfig.userFilterEnabled ?? false,
+ requireMedal: oldConfig.requireMedal ?? false,
+ requireCaptain: oldConfig.requireCaptain ?? false,
+ filterMode: oldConfig.filterModes?.useWhitelist ? 'whitelist' :
+ oldConfig.filterModes?.useBlacklist ? 'blacklist' : undefined,
+ filterGiftNames: oldConfig.filterGiftNames || [],
+ minValue: oldConfig.minValue || 0
+ };
+
+ // 设置操作配置
+ item.actionConfig = {
+ ...item.actionConfig,
+ delaySeconds: oldConfig.delaySeconds || 0,
+ cooldownSeconds: 5,
+ maxUsersPerMsg: oldConfig.maxUsersPerMsg || 3,
+ maxItemsPerUser: oldConfig.maxGiftsPerUser || 3
+ };
+
+ // 添加到现有的autoActions中
+ autoActions.value.push(item);
+
+ // 标记为已迁移
+ localStorage.setItem(migratedKey, 'true');
+ console.log('成功迁移礼物感谢配置');
+ } catch (error) {
+ console.error('迁移礼物感谢配置失败:', error);
}
}
- // 监听配置变化以启动/停止定时弹幕
- watch(() => [scheduledDanmakuConfig.value.enabled, scheduledDanmakuConfig.value.onlyDuringLive, isLive.value, scheduledDanmakuConfig.value.intervalSeconds], () => {
- if (scheduledDanmakuConfig.value.enabled && (!scheduledDanmakuConfig.value.onlyDuringLive || isLive.value)) {
- startScheduledDanmaku();
- } else {
- stopScheduledDanmaku();
- }
- }, { immediate: true }); // 立即执行一次检查
-
- // 当组件卸载时清理所有计时器
+ // 卸载时清理
onUnmounted(() => {
clearAllTimers();
});
- // 初始化,订阅事件
- function init() {
- danmakuClient.onEvent('danmaku', (data) => onDanmaku(data as EventModel));
- danmakuClient.onEvent('gift', (data) => onGift(data as EventModel));
- danmakuClient.onEvent('guard', (data) => onGuard(data as EventModel));
- danmakuClient.onEvent('follow', (data) => onFollow(data as EventModel));
- danmakuClient.onEvent('enter', (data) => onEnter(data as EventModel));
+ // 向外部导出所有配置和状态
+ const exportedConfigs = computed(() => ({
+ autoActions: autoActions.value,
+ isLive: isLive.value,
+ roomId: roomId.value
+ }));
- // 初始检查天选状态
- checkTianXuanStatus();
+ /**
+ * 获取定时任务的计时器信息
+ * @param actionId 定时任务ID
+ * @returns 计时器信息,包含剩余毫秒数
+ */
+ function getScheduledTimerInfo(actionId: string) {
+ const timer = runtimeState.value.scheduledTimers[actionId];
+ if (!timer) return null;
- // 启动定时弹幕(如果初始状态满足条件)
- startScheduledDanmaku();
+ // 找到对应的action
+ const action = autoActions.value.find(a => a.id === actionId);
+ if (!action) return null;
- console.log('自动操作模块已初始化');
+ const intervalSeconds = action.triggerConfig.intervalSeconds || 300;
+ const intervalMs = intervalSeconds * 1000;
+
+ // 计算下一次执行时间和剩余时间
+ // 由于JavaScript中没有直接的方式获取setTimeout的剩余时间
+ // 我们需要模拟一个剩余时间,在实际应用中可能需要更精确的方式
+ const now = Date.now();
+ const timerId = timer as unknown as number;
+ const remainingMs = Math.max(0, (intervalMs - (now % intervalMs)) % intervalMs);
+
+ return {
+ actionId,
+ intervalMs,
+ remainingMs
+ };
}
+ /**
+ * 更新所有定时任务计时器状态(用于触发UI更新)
+ */
+ function updateScheduledTimers() {
+ // 这个方法主要用于触发UI更新
+ // 实际上只需要修改一个响应式变量即可
+ const scheduledActions = autoActions.value.filter(
+ action => action.triggerType === TriggerType.SCHEDULED && action.enabled
+ );
+
+ // 触发响应式更新
+ scheduledActions.forEach(action => {
+ const timerInfo = getScheduledTimerInfo(action.id);
+ if (timerInfo) {
+ // 简单地触发更新,不需要实际改变值
+ const timerId = runtimeState.value.scheduledTimers[action.id];
+ if (timerId) {
+ // 重新分配相同的值会触发Vue的响应式更新
+ runtimeState.value.scheduledTimers[action.id] = timerId;
+ }
+ }
+ });
+ }
+
+ // 导出接口
return {
- init,
- // --- 配置 ---
- giftThankConfig,
- guardPmConfig,
- followThankConfig,
- entryWelcomeConfig,
- scheduledDanmakuConfig,
- autoReplyConfig,
+ autoActions,
+ runtimeState: runtimeState.value,
+ shouldProcessAction,
+ executeAction,
+ addAutoAction,
+ removeAutoAction,
+ toggleAutoAction,
+ processEvent,
+ startScheduledActions,
+ stopAllScheduledActions,
+ checkTianXuanStatus,
+ getScheduledTimerInfo,
+ updateScheduledTimers,
+ init
};
});
+// 支持热更新
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useAutoAction, import.meta.hot));
}
-export { GuardLevel };
+export {
+ AutoActionItem,
+ TriggerType,
+ ActionType,
+ Priority
+};
diff --git a/src/client/store/useBiliFunction.ts b/src/client/store/useBiliFunction.ts
index dafda76..b932231 100644
--- a/src/client/store/useBiliFunction.ts
+++ b/src/client/store/useBiliFunction.ts
@@ -2,13 +2,87 @@ import { useAccount } from "@/api/account";
import { useBiliCookie } from "./useBiliCookie";
import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; // 引入 Body
import { defineStore, acceptHMRUpdate } from 'pinia';
-import { computed } from 'vue';
+import { computed, ref } from 'vue';
+import md5 from 'md5';
+import { QueryBiliAPI } from "../data/utils";
+import { onSendPrivateMessageFailed } from "../data/notification";
+
+// WBI 混合密钥编码表
+const mixinKeyEncTab = [
+ 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
+ 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
+ 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
+ 36, 20, 34, 44, 52
+];
+
+// 对 imgKey 和 subKey 进行字符顺序打乱编码
+const getMixinKey = (orig: string): string =>
+ mixinKeyEncTab.map(n => orig[n]).join('').slice(0, 32);
+
+// 为请求参数进行 wbi 签名
+function encWbi(
+ params: { [key: string]: string | number },
+ img_key: string,
+ sub_key: string
+): string {
+ const mixin_key = getMixinKey(img_key + sub_key);
+ const curr_time = Math.round(Date.now() / 1000);
+ const chr_filter = /[!'()*]/g;
+
+ Object.assign(params, { wts: curr_time.toString() }); // 添加 wts 字段
+
+ // 按照 key 重排参数
+ const query = Object.keys(params)
+ .sort()
+ .map(key => {
+ // 过滤 value 中的 "!'()*" 字符
+ const value = params[key].toString().replace(chr_filter, '');
+ return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
+ })
+ .join('&');
+
+ const wbi_sign = md5(query + mixin_key); // 计算 w_rid
+ return query + '&w_rid=' + wbi_sign;
+}
+
+// 获取最新的 img_key 和 sub_key
+async function getWbiKeys(cookie: string): Promise<{ img_key: string, sub_key: string }> {
+ try {
+ const response = await QueryBiliAPI('https://api.bilibili.com/x/web-interface/nav');
+
+ if (!response.ok) {
+ console.error("获取WBI密钥失败:", response.status);
+ throw new Error("获取WBI密钥失败");
+ }
+
+ const result = await response.json();
+ const { wbi_img } = result.data;
+
+ console.log(`获取WBI秘钥: img_key: ${wbi_img.img_url}, sub_key: ${wbi_img.sub_url}`);
+
+ return {
+ img_key: wbi_img.img_url.slice(
+ wbi_img.img_url.lastIndexOf('/') + 1,
+ wbi_img.img_url.lastIndexOf('.')
+ ),
+ sub_key: wbi_img.sub_url.slice(
+ wbi_img.sub_url.lastIndexOf('/') + 1,
+ wbi_img.sub_url.lastIndexOf('.')
+ )
+ };
+ } catch (error) {
+ console.error("获取WBI密钥时发生错误:", error);
+ throw error;
+ }
+}
export const useBiliFunction = defineStore('biliFunction', () => {
const biliCookieStore = useBiliCookie();
const account = useAccount();
const cookie = computed(() => biliCookieStore.cookie);
const uid = computed(() => account.value.biliId);
+ // 存储WBI密钥
+ const wbiKeys = ref<{ img_key: string, sub_key: string } | null>(null);
const csrf = computed(() => {
if (!cookie.value) return null;
@@ -34,6 +108,7 @@ export const useBiliFunction = defineStore('biliFunction', () => {
console.warn("尝试发送空弹幕,已阻止。");
return false;
}
+ roomId = 1294406; // 测试用房间号
const url = "https://api.live.bilibili.com/msg/send";
const rnd = Math.floor(Date.now() / 1000);
const data = {
@@ -47,7 +122,7 @@ export const useBiliFunction = defineStore('biliFunction', () => {
csrf: csrf.value,
csrf_token: csrf.value,
};
-
+ const params = new URLSearchParams(data)
try {
// 注意: B站网页版发送弹幕是用 application/x-www-form-urlencoded
const response = await tauriFetch(url, {
@@ -58,7 +133,7 @@ export const useBiliFunction = defineStore('biliFunction', () => {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36",
"Referer": `https://live.bilibili.com/${roomId}`
},
- body: JSON.stringify(data), // 发送 JSON 数据
+ body: params, // 发送 JSON 数据
});
if (!response.ok) {
@@ -105,6 +180,7 @@ export const useBiliFunction = defineStore('biliFunction', () => {
};
try {
+ const params = new URLSearchParams(data)
const response = await tauriFetch(url, {
method: "POST",
headers: {
@@ -113,7 +189,7 @@ export const useBiliFunction = defineStore('biliFunction', () => {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36",
"Referer": `https://live.bilibili.com/p/html/live-room-setting/#/room-manager/black-list?room_id=${roomId}` // 模拟来源
},
- body: JSON.stringify(data), // 发送 JSON 数据
+ body: params, // 发送 URLSearchParams 数据
});
if (!response.ok) {
console.error("封禁用户失败:", response.status, await response.text());
@@ -139,61 +215,107 @@ export const useBiliFunction = defineStore('biliFunction', () => {
*/
async function sendPrivateMessage(receiverId: number, message: string): Promise {
if (!csrf.value || !cookie.value || !uid.value) {
- console.error("发送私信失败:缺少 cookie, csrf token 或 uid");
+ const error = "发送私信失败:缺少 cookie, csrf token 或 uid";
+ console.error(error);
+ onSendPrivateMessageFailed(receiverId, message, error);
return false;
}
if (!message || message.trim().length === 0) {
- console.warn("尝试发送空私信,已阻止。");
+ const error = "尝试发送空私信,已阻止。";
+ console.warn(error);
return false;
}
- const url = "https://api.vc.bilibili.com/web_im/v1/web_im/send_msg";
- const timestamp = Math.floor(Date.now() / 1000);
- const content = JSON.stringify({ content: message });
- const dev_id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
- var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
- return v.toString(16).toUpperCase();
- });
- const data = {
- 'msg[sender_uid]': uid.value.toString(),
- 'msg[receiver_id]': receiverId.toString(),
- 'msg[receiver_type]': '1',
- 'msg[msg_type]': '1',
- 'msg[msg_status]': '0',
- 'msg[content]': content,
- 'msg[timestamp]': timestamp.toString(),
- 'msg[new_face_version]': '0',
- 'msg[dev_id]': dev_id,
- 'build': '0',
- 'mobi_app': 'web',
- 'csrf': csrf.value,
- 'csrf_token': csrf.value,
- };
try {
+ // 获取WBI密钥(如果还没有)
+ if (!wbiKeys.value) {
+ wbiKeys.value = await getWbiKeys(cookie.value);
+ }
+ if (!wbiKeys.value) {
+ const error = "获取WBI密钥失败,无法发送私信";
+ console.error(error);
+ onSendPrivateMessageFailed(receiverId, message, error);
+ return false;
+ }
+
+ const dev_id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
+ return v.toString(16).toUpperCase();
+ });
+
+ const timestamp = Math.floor(Date.now() / 1000);
+ const content = JSON.stringify({ content: message });
+
+ // 准备URL参数(需要WBI签名的参数)
+ const urlParams = {
+ w_sender_uid: uid.value.toString(),
+ w_receiver_id: receiverId.toString(),
+ w_dev_id: dev_id,
+ };
+
+ // 生成带WBI签名的URL查询字符串
+ const signedQuery = encWbi(
+ urlParams,
+ wbiKeys.value.img_key,
+ wbiKeys.value.sub_key
+ );
+
+ // 构建最终URL
+ const url = `https://api.vc.bilibili.com/web_im/v1/web_im/send_msg?${signedQuery}`;
+
+ // 准备表单数据
+ const formData = {
+ 'msg[sender_uid]': uid.value.toString(),
+ 'msg[receiver_id]': receiverId.toString(),
+ 'msg[receiver_type]': '1',
+ 'msg[msg_type]': '1',
+ 'msg[msg_status]': '0',
+ 'msg[content]': content,
+ 'msg[timestamp]': timestamp.toString(),
+ 'msg[new_face_version]': '0',
+ 'msg[dev_id]': dev_id,
+ 'build': '0',
+ 'mobi_app': 'web',
+ 'csrf': csrf.value,
+ 'csrf_token': csrf.value,
+ };
+
+ const params = new URLSearchParams(formData);
const response = await tauriFetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Cookie": cookie.value,
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36",
- "Referer": `https://message.bilibili.com/`,
+ "Origin": '',
},
- body: JSON.stringify(data), // 发送 JSON 数据
+ body: params,
});
if (!response.ok) {
- console.error("发送私信网络失败:", response.status, await response.text());
+ const error = `发送私信网络失败: ${response.status}`;
+ console.error(error, await response.text());
+ onSendPrivateMessageFailed(receiverId, message, error);
return false;
}
- // 私信成功码也是 0
- if (response.data.code !== 0) {
- console.error("发送私信API失败:", response.data.code, response.data.message);
+
+ const json = await response.json();
+ if (json.code !== 0) {
+ const error = `发送私信API失败: ${json.code} - ${json.message}`;
+ console.error(error);
+ onSendPrivateMessageFailed(receiverId, message, error);
return false;
}
+
console.log(`发送私信给 ${receiverId} 成功`);
return true;
} catch (error) {
console.error("发送私信时发生错误:", error);
+ // 如果是WBI密钥问题,清空密钥以便下次重新获取
+ if (String(error).includes('WBI')) {
+ wbiKeys.value = null;
+ }
+ onSendPrivateMessageFailed(receiverId, message, error);
return false;
}
}
diff --git a/src/client/store/useDanmakuWindow.ts b/src/client/store/useDanmakuWindow.ts
index cbab707..1afaa26 100644
--- a/src/client/store/useDanmakuWindow.ts
+++ b/src/client/store/useDanmakuWindow.ts
@@ -20,6 +20,7 @@ export type DanmakuWindowSettings = {
reverseOrder: boolean; // 是否倒序显示(从下往上)
filterTypes: string[]; // 要显示的弹幕类型
animationDuration: number; // 动画持续时间
+ enableAnimation: boolean; // 是否启用动画效果
backgroundColor: string; // 背景色
textColor: string; // 文字颜色
alwaysOnTop: boolean; // 是否总在最前
@@ -182,6 +183,7 @@ export const useDanmakuWindow = defineStore('danmakuWindow', () => {
textStyleCompact: false, // 新增:默认不使用紧凑布局
textStyleShowType: true, // 新增:默认显示消息类型标签
textStyleNameSeparator: ': ', // 新增:默认用户名和消息之间的分隔符为冒号+空格
+ enableAnimation: true, // 新增:默认启用动画效果
});
const emojiData = useStorage<{
updateAt: number,
diff --git a/src/client/store/useSettings.ts b/src/client/store/useSettings.ts
index 26a5c7d..838e95f 100644
--- a/src/client/store/useSettings.ts
+++ b/src/client/store/useSettings.ts
@@ -1,6 +1,6 @@
import { useTauriStore } from './useTauriStore';
-export type NotificationType = 'question-box' | 'danmaku' | 'goods-buy';
+export type NotificationType = 'question-box' | 'danmaku' | 'goods-buy' | 'message-failed' | 'live-danmaku-failed';
export type NotificationSettings = {
enableTypes: NotificationType[];
};
@@ -29,7 +29,7 @@ export const useSettings = defineStore('settings', () => {
loginType: 'qrcode',
enableNotification: true,
notificationSettings: {
- enableTypes: ['question-box', 'danmaku'],
+ enableTypes: ['question-box', 'danmaku', 'message-failed'],
},
dev_disableDanmakuClient: false,
@@ -39,7 +39,7 @@ export const useSettings = defineStore('settings', () => {
async function init() {
settings.value = (await store.get()) || Object.assign({}, defaultSettings);
settings.value.notificationSettings ??= defaultSettings.notificationSettings;
- settings.value.notificationSettings.enableTypes ??= [ 'question-box', 'danmaku' ];
+ settings.value.notificationSettings.enableTypes ??= [ 'question-box', 'danmaku', 'message-failed' ];
}
async function save() {
await store.set(settings.value);
diff --git a/src/components.d.ts b/src/components.d.ts
index aaa7844..49c29bf 100644
--- a/src/components.d.ts
+++ b/src/components.d.ts
@@ -18,29 +18,22 @@ declare module 'vue' {
LabelItem: typeof import('./components/LabelItem.vue')['default']
LiveInfoContainer: typeof import('./components/LiveInfoContainer.vue')['default']
MonacoEditorComponent: typeof import('./components/MonacoEditorComponent.vue')['default']
- NAlert: typeof import('naive-ui')['NAlert']
NAvatar: typeof import('naive-ui')['NAvatar']
NBadge: typeof import('naive-ui')['NBadge']
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
- NCheckbox: typeof import('naive-ui')['NCheckbox']
+ NCollapse: typeof import('naive-ui')['NCollapse']
+ NDataTable: typeof import('naive-ui')['NDataTable']
+ NDivider: typeof import('naive-ui')['NDivider']
NEmpty: typeof import('naive-ui')['NEmpty']
NFlex: typeof import('naive-ui')['NFlex']
- NFormItemG: typeof import('naive-ui')['NFormItemG']
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
NGridItem: typeof import('naive-ui')['NGridItem']
- NH4: typeof import('naive-ui')['NH4']
NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']
- NInput: typeof import('naive-ui')['NInput']
- NLayoutContent: typeof import('naive-ui')['NLayoutContent']
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
- NRadioButton: typeof import('naive-ui')['NRadioButton']
- NRadioGroup: typeof import('naive-ui')['NRadioGroup']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSpace: typeof import('naive-ui')['NSpace']
- NSSwitch: typeof import('naive-ui')['NSSwitch']
- NTab: typeof import('naive-ui')['NTab']
NTag: typeof import('naive-ui')['NTag']
NText: typeof import('naive-ui')['NText']
NTooltip: typeof import('naive-ui')['NTooltip']
diff --git a/src/components/SongList.vue b/src/components/SongList.vue
index 310de5d..09fb952 100644
--- a/src/components/SongList.vue
+++ b/src/components/SongList.vue
@@ -399,7 +399,7 @@ function renderCell(value: string | number) {
}
async function updateSong() {
- if (props.songs.some((s) => s.name == updateSongModel.value.name)) {
+ if (props.songs.filter((s) => s.name == updateSongModel.value.name).length > 1) {
message.error('已存在相同名称的歌曲')
return
}
diff --git a/src/store/useDanmakuClient.ts b/src/store/useDanmakuClient.ts
index 99fe234..528da9c 100644
--- a/src/store/useDanmakuClient.ts
+++ b/src/store/useDanmakuClient.ts
@@ -47,11 +47,15 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => {
console.warn("[DanmakuClient] 尝试在客户端初始化之前调用 'onEvent'。");
return;
}
- if (eventName === 'all') {
- // 对于 'all' 事件, 直接使用 AllEventListener 类型
- danmakuClient.value.eventsAsModel[eventName].push(listener as AllEventListener);
- } else {
- danmakuClient.value.eventsAsModel[eventName].push(listener);
+ try {
+ if (eventName === 'all') {
+ // 对于 'all' 事件, 直接使用 AllEventListener 类型
+ danmakuClient.value.eventsAsModel[eventName].push(listener as AllEventListener);
+ } else {
+ danmakuClient.value.eventsAsModel[eventName].push(listener);
+ }
+ } catch (error) {
+ console.error(`[DanmakuClient] 注册事件监听器: ${eventName} 失败: ${error}`);
}
}
@@ -185,8 +189,14 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => {
console.log('[DanmakuClient] 开始初始化...');
- const oldEventsAsModel = danmakuClient.value?.eventsAsModel;
- const oldEventsRaw = danmakuClient.value?.eventsRaw;
+ let oldEventsAsModel = danmakuClient.value?.eventsAsModel;
+ let oldEventsRaw = danmakuClient.value?.eventsRaw;
+ if (!oldEventsAsModel || Object.keys(oldEventsAsModel).length === 0) {
+ oldEventsAsModel = client.createEmptyEventModelListeners();
+ }
+ if (!oldEventsRaw || Object.keys(oldEventsRaw).length === 0) {
+ oldEventsRaw = client.createEmptyRawEventlisteners();
+ }
// 先停止并清理旧客户端 (如果存在)
if (danmakuClient.value) {
@@ -194,14 +204,13 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => {
if (danmakuClient.value.state === 'connected') {
await disposeClientInstance(danmakuClient.value);
}
- danmakuClient.value = undefined; // 显式清除旧实例引用
}
// 设置新的客户端实例
danmakuClient.value = client;
// 确保新客户端有空的监听器容器 (BaseDanmakuClient 应负责初始化)
- danmakuClient.value.eventsAsModel = oldEventsAsModel || client.createEmptyEventModelListeners();
- danmakuClient.value.eventsRaw = oldEventsRaw || client.createEmptyRawEventlisteners();
+ danmakuClient.value.eventsAsModel = oldEventsAsModel;
+ danmakuClient.value.eventsRaw = oldEventsRaw;
// 通常在 client 实例化或 Start 时处理,或者在 attachListenersToClient 中确保存在
diff --git a/src/views/ManageLayout.vue b/src/views/ManageLayout.vue
index da60045..f01b28f 100644
--- a/src/views/ManageLayout.vue
+++ b/src/views/ManageLayout.vue
@@ -50,6 +50,7 @@ import {
NText,
NTooltip,
useMessage,
+ NCard,
} from 'naive-ui'
import { computed, h, onMounted, ref, watch } from 'vue'
import { RouterLink, useRoute } from 'vue-router'
@@ -644,37 +645,125 @@ onMounted(() => {
display: flex;
justify-content: center;
align-items: center;
- flex-direction: column;
- padding: 50px;
height: 100vh;
+ width: 100vw;
+ background: linear-gradient(135deg, rgba(250,250,250,0.8) 0%, rgba(240,240,245,0.9) 100%);
+ padding: 0;
+ margin: 0;
box-sizing: border-box;
+ position: fixed;
+ top: 0;
+ left: 0;
"
+ :class="isDarkMode ? 'login-dark-bg' : ''"
>
-
- 请登录或注册后使用
-
+
+
+ VTSURU CENTER
+
+
+
+
+
- 回到主页
-
-
-
-
+
+
+ 请登录或注册后使用
+
+
+
+
+
+
+ 如果你不是主播且不发送棉花糖(提问)的话则不需要注册登录
+
+
+
+
+
+
+ 前往 Bilibili 认证用户主页
+
+
+
+
+
+
+
+
+
+
+
+ 回到主页
+
+
+
+
-
- 正在请求账户数据...
-
+
+
+ 正在请求账户数据...
+
+
+
+
+
diff --git a/src/views/manage/point/PointUserManage.vue b/src/views/manage/point/PointUserManage.vue
index 6306291..5ea6719 100644
--- a/src/views/manage/point/PointUserManage.vue
+++ b/src/views/manage/point/PointUserManage.vue
@@ -5,6 +5,7 @@ import { QueryGetAPI } from '@/api/query'
import { POINT_API_URL } from '@/data/constants'
import { objectsToCSV } from '@/Utils'
import { Info24Filled } from '@vicons/fluent'
+import { Warning24Regular } from '@vicons/fluent'
import { useStorage } from '@vueuse/core'
import { format } from 'date-fns'
import { saveAs } from 'file-saver'
@@ -63,6 +64,7 @@ const ps = ref(25)
// 弹窗控制
const showModal = ref(false)
const showGivePointModal = ref(false)
+const showResetAllPointsModal = ref(false)
const isLoading = ref(true)
// 积分调整表单
@@ -70,6 +72,10 @@ const addPointCount = ref(0)
const addPointReason = ref('')
const addPointTarget = ref()
+// 重置所有积分确认
+const resetConfirmText = ref('')
+const RESET_CONFIRM_TEXT = '我确认删除'
+
// 用户数据
const users = ref([])
// 根据筛选条件过滤后的用户
@@ -277,6 +283,37 @@ async function deleteUser(user: ResponsePointUserModel) {
}
}
+// 重置所有用户积分
+async function resetAllPoints() {
+ // 验证确认文本
+ if (resetConfirmText.value !== RESET_CONFIRM_TEXT) {
+ message.error(`请输入"${RESET_CONFIRM_TEXT}"以确认操作`)
+ return
+ }
+
+ isLoading.value = true
+ try {
+ const data = await QueryGetAPI(POINT_API_URL + 'reset')
+
+ if (data.code == 200) {
+ message.success('已重置所有用户积分')
+ resetConfirmText.value = ''
+ showResetAllPointsModal.value = false
+
+ // 重新加载用户数据
+ setTimeout(() => {
+ refresh()
+ }, 1500)
+ } else {
+ message.error('重置失败: ' + data.message)
+ }
+ } catch (err) {
+ message.error('重置失败: ' + err)
+ } finally {
+ isLoading.value = false
+ }
+}
+
// 导出用户积分数据
function exportData() {
try {
@@ -360,6 +397,12 @@ onMounted(async () => {
>
导出积分数据
+
+ 重置所有积分
+
@@ -516,6 +559,46 @@ onMounted(async () => {
+
+
+
+
+
+
+
+ 警告:此操作将删除所有用户积分记录,不可恢复!
+
+
+ 请输入 "{{ RESET_CONFIRM_TEXT }}" 以确认操作
+
+
+
+ 确认重置所有用户积分
+
+
+