diff --git a/.cursorindexingignore b/.cursorindexingignore
new file mode 100644
index 0000000..68347b3
--- /dev/null
+++ b/.cursorindexingignore
@@ -0,0 +1,2 @@
+# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
+.specstory/**
diff --git a/.gitignore b/.gitignore
index 9805cf2..ac6d2a8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,4 @@ pnpm-debug.log*
*.sln
*.sw?
env.d.ts
+/.specstory
diff --git a/src/api/api-models.ts b/src/api/api-models.ts
index 21a0e4d..76b1557 100644
--- a/src/api/api-models.ts
+++ b/src/api/api-models.ts
@@ -558,7 +558,7 @@ export interface EventModel {
time: number
num: number
price: number
- guard_level: number
+ guard_level: GuardLevel
fans_medal_level: number
fans_medal_name: string
fans_medal_wearing_status: boolean
@@ -572,7 +572,8 @@ export enum EventDataTypes {
Message,
Like,
SCDel,
- Enter
+ Enter,
+ Follow
}
export interface ResponseQueueModel {
id: number
diff --git a/src/client/ClientAutoAction.vue b/src/client/ClientAutoAction.vue
index dc2b226..f197411 100644
--- a/src/client/ClientAutoAction.vue
+++ b/src/client/ClientAutoAction.vue
@@ -1,6 +1,8 @@
-
- 施工中
-
{
+
+
+ 部分需要发送弹幕或私信的自动操作(如自动回复、上舰感谢)将无法执行。请前往【设置】- 【账号设置】页面登录。
+
+
-
{
:tab="label"
>
-
- {{ typeEnabledStatus[type] ? '启用' : '禁用' }}所有{{ label }}
+ {{ enabledTriggerTypes[type] ? '启用' : '禁用' }}所有{{ label }}
-
+
+ 当前连接模式 (OpenLive) 无法获取用户UID,因此无法执行【发送私信】操作。如需使用私信功能,请考虑切换至直连模式。
+
+
+
+ handleTestClick(type as TriggerType)"
+ >
+
+
+ 测试 {{ label }} 类型
+
+
+ {{ `确认模拟一个 ${label} 事件来测试所有启用的 ${label} 操作吗?\n注意:这可能会发送真实的消息、执行操作,并可能触发B站风控限制。` }}
+
+
+
+
+
+
+
+
+
+
+ 下一个执行:
+
+ {{ autoActionStore.nextScheduledAction?.name || '未命名操作' }}
+
+
+
+
+
+
+
+
+ 手动指定
+
+
+ 手动设置下一个要执行的操作
+
+
+
+
+ 定时发送类型已被禁用,所有相关操作不会执行。
+
+
{
{ selectedTriggerType = type; showAddModal = true; }"
+ @click="() => { selectedTriggerType = type as TriggerType; showAddModal = true; }"
>
- 添加{{ typeMap[type] }}
+ 添加{{ typeMap[type as TriggerType] }}
-
{
-
{ selectedTriggerType = type; showAddModal = true; }"
+ @click="() => { selectedTriggerType = type as TriggerType; showAddModal = true; }"
>
- + 添加{{ typeMap[type] }}
+ + 添加{{ typeMap[type as TriggerType] }}
-
{
:key="action.id"
class="action-item"
>
-
@@ -521,11 +678,19 @@ onMounted(() => {
+
+
+
+
+
+
-
{
/>
+
+
+
+ 选择下一个要执行的定时操作:
+
+
+ 只会列出当前已启用、类型也已启用且使用全局定时器的操作。
+ 选择后,下一个全局定时周期将执行您指定的操作。
+
+
+
+
+
+
+ 请输入私信接收者的UID:
+
+
+ 这是接收私信消息的B站用户UID,测试将向此UID发送私信。请确保该UID有效且您有权限向其发送私信。
+
+
+
@@ -579,6 +798,7 @@ code {
.fade-leave-active {
transition: opacity 0.3s ease;
}
+
.fade-enter-from,
.fade-leave-to {
opacity: 0;
@@ -588,15 +808,18 @@ code {
.list-item {
transition: all 0.3s ease;
}
+
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
+
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(20px);
}
+
.list-move {
transition: transform 0.3s ease;
}
@@ -606,10 +829,12 @@ code {
.fade-slide-leave-active {
transition: all 0.3s ease;
}
+
.fade-slide-enter-from {
opacity: 0;
transform: translateX(20px);
}
+
.fade-slide-leave-to {
opacity: 0;
transform: translateX(-20px);
@@ -619,6 +844,7 @@ code {
.btn-with-transition {
transition: all 0.2s ease;
}
+
.btn-with-transition:hover {
transform: translateY(-2px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
@@ -629,6 +855,7 @@ code {
position: relative;
overflow: hidden;
}
+
.back-btn::after {
content: '';
position: absolute;
@@ -639,6 +866,7 @@ code {
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s ease;
}
+
.back-btn:hover::after {
left: 100%;
}
@@ -649,4 +877,12 @@ code {
transition: all 0.4s ease;
transform-origin: center top;
}
+
+.next-action-display {
+ margin-top: 12px;
+ padding: 8px 12px;
+ background-color: var(--n-color-embedded);
+ border-radius: var(--n-border-radius);
+ font-size: 13px;
+}
\ No newline at end of file
diff --git a/src/client/components/autoaction/AutoActionEditor.vue b/src/client/components/autoaction/AutoActionEditor.vue
index 92622ea..9f29444 100644
--- a/src/client/components/autoaction/AutoActionEditor.vue
+++ b/src/client/components/autoaction/AutoActionEditor.vue
@@ -48,48 +48,48 @@ const TriggerSettings = getTriggerSettings();
-
-
-
-
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
- 高级选项
-
-
-
-
-
+
高级选项
-
-
-
-
+
+
+
+
+
+ 高级选项
+
+
+
diff --git a/src/client/components/autoaction/DataManager.vue b/src/client/components/autoaction/DataManager.vue
new file mode 100644
index 0000000..73acb28
--- /dev/null
+++ b/src/client/components/autoaction/DataManager.vue
@@ -0,0 +1,352 @@
+
+
+
+
+
+
+
+ 这里显示的是脚本通过 getData, setData 管理的数据。
+ 这些数据仅在程序运行期间保留,程序关闭后将丢失。
+
+
+
+ 刷新
+
+
+
+
+ 清除所有运行时数据
+
+
+ 确定要清除所有当前会话的运行时数据吗?此操作不可逆!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 这是持久化数据,程序关闭后不会丢失。
+
+
+
+ 刷新
+
+
+
+
+ 清除所有用户数据
+
+
+ 确定要清除所有由自动操作脚本存储的用户数据吗?应用配置不会被清除。此操作不可逆!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/client/components/autoaction/SingleTemplateEditor.vue b/src/client/components/autoaction/SingleTemplateEditor.vue
new file mode 100644
index 0000000..906c136
--- /dev/null
+++ b/src/client/components/autoaction/SingleTemplateEditor.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/client/components/autoaction/TemplateEditor.vue b/src/client/components/autoaction/TemplateEditor.vue
index fa95ec5..580f4c0 100644
--- a/src/client/components/autoaction/TemplateEditor.vue
+++ b/src/client/components/autoaction/TemplateEditor.vue
@@ -1,14 +1,18 @@
@@ -151,55 +245,18 @@ const highlightPatterns = computed(() => {
v-if="mergedPlaceholders.length > 0"
#header-extra
>
-
-
-
- 变量说明
-
-
-
-
-
-
-
- 在模板中使用 {{ '\{\{js:\}\}' }} 语法可以执行简单的JavaScript表达式
-
-
-
- {{ '\{\{js: user.name.toUpperCase()\}\}' }} → 将用户名转为大写
-
-
- {{ '\{\{js: gift.count > 10 ? "大量" : "少量"\}\}' }} → 根据数量显示不同文本
-
-
-
-
-
- {{ ph.name }}: {{ ph.description }}
-
-
-
+
+ 变量与语法说明
+
{
{{ description }}
-
+
{
>
-
-
+
-
-
-
+
+
+
+
-
+
+
+
+
-
+
+
+ {{ showLivePreview ? '隐藏预览' : '显示预览' }}
+
+
+
+
+
+
-
-
- 测试
-
-
- 编辑
-
-
-
-
- 删除
-
-
- 确定要删除这个模板吗?
-
-
-
-
-
-
-
-
-
-
-
+
-
+
+ 占位符转表达式
+
-
-
-
- 转换为表达式
-
-
+
+ 测试模板
+
+
-
-
+
+
+
+
- 取消
-
-
- {{ isEditing ? '保存' : '添加' }}
-
-
-
-
-
+
{{ category.title }}
+
+
+ {{ example.label }}
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+ 模板支持插入变量和执行 JavaScript。
+
+ 1. 简单变量替换:
+ 直接使用 {{ '\{\{变量名.属性\}\}' }} 插入值。
+ 示例: {{ '\{\{user.name\}\}' }} → 显示用户名
+
+ 2. JS 表达式求值 (js:):
+ 使用 {{ '\{\{js: 表达式\}\}' }} 执行单个 JS 表达式并插入结果 (隐式返回)。
+ 适合简单计算、字符串操作、三元运算等。
+ 示例: {{ '\{\{js: user.guardLevel > 0 ? "舰长" : "非舰长\}\}' }}
+ 示例: {{ '\{\{js: gift.price * gift.count\}\}' }}
+
+ 3. JS 代码块执行 (js+: 或 js-run:):
+ 使用 {{ '\{\{js+: 代码...\}\}' }} 或 {{ '\{\{js-run: 代码...\}\}' }} 执行多行 JS 代码。
+ 需要显式使用 return 语句来指定输出到模板的值。
+ 适合需要临时变量、多步逻辑或调用 getData/setData 等函数的场景。
+ {{ '\{\{js+:\n const count = (getData(\'greetCount\') || 0) + 1;\n setData(\'greetCount\', count);\n return \`这是第 ${count} 次问候!\`;\n\}\}' }}
+
+
+
+
+
+ 运行时数据仅在本次运行有效, 重启后就没了,且操作是同步的。
+
+
+ getData(key, defaultValue?): 获取运行时数据。
+ setData(key, value): 设置运行时数据。
+ containsData(key): 检查运行时数据是否存在。
+ removeData(key): 移除运行时数据。
+
+
+
+
+
+ 持久化数据会长期保留,但操作是异步的 (返回 Promise)。
+ 在 js+ 或 js-run 中使用 await 处理或使用 .then()。
+
+
+ getStorageData(key, defaultValue?): 获取持久化数据 (异步)。
+ setStorageData(key, value): 设置持久化数据 (异步)。
+ hasStorageData(key): 检查持久化数据是否存在 (异步)。
+ removeStorageData(key): 移除持久化数据 (异步)。
+ clearStorageData(): 清除所有用户持久化数据 (异步)。
+
+ {{ '\{\{js+:\n // 异步获取并设置持久化数据\n const key = \`user:${user.uid}:visitCount\`;\n const count = (await getStorageData(key, 0)) + 1;\n await setStorageData(key, count);\n return \`你是第 ${count} 次访问!\`;\n\}\}' }}
+
+
+
+ 可用变量 (基础):
+
+
+
+ {{ ph.name }}
+ : {{ ph.description }}
+
+
+
+
diff --git a/src/client/components/autoaction/TemplateTester.vue b/src/client/components/autoaction/TemplateTester.vue
index 900fac9..bfc64eb 100644
--- a/src/client/components/autoaction/TemplateTester.vue
+++ b/src/client/components/autoaction/TemplateTester.vue
@@ -46,6 +46,9 @@
import { ref, computed } from 'vue';
import { NSpace, NInput, NInputGroup, NInputGroupLabel, NButton, useMessage, NDivider } from 'naive-ui';
import { evaluateTemplateExpressions } from '@/client/store/autoAction/expressionEvaluator';
+import { EventModel } from '@/api/api-models';
+import { TriggerType } from '@/client/store/autoAction/types';
+import { buildExecutionContext } from '@/client/store/autoAction/utils';
const props = defineProps({
defaultTemplate: {
@@ -63,9 +66,14 @@ const result = ref('');
const hasResult = computed(() => result.value !== '');
const message = useMessage();
+function evaluateTemplateForUI(template: string, contextObj: Record): string {
+ const tempContext = buildExecutionContext(contextObj, undefined, TriggerType.DANMAKU);
+ return evaluateTemplateExpressions(template, tempContext);
+}
+
function testTemplate() {
try {
- result.value = evaluateTemplateExpressions(template.value, props.context);
+ result.value = evaluateTemplateForUI(template.value, props.context);
} catch (error) {
message.error(`表达式求值错误: ${(error as Error).message}`);
result.value = `[错误] ${(error as Error).message}`;
diff --git a/src/client/components/autoaction/TimerCountdown.vue b/src/client/components/autoaction/TimerCountdown.vue
new file mode 100644
index 0000000..e71f229
--- /dev/null
+++ b/src/client/components/autoaction/TimerCountdown.vue
@@ -0,0 +1,151 @@
+
+
+
+ {{ remainingSecondsDisplay }}
+
\ No newline at end of file
diff --git a/src/client/components/autoaction/settings/AdvancedSettings.vue b/src/client/components/autoaction/settings/AdvancedSettings.vue
index 8a7448a..53f916e 100644
--- a/src/client/components/autoaction/settings/AdvancedSettings.vue
+++ b/src/client/components/autoaction/settings/AdvancedSettings.vue
@@ -1,7 +1,8 @@
@@ -21,8 +70,10 @@ const showUserFilter = computed(() => {
{
align="center"
justify="space-between"
style="width: 100%"
- class="setting-item"
+ :class="['setting-item', {'setting-modified': isModified('triggerConfig.userFilterEnabled', action.triggerConfig.userFilterEnabled)}]"
>
启用用户过滤:
@@ -46,7 +97,7 @@ const showUserFilter = computed(() => {
align="center"
justify="space-between"
style="width: 100%"
- class="setting-item"
+ :class="['setting-item', {'setting-modified': isModified('triggerConfig.requireMedal', action.triggerConfig.requireMedal)}]"
>
要求本房间勋章:
@@ -57,7 +108,7 @@ const showUserFilter = computed(() => {
align="center"
justify="space-between"
style="width: 100%"
- class="setting-item"
+ :class="['setting-item', {'setting-modified': isModified('triggerConfig.requireCaptain', action.triggerConfig.requireCaptain)}]"
>
要求任意舰长:
@@ -69,8 +120,10 @@ const showUserFilter = computed(() => {
{
align="center"
justify="space-between"
style="width: 100%"
- class="setting-item"
+ :class="['setting-item', {'setting-modified': isModified('ignoreCooldown', action.ignoreCooldown)}]"
>
忽略全局冷却:
@@ -89,7 +142,7 @@ const showUserFilter = computed(() => {
align="center"
justify="space-between"
style="width: 100%"
- class="setting-item"
+ :class="['setting-item', {'setting-modified': isModified('actionConfig.delaySeconds', action.actionConfig.delaySeconds)}]"
>
延迟执行(秒):
{
align="center"
justify="space-between"
style="width: 100%"
- class="setting-item"
+ :class="['setting-item', {'setting-modified': isModified('actionConfig.cooldownSeconds', action.actionConfig.cooldownSeconds)}]"
>
冷却时间(秒):
{
@@ -133,6 +188,7 @@ const showUserFilter = computed(() => {
type="textarea"
placeholder="输入表达式,留空则始终为真"
:autosize="{ minRows: 2, maxRows: 5 }"
+ :class="{'input-modified': isModified('logicalExpression', action.logicalExpression)}"
/>
@@ -140,8 +196,10 @@ const showUserFilter = computed(() => {
@@ -153,6 +211,7 @@ const showUserFilter = computed(() => {
type="textarea"
placeholder="输入要执行的JS代码"
:autosize="{ minRows: 3, maxRows: 8 }"
+ :class="{'input-modified': isModified('executeCommand', action.executeCommand)}"
/>
@@ -179,6 +238,31 @@ const showUserFilter = computed(() => {
background-color: rgba(0, 0, 0, 0.02);
}
+.setting-modified {
+ font-weight: bold;
+ background-color: rgba(255, 230, 186, 0.2);
+}
+
+.setting-modified:hover {
+ background-color: rgba(255, 230, 186, 0.3);
+}
+
+.input-modified {
+ border-color: #faad14;
+ background-color: rgba(255, 230, 186, 0.1);
+}
+
+.section-modified :deep(.n-collapse-item__header-main) {
+ font-weight: bold;
+ color: #fa8c16;
+}
+
+.section-modified :deep(.n-collapse-item__header-extra) {
+ font-size: 12px;
+ color: #fa8c16;
+ font-weight: normal;
+}
+
.description {
margin-top: 8px;
font-size: 13px;
diff --git a/src/client/components/autoaction/settings/DanmakuSettings.vue b/src/client/components/autoaction/settings/DanmakuSettings.vue
index 2ddfbd2..40ea432 100644
--- a/src/client/components/autoaction/settings/DanmakuSettings.vue
+++ b/src/client/components/autoaction/settings/DanmakuSettings.vue
@@ -1,7 +1,9 @@
@@ -83,6 +105,24 @@ function removeBlockword(index: number) {
+
+ 匹配方式:
+
+
+ 完全
+
+
+ 包含
+
+
+ 正则
+
+
+
+
+
+ 匹配方式:
+
+
+ 完全
+
+
+ 包含
+
+
+ 正则
+
+
+
+
入场过滤模式:
diff --git a/src/client/components/autoaction/settings/GiftSettings.vue b/src/client/components/autoaction/settings/GiftSettings.vue
index 5448026..6acea68 100644
--- a/src/client/components/autoaction/settings/GiftSettings.vue
+++ b/src/client/components/autoaction/settings/GiftSettings.vue
@@ -61,13 +61,13 @@ function removeGiftName(index: number) {
>
礼物过滤模式:
-
+
-
+
+import {
+ NCard,
+ NSpace,
+ NInputNumber,
+ NRadioGroup,
+ NRadio,
+ NText,
+ NDivider
+} from 'naive-ui';
+import { useAutoAction } from '@/client/store/useAutoAction';
+import { watch } from 'vue';
+
+const autoActionStore = useAutoAction();
+
+// 定时模式选项
+const schedulingModeOptions = [
+ { label: '随机模式', value: 'random' },
+ { label: '顺序模式', value: 'sequential' }
+];
+
+// 监听变化,触发定时器重启(如果间隔改变)
+watch(() => autoActionStore.globalIntervalSeconds, () => {
+ autoActionStore.restartGlobalTimer(); // 确保间隔改变时定时器更新
+});
+
+
+
+
+
+
+
+ 这里的设置将应用于所有启用了 "使用全局定时器" 选项的定时触发操作。
+
+
+
+ 全局发送间隔 (秒):
+
+
+
+
+ 全局发送模式:
+
+
+
+ {{ option.label }}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/client/components/autoaction/settings/ScheduledSettings.vue b/src/client/components/autoaction/settings/ScheduledSettings.vue
index efb9e57..3d2152e 100644
--- a/src/client/components/autoaction/settings/ScheduledSettings.vue
+++ b/src/client/components/autoaction/settings/ScheduledSettings.vue
@@ -1,7 +1,16 @@
@@ -22,11 +50,47 @@ const schedulingModeOptions = [
v-if="action.triggerType === TriggerType.SCHEDULED"
title="定时触发设置"
>
-
+
+ 使用全局定时器设置:
+
+
+ 是
+
+
+ 否
+
+
+
+
+
+ 启用后,此操作将遵循全局定时设置(间隔、模式),忽略下方独立设置。
+ 全局设置需在【自动化 -> 全局配置】中修改。
+
+
+
+ 独立设置 (仅在不使用全局定时器时生效)
+
+
+
发送间隔 (秒):
@@ -41,9 +106,13 @@ const schedulingModeOptions = [
align="center"
justify="space-between"
style="width: 100%"
+ :class="{ 'disabled-setting': useGlobalTimer }"
>
发送模式:
-
+
+
+
diff --git a/src/client/components/autoaction/settings/SuperChatSettings.vue b/src/client/components/autoaction/settings/SuperChatSettings.vue
index 797d308..706cbbf 100644
--- a/src/client/components/autoaction/settings/SuperChatSettings.vue
+++ b/src/client/components/autoaction/settings/SuperChatSettings.vue
@@ -43,7 +43,7 @@ const scFilterModeOptions = [
>
最低价格 (元):
diff --git a/src/client/components/autoaction/settings/TemplateSettings.vue b/src/client/components/autoaction/settings/TemplateSettings.vue
index 5bfff6a..21cf9bd 100644
--- a/src/client/components/autoaction/settings/TemplateSettings.vue
+++ b/src/client/components/autoaction/settings/TemplateSettings.vue
@@ -11,59 +11,6 @@ const props = defineProps({
}
});
-// 模板变量占位符选项,根据触发类型动态生成
-const placeholders = computed(() => {
- const commonPlaceholders = [
- { name: '{{user.name}}', description: '用户名称' },
- { name: '{{user.uid}}', description: '用户ID' },
- { name: '{{date.formatted}}', description: '当前日期时间' },
- { name: '{{timeOfDay()}}', description: '当前时段(早上/下午/晚上)' },
- ];
-
- let specificPlaceholders: { name: string, description: string }[] = [];
-
- switch (props.action.triggerType) {
- case TriggerType.GIFT:
- specificPlaceholders = [
- { name: '{{gift.name}}', description: '礼物名称' },
- { name: '{{gift.count}}', description: '礼物数量' },
- { name: '{{gift.price}}', description: '礼物单价' },
- { name: '{{gift.totalPrice}}', description: '礼物总价值' },
- { name: '{{gift.summary}}', description: '礼物摘要(如:5个辣条)' },
- ];
- break;
- case TriggerType.GUARD:
- specificPlaceholders = [
- { name: '{{guard.level}}', description: '舰长等级' },
- { name: '{{guard.levelName}}', description: '舰长等级名称' },
- { name: '{{guard.giftCode}}', description: '礼品码(如已配置)' },
- ];
- break;
- case TriggerType.SUPER_CHAT:
- specificPlaceholders = [
- { name: '{{sc.message}}', description: 'SC消息内容' },
- { name: '{{sc.price}}', description: 'SC价格' },
- ];
- break;
- case TriggerType.FOLLOW:
- specificPlaceholders = [
- { name: '{{follow.time}}', description: '关注时间' },
- { name: '{{follow.isNew}}', description: '是否新关注' },
- ];
- break;
- case TriggerType.ENTER:
- specificPlaceholders = [
- { name: '{{enter.time}}', description: '入场时间' },
- { name: '{{enter.guardLevel}}', description: '舰长等级' },
- { name: '{{enter.medalName}}', description: '勋章名称' },
- { name: '{{enter.medalLevel}}', description: '勋章等级' },
- ];
- break;
- }
-
- return [...commonPlaceholders, ...specificPlaceholders];
-});
-
// 根据操作类型获取模板标题
const templateTitle = computed(() => {
switch (props.action.actionType) {
@@ -91,6 +38,15 @@ const templateDescription = computed(() => {
return '消息内容模板';
}
});
+
+// Handle template updates from TemplateEditor
+function handleTemplateUpdate(payload: { index: number, value: string }) {
+ // Assuming index will always be 0 here as we only render one editor
+ // And assuming action.templates is a string based on previous findings
+ if (payload.index === 0) {
+ props.action.template = payload.value;
+ }
+}
@@ -100,11 +56,13 @@ const templateDescription = computed(() => {
appear
>
diff --git a/src/client/store/autoAction/actionUtils.ts b/src/client/store/autoAction/actionUtils.ts
new file mode 100644
index 0000000..5330d3b
--- /dev/null
+++ b/src/client/store/autoAction/actionUtils.ts
@@ -0,0 +1,287 @@
+import { Ref } from 'vue';
+import { EventModel } from '@/api/api-models';
+import {
+ AutoActionItem,
+ TriggerType,
+ RuntimeState,
+ ActionType,
+ ExecutionContext
+} from './types';
+import { buildExecutionContext, getRandomTemplate } from './utils';
+import { evaluateTemplateExpressions } from './expressionEvaluator';
+import { evaluateExpression } from './utils';
+import { useBiliCookie } from '../useBiliCookie';
+
+/**
+ * 过滤有效的自动操作项
+ * @param actions 所有操作项列表
+ * @param triggerType 触发类型
+ * @param isLive 是否直播中
+ * @param isTianXuanActive 是否天选时刻激活
+ * @param options 额外过滤选项
+ * @returns 过滤后的操作项
+ */
+export function filterValidActions(
+ actions: AutoActionItem[],
+ triggerType: TriggerType,
+ isLive: Ref,
+ isTianXuanActive?: Ref,
+ options?: {
+ actionType?: ActionType; // 特定操作类型
+ customFilter?: (action: AutoActionItem) => boolean; // 自定义过滤器
+ }
+): AutoActionItem[] {
+ return actions.filter(action => {
+ // 基本过滤条件
+ if (action.triggerType !== triggerType || !action.enabled) {
+ return false;
+ }
+
+ // 直播状态过滤
+ if (action.triggerConfig.onlyDuringLive && !isLive.value) {
+ return false;
+ }
+
+ // 天选时刻过滤
+ if (isTianXuanActive && action.triggerConfig.ignoreTianXuan && isTianXuanActive.value) {
+ return false;
+ }
+
+ // 操作类型过滤
+ if (options?.actionType && action.actionType !== options.actionType) {
+ return false;
+ }
+
+ // 自定义过滤器
+ if (options?.customFilter && !options.customFilter(action)) {
+ return false;
+ }
+
+ return true;
+ });
+}
+
+/**
+ * 检查用户是否满足过滤条件
+ * @param action 操作项
+ * @param event 事件数据
+ * @returns 是否满足条件
+ */
+export function checkUserFilters(action: AutoActionItem, event: EventModel): boolean {
+ if (!action.triggerConfig.userFilterEnabled) {
+ return true;
+ }
+
+ if (action.triggerConfig.requireMedal && !event.fans_medal_wearing_status) {
+ return false;
+ }
+
+ if (action.triggerConfig.requireCaptain && !event.guard_level) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * 检查冷却时间
+ * @param action 操作项
+ * @param runtimeState 运行时状态
+ * @returns 是否可以执行(已过冷却期)
+ */
+export function checkCooldown(action: AutoActionItem, runtimeState: RuntimeState): boolean {
+ if (action.ignoreCooldown) {
+ return true;
+ }
+
+ const now = Date.now();
+ const lastExecTime = runtimeState.lastExecutionTime[action.id] || 0;
+ const cooldownMs = (action.actionConfig.cooldownSeconds || 0) * 1000;
+
+ return now - lastExecTime >= cooldownMs;
+}
+
+/**
+ * 处理模板并返回格式化后的内容
+ * @param action 操作项
+ * @param context 执行上下文
+ * @param options 可选配置
+ * @returns 格式化后的内容,如果没有有效模板则返回null
+ */
+export function processTemplate(
+ action: AutoActionItem,
+ context: any,
+ options?: {
+ useRandomTemplate?: boolean; // 是否随机选择模板,默认true
+ defaultValue?: string; // 如果模板为空或格式化失败时的默认值
+ }
+): string | null {
+ if (!action.template || action.template.trim() === '') {
+ console.warn(`跳过操作 "${action.name || '未命名'}":未设置有效模板`);
+ return options?.defaultValue || null;
+ }
+
+ try {
+ // 获取模板内容
+ let template: string;
+ if (options?.useRandomTemplate !== false) {
+ // 使用随机模板 (默认行为)
+ const randomTemplate = getRandomTemplate(action.template);
+ if (!randomTemplate) {
+ return options?.defaultValue || null;
+ }
+ template = randomTemplate;
+ } else {
+ // 使用整个模板字符串
+ template = action.template;
+ }
+
+ // 格式化模板
+ const formattedContent = evaluateTemplateExpressions(template, context);
+ return formattedContent;
+ } catch (error) {
+ console.error(`模板处理错误 (${action.name || action.id}):`, error);
+ return options?.defaultValue || null;
+ }
+}
+
+/**
+ * 执行操作的通用函数
+ * @param actions 过滤后的操作列表
+ * @param event 触发事件
+ * @param triggerType 触发类型
+ * @param roomId 房间ID
+ * @param runtimeState 运行时状态
+ * @param handlers 操作处理器
+ * @param options 额外选项
+ */
+export function executeActions(
+ actions: AutoActionItem[],
+ event: EventModel | null,
+ triggerType: TriggerType,
+ roomId: number,
+ runtimeState: RuntimeState,
+ handlers: {
+ sendLiveDanmaku?: (roomId: number, message: string) => Promise;
+ sendPrivateMessage?: (userId: number, message: string) => Promise;
+ // 可以扩展其他类型的发送处理器
+ },
+ options?: {
+ customContextBuilder?: (event: EventModel | null, roomId: number, triggerType: TriggerType) => ExecutionContext;
+ customFilters?: Array<(action: AutoActionItem, context: ExecutionContext) => boolean>;
+ skipUserFilters?: boolean;
+ skipCooldownCheck?: boolean;
+ onSuccess?: (action: AutoActionItem, context: ExecutionContext) => void;
+ }
+) {
+ if (!roomId || actions.length === 0) return;
+ const biliCookie = useBiliCookie()
+ // 对每个操作进行处理
+ for (const action of actions) {
+ // 构建执行上下文
+ const context = options?.customContextBuilder
+ ? options.customContextBuilder(event, roomId, triggerType)
+ : buildExecutionContext(event, roomId, triggerType);
+
+ // 应用自定义过滤器
+ if (options?.customFilters) {
+ const passesAllFilters = options.customFilters.every(filter => filter(action, context));
+ if (!passesAllFilters) continue;
+ }
+
+ // 检查用户过滤条件
+ if (!options?.skipUserFilters && event && !checkUserFilters(action, event)) {
+ continue;
+ }
+
+ // 检查逻辑表达式
+ if (action.logicalExpression && event) {
+ if (!evaluateExpression(action.logicalExpression, context)) {
+ continue;
+ }
+ }
+
+ // 检查冷却时间
+ if (!options?.skipCooldownCheck && !checkCooldown(action, runtimeState)) {
+ continue;
+ }
+
+ // 根据操作类型执行不同的处理逻辑
+ switch (action.actionType) {
+ case ActionType.SEND_DANMAKU:
+ if (!biliCookie.isCookieValid) {
+ continue; // 如果未登录,则跳过
+ }
+ if (handlers.sendLiveDanmaku) {
+ // 处理弹幕发送
+ const message = processTemplate(action, context);
+ if (message) {
+ // 更新冷却时间
+ runtimeState.lastExecutionTime[action.id] = Date.now();
+
+ // 延迟发送
+ if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
+ setTimeout(() => {
+ handlers.sendLiveDanmaku!(roomId, message)
+ .catch(err => console.error(`[AutoAction] 发送弹幕失败 (${action.name || action.id}):`, err));
+ }, action.actionConfig.delaySeconds * 1000);
+ } else {
+ handlers.sendLiveDanmaku(roomId, message)
+ .catch(err => console.error(`[AutoAction] 发送弹幕失败 (${action.name || action.id}):`, err));
+ }
+ }
+ } else {
+ console.warn(`[AutoAction] 未提供弹幕发送处理器,无法执行操作: ${action.name || action.id}`);
+ }
+ break;
+
+ case ActionType.SEND_PRIVATE_MSG:
+ if (!biliCookie.isCookieValid) {
+ continue; // 如果未登录,则跳过
+ }
+ if (handlers.sendPrivateMessage && event && event.uid) {
+ // 处理私信发送
+ const message = processTemplate(action, context);
+ if (message) {
+ // 更新冷却时间(私信也可以有冷却时间)
+ runtimeState.lastExecutionTime[action.id] = Date.now();
+
+ const sendPmPromise = (uid: number, msg: string) => {
+ return handlers.sendPrivateMessage!(uid, msg)
+ .then(success => {
+ if (success && options?.onSuccess) {
+ // 发送成功后调用 onSuccess 回调
+ options.onSuccess(action, context);
+ }
+ return success;
+ })
+ .catch(err => {
+ console.error(`[AutoAction] 发送私信失败 (${action.name || action.id}):`, err);
+ return false; // 明确返回 false 表示失败
+ });
+ };
+
+ // 私信通常不需要延迟,但我们也可以支持
+ if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
+ setTimeout(() => {
+ sendPmPromise(event.uid, message);
+ }, action.actionConfig.delaySeconds * 1000);
+ } else {
+ sendPmPromise(event.uid, message);
+ }
+ }
+ } else {
+ console.warn(`[AutoAction] 未提供私信发送处理器或事件缺少UID,无法执行操作: ${action.name || action.id}`);
+ }
+ break;
+
+ case ActionType.EXECUTE_COMMAND:
+ // 执行自定义命令(未实现)
+ console.warn(`[AutoAction] 暂不支持执行自定义命令: ${action.name || action.id}`);
+ break;
+
+ default:
+ console.warn(`[AutoAction] 未知的操作类型: ${action.actionType}`);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/client/store/autoAction/expressionEvaluator.ts b/src/client/store/autoAction/expressionEvaluator.ts
index a5109d1..288dd45 100644
--- a/src/client/store/autoAction/expressionEvaluator.ts
+++ b/src/client/store/autoAction/expressionEvaluator.ts
@@ -2,36 +2,106 @@
* 表达式求值工具 - 用于在自动操作模板中支持简单的JavaScript表达式
*/
+// 导入ExecutionContext类型
+import { ExecutionContext } from './types';
+
// 表达式模式匹配
-// {{js: expression}} - 完整的JavaScript表达式
-const JS_EXPRESSION_REGEX = /\{\{\s*js:\s*(.*?)\s*\}\}/g;
+// {{js: expression}} - 简单的JavaScript表达式 (隐式return)
+// {{js+: code block}} - JavaScript代码块 (需要显式return)
+// {{js-run: code block}} - JavaScript代码块 (需要显式return)
+export const JS_EXPRESSION_REGEX = /\{\{\s*(js(?:\+|\-run)?):\s*(.*?)\s*\}\}/gs; // 使用 s 标志允许多行匹配
/**
* 处理模板中的表达式
* @param template 包含表达式的模板字符串
- * @param context 上下文对象,包含可在表达式中访问的变量
+ * @param context 执行上下文对象
* @returns 处理后的字符串
*/
-export function evaluateTemplateExpressions(template: string, context: Record): string {
+export function evaluateTemplateExpressions(template: string, context: ExecutionContext): string {
+ // 增加严格的类型检查
+ if (typeof template !== 'string') {
+ console.error('[evaluateTemplateExpressions] Error: Expected template to be a string, but received:', typeof template, template);
+ return ""; // 或者抛出错误,或者返回一个默认值
+ }
+
if (!template) return "";
- return template.replace(JS_EXPRESSION_REGEX, (match, expression) => {
- try {
- // 创建一个安全的求值函数
- const evalInContext = new Function(...Object.keys(context), `
- try {
- return ${expression};
- } catch (e) {
- return "[表达式错误: " + e.message + "]";
- }
- `);
+ // 获取基础变量和数据管理函数
+ const variables = context.variables;
+ const dataFunctions = {
+ getData: context.getData,
+ setData: context.setData,
+ containsData: context.containsData,
+ removeData: context.removeData,
+ getStorageData: context.getStorageData,
+ setStorageData: context.setStorageData,
+ hasStorageData: context.hasStorageData,
+ removeStorageData: context.removeStorageData,
+ clearStorageData: context.clearStorageData,
+ };
- // 执行表达式并返回结果
- const result = evalInContext(...Object.values(context));
- return result !== undefined ? String(result) : "";
+ // 合并基础变量和数据管理函数的作用域
+ const scopeVariables = { ...variables, ...dataFunctions };
+ const scopeKeys = Object.keys(scopeVariables);
+ const scopeValues = Object.values(scopeVariables);
+
+ // 第一步:处理简单的文本替换 {{variable.path}}
+ let result = template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
+ if (path.trim().startsWith('js:') || path.trim().startsWith('js+:') || path.trim().startsWith('js-run:')) {
+ return match; // 跳过所有JS变体,留给下一步
+ }
+
+ try {
+ // 解析路径
+ const parts = path.trim().split('.');
+ let value: any = scopeVariables;
+
+ // 递归获取嵌套属性
+ for (const part of parts) {
+ if (value === undefined || value === null) return match;
+ if (dataFunctions.hasOwnProperty(part) && parts.length === 1) {
+ value = value[part]; // 不要调用顶层函数
+ } else if (typeof value[part] === 'function') {
+ value = value[part]();
+ } else {
+ value = value[part];
+ }
+ if (typeof value === 'function' && !dataFunctions.hasOwnProperty(part)) value = value();
+ }
+
+ return value !== undefined && value !== null ? String(value) : match;
} catch (error) {
- console.error("表达式求值错误:", error);
- return `[表达式错误: ${(error as Error).message}]`;
+ console.error('模板格式化错误:', error);
+ return match; // 出错时返回原始匹配项
+ }
+ });
+
+ // 第二步:处理 JS 表达式和代码块 {{js: ...}}, {{js+: ...}}, {{js-run: ...}}
+ return result.replace(JS_EXPRESSION_REGEX, (match, type, code) => {
+ try {
+ let functionBody: string;
+
+ if (type === 'js') {
+ // 简单表达式: 隐式 return
+ functionBody = `try { return (${code}); } catch (e) { console.error("表达式[js:]执行错误:", e, "代码:", ${JSON.stringify(code)}); return \"[表达式错误: \" + e.message + \"]\"; }`;
+ } else { // js+ 或 js-run
+ // 代码块: 需要显式 return
+ functionBody = `try { ${code} } catch (e) { console.error("代码块[js+/js-run:]执行错误:", e, "代码:", ${JSON.stringify(code)}); return \"[代码块错误: \" + e.message + \"]\"; }`;
+ }
+
+ const evalInContext = new Function(...scopeKeys, functionBody);
+
+ const evalResult = evalInContext(...scopeValues);
+
+ // 对结果进行处理,将 undefined/null 转换为空字符串,除非是错误消息
+ return typeof evalResult === 'string' && (evalResult.startsWith('[表达式错误:') || evalResult.startsWith('[代码块错误:'))
+ ? evalResult
+ : String(evalResult ?? '');
+
+ } catch (error) {
+ // 捕获 Function 构造或顶层执行错误
+ console.error("JS占位符处理错误:", error, "类型:", type, "代码:", code);
+ return `[处理错误: ${(error as Error).message}]`;
}
});
}
@@ -61,7 +131,7 @@ export function escapeRegExp(string: string): string {
* @param placeholders 占位符列表
* @returns 转换后的模板
*/
-export function convertToJsExpressions(template: string, placeholders: {name: string, description: string}[]): string {
+export function convertToJsExpressions(template: string, placeholders: { name: string, description: string }[]): string {
let result = template;
placeholders.forEach(p => {
@@ -74,6 +144,22 @@ export function convertToJsExpressions(template: string, placeholders: {name: st
return result;
}
+/**
+ * 从模板字符串中提取所有 JS 表达式占位符。
+ * 例如,从 'Hello {{js: user.name}}, time: {{js: Date.now()}}' 提取出
+ * ['{{js: user.name}}', '{{js: Date.now()}}']
+ * @param template 模板字符串
+ * @returns 包含所有匹配的 JS 表达式字符串的数组
+ */
+export function extractJsExpressions(template: string): string[] {
+ if (!template) {
+ return [];
+ }
+ // 使用全局匹配来查找所有出现
+ const matches = template.match(JS_EXPRESSION_REGEX);
+ return matches || []; // match 返回 null 或字符串数组
+}
+
/**
* 为礼物感谢模块创建上下文对象
* @param user 用户信息
@@ -81,7 +167,7 @@ export function convertToJsExpressions(template: string, placeholders: {name: st
* @returns 上下文对象
*/
export function createGiftThankContext(user: { uid: number; name: string },
- gift: { name: string; count: number; price: number }): Record {
+ gift: { name: string; count: number; price: number }): Record {
return {
user: {
uid: user.uid,
@@ -110,66 +196,4 @@ export function createGiftThankContext(user: { uid: number; name: string },
formatted: new Intl.DateTimeFormat('zh-CN').format(new Date())
}
};
-}
-
-/**
- * 为入场欢迎模块创建上下文对象
- * @param user 用户信息
- * @returns 上下文对象
- */
-export function createEntryWelcomeContext(user: { uid: number; name: string; medal?: { level: number; name: string } }): Record {
- 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
index 046c8fd..d14aabe 100644
--- a/src/client/store/autoAction/modules/autoReply.ts
+++ b/src/client/store/autoAction/modules/autoReply.ts
@@ -3,15 +3,16 @@ import { EventModel } from '@/api/api-models';
import {
AutoActionItem,
TriggerType,
- ExecutionContext,
- RuntimeState
+ RuntimeState,
+ KeywordMatchType
} from '../types';
import {
- formatTemplate,
- getRandomTemplate,
- shouldProcess,
- evaluateExpression
+ buildExecutionContext
} from '../utils';
+import {
+ filterValidActions,
+ executeActions
+} from '../actionUtils';
/**
* 自动回复模块
@@ -27,6 +28,32 @@ export function useAutoReply(
// 运行时数据 - 记录特定关键词的最后回复时间
const lastReplyTimestamps = ref<{ [keyword: string]: number; }>({});
+ /**
+ * 检查关键词匹配
+ * @param text 要检查的文本
+ * @param keyword 关键词
+ * @param matchType 匹配类型
+ * @returns 是否匹配
+ */
+ function isKeywordMatch(text: string, keyword: string, matchType: KeywordMatchType = KeywordMatchType.Contains): boolean {
+ switch (matchType) {
+ case KeywordMatchType.Full:
+ return text === keyword;
+ case KeywordMatchType.Contains:
+ return text.includes(keyword);
+ case KeywordMatchType.Regex:
+ try {
+ const regex = new RegExp(keyword);
+ return regex.test(text);
+ } catch (e) {
+ console.warn('无效的正则表达式:', keyword, e);
+ return false;
+ }
+ default:
+ return text.includes(keyword); // 默认使用包含匹配
+ }
+ }
+
/**
* 处理弹幕事件
* @param event 弹幕事件
@@ -40,95 +67,57 @@ export function useAutoReply(
) {
if (!roomId.value) return;
- // 过滤出有效的自动回复操作
- const replyActions = actions.filter(action =>
- action.triggerType === TriggerType.DANMAKU &&
- action.enabled &&
- (!action.triggerConfig.onlyDuringLive || isLive.value)
- );
+ // 使用通用函数过滤有效的自动回复操作
+ const replyActions = filterValidActions(actions, TriggerType.DANMAKU, isLive);
- if (replyActions.length === 0) return;
+ if (replyActions.length > 0 && roomId.value) {
+ const message = event.msg;
- const message = event.msg;
- const now = Date.now();
+ executeActions(
+ replyActions,
+ event,
+ TriggerType.DANMAKU,
+ roomId.value,
+ runtimeState,
+ { sendLiveDanmaku },
+ {
+ customFilters: [
+ // 关键词和屏蔽词检查
+ (action, context) => {
+ const keywordMatchType = action.triggerConfig.keywordMatchType || KeywordMatchType.Contains;
+ const keywordMatch = action.triggerConfig.keywords?.some(kw =>
+ isKeywordMatch(message, kw, keywordMatchType)
+ );
+ if (!keywordMatch) return false;
- // 准备执行上下文
- const context: ExecutionContext = {
- event,
- roomId: roomId.value,
- variables: {
- user: {
- name: event.uname,
- uid: event.uid,
- guardLevel: event.guard_level,
- hasMedal: event.fans_medal_wearing_status,
- medalLevel: event.fans_medal_level,
- medalName: event.fans_medal_name
- },
- message: event.msg,
- timeOfDay: () => {
- const hour = new Date().getHours();
- if (hour < 6) return '凌晨';
- if (hour < 9) return '早上';
- if (hour < 12) return '上午';
- if (hour < 14) return '中午';
- if (hour < 18) return '下午';
- if (hour < 22) return '晚上';
- return '深夜';
- },
- date: {
- formatted: new Date().toLocaleString('zh-CN')
+ const blockwordMatchType = action.triggerConfig.blockwordMatchType || KeywordMatchType.Contains;
+ const blockwordMatch = action.triggerConfig.blockwords?.some(bw =>
+ isKeywordMatch(message, bw, blockwordMatchType)
+ );
+ return !blockwordMatch; // 如果匹配屏蔽词返回false,否则返回true
+ }
+ ],
+ // 附加选项:只处理第一个匹配的自动回复
+ customContextBuilder: (event, roomId, triggerType) => {
+ const now = Date.now();
+ const context = buildExecutionContext(event, roomId, triggerType);
+
+ // 添加时间段判断变量
+ context.variables.timeOfDay = () => {
+ const hour = new Date().getHours();
+ if (hour < 6) return '凌晨';
+ if (hour < 9) return '早上';
+ if (hour < 12) return '上午';
+ if (hour < 14) return '中午';
+ if (hour < 18) return '下午';
+ if (hour < 22) return '晚上';
+ return '深夜';
+ };
+
+ return context;
+ }
}
- },
- timestamp: now
- };
-
- // 检查每个操作
- for (const action of replyActions) {
- // 检查用户过滤条件
- if (action.triggerConfig.userFilterEnabled) {
- if (action.triggerConfig.requireMedal && !event.fans_medal_wearing_status) continue;
- if (action.triggerConfig.requireCaptain && !event.guard_level) continue;
- }
-
- // 关键词和屏蔽词检查
- const keywordMatch = action.triggerConfig.keywords?.some(kw => message.includes(kw));
- if (!keywordMatch) continue;
-
- const blockwordMatch = action.triggerConfig.blockwords?.some(bw => message.includes(bw));
- if (blockwordMatch) continue; // 包含屏蔽词,不回复
-
- // 评估逻辑表达式
- if (action.logicalExpression && !evaluateExpression(action.logicalExpression, context)) {
- continue;
- }
-
- // 检查冷却
- const lastExecTime = runtimeState.lastExecutionTime[action.id] || 0;
- if (!action.ignoreCooldown && now - lastExecTime < (action.actionConfig.cooldownSeconds || 0) * 1000) {
- continue; // 仍在冷却中
- }
-
- // 选择回复并发送
- const template = getRandomTemplate(action.templates);
- if (template) {
- // 格式化并发送
- const formattedReply = formatTemplate(template, context);
-
- // 更新冷却时间
- runtimeState.lastExecutionTime[action.id] = now;
-
- // 执行延迟处理
- if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
- setTimeout(() => {
- sendLiveDanmaku(roomId.value!, formattedReply);
- }, action.actionConfig.delaySeconds * 1000);
- } else {
- sendLiveDanmaku(roomId.value!, formattedReply);
- }
-
- break; // 匹配到一个规则就停止
- }
+ );
}
}
diff --git a/src/client/store/autoAction/modules/entryWelcome.ts b/src/client/store/autoAction/modules/entryWelcome.ts
index 086113e..01b64a8 100644
--- a/src/client/store/autoAction/modules/entryWelcome.ts
+++ b/src/client/store/autoAction/modules/entryWelcome.ts
@@ -1,14 +1,13 @@
+import { EventModel } from '@/api/api-models';
import { ref, Ref } from 'vue';
-import { EventModel, EventDataTypes } from '@/api/api-models';
import {
- formatTemplate,
- getRandomTemplate,
- buildExecutionContext
-} from '../utils';
+ executeActions,
+ filterValidActions
+} from '../actionUtils';
import {
AutoActionItem,
- TriggerType,
- RuntimeState
+ RuntimeState,
+ TriggerType
} from '../types';
/**
@@ -25,7 +24,7 @@ export function useEntryWelcome(
sendLiveDanmaku: (roomId: number, message: string) => Promise
) {
// 运行时数据
- const timer = ref(null);
+ const timer = ref(null);
/**
* 处理入场事件 - 支持新的AutoActionItem结构
@@ -40,56 +39,31 @@ export function useEntryWelcome(
) {
if (!roomId.value) return;
- // 过滤出有效的入场欢迎操作
- const enterActions = actions.filter(action =>
- action.triggerType === TriggerType.ENTER &&
- action.enabled &&
- (!action.triggerConfig.onlyDuringLive || isLive.value) &&
- (!action.triggerConfig.ignoreTianXuan || !isTianXuanActive.value)
- );
+ // 使用通用函数过滤有效的入场欢迎操作
+ const enterActions = filterValidActions(actions, TriggerType.ENTER, isLive, isTianXuanActive);
- if (enterActions.length === 0) return;
-
- // 创建执行上下文
- const context = buildExecutionContext(event, roomId.value, TriggerType.ENTER);
-
- // 处理每个符合条件的操作
- for (const action of enterActions) {
- // 跳过不符合用户过滤条件的
- if (action.triggerConfig.userFilterEnabled) {
- if (action.triggerConfig.requireMedal && !event.fans_medal_wearing_status) continue;
- if (action.triggerConfig.requireCaptain && !event.guard_level) continue;
- }
-
- // 检查入场过滤条件 (可以在未来扩展更多条件)
- if (action.triggerConfig.filterMode === 'blacklist' &&
- action.triggerConfig.filterGiftNames?.includes(event.uname)) continue;
-
- // 检查冷却时间
- const lastExecTime = runtimeState.lastExecutionTime[action.id] || 0;
- if (!action.ignoreCooldown &&
- Date.now() - lastExecTime < (action.actionConfig.cooldownSeconds || 0) * 1000) {
- continue; // 仍在冷却中
- }
-
- // 选择并发送回复
- const template = getRandomTemplate(action.templates);
- if (template) {
- // 更新冷却时间
- runtimeState.lastExecutionTime[action.id] = Date.now();
-
- // 格式化并发送
- const formattedReply = formatTemplate(template, context);
-
- // 延迟发送
- if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
- setTimeout(() => {
- sendLiveDanmaku(roomId.value!, formattedReply);
- }, action.actionConfig.delaySeconds * 1000);
- } else {
- sendLiveDanmaku(roomId.value!, formattedReply);
+ // 使用通用执行函数处理入场事件
+ if (enterActions.length > 0 && roomId.value) {
+ executeActions(
+ enterActions,
+ event,
+ TriggerType.ENTER,
+ roomId.value,
+ runtimeState,
+ { sendLiveDanmaku },
+ {
+ customFilters: [
+ // 检查入场过滤条件
+ (action, context) => {
+ if (action.triggerConfig.filterMode === 'blacklist' &&
+ action.triggerConfig.filterGiftNames?.includes(event.uname)) {
+ return false;
+ }
+ return true;
+ }
+ ]
}
- }
+ );
}
}
diff --git a/src/client/store/autoAction/modules/followThank.ts b/src/client/store/autoAction/modules/followThank.ts
index 8707d1d..7bbdce0 100644
--- a/src/client/store/autoAction/modules/followThank.ts
+++ b/src/client/store/autoAction/modules/followThank.ts
@@ -1,8 +1,6 @@
import { ref, Ref } from 'vue';
-import { EventModel, EventDataTypes } from '@/api/api-models';
+import { EventModel } from '@/api/api-models';
import {
- formatTemplate,
- getRandomTemplate,
buildExecutionContext
} from '../utils';
import {
@@ -10,6 +8,10 @@ import {
TriggerType,
RuntimeState
} from '../types';
+import {
+ filterValidActions,
+ executeActions
+} from '../actionUtils';
/**
* 关注感谢模块
@@ -26,7 +28,7 @@ export function useFollowThank(
) {
// 运行时数据
const aggregatedFollows = ref<{uid: number, name: string, timestamp: number}[]>([]);
- const timer = ref(null);
+ const timer = ref(null);
/**
* 处理关注事件 - 支持新的AutoActionItem结构
@@ -41,52 +43,19 @@ export function useFollowThank(
) {
if (!roomId.value) return;
- // 过滤出有效的关注感谢操作
- const followActions = actions.filter(action =>
- action.triggerType === TriggerType.FOLLOW &&
- action.enabled &&
- (!action.triggerConfig.onlyDuringLive || isLive.value) &&
- (!action.triggerConfig.ignoreTianXuan || !isTianXuanActive.value)
- );
+ // 使用通用函数过滤有效的关注感谢操作
+ const followActions = filterValidActions(actions, TriggerType.FOLLOW, isLive, isTianXuanActive);
- if (followActions.length === 0) return;
-
- // 创建执行上下文
- const context = buildExecutionContext(event, roomId.value, TriggerType.FOLLOW);
-
- // 处理每个符合条件的操作
- for (const action of followActions) {
- // 跳过不符合用户过滤条件的
- if (action.triggerConfig.userFilterEnabled) {
- if (action.triggerConfig.requireMedal && !event.fans_medal_wearing_status) continue;
- if (action.triggerConfig.requireCaptain && !event.guard_level) continue;
- }
-
- // 检查冷却时间
- const lastExecTime = runtimeState.lastExecutionTime[action.id] || 0;
- if (!action.ignoreCooldown &&
- Date.now() - lastExecTime < (action.actionConfig.cooldownSeconds || 0) * 1000) {
- continue; // 仍在冷却中
- }
-
- // 选择并发送回复
- const template = getRandomTemplate(action.templates);
- if (template) {
- // 更新冷却时间
- runtimeState.lastExecutionTime[action.id] = Date.now();
-
- // 格式化并发送
- const formattedReply = formatTemplate(template, context);
-
- // 延迟发送
- if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
- setTimeout(() => {
- sendLiveDanmaku(roomId.value!, formattedReply);
- }, action.actionConfig.delaySeconds * 1000);
- } else {
- sendLiveDanmaku(roomId.value!, formattedReply);
- }
- }
+ // 使用通用执行函数处理关注事件
+ if (followActions.length > 0 && roomId.value) {
+ executeActions(
+ followActions,
+ event,
+ TriggerType.FOLLOW,
+ roomId.value,
+ runtimeState,
+ { sendLiveDanmaku }
+ );
}
}
diff --git a/src/client/store/autoAction/modules/giftThank.ts b/src/client/store/autoAction/modules/giftThank.ts
index 2d09d9f..b2e8507 100644
--- a/src/client/store/autoAction/modules/giftThank.ts
+++ b/src/client/store/autoAction/modules/giftThank.ts
@@ -1,16 +1,19 @@
import { ref, Ref } from 'vue';
import { EventModel, EventDataTypes } from '@/api/api-models';
import {
- formatTemplate,
getRandomTemplate,
buildExecutionContext
} from '../utils';
+import { evaluateTemplateExpressions } from '../expressionEvaluator';
import {
AutoActionItem,
TriggerType,
- ExecutionContext,
RuntimeState
} from '../types';
+import {
+ filterValidActions,
+ executeActions
+} from '../actionUtils';
/**
* 礼物感谢模块
@@ -25,10 +28,6 @@ export function useGiftThank(
isTianXuanActive: Ref,
sendLiveDanmaku: (roomId: number, message: string) => Promise
) {
- // 测试发送功能状态
- const lastTestTime = ref(0);
- const testCooldown = 5000; // 5秒冷却时间
- const testLoading = ref(false);
/**
* 处理礼物事件
@@ -43,171 +42,52 @@ export function useGiftThank(
) {
if (!roomId.value) return;
- // 过滤出有效的礼物感谢操作
- const giftActions = actions.filter(action =>
- action.triggerType === TriggerType.GIFT &&
- action.enabled &&
- (!action.triggerConfig.onlyDuringLive || isLive.value) &&
- (!action.triggerConfig.ignoreTianXuan || !isTianXuanActive.value)
- );
+ // 使用通用函数过滤有效的礼物感谢操作
+ const giftActions = filterValidActions(actions, TriggerType.GIFT, isLive, isTianXuanActive);
- if (giftActions.length === 0) return;
+ // 使用通用执行函数处理礼物事件
+ if (giftActions.length > 0 && roomId.value) {
+ // 礼物基本信息
+ const giftName = event.msg;
+ const giftPrice = event.price / 1000;
- // 礼物基本信息
- const giftName = event.msg;
- const giftPrice = event.price / 1000;
- const giftCount = event.num;
+ executeActions(
+ giftActions,
+ event,
+ TriggerType.GIFT,
+ roomId.value,
+ runtimeState,
+ { sendLiveDanmaku },
+ {
+ customFilters: [
+ // 礼物过滤逻辑
+ (action, context) => {
+ // 黑名单模式
+ if (action.triggerConfig.filterMode === 'blacklist' &&
+ action.triggerConfig.filterGiftNames?.includes(giftName)) {
+ return false;
+ }
- // 创建执行上下文
- const context = buildExecutionContext(event, roomId.value, TriggerType.GIFT);
+ // 白名单模式
+ if (action.triggerConfig.filterMode === 'whitelist' &&
+ !action.triggerConfig.filterGiftNames?.includes(giftName)) {
+ return false;
+ }
- // 处理每个符合条件的操作
- for (const action of giftActions) {
- // 跳过不符合用户过滤条件的
- if (action.triggerConfig.userFilterEnabled) {
- if (action.triggerConfig.requireMedal && !event.fans_medal_wearing_status) continue;
- if (action.triggerConfig.requireCaptain && !event.guard_level) continue;
- }
+ // 礼物价值过滤
+ if (action.triggerConfig.minValue && giftPrice < action.triggerConfig.minValue) {
+ return false;
+ }
- // 礼物过滤逻辑
- if (action.triggerConfig.filterMode === 'blacklist' &&
- action.triggerConfig.filterGiftNames?.includes(giftName)) continue;
-
- if (action.triggerConfig.filterMode === 'whitelist' &&
- !action.triggerConfig.filterGiftNames?.includes(giftName)) continue;
-
- if (action.triggerConfig.minValue && giftPrice < action.triggerConfig.minValue) continue;
-
- // 检查冷却时间
- const lastExecTime = runtimeState.lastExecutionTime[action.id] || 0;
- if (!action.ignoreCooldown &&
- Date.now() - lastExecTime < (action.actionConfig.cooldownSeconds || 0) * 1000) {
- continue; // 仍在冷却中
- }
-
- // 选择并发送回复
- const template = getRandomTemplate(action.templates);
- if (template) {
- // 更新冷却时间
- runtimeState.lastExecutionTime[action.id] = Date.now();
-
- // 格式化并发送
- const formattedReply = formatTemplate(template, context);
-
- // 延迟发送
- if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
- setTimeout(() => {
- sendLiveDanmaku(roomId.value!, formattedReply);
- }, (action.actionConfig.delaySeconds || 0) * 1000);
- } else {
- sendLiveDanmaku(roomId.value!, formattedReply);
+ return true;
+ }
+ ]
}
- }
- }
- }
-
- /**
- * 测试发送礼物感谢弹幕
- */
- async function testSendThankMessage(
- action?: AutoActionItem
- ): Promise<{ success: boolean; message: string }> {
- // 检查是否在冷却期
- const now = Date.now();
- if (now - lastTestTime.value < testCooldown) {
- return {
- success: false,
- message: `请等待${Math.ceil((testCooldown - (now - lastTestTime.value)) / 1000)}秒后再次测试发送`
- };
- }
-
- if (!roomId.value) {
- return {
- success: false,
- message: '未设置房间号'
- };
- }
-
- if (!action) {
- return {
- success: false,
- message: '未指定要测试的操作'
- };
- }
-
- if (!action.templates || action.templates.length === 0) {
- return {
- success: false,
- message: '请至少添加一条模板'
- };
- }
-
- testLoading.value = true;
- lastTestTime.value = now;
-
- try {
- // 构建测试事件对象
- const testEvent: EventModel = {
- type: EventDataTypes.Gift,
- uname: '测试用户',
- uface: 'https://i0.hdslb.com/bfs/face/member/noface.jpg',
- uid: 123456,
- open_id: '123456',
- msg: '测试礼物',
- time: Date.now(),
- num: 1,
- price: 100000, // 100元
- guard_level: 0,
- fans_medal_level: 0,
- fans_medal_name: '',
- fans_medal_wearing_status: false,
- ouid: '123456'
- };
-
- // 创建测试上下文
- const context = buildExecutionContext(testEvent, roomId.value, TriggerType.GIFT);
-
- // 获取模板并格式化
- const template = getRandomTemplate(action.templates);
- if (!template) {
- return {
- success: false,
- message: '无法获取模板'
- };
- }
-
- const testMessage = formatTemplate(template, context);
-
- // 发送测试弹幕
- const success = await sendLiveDanmaku(roomId.value, testMessage);
-
- if (success) {
- return {
- success: true,
- message: '测试弹幕发送成功!'
- };
- } else {
- return {
- success: false,
- message: '测试弹幕发送失败,请检查B站登录状态和网络连接'
- };
- }
- } catch (error) {
- console.error('测试发送出错:', error);
- return {
- success: false,
- message: '发送过程出错'
- };
- } finally {
- testLoading.value = false;
+ );
}
}
return {
processGift,
- testSendThankMessage,
- testLoading,
- lastTestTime,
- testCooldown
};
}
\ No newline at end of file
diff --git a/src/client/store/autoAction/modules/guardPm.ts b/src/client/store/autoAction/modules/guardPm.ts
index ef64499..956ec06 100644
--- a/src/client/store/autoAction/modules/guardPm.ts
+++ b/src/client/store/autoAction/modules/guardPm.ts
@@ -1,162 +1,172 @@
-import { Ref } from 'vue';
-import { useStorage } from '@vueuse/core';
+import { computed, Ref } from 'vue';
import { GuardLevel, EventModel } from '@/api/api-models';
import {
AutoActionItem,
TriggerType,
- ActionType,
- RuntimeState
+ RuntimeState,
+ ExecutionContext,
+ ActionType
} from '../types';
-import { formatTemplate, buildExecutionContext } from '../utils';
+import {
+ filterValidActions,
+ executeActions
+} from '../actionUtils';
+import { buildExecutionContext } from '../utils';
/**
* 舰长私信模块
- * @param isLive 是否处于直播状态
* @param roomId 房间ID
* @param sendPrivateMessage 发送私信函数
* @param sendLiveDanmaku 发送弹幕函数
*/
export function useGuardPm(
- isLive: Ref,
roomId: Ref,
- sendPrivateMessage: (userId: number, message: string) => Promise,
- sendLiveDanmaku?: (roomId: number, message: string) => Promise
+ sendPrivateMessage: (uid: 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 event 舰长购买事件
* @param runtimeState 运行时状态
*/
- function processGuard(
- event: EventModel,
+ function handleGuardBuy(
actions: AutoActionItem[],
+ event: any,
runtimeState: RuntimeState
) {
if (!roomId.value) return;
- const guardLevel = event.guard_level;
- if (guardLevel === GuardLevel.None) return; // 不是上舰事件
+ // 使用通用函数过滤舰长事件的操作
+ const isLiveRef = computed(() => true);
+ const guardActions = filterValidActions(actions, TriggerType.GUARD, isLiveRef);
- // 过滤出有效的舰长私信操作
- const guardActions = actions.filter(action =>
- action.triggerType === TriggerType.GUARD &&
- action.enabled &&
- action.actionType === ActionType.SEND_PRIVATE_MSG &&
- (!action.triggerConfig.onlyDuringLive || isLive.value)
- );
+ // 使用通用执行函数处理舰长事件
+ if (guardActions.length > 0 && roomId.value) {
+ executeActions(
+ guardActions,
+ event,
+ TriggerType.GUARD,
+ roomId.value,
+ runtimeState,
+ { sendPrivateMessage, sendLiveDanmaku },
+ {
+ customFilters: [
+ // 防止重复发送检查
+ (action, context) => {
+ if (action.triggerConfig.preventRepeat && event && event.uid) {
+ // 确保 uid 是数字类型
+ const uid = typeof event.uid === 'number' ? event.uid : parseInt(event.uid, 10);
- if (guardActions.length === 0) return;
+ // 检查是否已经发送过
+ if (runtimeState.sentGuardPms.has(uid)) {
+ return false;
+ }
- // 创建执行上下文
- const context = buildExecutionContext(event, roomId.value, TriggerType.GUARD);
-
- // 处理礼品码
- for (const action of guardActions) {
- // 防止重复发送
- if (action.triggerConfig.preventRepeat) {
- if (runtimeState.sentGuardPms.has(event.uid)) {
- console.log(`用户 ${event.uname} (${event.uid}) 已发送过上舰私信,跳过。`);
- continue;
- }
- }
-
- // 特定舰长等级过滤
- if (action.triggerConfig.guardLevels && !action.triggerConfig.guardLevels.includes(guardLevel)) {
- continue;
- }
-
- // 获取礼品码
- let giftCode = '';
- if (action.triggerConfig.giftCodes && action.triggerConfig.giftCodes.length > 0) {
- // 查找匹配等级的礼品码
- const levelCodes = action.triggerConfig.giftCodes.find(gc => gc.level === guardLevel);
- if (levelCodes && levelCodes.codes.length > 0) {
- giftCode = levelCodes.codes.shift() || '';
- } else {
- // 查找通用码 (level 0)
- const commonCodes = action.triggerConfig.giftCodes.find(gc => gc.level === GuardLevel.None);
- if (commonCodes && commonCodes.codes.length > 0) {
- giftCode = commonCodes.codes.shift() || '';
- } else {
- console.warn(`等级 ${guardLevel} 或通用礼品码已用完,无法发送给 ${event.uname}`);
- }
- }
- }
-
- // 更新上下文中的礼品码
- if (context.variables.guard) {
- context.variables.guard.giftCode = giftCode;
- }
-
- // 选择模板并格式化
- if (action.templates.length > 0) {
- const template = action.templates[0]; // 对于私信,使用第一个模板
- const formattedMessage = formatTemplate(template, context);
-
- // 发送私信
- sendPrivateMessage(event.uid, formattedMessage).then(success => {
- if (success) {
- console.log(`成功发送上舰私信给 ${event.uname} (${event.uid})`);
- if (action.triggerConfig.preventRepeat) {
- runtimeState.sentGuardPms.add(event.uid);
- }
-
- // 发送弹幕确认
- if (roomId.value && sendLiveDanmaku) {
- // 查找确认弹幕的设置
- const confirmActions = actions.filter(a =>
- a.triggerType === TriggerType.GUARD &&
- a.enabled &&
- a.actionType === ActionType.SEND_DANMAKU
- );
-
- if (confirmActions.length > 0 && confirmActions[0].templates.length > 0) {
- const confirmMsg = formatTemplate(confirmActions[0].templates[0], context);
- sendLiveDanmaku(roomId.value, confirmMsg);
+ // 添加到已发送集合
+ runtimeState.sentGuardPms.add(uid);
}
+ return true;
}
- } else {
- console.error(`发送上舰私信给 ${event.uname} (${event.uid}) 失败`);
- // 失败时归还礼品码
- if (giftCode && action.triggerConfig.giftCodes) {
- const levelCodes = action.triggerConfig.giftCodes.find(gc => gc.level === guardLevel);
- if (levelCodes) {
- levelCodes.codes.push(giftCode);
+ ],
+ customContextBuilder: (eventData, roomId, triggerType): ExecutionContext => {
+ // 使用标准上下文构建方法
+ const context = buildExecutionContext(eventData, roomId, triggerType);
+
+ // 如果是舰长事件且有事件数据,处理礼品码
+ if (triggerType === TriggerType.GUARD && eventData && eventData.guard_level !== undefined) {
+ const guardLevel = eventData.guard_level;
+
+ // 查找包含礼品码的操作
+ guardActions.forEach(action => {
+ // 找到对应等级的礼品码
+ if (action.triggerConfig.giftCodes && action.triggerConfig.giftCodes.length > 0) {
+ // 优先查找特定等级的礼品码
+ let levelCodesEntry = action.triggerConfig.giftCodes.find(gc => gc.level === guardLevel);
+
+ // 如果没有找到特定等级的礼品码,尝试查找通用礼品码(level为0)
+ if (!levelCodesEntry) {
+ levelCodesEntry = action.triggerConfig.giftCodes.find(gc => gc.level === 0);
+ }
+
+ if (levelCodesEntry && levelCodesEntry.codes && levelCodesEntry.codes.length > 0) {
+ // 随机选择一个礼品码
+ const randomIndex = Math.floor(Math.random() * levelCodesEntry.codes.length);
+ const randomCode = levelCodesEntry.codes[randomIndex];
+ // 确保guard变量存在并设置礼品码
+ if (context.variables.guard) {
+ context.variables.guard.giftCode = randomCode;
+ // 在上下文中存储选中的礼品码信息以供后续消耗
+ context.variables.guard.selectedGiftCode = {
+ code: randomCode,
+ level: levelCodesEntry.level
+ };
+ }
+ }
+ }
+ });
+ }
+
+ return context;
+ },
+ onSuccess: (action: AutoActionItem, context: ExecutionContext) => {
+ // 检查是否需要消耗礼品码
+ if (
+ action.actionType === ActionType.SEND_PRIVATE_MSG &&
+ action.triggerConfig.consumeGiftCode &&
+ context.variables.guard?.selectedGiftCode
+ ) {
+ const { code: selectedCode, level: selectedLevel } = context.variables.guard.selectedGiftCode;
+
+ console.log(`[AutoAction] 尝试消耗礼品码: ActionID=${action.id}, Level=${selectedLevel}, Code=${selectedCode}`);
+
+ // 确保 giftCodes 存在且为数组
+ if (Array.isArray(action.triggerConfig.giftCodes)) {
+ // 找到对应等级的礼品码条目
+ const levelCodesEntry = action.triggerConfig.giftCodes.find(gc => gc.level === selectedLevel);
+
+ if (levelCodesEntry && Array.isArray(levelCodesEntry.codes)) {
+ // 找到要删除的礼品码的索引
+ const codeIndex = levelCodesEntry.codes.indexOf(selectedCode);
+
+ if (codeIndex > -1) {
+ // 从数组中移除礼品码
+ levelCodesEntry.codes.splice(codeIndex, 1);
+ console.log(`[AutoAction] 成功消耗礼品码: ActionID=${action.id}, Level=${selectedLevel}, Code=${selectedCode}. 剩余 ${levelCodesEntry.codes.length} 个。`);
+ // !!! 重要提示: 此处直接修改了 action 对象。
+ // !!! 请确保你的状态管理允许这种修改,或者调用 store action 来持久化更新。
+ // 例如: store.updateActionGiftCodes(action.id, selectedLevel, levelCodesEntry.codes);
+ } else {
+ console.warn(`[AutoAction] 未能在等级 ${selectedLevel} 中找到要消耗的礼品码: ${selectedCode}, ActionID=${action.id}`);
+ }
+ } else {
+ console.warn(`[AutoAction] 未找到等级 ${selectedLevel} 的礼品码列表或列表格式不正确, ActionID=${action.id}`);
+ }
+ } else {
+ console.warn(`[AutoAction] Action ${action.id} 的 giftCodes 配置不存在或不是数组。`);
}
}
}
- });
- }
+ }
+ );
+ }
+ }
+
+ /**
+ * 获取舰长等级名称
+ * @param level 舰长等级
+ * @returns 舰长等级名称
+ */
+ function getGuardLevelName(level: number): string {
+ switch (level) {
+ case 1: return '总督';
+ case 2: return '提督';
+ case 3: return '舰长';
+ default: return '未知等级';
}
}
return {
- config,
- processGuard
+ handleGuardBuy
};
}
\ No newline at end of file
diff --git a/src/client/store/autoAction/modules/scheduledDanmaku.ts b/src/client/store/autoAction/modules/scheduledDanmaku.ts
index 34d5ad0..db53740 100644
--- a/src/client/store/autoAction/modules/scheduledDanmaku.ts
+++ b/src/client/store/autoAction/modules/scheduledDanmaku.ts
@@ -1,15 +1,18 @@
import { ref, watch, Ref, computed } from 'vue';
import { useStorage } from '@vueuse/core';
import {
- getRandomTemplate,
- formatTemplate,
buildExecutionContext
} from '../utils';
import {
AutoActionItem,
TriggerType,
- RuntimeState
+ RuntimeState,
+ ExecutionContext
} from '../types';
+import {
+ filterValidActions,
+ executeActions
+} from '../actionUtils';
/**
* 定时弹幕模块
@@ -38,12 +41,8 @@ export function useScheduledDanmaku(
) {
if (!roomId.value) return;
- // 获取定时消息操作
- const scheduledActions = actions.filter(action =>
- action.triggerType === TriggerType.SCHEDULED &&
- action.enabled &&
- (!action.triggerConfig.onlyDuringLive || isLive.value)
- );
+ // 使用通用函数过滤有效的定时弹幕操作
+ const scheduledActions = filterValidActions(actions, TriggerType.SCHEDULED, isLive);
// 为每个定时操作设置定时器
scheduledActions.forEach(action => {
@@ -54,22 +53,30 @@ export function useScheduledDanmaku(
// 创建定时器函数
const timerFn = () => {
- // 创建执行上下文
- const context = buildExecutionContext(null, roomId.value, TriggerType.SCHEDULED);
-
- // 选择并发送消息
- const template = getRandomTemplate(action.templates);
- if (template && roomId.value) {
- const formattedMessage = formatTemplate(template, context);
- sendLiveDanmaku(roomId.value, formattedMessage);
+ // 使用通用执行函数处理定时操作
+ if (roomId.value) {
+ executeActions(
+ [action], // 只处理单个操作
+ null, // 定时操作没有触发事件
+ TriggerType.SCHEDULED,
+ roomId.value,
+ runtimeState,
+ { sendLiveDanmaku },
+ {
+ skipUserFilters: true, // 定时任务不需要用户过滤
+ skipCooldownCheck: false // 可以保留冷却检查
+ }
+ );
}
// 设置下一次定时
runtimeState.scheduledTimers[action.id] = setTimeout(timerFn, intervalSeconds * 1000);
+ runtimeState.timerStartTimes[action.id] = Date.now(); // 更新定时器启动时间
};
// 首次启动定时器
runtimeState.scheduledTimers[action.id] = setTimeout(timerFn, intervalSeconds * 1000);
+ runtimeState.timerStartTimes[action.id] = Date.now(); // 记录定时器启动时间
});
}
diff --git a/src/client/store/autoAction/modules/superChatThank.ts b/src/client/store/autoAction/modules/superChatThank.ts
new file mode 100644
index 0000000..ccb13cd
--- /dev/null
+++ b/src/client/store/autoAction/modules/superChatThank.ts
@@ -0,0 +1,79 @@
+import { EventModel } from '@/api/api-models';
+import { Ref } from 'vue';
+import {
+ executeActions,
+ filterValidActions
+} from '../actionUtils';
+import {
+ AutoActionItem,
+ RuntimeState,
+ TriggerType
+} from '../types';
+
+/**
+ * 醒目留言感谢模块
+ * @param isLive 是否处于直播状态
+ * @param roomId 房间ID
+ * @param isTianXuanActive 是否处于天选时刻
+ * @param sendLiveDanmaku 发送弹幕函数
+ */
+export function useSuperChatThank(
+ isLive: Ref,
+ roomId: Ref,
+ isTianXuanActive: Ref,
+ sendLiveDanmaku: (roomId: number, message: string) => Promise
+) {
+
+ /**
+ * 处理醒目留言事件
+ * @param event 醒目留言事件
+ * @param actions 自动操作列表
+ * @param runtimeState 运行时状态
+ */
+ function processSuperChat(
+ event: EventModel,
+ actions: AutoActionItem[],
+ runtimeState: RuntimeState
+ ) {
+ if (!roomId.value) return;
+
+ // 使用通用函数过滤有效的SC感谢操作
+ const scActions = filterValidActions(actions, TriggerType.SUPER_CHAT, isLive, isTianXuanActive);
+
+ // 使用通用执行函数处理SC事件
+ if (scActions.length > 0 && roomId.value) {
+ executeActions(
+ scActions,
+ event,
+ TriggerType.SUPER_CHAT,
+ roomId.value,
+ runtimeState,
+ { sendLiveDanmaku },
+ {
+ customFilters: [
+ // SC价格过滤
+ (action, context) => {
+ // 如果未设置SC过滤或选择了不过滤模式
+ if (!action.triggerConfig.scFilterMode || action.triggerConfig.scFilterMode === 'none') {
+ return true;
+ }
+
+ // 价格过滤模式
+ if (action.triggerConfig.scFilterMode === 'price' &&
+ action.triggerConfig.scMinPrice &&
+ event.price < action.triggerConfig.scMinPrice * 1000) {
+ return false;
+ }
+
+ return true;
+ }
+ ]
+ }
+ );
+ }
+ }
+
+ return {
+ processSuperChat,
+ };
+}
\ No newline at end of file
diff --git a/src/client/store/autoAction/types.ts b/src/client/store/autoAction/types.ts
index 7a29a9b..0486927 100644
--- a/src/client/store/autoAction/types.ts
+++ b/src/client/store/autoAction/types.ts
@@ -1,6 +1,6 @@
// 统一的自动操作类型定义
-import { EventModel } from '@/api/api-models';
+import { EventModel, GuardLevel } from '@/api/api-models';
// 触发条件类型
export enum TriggerType {
@@ -20,6 +20,13 @@ export enum ActionType {
EXECUTE_COMMAND = 'execute_command', // 执行命令
}
+// 关键词匹配类型
+export enum KeywordMatchType {
+ Full = 'full', // 完全匹配
+ Contains = 'contains', // 包含匹配
+ Regex = 'regex', // 正则匹配
+}
+
// 优先级
export enum Priority {
HIGHEST = 0,
@@ -36,7 +43,7 @@ export type AutoActionItem = {
enabled: boolean; // 是否启用
triggerType: TriggerType; // 触发类型
actionType: ActionType; // 操作类型
- templates: string[]; // 模板列表
+ template: string; // 模板
priority: Priority; // 优先级
// 高级配置
@@ -45,33 +52,7 @@ export type AutoActionItem = {
executeCommand: string; // 要执行的JS代码
// 触发器特定配置
- triggerConfig: {
- // 通用
- userFilterEnabled?: boolean; // 是否启用用户过滤
- requireMedal?: boolean; // 要求本房间勋章
- requireCaptain?: boolean; // 要求任意舰长
- onlyDuringLive?: boolean; // 仅直播中启用
- ignoreTianXuan?: boolean; // 天选时刻忽略
-
- // 弹幕触发特定
- keywords?: string[]; // 触发关键词
- blockwords?: string[]; // 屏蔽词
-
- // 礼物触发特定
- filterMode?: 'none' | 'blacklist' | 'whitelist' | 'value' | 'free'; // 礼物过滤模式
- filterGiftNames?: string[]; // 礼物黑/白名单
- minValue?: number; // 最低礼物价值
- includeQuantity?: boolean; // 是否包含礼物数量
-
- // 定时触发特定
- intervalSeconds?: number; // 间隔秒数
- schedulingMode?: 'random' | 'sequential'; // 定时模式
-
- // 上舰特定
- guardLevels?: number[]; // 舰长等级过滤
- preventRepeat?: boolean; // 防止重复发送
- giftCodes?: {level: number, codes: string[]}[]; // 礼品码
- };
+ triggerConfig: TriggerConfig;
// 动作特定配置
actionConfig: {
@@ -88,12 +69,78 @@ export interface ExecutionContext {
roomId?: number; // 直播间ID
variables: Record; // 额外变量
timestamp: number; // 时间戳
+
+ // --- 新增运行时数据管理函数 ---
+ /** 获取运行时数据 */
+ getData: (key: string, defaultValue?: T) => T | undefined;
+ /** 设置运行时数据 */
+ setData: (key: string, value: T) => void;
+ /** 检查运行时数据是否存在 */
+ containsData: (key: string) => boolean;
+ /** 移除运行时数据 */
+ removeData: (key: string) => void;
+
+ // --- 新增持久化数据管理函数 ---
+ /** 获取持久化存储的数据 */
+ getStorageData: (key: string, defaultValue?: T) => Promise;
+ /** 设置持久化存储的数据 */
+ setStorageData: (key: string, value: T) => Promise;
+ /** 检查持久化存储中是否存在指定的键 */
+ hasStorageData: (key: string) => Promise;
+ /** 从持久化存储中删除数据 */
+ removeStorageData: (key: string) => Promise;
+ /** 清除所有持久化存储的数据 */
+ clearStorageData: () => Promise;
}
// 运行状态接口
export interface RuntimeState {
lastExecutionTime: Record; // 上次执行时间
aggregatedEvents: Record; // 聚合的事件
- scheduledTimers: Record; // 定时器
+ scheduledTimers: Record; // 定时器 ID
+ timerStartTimes: Record; // <--- 新增:独立定时器启动时间戳
+ globalTimerStartTime: number | null; // <--- 新增:全局定时器启动时间戳
sentGuardPms: Set; // 已发送的舰长私信
+}
+
+export interface TriggerConfig {
+ // User filters
+ userFilterEnabled?: boolean;
+ requireMedal?: boolean;
+ requireCaptain?: boolean;
+
+ // Common conditions
+ onlyDuringLive?: boolean;
+ ignoreTianXuan?: boolean;
+
+ // Keywords for autoReply
+ keywords?: string[];
+ keywordMatchType?: KeywordMatchType;
+ blockwords?: string[];
+ blockwordMatchType?: KeywordMatchType;
+
+ // Gift filters
+ filterMode?: 'blacklist' | 'whitelist' | 'value' | 'none' | 'free';
+ filterGiftNames?: string[];
+ minValue?: number; // For gift and SC minimum value (元)
+ includeQuantity?: boolean; // 是否包含礼物数量
+
+ // SC相关配置
+ scFilterMode?: 'none' | 'price'; // SC过滤模式
+ scMinPrice?: number; // SC最低价格(元)
+
+ // Scheduled options
+ useGlobalTimer?: boolean;
+ intervalSeconds?: number;
+ schedulingMode?: 'random' | 'sequential';
+
+ // Guard related
+ guardLevels?: GuardLevel[];
+ preventRepeat?: boolean;
+ giftCodes?: { level: number; codes: string[] }[];
+ consumeGiftCode?: boolean; // 是否消耗礼品码
+
+ // Confirm message options
+ sendDanmakuConfirm?: boolean; // 是否发送弹幕确认
+ isConfirmMessage?: boolean; // 标记这是一个确认消息
}
\ No newline at end of file
diff --git a/src/client/store/autoAction/utils.ts b/src/client/store/autoAction/utils.ts
index e604f47..164b9e9 100644
--- a/src/client/store/autoAction/utils.ts
+++ b/src/client/store/autoAction/utils.ts
@@ -7,6 +7,16 @@ import {
RuntimeState,
ExecutionContext
} from './types';
+import { get, set, del, clear, keys as idbKeys, createStore } from 'idb-keyval'; // 导入 useIDBKeyval
+
+// --- 定义用户持久化数据的自定义存储区 ---
+const USER_DATA_DB_NAME = 'AutoActionUserDataDB';
+const USER_DATA_STORE_NAME = 'userData';
+const userDataStore = createStore(USER_DATA_DB_NAME, USER_DATA_STORE_NAME);
+// ----------------------------------------
+
+// --- 定义运行时数据的前缀 (避免与页面其他 sessionStorage 冲突) ---
+const RUNTIME_STORAGE_PREFIX = 'autoaction_runtime_';
/**
* 创建默认的运行时状态
@@ -15,6 +25,8 @@ export function createDefaultRuntimeState(): RuntimeState {
return {
lastExecutionTime: {},
scheduledTimers: {},
+ timerStartTimes: {},
+ globalTimerStartTime: null,
sentGuardPms: new Set(),
aggregatedEvents: {}
};
@@ -28,14 +40,14 @@ export function createDefaultAutoAction(triggerType: TriggerType): AutoActionIte
const id = `auto-action-${nanoid(8)}`;
// 根据不同触发类型设置默认模板
- const defaultTemplates: Record = {
- [TriggerType.DANMAKU]: ['收到 @{user.name} 的弹幕: {event.msg}'],
- [TriggerType.GIFT]: ['感谢 @{user.name} 赠送的 {gift.summary}'],
- [TriggerType.GUARD]: ['感谢 @{user.name} 开通了{guard.levelName}!'],
- [TriggerType.FOLLOW]: ['感谢 @{user.name} 的关注!'],
- [TriggerType.ENTER]: ['欢迎 @{user.name} 进入直播间'],
- [TriggerType.SCHEDULED]: ['这是一条定时消息,当前时间: {date.formatted}'],
- [TriggerType.SUPER_CHAT]: ['感谢 @{user.name} 的SC: {sc.message}'],
+ const defaultTemplates: Record = {
+ [TriggerType.DANMAKU]: '收到 {{user.name}} 的弹幕: {{danmaku.msg}}',
+ [TriggerType.GIFT]: '感谢 {{user.name}} 赠送的 {{gift.summary}}',
+ [TriggerType.GUARD]: '感谢 {{user.name}} 开通了{{danmaku.msg}}!',
+ [TriggerType.FOLLOW]: '感谢 {{user.name}} 的关注!',
+ [TriggerType.ENTER]: '欢迎 {{user.name}} 进入直播间',
+ [TriggerType.SCHEDULED]: '这是一条定时消息,当前时间: {{date.formatted}}',
+ [TriggerType.SUPER_CHAT]: '感谢 {{user.name}} 的SC!',
};
// 根据不同触发类型设置默认名称
@@ -56,7 +68,7 @@ export function createDefaultAutoAction(triggerType: TriggerType): AutoActionIte
triggerType,
actionType: triggerType === TriggerType.GUARD ? ActionType.SEND_PRIVATE_MSG : ActionType.SEND_DANMAKU,
priority: Priority.NORMAL,
- templates: defaultTemplates[triggerType] || ['默认模板'],
+ template: defaultTemplates[triggerType] || '默认模板',
logicalExpression: '',
executeCommand: '',
ignoreCooldown: false,
@@ -79,13 +91,12 @@ export function createDefaultAutoAction(triggerType: TriggerType): AutoActionIte
}
/**
- * 从模板数组中随机选择一个
- * @param templates 模板数组
+ * 处理模板字符串
+ * @param template 模板字符串
*/
-export function getRandomTemplate(templates: string[]): string | null {
- if (!templates || templates.length === 0) return null;
- const index = Math.floor(Math.random() * templates.length);
- return templates[index];
+export function getRandomTemplate(template: string): string | null {
+ if (!template) return null;
+ return template;
}
/**
@@ -268,7 +279,6 @@ export function buildExecutionContext(
const now = Date.now();
const dateObj = new Date(now);
- // 基础上下文
const context: ExecutionContext = {
event,
roomId,
@@ -295,6 +305,88 @@ export function buildExecutionContext(
if (hour < 22) return '晚上';
return '深夜';
}
+ },
+ // --- 实现运行时数据管理函数 (使用 sessionStorage) ---
+ getData: (key: string, defaultValue?: T): T | undefined => {
+ const prefixedKey = RUNTIME_STORAGE_PREFIX + key;
+ try {
+ const storedValue = sessionStorage.getItem(prefixedKey);
+ if (storedValue === null) {
+ return defaultValue;
+ }
+ return JSON.parse(storedValue) as T;
+ } catch (error) {
+ console.error(`[Runtime SessionStorage] Error getting/parsing key '${key}':`, error);
+ return defaultValue;
+ }
+ },
+ setData: (key: string, value: T): void => {
+ const prefixedKey = RUNTIME_STORAGE_PREFIX + key;
+ try {
+ // 不存储 undefined
+ if (value === undefined) {
+ sessionStorage.removeItem(prefixedKey);
+ return;
+ }
+ sessionStorage.setItem(prefixedKey, JSON.stringify(value));
+ } catch (error) {
+ console.error(`[Runtime SessionStorage] Error setting key '${key}':`, error);
+ // 如果序列化失败,可以选择移除旧键或保留
+ sessionStorage.removeItem(prefixedKey);
+ }
+ },
+ containsData: (key: string): boolean => {
+ const prefixedKey = RUNTIME_STORAGE_PREFIX + key;
+ return sessionStorage.getItem(prefixedKey) !== null;
+ },
+ removeData: (key: string): void => {
+ const prefixedKey = RUNTIME_STORAGE_PREFIX + key;
+ sessionStorage.removeItem(prefixedKey);
+ },
+ // --- 持久化数据管理函数 (不变,继续使用 userDataStore) ---
+ getStorageData: async (key: string, defaultValue?: T): Promise => {
+ try {
+ // 使用 userDataStore
+ const value = await get(key, userDataStore);
+ return value === undefined ? defaultValue : value;
+ } catch (error) {
+ console.error(`[UserData IDB] getStorageData error for key '${key}':`, error);
+ return defaultValue;
+ }
+ },
+ setStorageData: async (key: string, value: T): Promise => {
+ try {
+ // 使用 userDataStore
+ await set(key, value, userDataStore);
+ } catch (error) {
+ console.error(`[UserData IDB] setStorageData error for key '${key}':`, error);
+ }
+ },
+ hasStorageData: async (key: string): Promise => {
+ try {
+ // 使用 userDataStore
+ const value = await get(key, userDataStore);
+ return value !== undefined;
+ } catch (error) {
+ console.error(`[UserData IDB] hasStorageData error for key '${key}':`, error);
+ return false;
+ }
+ },
+ removeStorageData: async (key: string): Promise => {
+ try {
+ // 使用 userDataStore
+ await del(key, userDataStore);
+ } catch (error) {
+ console.error(`[UserData IDB] removeStorageData error for key '${key}':`, error);
+ }
+ },
+ clearStorageData: async (): Promise => {
+ try {
+ // 使用 userDataStore
+ await clear(userDataStore);
+ } catch (error) {
+ console.error('[UserData IDB] clearStorageData error:', error);
+ }
}
};
@@ -308,7 +400,7 @@ export function buildExecutionContext(
medalLevel: event.fans_medal_level,
medalName: event.fans_medal_name
};
-
+ context.variables.danmaku = event;
context.variables.message = event.msg;
// 根据不同触发类型添加特定变量
diff --git a/src/client/store/useAutoAction.ts b/src/client/store/useAutoAction.ts
index 0b0c055..5e5028e 100644
--- a/src/client/store/useAutoAction.ts
+++ b/src/client/store/useAutoAction.ts
@@ -1,641 +1,997 @@
+// 导入 Vue 和 Pinia 相关函数
import { ref, computed, onUnmounted, watch } from 'vue';
import { defineStore, acceptHMRUpdate } from 'pinia';
-import { EventModel, GuardLevel } from '@/api/api-models.js';
+// 导入 API 模型和类型
+import { EventModel, GuardLevel, EventDataTypes } from '@/api/api-models.js';
import { useDanmakuClient } from '@/store/useDanmakuClient.js';
import { useBiliFunction } from './useBiliFunction.js';
import { useAccount } from '@/api/account.js';
+// 导入 VueUse 工具库
import { useStorage } from '@vueuse/core';
+import { useIDBKeyval } from '@vueuse/integrations/useIDBKeyval';
+// 导入自动操作相关的类型和工具函数
import {
TriggerType,
ActionType,
Priority,
RuntimeState,
type AutoActionItem,
- ExecutionContext
+ ExecutionContext,
+ KeywordMatchType
} from './autoAction/types.js';
import {
- evaluateExpression,
- formatTemplate,
getRandomTemplate,
+ buildExecutionContext,
+ evaluateExpression,
+ shouldProcess,
createDefaultAutoAction,
createDefaultRuntimeState
-} from './autoAction/utils.js';
+} from './autoAction/utils';
+import { evaluateTemplateExpressions } from './autoAction/expressionEvaluator';
+// 导入 nanoid 用于生成唯一 ID
+import { nanoid } from 'nanoid';
+// 导入开发环境判断标志
+import { isDev } from '@/data/constants.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';
+import { useSuperChatThank } from './autoAction/modules/superChatThank.js';
+// 定义名为 'autoAction' 的 Pinia store
export const useAutoAction = defineStore('autoAction', () => {
- const danmakuClient = useDanmakuClient();
- const biliFunc = useBiliFunction();
- const account = useAccount(); // 用于获取房间ID和直播状态
+ // 获取 Pinia store 实例
+ const danmakuClient = useDanmakuClient(); // 弹幕客户端
+ const biliFunc = useBiliFunction(); // B站相关功能函数
+ const account = useAccount(); // 账户信息,用于获取房间ID和直播状态
// --- 共享状态 ---
const isLive = computed(() => account.value.streamerInfo?.isStreaming ?? false); // 获取直播状态
- const roomId = computed(() => isDev ? 1294406 : account.value.streamerInfo?.roomId); // 获取房间ID
- const isTianXuanActive = ref(false); // 天选时刻状态
+ const roomId = computed(() => isDev ? 1294406 : account.value.streamerInfo?.roomId); // 获取房间ID (开发环境使用固定ID)
+ const isTianXuanActive = ref(false); // 天选时刻活动状态
- // --- 存储所有自动操作项 ---
- const { data: autoActions } = useIDBKeyval('autoAction.items', []);
+ // --- 存储所有自动操作项 (使用 IndexedDB 持久化) ---
+ const { data: autoActions, isFinished: isActionsLoaded } = useIDBKeyval('autoAction.items', [], {
+ onError: (err) => {
+ console.error('[AutoAction] IDB 错误 (项目):', err); // 报告 IndexedDB 错误
+ }
+ });
- // --- 运行时状态 ---
+ // --- 运行时状态 (非持久化) ---
const runtimeState = ref(createDefaultRuntimeState());
- // --- 初始化各个模块 ---
- const giftThank = useGiftThank(
- isLive,
- roomId,
- isTianXuanActive,
- (roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
- );
+ // --- 添加触发类型启用状态持久化 ---
+ const { data: enabledTriggerTypes, isFinished: isTriggersLoaded } = useIDBKeyval>('autoAction.enabledTriggers', {
+ [TriggerType.DANMAKU]: true,
+ [TriggerType.GIFT]: true,
+ [TriggerType.GUARD]: true,
+ [TriggerType.FOLLOW]: true,
+ [TriggerType.ENTER]: true,
+ [TriggerType.SCHEDULED]: true,
+ [TriggerType.SUPER_CHAT]: true
+ }, {
+ onError: (err) => console.error('[AutoAction] IDB 错误 (触发类型):', err) // 报告 IndexedDB 错误
+ });
- const guardPm = useGuardPm(
- isLive,
- roomId,
- (userId: number, message: string) => biliFunc.sendPrivateMessage(userId, message)
- );
+ /**
+ * 设置触发类型启用状态
+ * @param triggerType 触发类型
+ * @param enabled 是否启用
+ */
+ function setTriggerTypeEnabled(triggerType: TriggerType, enabled: boolean) {
+ if (enabledTriggerTypes.value) {
+ enabledTriggerTypes.value[triggerType] = enabled;
- const followThank = useFollowThank(
- isLive,
- roomId,
- isTianXuanActive,
- (roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
- );
-
- const entryWelcome = useEntryWelcome(
- isLive,
- roomId,
- isTianXuanActive,
- (roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
- );
-
- const autoReply = useAutoReply(
- isLive,
- roomId,
- (roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
- );
-
- const scheduledDanmaku = useScheduledDanmaku(
- isLive,
- roomId,
- (roomId: number, message: string) => biliFunc.sendLiveDanmaku(roomId, message)
- );
-
- // --- 共享函数 ---
-
- // 检查是否处于天选时刻
- function checkTianXuanStatus() {
- return false;
- if (!roomId.value) return;
-
- // 调用B站API检查天选时刻状态
- /*biliFunc.checkRoomTianXuanStatus(roomId.value).then(active => {
- isTianXuanActive.value = active;
- }).catch(err => {
- console.error('检查天选时刻状态失败:', err);
- });*/
- }
-
- // 每5分钟更新一次天选状态
- const tianXuanTimer = setInterval(checkTianXuanStatus, 5 * 60 * 1000);
-
- // 清理所有计时器
- function clearAllTimers() {
- // 清理所有定时弹幕计时器
- 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;
-
- // 直播状态检查
- if (action.triggerConfig.onlyDuringLive && !isLive.value) return false;
-
- // 天选时刻检查
- if (action.triggerConfig.ignoreTianXuan && isTianXuanActive.value) return false;
-
- // 用户过滤检查
- 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;
- }
-
- // 评估逻辑表达式
- if (action.logicalExpression && event) {
- const context: ExecutionContext = {
- event,
- roomId: roomId.value,
- variables: {},
- timestamp: Date.now()
- };
-
- 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.processGuard(event, autoActions.value, runtimeState.value);
- break;
-
- case TriggerType.FOLLOW:
- followThank.processFollow(event, autoActions.value, runtimeState.value);
- 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 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);
-
- if (matchingActions.length === 0) return;
-
- // 准备执行上下文
- 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; // 仍在冷却中
+ // 如果是定时任务类型,且状态改变,则需要相应处理定时器
+ if (triggerType === TriggerType.SCHEDULED) {
+ if (enabled) {
+ // 启用时,启动相关定时器
+ startIndividualScheduledActions();
+ startGlobalTimer();
+ } else {
+ // 禁用时,停止相关定时器
+ stopAllIndividualScheduledActions();
+ // 移除 stopGlobalTimer() 调用,让计时器继续运行,但回调会提前返回
+ // stopGlobalTimer();
+ }
}
}
-
- // 获取随机模板
- const template = getRandomTemplate(templates);
- if (!template) return;
-
- // 格式化模板
- const formattedContent = formatTemplate(template, context);
- if (!formattedContent) return;
-
- // 根据操作类型执行不同的动作
- const executeActionFunc = () => {
- // 记录执行时间
- runtimeState.value.lastExecutionTime[id] = Date.now();
-
- switch (actionType) {
- case ActionType.SEND_DANMAKU:
- if (context.roomId) {
- biliFunc.sendLiveDanmaku(context.roomId, formattedContent);
- }
- break;
-
- case ActionType.SEND_PRIVATE_MSG:
- if (context.event) {
- biliFunc.sendPrivateMessage(context.event.uid, formattedContent);
-
- // 如果是上舰私信,记录已发送
- if (action.triggerType === TriggerType.GUARD && action.triggerConfig.preventRepeat) {
- runtimeState.value.sentGuardPms.add(context.event.uid);
- }
- }
- break;
-
- 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;
- }
- };
-
- // 延迟执行
- if (delaySeconds > 0) {
- setTimeout(executeActionFunc, delaySeconds * 1000);
- } else {
- executeActionFunc();
- }
}
- // 启动定时任务
- function startScheduledActions() {
- if (!roomId.value) return;
+ // --- 全局定时器设置 (使用 IndexedDB 持久化) ---
+ const { data: globalIntervalSeconds, isFinished: isIntervalLoaded } = useIDBKeyval('autoAction.globalInterval', 300, {
+ onError: (err) => console.error('[AutoAction] IDB 错误 (间隔):', err) // 报告 IndexedDB 错误
+ });
+ const { data: globalSchedulingMode, isFinished: isModeLoaded } = useIDBKeyval<'random' | 'sequential'>('autoAction.globalMode', 'random', {
+ onError: (err) => console.error('[AutoAction] IDB 错误 (模式):', err) // 报告 IndexedDB 错误
+ });
+ // 持久化上次全局顺序执行的索引 (使用 IndexedDB 持久化)
+ const { data: lastGlobalActionIndex, isFinished: isIndexLoaded } = useIDBKeyval('autoAction.lastGlobalIndex', -1, {
+ onError: (err) => console.error('[AutoAction] IDB 错误 (上次索引):', err) // 报告 IndexedDB 错误
+ });
- // 使用专用模块处理定时发送
- scheduledDanmaku.processScheduledActions(autoActions.value, runtimeState.value);
+ const globalTimer = ref(null); // 单个全局定时器实例
- // 同时处理自定义的定时任务
- const scheduledActions = autoActions.value.filter(
- action => action.triggerType === TriggerType.SCHEDULED && action.enabled
+ // --- 全局定时器逻辑 ---
+
+ /**
+ * 全局定时器触发处理函数
+ */
+ function handleGlobalTimerTick() {
+ // 1. 基本状态检查 (如果条件不满足,返回但不停止计时器)
+ if (!roomId.value || !isActionsLoaded.value) {
+ console.warn('[AutoAction] 全局定时器触发跳过: 房间ID或操作项未就绪.');
+ // Schedule next tick? No, rely on restart when ready.
+ return; // <- Changed from stopGlobalTimer()
+ }
+ // 检查触发类型是否启用
+ if (!enabledTriggerTypes.value || !enabledTriggerTypes.value[TriggerType.SCHEDULED]) {
+ console.log('[AutoAction] 全局定时器触发跳过: 定时任务类型已禁用.');
+ // Schedule next tick? No.
+ return; // <- Changed from stopGlobalTimer()
+ }
+
+ // 3. 筛选出当前*符合条件*的已启用操作
+ const eligibleActions = autoActions.value.filter(action =>
+ action.triggerType === TriggerType.SCHEDULED &&
+ action.enabled &&
+ action.triggerConfig.useGlobalTimer && // 必须选择使用全局定时器
+ (!action.triggerConfig.onlyDuringLive || isLive.value) && // 检查是否仅直播时触发
+ (!action.triggerConfig.ignoreTianXuan || !isTianXuanActive.value) // 检查是否忽略天选时刻
);
- scheduledActions.forEach(action => {
- // 清理可能存在的旧定时器
- if (runtimeState.value.scheduledTimers[action.id]) {
- clearTimeout(runtimeState.value.scheduledTimers[action.id]!);
- }
-
- const intervalSeconds = action.triggerConfig.intervalSeconds || 300; // 默认5分钟
-
- const timerFunc = () => {
- // 仅在检查时判断直播状态,不停止定时器
- const shouldExecute =
- !action.triggerConfig.onlyDuringLive || isLive.value;
-
- if (shouldExecute && !(action.triggerConfig.ignoreTianXuan && isTianXuanActive.value)) {
- // 创建执行上下文
- 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 stopAllScheduledActions() {
- // 清理所有定时任务
- Object.entries(runtimeState.value.scheduledTimers).forEach(([id, timer]) => {
- if (timer) {
- clearTimeout(timer);
- runtimeState.value.scheduledTimers[id] = null;
- }
- });
-
- // 清理模块定时任务
- 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) {
- // 如果已有定时器,先清理
- if (runtimeState.value.scheduledTimers[id]) {
- clearTimeout(runtimeState.value.scheduledTimers[id]!);
- runtimeState.value.scheduledTimers[id] = null;
- }
-
- // 启用时单独启动这个定时器
- const intervalSeconds = action.triggerConfig.intervalSeconds || 300;
-
- const timerFunc = () => {
- // 仅在检查时判断直播状态,不停止定时器
- const shouldExecute =
- !action.triggerConfig.onlyDuringLive || isLive.value;
-
- if (shouldExecute && !(action.triggerConfig.ignoreTianXuan && isTianXuanActive.value)) {
- // 创建执行上下文
- 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[id] = setTimeout(timerFunc, intervalSeconds * 1000);
- };
-
- // 启动定时器
- runtimeState.value.scheduledTimers[id] = setTimeout(timerFunc, intervalSeconds * 1000);
- } else if (runtimeState.value.scheduledTimers[id]) {
- // 禁用时清理定时器
- clearTimeout(runtimeState.value.scheduledTimers[id]!);
- runtimeState.value.scheduledTimers[id] = null;
- }
- }
- }
- }
-
- // 初始化
- function init() {
- // 初始检查天选状态
- checkTianXuanStatus();
-
- // 启动所有定时发送任务
- startScheduledActions();
-
- // 不再根据直播状态停止定时任务,只在回调中判断
- /*watch(isLive, (newIsLive) => {
- if (newIsLive) {
- startScheduledActions();
+ // 4. 执行操作 (仅当有符合条件的任务时)
+ if (eligibleActions.length > 0) {
+ let actionToExecute: AutoActionItem | null = null;
+ if (globalSchedulingMode.value === 'random') {
+ // 随机模式:随机选择一个
+ const randomIndex = Math.floor(Math.random() * eligibleActions.length);
+ actionToExecute = eligibleActions[randomIndex];
} else {
- stopAllScheduledActions();
+ // 顺序模式:按顺序循环选择
+ lastGlobalActionIndex.value = (lastGlobalActionIndex.value + 1) % eligibleActions.length;
+ actionToExecute = eligibleActions[lastGlobalActionIndex.value];
}
- });*/
- // 安全地订阅事件
+ if (actionToExecute) {
+ // 构建执行上下文并执行选中的操作
+ const context = buildExecutionContext(null, roomId.value, TriggerType.SCHEDULED);
+ // 手动执行定时任务
+ const template = getRandomTemplate(actionToExecute.template);
+ if (template && roomId.value) {
+ const formattedContent = evaluateTemplateExpressions(template, context);
+ // 更新执行时间
+ runtimeState.value.lastExecutionTime[actionToExecute.id] = Date.now();
+ // 发送弹幕
+ if (actionToExecute.actionConfig.delaySeconds && actionToExecute.actionConfig.delaySeconds > 0) {
+ setTimeout(() => {
+ biliFunc.sendLiveDanmaku(roomId.value!, formattedContent).catch(err => console.error("[AutoAction] 发送弹幕失败:", err));
+ }, actionToExecute.actionConfig.delaySeconds * 1000);
+ } else {
+ biliFunc.sendLiveDanmaku(roomId.value, formattedContent).catch(err => console.error("[AutoAction] 发送弹幕失败:", err));
+ }
+ }
+ }
+ } else {
+ // 没有符合条件的任务,跳过本次执行,但定时器继续
+ console.log('[AutoAction] 当前没有符合条件的全局定时任务可执行,跳过本次执行。');
+ }
+
+ // 5. 安排 *下一次* 触发 (只要有任务配置了全局定时器且间隔有效)
+ const intervalMs = globalIntervalSeconds.value * 1000;
+ if (intervalMs > 0) {
+ // 确保在设置新的定时器之前清除任何旧的句柄
+ if (globalTimer.value) {
+ clearTimeout(globalTimer.value);
+ }
+ globalTimer.value = setTimeout(handleGlobalTimerTick, intervalMs);
+ runtimeState.value.globalTimerStartTime = Date.now(); // 记录下一次间隔的开始时间
+ } else {
+ console.warn('[AutoAction] 全局定时器间隔无效,无法安排下一次触发。');
+ // 不停止定时器,等待间隔被修正后由 restartGlobalTimer 恢复
+ }
+ }
+
+ /**
+ * 启动全局定时器 (如果尚未运行且有需要)
+ * 仅安排 *第一次* 触发
+ */
+ function startGlobalTimer() {
+ // 如果定时器已在运行或操作尚未加载完成,则不执行
+ if (globalTimer.value || !isActionsLoaded.value) return;
+
+ // 如果定时任务类型被禁用,则不启动定时器
+ // (handleGlobalTimerTick 会处理返回,这里不需要停止)
+ if (!enabledTriggerTypes.value || !enabledTriggerTypes.value[TriggerType.SCHEDULED]) return; // <- Added enabledTriggerTypes check
+
+ // 检查是否有任何启用的定时任务需要全局定时器
+ const needsGlobalTimer = autoActions.value.some(action =>
+ action.triggerType === TriggerType.SCHEDULED &&
+ // action.enabled && // Don't require enabled here, just configured
+ action.triggerConfig.useGlobalTimer
+ );
+
+ // 如果需要全局定时器且间隔时间有效
+ if (needsGlobalTimer && globalIntervalSeconds.value > 0) {
+ // 这里只 *安排* 第一次触发,`handleGlobalTimerTick` 会处理后续的触发
+ const intervalMs = globalIntervalSeconds.value * 1000;
+ // console.log(`[AutoAction] 安排首次全局定时器触发于 ${globalIntervalSeconds.value} 秒后`); // 移除调试日志
+ // 以防万一先清除旧的定时器 (例如,快速切换状态)
+ if (globalTimer.value) clearTimeout(globalTimer.value);
+ globalTimer.value = setTimeout(handleGlobalTimerTick, intervalMs);
+ runtimeState.value.globalTimerStartTime = Date.now(); // 记录首次间隔的开始时间
+ } else {
+ // 如果没有任务配置需要全局定时器,或者间隔无效,则确保停止
+ // console.log('[AutoAction] 无操作需要全局定时器或间隔无效.'); // 移除信息日志
+ stopGlobalTimer(); // 保持这个停止调用
+ }
+ }
+
+ /**
+ * 停止全局定时器
+ */
+ function stopGlobalTimer() {
+ if (globalTimer.value) {
+ console.log('[AutoAction] 停止全局定时器.');
+ clearTimeout(globalTimer.value);
+ globalTimer.value = null;
+ lastGlobalActionIndex.value = -1; // 重置顺序索引
+ runtimeState.value.globalTimerStartTime = null; // 清除启动时间
+ }
+ }
+
+ /**
+ * 重启全局定时器
+ */
+ function restartGlobalTimer() {
+ stopGlobalTimer();
+ // 确保操作加载完成后再启动
+ if (isActionsLoaded.value) {
+ startGlobalTimer();
+ }
+ }
+
+ // --- 独立定时任务管理 ---
+
+ /**
+ * 停止指定的独立定时器
+ * @param actionId 操作项ID
+ */
+ function stopIndividualTimer(actionId: string) {
+ const timer = runtimeState.value.scheduledTimers[actionId];
+ if (timer) {
+ // console.log(`[AutoAction] 停止独立定时器: ${actionId}`); // 移除调试日志
+ clearTimeout(timer);
+ delete runtimeState.value.scheduledTimers[actionId];
+ delete runtimeState.value.timerStartTimes[actionId]; // 清除启动时间记录
+ }
+ }
+
+ /**
+ * 为指定的独立定时任务启动定时器
+ * @param action 操作项配置
+ */
+ function startIndividualTimer(action: AutoActionItem) {
+ // 如果定时器已存在、操作未启用或操作使用全局定时器,则不启动
+ if (runtimeState.value.scheduledTimers[action.id] || !action.enabled || action.triggerConfig.useGlobalTimer) return;
+
+ // 获取或设置默认间隔时间
+ const intervalSeconds = action.triggerConfig.intervalSeconds || 300;
+ if (intervalSeconds <= 0) return; // 间隔无效
+
+ const intervalMs = intervalSeconds * 1000;
+ // console.log(`[AutoAction] 启动独立定时器: ${action.name} (${action.id})`); // 移除调试日志
+
+ // 定义定时器触发时执行的函数
+ const timerFunc = () => {
+ // 获取最新的操作状态,以防在此期间发生变化
+ const currentAction = autoActions.value.find(a => a.id === action.id);
+ if (!currentAction) {
+ // 如果操作已被删除,停止定时器
+ stopIndividualTimer(action.id);
+ return;
+ }
+ // 在执行前再次检查条件
+ const shouldExecute = currentAction.enabled &&
+ !currentAction.triggerConfig.useGlobalTimer && // 确认仍未使用全局定时器
+ (!currentAction.triggerConfig.onlyDuringLive || isLive.value) &&
+ (!currentAction.triggerConfig.ignoreTianXuan || !isTianXuanActive.value);
+
+ if (shouldExecute) {
+ // 构建上下文并执行操作
+ const context = buildExecutionContext(null, roomId.value, TriggerType.SCHEDULED);
+ // 手动执行定时任务
+ const template = getRandomTemplate(currentAction.template);
+ if (template && roomId.value) {
+ const formattedContent = evaluateTemplateExpressions(template, context);
+ // 更新执行时间
+ runtimeState.value.lastExecutionTime[currentAction.id] = Date.now();
+ // 发送弹幕
+ if (currentAction.actionConfig.delaySeconds && currentAction.actionConfig.delaySeconds > 0) {
+ setTimeout(() => {
+ biliFunc.sendLiveDanmaku(roomId.value!, formattedContent).catch(err => console.error("[AutoAction] 发送弹幕失败:", err));
+ }, currentAction.actionConfig.delaySeconds * 1000);
+ } else {
+ biliFunc.sendLiveDanmaku(roomId.value, formattedContent).catch(err => console.error("[AutoAction] 发送弹幕失败:", err));
+ }
+ }
+ }
+
+ // 仅当操作仍然启用且未使用全局定时器时,才重新安排下一次触发
+ if (currentAction.enabled && !currentAction.triggerConfig.useGlobalTimer) {
+ const rescheduleIntervalMs = (currentAction.triggerConfig.intervalSeconds || 300) * 1000;
+ runtimeState.value.scheduledTimers[action.id] = setTimeout(timerFunc, rescheduleIntervalMs);
+ runtimeState.value.timerStartTimes[action.id] = Date.now(); // 重设计时器时更新启动时间
+ } else {
+ // 如果条件不再满足 (例如,被禁用或切换到全局定时器),停止此独立定时器
+ stopIndividualTimer(action.id);
+ }
+ };
+ // 首次启动定时器
+ runtimeState.value.scheduledTimers[action.id] = setTimeout(timerFunc, intervalMs);
+ runtimeState.value.timerStartTimes[action.id] = Date.now(); // 记录首次启动时间
+ }
+
+ /**
+ * 启动所有已启用的独立定时任务
+ */
+ function startIndividualScheduledActions() {
+ if (!roomId.value || !autoActions.value) return;
+
+ // 如果定时任务类型被禁用,则不启动任何定时器
+ if (!enabledTriggerTypes.value[TriggerType.SCHEDULED]) return;
+
+ // 筛选出所有启用且不使用全局定时器的定时任务
+ const individualActions = autoActions.value.filter(action =>
+ action.triggerType === TriggerType.SCHEDULED &&
+ action.enabled &&
+ !action.triggerConfig.useGlobalTimer
+ );
+ // 为每个符合条件的任务启动独立定时器
+ individualActions.forEach(action => startIndividualTimer(action));
+ }
+
+ /**
+ * 停止所有独立定时任务的定时器
+ */
+ function stopAllIndividualScheduledActions() {
+ console.log('[AutoAction] 停止所有独立定时器.');
+ // 遍历并停止所有记录在 runtimeState 中的独立定时器
+ Object.keys(runtimeState.value.scheduledTimers).forEach(stopIndividualTimer);
+ }
+
+ // --- 初始化与事件监听 ---
+
+ /**
+ * 初始化自动操作系统
+ */
+ function init() {
+ // 计算属性,判断所有持久化数据是否加载完成
+ const allLoaded = computed(() => isActionsLoaded.value && isIntervalLoaded.value && isModeLoaded.value && isIndexLoaded.value && isTriggersLoaded.value);
+
+ // 监听所有数据加载状态
+ watch(allLoaded, (loaded) => {
+ if (loaded) {
+ console.log('[AutoAction] 所有设置已从 IDB 加载.');
+ // 确保加载的操作项有默认配置
+ autoActions.value.forEach(action => {
+ if (!action.triggerConfig) action.triggerConfig = {};
+ if (action.triggerType === TriggerType.SCHEDULED) {
+ // 为定时任务设置默认值(如果缺失)
+ if (action.triggerConfig.useGlobalTimer === undefined) action.triggerConfig.useGlobalTimer = false;
+ if (action.triggerConfig.intervalSeconds === undefined) action.triggerConfig.intervalSeconds = 300;
+ if (action.triggerConfig.schedulingMode === undefined) action.triggerConfig.schedulingMode = 'random';
+ }
+ });
+ // 确保全局顺序索引已初始化 (以防 IDB 返回 null/undefined)
+ if (lastGlobalActionIndex.value === null || lastGlobalActionIndex.value === undefined) {
+ lastGlobalActionIndex.value = -1;
+ }
+ // 确保触发类型启用状态已初始化
+ if (!enabledTriggerTypes.value) {
+ enabledTriggerTypes.value = {
+ [TriggerType.DANMAKU]: true,
+ [TriggerType.GIFT]: true,
+ [TriggerType.GUARD]: true,
+ [TriggerType.FOLLOW]: true,
+ [TriggerType.ENTER]: true,
+ [TriggerType.SCHEDULED]: true,
+ [TriggerType.SUPER_CHAT]: true
+ };
+ }
+ checkTianXuanStatus(); // 检查天选状态
+ startIndividualScheduledActions(); // 启动独立定时任务
+ startGlobalTimer(); // 启动全局定时器 (如果需要)
+ registerEventListeners(); // 注册弹幕事件监听器
+ }
+ }, { immediate: true }); // 立即执行一次检查
+ }
+
+ // 初始化模块
+ const giftThankModule = useGiftThank(isLive, roomId, isTianXuanActive, biliFunc.sendLiveDanmaku);
+ const guardPmModule = useGuardPm(
+ roomId,
+ (userId: number, message: string) => biliFunc.sendPrivateMessage(userId, message),
+ biliFunc.sendLiveDanmaku
+ );
+ const followThankModule = useFollowThank(isLive, roomId, isTianXuanActive, biliFunc.sendLiveDanmaku);
+ const entryWelcomeModule = useEntryWelcome(isLive, roomId, isTianXuanActive, biliFunc.sendLiveDanmaku);
+ const autoReplyModule = useAutoReply(isLive, roomId, biliFunc.sendLiveDanmaku);
+ const scheduledDanmakuModule = useScheduledDanmaku(isLive, roomId, biliFunc.sendLiveDanmaku);
+ const superChatThankModule = useSuperChatThank(isLive, roomId, isTianXuanActive, biliFunc.sendLiveDanmaku);
+
+ /**
+ * 向弹幕客户端注册事件监听器
+ */
+ function registerEventListeners() {
+ // 检查弹幕客户端连接状态
+ if (danmakuClient.state !== 'connected') {
+ console.warn('[AutoAction] 弹幕客户端未就绪, 延迟注册监听器.');
+ // 可选: 等待弹幕客户端发出就绪事件
+ // return;
+ }
try {
+ // 监听各种事件,并交由 processEvent 处理
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));
+ danmakuClient.onEvent('follow', (event) => processEvent(event, TriggerType.FOLLOW));
+ console.log('[AutoAction] 事件监听器已注册.');
} catch (err) {
- console.error('注册事件监听器失败:', err);
- }
-
- // 注册HMR清理
- if (import.meta.hot) {
- import.meta.hot.dispose(() => {
- clearAllTimers();
- });
+ console.error('[AutoAction] 注册事件监听器时出错:', err);
}
}
- // 卸载时清理
- onUnmounted(() => {
- clearAllTimers();
- });
-
- // 向外部导出所有配置和状态
- const exportedConfigs = computed(() => ({
- autoActions: autoActions.value,
- isLive: isLive.value,
- roomId: roomId.value
- }));
/**
- * 获取定时任务的计时器信息
- * @param actionId 定时任务ID
- * @returns 计时器信息,包含剩余毫秒数
+ * 组件卸载时清理资源
*/
- function getScheduledTimerInfo(actionId: string) {
- const timer = runtimeState.value.scheduledTimers[actionId];
- if (!timer) return null;
+ onUnmounted(() => {
+ console.log('[AutoAction] 清理定时器和监听器.');
+ stopAllIndividualScheduledActions(); // 停止所有独立定时器
+ stopGlobalTimer(); // 停止全局定时器
+ clearInterval(tianXuanTimer); // 清除天选状态检查定时器
+ });
- // 找到对应的action
+ // --- 操作项管理 (增删改查) ---
+
+ /**
+ * 添加一个新的自动操作项
+ * @param triggerType 触发器类型
+ * @returns 新创建的操作项
+ */
+ function addAutoAction(triggerType: TriggerType): AutoActionItem {
+ const newAction = createDefaultAutoAction(triggerType); // 创建默认操作项
+ autoActions.value.push(newAction); // 添加到列表
+ // 如果是定时任务,根据配置启动相应的定时器
+ if (triggerType === TriggerType.SCHEDULED) {
+ if (newAction.triggerConfig.useGlobalTimer) {
+ restartGlobalTimer(); // 可能需要启动或重启全局定时器
+ } else {
+ startIndividualTimer(newAction); // 启动独立的定时器
+ }
+ }
+ // console.log('[AutoAction] 已添加:', newAction.name, newAction.id); // 移除调试日志
+ return newAction;
+ }
+
+ /**
+ * 移除一个自动操作项
+ * @param id 要移除的操作项 ID
+ */
+ function removeAutoAction(id: string) {
+ const index = autoActions.value.findIndex(action => action.id === id);
+ if (index !== -1) {
+ const removedAction = autoActions.value[index];
+ // console.log('[AutoAction] 正在移除:', removedAction.name, id); // 移除调试日志
+
+ let needsGlobalTimerAfterRemoval = false;
+ let wasGlobalTimerAction = false;
+
+ // 如果移除的是定时任务,需要特殊处理定时器
+ if (removedAction.triggerType === TriggerType.SCHEDULED) {
+ stopIndividualTimer(id); // 停止可能存在的独立定时器
+ }
+
+ // 从列表中移除操作项
+ autoActions.value.splice(index, 1);
+
+ // 如果移除了一个全局定时任务,但还有其他全局任务存在,则重启全局定时器以更新状态
+ if (wasGlobalTimerAction && needsGlobalTimerAfterRemoval) {
+ restartGlobalTimer();
+ }
+ }
+ }
+
+ /**
+ * 切换自动操作项的启用/禁用状态
+ * @param id 操作项 ID
+ * @param enabled 目标状态 (true 为启用, false 为禁用)
+ */
+ function toggleAutoAction(id: string, enabled: boolean) {
+ const action = autoActions.value.find(action => action.id === id);
+ if (action) {
+ // console.log(`[AutoAction] 切换 ${action.name} (${id}) 状态为 ${enabled}`); // 移除调试日志
+ action.enabled = enabled;
+
+ // 如果是定时任务,需要相应地启动或停止定时器
+ if (action.triggerType === TriggerType.SCHEDULED) {
+ if (action.triggerConfig.useGlobalTimer) {
+ // 启用/禁用全局定时任务时,重启全局定时器以确保其根据剩余任务正确运行或停止
+ restartGlobalTimer();
+ } else {
+ // 独立定时任务
+ if (enabled) {
+ startIndividualTimer(action); // 尝试启动其独立定时器
+ } else {
+ stopIndividualTimer(id); // 停止其独立定时器
+ }
+ }
+ }
+ }
+ }
+
+ // --- 辅助函数与执行逻辑 ---
+
+ // 模拟的天选状态检查函数 (需替换为实际实现)
+ function checkTianXuanStatus() {
+ // TODO: 实现检查天选时刻状态的逻辑
+ // isTianXuanActive.value = a_real_check();
+ }
+ // 定时检查天选状态 (每5分钟)
+ const tianXuanTimer = setInterval(checkTianXuanStatus, 5 * 60 * 1000);
+
+ /**
+ * 判断是否应处理某个操作项 (基于事件和配置)
+ * @param action 操作项配置
+ * @param event 可选的事件数据
+ * @returns 是否应该处理
+ */
+ function shouldProcessAction(action: AutoActionItem, event?: EventModel | null): boolean {
+ if (!action.enabled) return false; // 未启用则跳过
+ if (!enabledTriggerTypes.value[action.triggerType]) return false; // 触发类型未启用则跳过
+
+ // 检查模板是否为空 (添加新的检查)
+ if (!action.template || action.template.trim() === '') {
+ console.warn(`[AutoAction] 跳过操作 "${action.name}":未设置有效模板`);
+ return false;
+ }
+
+ // 根据配置检查条件
+ if (action.triggerConfig.onlyDuringLive && !isLive.value) return false; // 仅直播时
+ if (action.triggerConfig.ignoreTianXuan && isTianXuanActive.value) return false; // 忽略天选时
+ // 用户过滤条件
+ 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; // 要求舰长
+ }
+ // 逻辑表达式判断
+ if (action.logicalExpression && event) {
+ const context = buildExecutionContext(event, roomId.value, action.triggerType);
+ if (!evaluateExpression(action.logicalExpression, context)) return false; // 表达式不满足
+ }
+ return true; // 所有条件满足
+ }
+
+ /**
+ * 处理接收到的事件
+ * @param event 事件数据
+ * @param triggerType 事件对应的触发器类型
+ */
+ function processEvent(event: EventModel, triggerType: TriggerType) {
+ if (!roomId.value) return; // 房间 ID 无效则跳过
+
+ // 检查触发类型是否启用
+ if (!enabledTriggerTypes.value[triggerType]) return;
+
+ // 根据触发类型调用相应模块的处理函数
+ switch (triggerType) {
+ case TriggerType.DANMAKU:
+ // 调用弹幕自动回复模块
+ autoReplyModule.onDanmaku(event, autoActions.value, runtimeState.value);
+ break;
+ case TriggerType.GIFT:
+ // 调用礼物感谢模块
+ giftThankModule.processGift(event, autoActions.value, runtimeState.value);
+ break;
+ case TriggerType.GUARD:
+ // 调用舰长感谢模块
+ guardPmModule.handleGuardBuy(autoActions.value, event, runtimeState.value);
+ break;
+ case TriggerType.FOLLOW:
+ // 调用关注感谢模块
+ followThankModule.processFollow(event, autoActions.value, runtimeState.value);
+ break;
+ case TriggerType.ENTER:
+ // 调用入场欢迎模块
+ entryWelcomeModule.processEnter(event, autoActions.value, runtimeState.value);
+ break;
+ case TriggerType.SUPER_CHAT:
+ // 调用醒目留言感谢模块
+ superChatThankModule.processSuperChat(event, autoActions.value, runtimeState.value);
+ break;
+ // 定时任务不在此处理,由定时器调用
+ default:
+ console.warn(`[AutoAction] 未知触发类型: ${triggerType}`);
+ }
+ }
+
+ /**
+ * 获取定时任务的计时器信息 (用于显示剩余时间等)
+ * @param actionId 定时任务ID
+ * @returns 计时器信息(包含剩余毫秒数估算),或 null (如果任务不存在、未启用或计时器未运行)
+ */
+ function getScheduledTimerInfo(actionId: string): { actionId: string; intervalMs: number; remainingMs: number } | null {
+ // 查找对应的操作项
const action = autoActions.value.find(a => a.id === actionId);
- if (!action) return null;
+ // 必须是已启用的定时任务
+ if (!action || action.triggerType !== TriggerType.SCHEDULED || !action.enabled) {
+ return null;
+ }
- const intervalSeconds = action.triggerConfig.intervalSeconds || 300;
- const intervalMs = intervalSeconds * 1000;
+ const usingGlobal = action.triggerConfig.useGlobalTimer ?? false; // 是否使用全局定时器
+ let intervalSeconds: number; // 间隔秒数
+ let isActive = false; // 定时器是否在运行
+ let startTime: number | null = null; // 定时器本轮启动时间
- // 计算下一次执行时间和剩余时间
- // 由于JavaScript中没有直接的方式获取setTimeout的剩余时间
- // 我们需要模拟一个剩余时间,在实际应用中可能需要更精确的方式
- const now = Date.now();
- const timerId = timer as unknown as number;
- const remainingMs = Math.max(0, (intervalMs - (now % intervalMs)) % intervalMs);
+ if (usingGlobal) {
+ // 使用全局定时器
+ intervalSeconds = globalIntervalSeconds.value;
+ isActive = globalTimer.value !== null; // 全局定时器是否在运行
+ startTime = runtimeState.value.globalTimerStartTime; // 获取全局定时器的启动时间
+ } else {
+ // 使用独立定时器
+ intervalSeconds = action.triggerConfig.intervalSeconds || 300;
+ isActive = !!runtimeState.value.scheduledTimers[actionId]; // 独立定时器是否在运行
+ startTime = runtimeState.value.timerStartTimes[actionId] ?? null; // 获取独立定时器的启动时间
+ }
+
+ // 如果定时器未激活、间隔无效或没有启动时间,则无法计算剩余时间
+ if (!isActive || intervalSeconds <= 0 || startTime === null) {
+ return null;
+ }
+
+ const intervalMs = intervalSeconds * 1000; // 间隔毫秒数
+ const now = Date.now(); // 当前时间
+ // 计算剩余时间 (确保不为负数)
+ const remainingMs = Math.max(0, startTime + intervalMs - now);
return {
actionId,
intervalMs,
- remainingMs
+ remainingMs // 返回计算出的剩余时间
};
}
/**
- * 更新所有定时任务计时器状态(用于触发UI更新)
+ * 在列表中向上或向下移动一个操作项
+ * 这会影响全局定时器顺序模式下的执行顺序
+ * @param id 要移动的操作项 ID
+ * @param direction 'up' (向上) 或 'down' (向下)
*/
- function updateScheduledTimers() {
- // 这个方法主要用于触发UI更新
- // 实际上只需要修改一个响应式变量即可
- const scheduledActions = autoActions.value.filter(
- action => action.triggerType === TriggerType.SCHEDULED && action.enabled
- );
+ function moveAction(id: string, direction: 'up' | 'down') {
+ const index = autoActions.value.findIndex(action => action.id === id); // 查找当前索引
+ if (index === -1) {
+ console.warn(`[AutoAction] 无法移动操作: 未找到 ID ${id}.`);
+ return;
+ }
- // 触发响应式更新
- 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;
- }
+ const actionToMove = autoActions.value[index]; // 获取要移动的操作项
+
+ // 简单的在整个列表中重新排序
+ // 更复杂的逻辑可以只在相同触发类型的操作中排序
+ let newIndex = index;
+ if (direction === 'up' && index > 0) {
+ // 向上移动
+ newIndex = index - 1;
+ } else if (direction === 'down' && index < autoActions.value.length - 1) {
+ // 向下移动
+ newIndex = index + 1;
+ }
+
+ if (newIndex !== index) { // 如果位置确实改变了
+ // console.log(`[AutoAction] 移动操作 ${actionToMove.name} (${id}) 从 ${index} 到 ${newIndex}`); // 移除调试日志
+ // 从原位置移除,并插入到新位置
+ autoActions.value.splice(index, 1);
+ autoActions.value.splice(newIndex, 0, actionToMove);
+ // 如果移动的是使用全局定时器的定时任务,重置全局顺序索引,以便下次重新计算
+ if (actionToMove.triggerType === TriggerType.SCHEDULED && actionToMove.triggerConfig.useGlobalTimer) {
+ lastGlobalActionIndex.value = -1; // 强制在下一次触发时重新计算索引
}
- });
+ }
}
- // 导出接口
- return {
- autoActions,
- runtimeState: runtimeState.value,
- shouldProcessAction,
- executeAction,
- addAutoAction,
- removeAutoAction,
- toggleAutoAction,
- processEvent,
- startScheduledActions,
- stopAllScheduledActions,
- checkTianXuanStatus,
- getScheduledTimerInfo,
- updateScheduledTimers,
- init
- };
-});
+ // 监听全局间隔设置的变化,如果变化且有效,则重启全局定时器
+ watch(globalIntervalSeconds, (newInterval, oldInterval) => {
+ if (newInterval !== oldInterval && newInterval > 0) {
+ console.log('[AutoAction] 全局间隔已更改, 重启全局定时器.');
+ restartGlobalTimer();
+ }
+ });
-// 支持热更新
+ /**
+ * 计算属性:获取下一个将在全局顺序模式下执行的操作
+ */
+ const nextScheduledAction = computed(() => {
+ // 仅在顺序模式下有效,且操作列表存在
+ if (globalSchedulingMode.value !== 'sequential' || !autoActions.value) {
+ return null;
+ }
+
+ // 筛选出当前符合全局定时器条件的活动操作
+ const eligibleActions = autoActions.value.filter(action =>
+ action.triggerType === TriggerType.SCHEDULED &&
+ action.enabled &&
+ action.triggerConfig.useGlobalTimer &&
+ (!action.triggerConfig.onlyDuringLive || isLive.value) &&
+ (!action.triggerConfig.ignoreTianXuan || !isTianXuanActive.value)
+ );
+
+ if (eligibleActions.length === 0) {
+ return null; // 没有符合条件的操作
+ }
+
+ // 计算下一个索引
+ // 注意:lastGlobalActionIndex 指向的是 *上次* 执行的操作在当时 eligibleActions 列表中的索引
+ const nextIndex = (lastGlobalActionIndex.value + 1) % eligibleActions.length;
+
+ return eligibleActions[nextIndex]; // 返回下一个将要执行的操作
+ });
+
+ /**
+ * 手动设置下一个在全局顺序模式下执行的操作
+ * @param actionId 要设置为下一个执行的操作ID
+ */
+ function setNextGlobalAction(actionId: string) {
+ if (globalSchedulingMode.value !== 'sequential') {
+ console.warn('[AutoAction] 只能在顺序模式下手动指定下一个操作。');
+ return;
+ }
+
+ // 筛选出当前符合条件的活动操作 (与 nextScheduledAction 逻辑一致)
+ const eligibleActions = autoActions.value.filter(action =>
+ action.triggerType === TriggerType.SCHEDULED &&
+ action.enabled &&
+ action.triggerConfig.useGlobalTimer &&
+ (!action.triggerConfig.onlyDuringLive || isLive.value) &&
+ (!action.triggerConfig.ignoreTianXuan || !isTianXuanActive.value)
+ );
+
+ if (eligibleActions.length === 0) {
+ console.warn('[AutoAction] 没有符合条件的活动操作可供指定。');
+ return;
+ }
+
+ // 找到目标操作在当前合格列表中的索引
+ const targetIndex = eligibleActions.findIndex(action => action.id === actionId);
+
+ if (targetIndex === -1) {
+ console.warn(`[AutoAction] 指定的操作ID ${actionId} 不存在或不符合当前执行条件。`);
+ return;
+ }
+
+ // 设置 lastGlobalActionIndex 为目标索引的前一个索引
+ // 这样,在下一次 handleGlobalTimerTick 中计算 (lastGlobalActionIndex + 1) % length 时,就会得到 targetIndex
+ // 如果目标是列表中的第一个 (index 0),则将 lastGlobalActionIndex 设置为列表最后一个元素的索引
+ lastGlobalActionIndex.value = (targetIndex === 0) ? eligibleActions.length - 1 : targetIndex - 1;
+
+ console.log(`[AutoAction] 手动指定下一个执行的操作为: ${eligibleActions[targetIndex].name} (ID: ${actionId}), 将在下一个计时周期执行。`);
+
+ // 重启全局计时器,以便立即应用更改并在下一个周期执行指定的操作
+ // (如果不重启,则会在当前周期结束后,按新的索引执行)
+ restartGlobalTimer();
+ }
+
+ /**
+ * 手动触发指定类型的测试逻辑
+ * @param triggerType 要测试的触发类型
+ * @param testUid 测试用的UID(仅用于私信测试)
+ */
+ function triggerTestActionByType(triggerType: TriggerType, testUid?: number) {
+ console.log(`[AutoAction Test] 准备测试类型: ${triggerType}`);
+
+ // 查找所有属于该类型且已启用的操作 (包括触发器类型本身是否启用)
+ const actionsToTest = autoActions.value.filter(a =>
+ a.triggerType === triggerType &&
+ a.enabled &&
+ enabledTriggerTypes.value[triggerType]
+ );
+
+ if (actionsToTest.length === 0) {
+ console.warn(`[AutoAction Test] 没有找到启用的 ${triggerType} 类型的操作可供测试。`);
+ if (!enabledTriggerTypes.value[triggerType]) {
+ console.warn(`[AutoAction Test] 触发类型 ${triggerType} 本身已被禁用。`);
+ }
+ return;
+ }
+
+ // 创建一个模拟事件用于测试
+ let testEvent: EventModel;
+
+ // 根据不同触发类型创建不同的模拟事件
+ switch (triggerType) {
+ case TriggerType.DANMAKU:
+ testEvent = {
+ type: EventDataTypes.Message,
+ uid: 10000,
+ uname: '测试用户',
+ uface: '',
+ open_id: 'test-open-id',
+ ouid: 'test-ouid',
+ msg: '测试弹幕消息',
+ time: Math.floor(Date.now() / 1000),
+ num: 1,
+ price: 0,
+ guard_level: 3,
+ fans_medal_wearing_status: true,
+ fans_medal_name: '测试牌子',
+ fans_medal_level: 10
+ };
+ break;
+ case TriggerType.GIFT:
+ testEvent = {
+ type: EventDataTypes.Gift,
+ uid: 10003,
+ uname: '测试送礼者',
+ uface: '',
+ open_id: 'test-open-id-gift',
+ ouid: 'test-ouid-gift',
+ msg: '感谢 测试送礼者 赠送的 测试礼物 x 1',
+ time: Math.floor(Date.now() / 1000),
+ num: 1,
+ price: 100,
+ guard_level: 1,
+ fans_medal_wearing_status: true,
+ fans_medal_name: '测试牌子',
+ fans_medal_level: 15
+ };
+ break;
+ case TriggerType.GUARD:
+ testEvent = {
+ type: EventDataTypes.Guard,
+ uid: testUid || 10002,
+ uname: '测试大航海成员',
+ uface: '',
+ open_id: 'test-open-id-guard',
+ ouid: 'test-ouid-guard',
+ msg: '测试大航海成员 开通了舰长',
+ time: Math.floor(Date.now() / 1000),
+ num: 1,
+ price: 138,
+ guard_level: Math.floor(Math.random() * 3) + 1, // 1-3
+ fans_medal_wearing_status: true,
+ fans_medal_name: '测试牌子',
+ fans_medal_level: 20
+ };
+ break;
+ case TriggerType.FOLLOW:
+ testEvent = {
+ type: EventDataTypes.Message,
+ uid: 10004,
+ uname: '测试关注者',
+ uface: '',
+ open_id: 'test-open-id-follow',
+ ouid: 'test-ouid-follow',
+ msg: '测试关注者 关注了直播间',
+ time: Math.floor(Date.now() / 1000),
+ num: 1,
+ price: 0,
+ guard_level: 0,
+ fans_medal_wearing_status: false,
+ fans_medal_name: '',
+ fans_medal_level: 0
+ };
+ break;
+ case TriggerType.ENTER:
+ testEvent = {
+ type: EventDataTypes.Enter,
+ uid: 10005,
+ uname: '测试入场观众',
+ uface: '',
+ open_id: 'test-open-id-enter',
+ ouid: 'test-ouid-enter',
+ msg: '测试入场观众 进入了直播间',
+ time: Math.floor(Date.now() / 1000),
+ num: 1,
+ price: 0,
+ guard_level: 2,
+ fans_medal_wearing_status: true,
+ fans_medal_name: '测试牌子',
+ fans_medal_level: 8
+ };
+ break;
+ case TriggerType.SUPER_CHAT:
+ testEvent = {
+ type: EventDataTypes.SC,
+ uid: 10006,
+ uname: '测试SC用户',
+ uface: '',
+ open_id: 'test-open-id-sc',
+ ouid: 'test-ouid-sc',
+ msg: '这是一条测试SC消息',
+ time: Math.floor(Date.now() / 1000),
+ num: 1,
+ price: 30,
+ guard_level: 0,
+ fans_medal_wearing_status: true,
+ fans_medal_name: '测试牌子',
+ fans_medal_level: 25
+ };
+ break;
+ case TriggerType.SCHEDULED:
+ // 对于定时任务,使用特殊的处理方式
+ if (actionsToTest.length > 0) {
+ const action = actionsToTest[0];
+ const context = buildExecutionContext(null, roomId.value, TriggerType.SCHEDULED);
+ const template = getRandomTemplate(action.template);
+
+ if (template && roomId.value) {
+ const formattedContent = evaluateTemplateExpressions(template, context);
+ runtimeState.value.lastExecutionTime[action.id] = Date.now();
+
+ console.log(`[定时任务测试] 正在测试定时任务: ${action.name}, 内容: ${formattedContent}`);
+
+ if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
+ console.log(`[定时任务测试] 将在 ${action.actionConfig.delaySeconds} 秒后发送弹幕`);
+ setTimeout(() => {
+ biliFunc.sendLiveDanmaku(roomId.value!, formattedContent)
+ .catch(err => console.error("[AutoAction] 发送弹幕失败:", err));
+ }, action.actionConfig.delaySeconds * 1000);
+ } else {
+ biliFunc.sendLiveDanmaku(roomId.value, formattedContent)
+ .catch(err => console.error("[AutoAction] 发送弹幕失败:", err));
+ }
+ }
+ }
+ return; // 定时任务不需要继续处理
+ default:
+ console.warn(`[AutoAction Test] 未知的触发类型: ${triggerType}`);
+ return;
+ }
+
+ console.log(`[AutoAction Test] 创建测试事件:`, testEvent);
+
+ // 直接调用processEvent进行测试,将创建的测试事件和触发类型传入
+ processEvent(testEvent, triggerType);
+ }
+
+ // --- 导出 Store 成员 ---
+ return {
+ autoActions, // 所有操作项列表 (ref)
+ runtimeState: computed(() => runtimeState.value), // 运行时状态 (计算属性)
+ globalIntervalSeconds, // 全局定时器间隔 (ref from IDB)
+ globalSchedulingMode, // 全局定时器模式 (ref from IDB)
+ nextScheduledAction, // 下一个顺序执行的操作 (计算属性)
+ isLive, // 直播状态 (计算属性)
+ isTianXuanActive, // 天选状态 (ref)
+ enabledTriggerTypes, // 触发类型启用状态
+ init, // 初始化函数
+ addAutoAction, // 添加操作
+ removeAutoAction, // 移除操作
+ toggleAutoAction, // 切换操作启用状态
+ moveAction, // 移动操作顺序
+ setNextGlobalAction, // 手动设置下一个全局顺序操作
+ restartGlobalTimer, // 重启全局定时器
+ getScheduledTimerInfo, // 获取定时任务计时器信息
+ setTriggerTypeEnabled, // 设置触发类型启用状态
+ // 暴露独立定时器控制函数 (如果 UI 需要单独控制)
+ startIndividualTimer,
+ stopIndividualTimer,
+ stopAllIndividualScheduledActions,
+ startIndividualScheduledActions,
+ triggerTestActionByType // 新的 action
+ };
+});// HMR (热模块替换) 支持
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useAutoAction, import.meta.hot));
}
-export {
- AutoActionItem,
- TriggerType,
- ActionType,
- Priority
-};
+// 重新导出类型,方便外部使用
+export { AutoActionItem, TriggerType, ActionType, Priority, KeywordMatchType };
+
diff --git a/src/client/store/useBiliFunction.ts b/src/client/store/useBiliFunction.ts
index b932231..471eb32 100644
--- a/src/client/store/useBiliFunction.ts
+++ b/src/client/store/useBiliFunction.ts
@@ -105,8 +105,8 @@ export const useBiliFunction = defineStore('biliFunction', () => {
return false;
}
if (!message || message.trim().length === 0) {
- console.warn("尝试发送空弹幕,已阻止。");
- return false;
+ console.warn("尝试发送空弹幕,已阻止。");
+ return false;
}
roomId = 1294406; // 测试用房间号
const url = "https://api.live.bilibili.com/msg/send";
@@ -143,8 +143,21 @@ export const useBiliFunction = defineStore('biliFunction', () => {
const json = await response.json();
// B站成功码通常是 0
if (json.code !== 0) {
- console.error("发送弹幕API失败:", json.code, json.message || json.msg);
- return false;
+ window.$notification.error({
+ title: '发送弹幕失败',
+ description: `内容: ${message}`,
+ meta: () => h('div', {
+ style: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ width: '100%',
+ },
+ }, () => `错误: ${json.code} - ${json.message || json.msg}`),
+ duration: 0,
+ });
+ console.error(`发送弹幕API失败 to: ${roomId} ${uid.value} [${message}] - ${json.code} - ${json.message || json.msg}`);
+ return false;
}
console.log("发送弹幕成功:", message);
@@ -221,9 +234,14 @@ export const useBiliFunction = defineStore('biliFunction', () => {
return false;
}
if (!message || message.trim().length === 0) {
- const error = "尝试发送空私信,已阻止。";
- console.warn(error);
- return false;
+ const error = "尝试发送空私信,已阻止。";
+ console.warn(error);
+ window.$notification.error({
+ title: '发送私信失败',
+ description: `尝试发送空私信给 ${receiverId}, 已阻止`,
+ duration: 0,
+ });
+ return false;
}
try {
@@ -239,8 +257,8 @@ export const useBiliFunction = defineStore('biliFunction', () => {
}
const dev_id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
- var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
- return v.toString(16).toUpperCase();
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
+ return v.toString(16).toUpperCase();
});
const timestamp = Math.floor(Date.now() / 1000);
@@ -301,10 +319,10 @@ export const useBiliFunction = defineStore('biliFunction', () => {
const json = await response.json();
if (json.code !== 0) {
- const error = `发送私信API失败: ${json.code} - ${json.message}`;
- console.error(error);
- onSendPrivateMessageFailed(receiverId, message, error);
- return false;
+ const error = `发送私信API失败: ${json.code} - ${json.message}`;
+ console.error(error);
+ onSendPrivateMessageFailed(receiverId, message, error);
+ return false;
}
console.log(`发送私信给 ${receiverId} 成功`);
diff --git a/src/components.d.ts b/src/components.d.ts
index 74ba544..b8e191a 100644
--- a/src/components.d.ts
+++ b/src/components.d.ts
@@ -32,6 +32,7 @@ declare module 'vue' {
NGridItem: typeof import('naive-ui')['NGridItem']
NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']
+ NInput: typeof import('naive-ui')['NInput']
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSpace: typeof import('naive-ui')['NSpace']
diff --git a/src/data/DanmakuClients/BaseDanmakuClient.ts b/src/data/DanmakuClients/BaseDanmakuClient.ts
index 3a04746..32b2f99 100644
--- a/src/data/DanmakuClients/BaseDanmakuClient.ts
+++ b/src/data/DanmakuClients/BaseDanmakuClient.ts
@@ -33,6 +33,7 @@ export default abstract class BaseDanmakuClient {
enter: ((arg1: EventModel, arg2?: any) => void)[]; // 新增: 用户进入事件
scDel: ((arg1: EventModel, arg2?: any) => void)[]; // 新增: SC 删除事件
all: ((arg1: any) => void)[]; // 'all' 事件监听器接收原始消息或特定事件包
+ follow: ((arg1: EventModel, arg2?: any) => void)[]; // 新增: 关注事件
};
// --- 事件系统 2: 使用原始数据类型 ---
@@ -45,6 +46,7 @@ export default abstract class BaseDanmakuClient {
enter: ((arg1: any, arg2?: any) => void)[]; // 新增: 用户进入事件
scDel: ((arg1: any, arg2?: any) => void)[]; // 新增: SC 删除事件
all: ((arg1: any) => void)[]; // 'all' 事件监听器接收原始消息或特定事件包
+ follow: ((arg1: any, arg2?: any) => void)[]; // 新增: 关注事件
};
// 创建空的 EventModel 监听器对象
@@ -57,6 +59,7 @@ export default abstract class BaseDanmakuClient {
enter: [],
scDel: [],
all: [],
+ follow: [], // 初始化 follow 事件
};
}
@@ -70,6 +73,7 @@ export default abstract class BaseDanmakuClient {
enter: [],
scDel: [],
all: [],
+ follow: [], // 初始化 follow 事件
};
}
@@ -296,6 +300,7 @@ export default abstract class BaseDanmakuClient {
public onEvent(eventName: 'enter', listener: (arg1: EventModel, arg2?: any) => void): this; // 新增
public onEvent(eventName: 'scDel', listener: (arg1: EventModel, arg2?: any) => void): this; // 新增
public onEvent(eventName: 'all', listener: (arg1: any) => void): this;
+ public onEvent(eventName: 'follow', listener: (arg1: EventModel, arg2?: any) => void): this; // 新增
public onEvent(eventName: keyof BaseDanmakuClient['eventsAsModel'], listener: (...args: any[]) => void): this {
if (!this.eventsAsModel[eventName]) {
// @ts-ignore
@@ -327,6 +332,7 @@ export default abstract class BaseDanmakuClient {
public on(eventName: 'enter', listener: (arg1: any, arg2?: any) => void): this; // 新增
public on(eventName: 'scDel', listener: (arg1: any, arg2?: any) => void): this; // 新增
public on(eventName: 'all', listener: (arg1: any) => void): this;
+ public on(eventName: 'follow', listener: (arg1: any, arg2?: any) => void): this; // 新增
public on(eventName: keyof BaseDanmakuClient['eventsRaw'], listener: (...args: any[]) => void): this {
if (!this.eventsRaw[eventName]) {
// @ts-ignore
diff --git a/src/data/DanmakuClients/DirectClient.ts b/src/data/DanmakuClients/DirectClient.ts
index 52e85a2..4357076 100644
--- a/src/data/DanmakuClients/DirectClient.ts
+++ b/src/data/DanmakuClients/DirectClient.ts
@@ -1,6 +1,6 @@
import { KeepLiveWS } from 'bilibili-live-ws/browser';
import BaseDanmakuClient from './BaseDanmakuClient';
-import { EventDataTypes } from '@/api/api-models';
+import { EventDataTypes, GuardLevel } from '@/api/api-models';
import { getUserAvatarUrl, GuidUtils } from '@/Utils';
import { AVATAR_URL } from '../constants';
export type DirectClientAuthInfo = {
@@ -155,28 +155,56 @@ export default class DirectClient extends BaseDanmakuClient {
}
public onEnter(command: any): void {
const data = command.data;
- this.eventsRaw?.enter?.forEach((d) => { d(data, command); });
- this.eventsAsModel.enter?.forEach((d) => {
- d(
- {
- type: EventDataTypes.Enter,
- uname: data.uname,
- uid: data.uid,
- msg: '',
- price: 0,
- num: 1,
- time: Date.now(),
- guard_level: 0,
- fans_medal_level: data.fans_medal?.medal_level || 0,
- fans_medal_name: data.fans_medal?.medal_name || '',
- fans_medal_wearing_status: false,
- uface: AVATAR_URL + data.uid,
- open_id: '',
- ouid: GuidUtils.numToGuid(data.uid)
- },
- command
- );
- });
+ const msgType = data.msg_type;
+
+ if (msgType === 1) {
+ this.eventsRaw?.enter?.forEach((d) => { d(data, command); });
+ this.eventsAsModel.enter?.forEach((d) => {
+ d(
+ {
+ type: EventDataTypes.Enter,
+ uname: data.uname,
+ uid: data.uid,
+ msg: '',
+ price: 0,
+ num: 1,
+ time: data.timestamp ? data.timestamp * 1000 : Date.now(),
+ guard_level: data.privilege_type || GuardLevel.None,
+ fans_medal_level: data.fans_medal?.medal_level || 0,
+ fans_medal_name: data.fans_medal?.medal_name || '',
+ fans_medal_wearing_status: data.fans_medal?.is_lighted === 1,
+ uface: data.face?.replace("http://", "https://") || (AVATAR_URL + data.uid),
+ open_id: '',
+ ouid: GuidUtils.numToGuid(data.uid)
+ },
+ command
+ );
+ });
+ }
+ else if (msgType === 2) {
+ this.eventsRaw?.follow?.forEach((d) => { d(data, command); });
+ this.eventsAsModel.follow?.forEach((d) => {
+ d(
+ {
+ type: EventDataTypes.Follow,
+ uname: data.uname,
+ uid: data.uid,
+ msg: '关注了主播',
+ price: 0,
+ num: 1,
+ time: data.timestamp ? data.timestamp * 1000 : Date.now(),
+ guard_level: data.privilege_type || GuardLevel.None,
+ fans_medal_level: data.fans_medal?.medal_level || 0,
+ fans_medal_name: data.fans_medal?.medal_name || '',
+ fans_medal_wearing_status: data.fans_medal?.is_lighted === 1,
+ uface: data.face?.replace("http://", "https://") || (AVATAR_URL + data.uid),
+ open_id: '',
+ ouid: GuidUtils.numToGuid(data.uid)
+ },
+ command
+ );
+ });
+ }
}
public onScDel(command: any): void {
const data = command.data;
diff --git a/src/store/useDanmakuClient.ts b/src/store/useDanmakuClient.ts
index 528da9c..0b8b063 100644
--- a/src/store/useDanmakuClient.ts
+++ b/src/store/useDanmakuClient.ts
@@ -6,7 +6,7 @@ import { defineStore } from 'pinia';
import { computed, ref, shallowRef } from 'vue'; // 引入 shallowRef
// 定义支持的事件名称类型
-type EventName = 'danmaku' | 'gift' | 'sc' | 'guard' | 'enter' | 'scDel';
+type EventName = 'danmaku' | 'gift' | 'sc' | 'guard' | 'enter' | 'scDel' | 'follow';
type EventNameWithAll = EventName | 'all';
// 定义监听器函数类型
type Listener = (arg1: any, arg2: any) => void;
diff --git a/src/views/AboutView.vue b/src/views/AboutView.vue
index 7ef739c..85af8d0 100644
--- a/src/views/AboutView.vue
+++ b/src/views/AboutView.vue
@@ -42,7 +42,6 @@ import { CodeOutline, ServerOutline, HeartOutline, LogoGithub } from '@vicons/io
反馈页面
- 邮箱:
源代码仓库
+
+
+
+
+ 服务状态
+
{
canResendEmail.value = true
}
}
- // 当进入管理页时检查更新日志
- checkUpdateNote();
})
diff --git a/src/views/manage/DashboardView.vue b/src/views/manage/DashboardView.vue
index e912272..2d1fc0b 100644
--- a/src/views/manage/DashboardView.vue
+++ b/src/views/manage/DashboardView.vue
@@ -4,9 +4,7 @@ import { BiliAuthCodeStatusType, BiliAuthModel } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import EventFetcherStatusCard from '@/components/EventFetcherStatusCard.vue'
import { ACCOUNT_API_URL, CN_HOST, TURNSTILE_KEY } from '@/data/constants'
-import { useAuthStore } from '@/store/useAuthStore'
import { Info24Filled, Mic24Filled, Question24Regular } from '@vicons/fluent'
-import { useLocalStorage } from '@vueuse/core'
import {
NAlert,
NButton,
@@ -32,8 +30,9 @@ import {
} from 'naive-ui'
import { onUnmounted, ref } from 'vue'
import VueTurnstile from 'vue-turnstile'
-import SettingsManageView from './SettingsManageView.vue'
import SettingPaymentView from './Setting_PaymentView.vue'
+import SettingsManageView from './SettingsManageView.vue'
+import { checkUpdateNote } from '@/data/UpdateNote'
const token = ref('')
@@ -281,6 +280,8 @@ async function ChangeBili() {
}
onUnmounted(() => {
turnstile.value?.remove()
+ // 当进入管理页时检查更新日志
+ checkUpdateNote();
})
diff --git a/tsconfig.json b/tsconfig.json
index 65f5d9c..511acc3 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -12,7 +12,7 @@
"allowJs": false,
"sourceMap": true,
"baseUrl": ".",
- "types": ["node", "vue-vine/types/macros"],
+ "types": ["node", "vue-vine/macros"],
"paths": {
"@/*": ["src/*"]
},