mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
feat: 在 ClientAutoAction 组件中新增操作历史标签页和相关功能
- 引入 ActionHistoryViewer 组件,展示执行历史。 - 更新主标签页逻辑,调整为操作管理和执行历史两个标签。 - 在自动操作逻辑中增加弹幕和私信发送历史记录功能,提升操作追踪能力。
This commit is contained in:
@@ -32,6 +32,7 @@ import AutoActionEditor from './components/autoaction/AutoActionEditor.vue';
|
||||
import GlobalScheduledSettings from './components/autoaction/settings/GlobalScheduledSettings.vue';
|
||||
import TimerCountdown from './components/autoaction/TimerCountdown.vue';
|
||||
import DataManager from './components/autoaction/DataManager.vue';
|
||||
import ActionHistoryViewer from './components/autoaction/ActionHistoryViewer.vue';
|
||||
|
||||
const autoActionStore = useAutoAction();
|
||||
const message = useMessage();
|
||||
@@ -54,6 +55,7 @@ const typeMap = {
|
||||
};
|
||||
|
||||
const activeTab = ref(TriggerType.GIFT);
|
||||
const activeMainTab = ref('action-management');
|
||||
const showAddModal = ref(false);
|
||||
const selectedTriggerType = ref<TriggerType>(TriggerType.GIFT);
|
||||
const editingActionId = ref<string | null>(null);
|
||||
@@ -499,194 +501,214 @@ onUnmounted(() => {
|
||||
size="large"
|
||||
>
|
||||
<NTabs
|
||||
v-model:value="activeTab"
|
||||
v-model:value="activeMainTab"
|
||||
type="line"
|
||||
animated
|
||||
@update:value="editingActionId = null"
|
||||
>
|
||||
<!-- 操作管理标签页 -->
|
||||
<NTabPane
|
||||
v-for="(label, type) in typeMap"
|
||||
:key="type"
|
||||
:name="type"
|
||||
:tab="label"
|
||||
name="action-management"
|
||||
tab="操作管理"
|
||||
>
|
||||
<NSpace vertical>
|
||||
<NSpace
|
||||
v-if="enabledTriggerTypes"
|
||||
align="center"
|
||||
style="padding: 8px 0; margin-bottom: 8px"
|
||||
>
|
||||
<NSwitch
|
||||
:value="enabledTriggerTypes[type]"
|
||||
@update:value="toggleTypeStatus(type)"
|
||||
/>
|
||||
<span>{{ enabledTriggerTypes[type] ? '启用' : '禁用' }}所有{{ label }}</span>
|
||||
</NSpace>
|
||||
|
||||
<NAlert
|
||||
v-if="type === TriggerType.GUARD && webFetcherStore.webfetcherType === 'openlive'"
|
||||
type="warning"
|
||||
title="功能限制提醒"
|
||||
style="margin-bottom: 12px;"
|
||||
:bordered="false"
|
||||
>
|
||||
当前连接模式 (OpenLive) 无法获取用户UID,因此无法执行【发送私信】操作。如需使用私信功能,请考虑切换至直连模式。
|
||||
</NAlert>
|
||||
|
||||
<NSpace
|
||||
justify="end"
|
||||
style="margin-bottom: 12px;"
|
||||
>
|
||||
<NPopconfirm
|
||||
:negative-text="'取消'"
|
||||
:positive-text="'确认测试'"
|
||||
@positive-click="() => handleTestClick(type as TriggerType)"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
size="small"
|
||||
type="warning"
|
||||
ghost
|
||||
>
|
||||
测试 {{ label }} 类型
|
||||
</NButton>
|
||||
</template>
|
||||
{{ `确认模拟一个 ${label} 事件来测试所有启用的 ${label} 操作吗?\n注意:这可能会发送真实的消息、执行操作,并可能触发B站风控限制。` }}
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
|
||||
<div v-if="activeTab === TriggerType.SCHEDULED">
|
||||
<GlobalScheduledSettings />
|
||||
<div
|
||||
v-if="enabledTriggerTypes && enabledTriggerTypes[TriggerType.SCHEDULED] && autoActionStore.globalSchedulingMode === 'sequential' && autoActionStore.nextScheduledAction"
|
||||
class="next-action-display"
|
||||
>
|
||||
<NDivider style="margin: 12px 0 8px 0;" />
|
||||
<NSpace
|
||||
align="center"
|
||||
justify="space-between"
|
||||
>
|
||||
<NText type="success">
|
||||
<NIcon
|
||||
:component="Target24Filled"
|
||||
style="vertical-align: -0.15em; margin-right: 4px;"
|
||||
/>
|
||||
下一个执行:
|
||||
<NTag
|
||||
type="info"
|
||||
size="small"
|
||||
round
|
||||
>
|
||||
{{ autoActionStore.nextScheduledAction?.name || '未命名操作' }}
|
||||
</NTag>
|
||||
</NText>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
text
|
||||
icon-placement="right"
|
||||
size="small"
|
||||
@click="openSetNextModal"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="Edit16Regular" />
|
||||
</template>
|
||||
手动指定
|
||||
</NButton>
|
||||
</template>
|
||||
手动设置下一个要执行的操作
|
||||
</NTooltip>
|
||||
</NSpace>
|
||||
</div>
|
||||
<NAlert
|
||||
v-else-if="enabledTriggerTypes && !enabledTriggerTypes[TriggerType.SCHEDULED]"
|
||||
type="warning"
|
||||
:bordered="false"
|
||||
style="margin-bottom: 12px;"
|
||||
>
|
||||
定时发送类型已被禁用,所有相关操作不会执行。
|
||||
</NAlert>
|
||||
</div>
|
||||
<NEmpty
|
||||
v-if="groupedActions[type].length === 0"
|
||||
description="暂无自动操作"
|
||||
>
|
||||
<template #extra>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="() => { selectedTriggerType = type as TriggerType; showAddModal = true; }"
|
||||
>
|
||||
添加{{ typeMap[type as TriggerType] }}
|
||||
</NButton>
|
||||
</template>
|
||||
</NEmpty>
|
||||
|
||||
<div
|
||||
v-else-if="editingActionId === null"
|
||||
class="overview-container"
|
||||
>
|
||||
<NDataTable
|
||||
:bordered="false"
|
||||
:single-line="false"
|
||||
:columns="getColumnsForType(type as TriggerType)"
|
||||
:data="groupedActions[type]"
|
||||
:row-key="(row: AutoActionItem) => row.id"
|
||||
>
|
||||
<template #empty>
|
||||
<NEmpty description="暂无数据" />
|
||||
</template>
|
||||
</NDataTable>
|
||||
|
||||
<NButton
|
||||
type="default"
|
||||
style="width: 100%; margin-top: 16px;"
|
||||
class="btn-with-transition"
|
||||
@click="() => { selectedTriggerType = type as TriggerType; showAddModal = true; }"
|
||||
>
|
||||
+ 添加{{ typeMap[type as TriggerType] }}
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="edit-container"
|
||||
<NTabs
|
||||
v-model:value="activeTab"
|
||||
type="segment"
|
||||
animated
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<NTabPane
|
||||
v-for="(label, type) in typeMap"
|
||||
:key="type"
|
||||
:name="type"
|
||||
:tab="label"
|
||||
>
|
||||
<NSpace vertical>
|
||||
<NButton
|
||||
size="small"
|
||||
style="align-self: flex-start; margin-bottom: 8px"
|
||||
class="back-btn btn-with-transition"
|
||||
@click="backToOverview"
|
||||
<NSpace
|
||||
v-if="enabledTriggerTypes"
|
||||
align="center"
|
||||
style="padding: 8px 0; margin-bottom: 8px"
|
||||
>
|
||||
← 返回列表
|
||||
</NButton>
|
||||
<NSwitch
|
||||
:value="enabledTriggerTypes[type]"
|
||||
@update:value="toggleTypeStatus(type)"
|
||||
/>
|
||||
<span>{{ enabledTriggerTypes[type] ? '启用' : '禁用' }}所有{{ label }}</span>
|
||||
</NSpace>
|
||||
|
||||
<transition-group
|
||||
name="fade-slide"
|
||||
tag="div"
|
||||
<NAlert
|
||||
v-if="type === TriggerType.GUARD && webFetcherStore.webfetcherType === 'openlive'"
|
||||
type="warning"
|
||||
title="功能限制提醒"
|
||||
style="margin-bottom: 12px;"
|
||||
:bordered="false"
|
||||
>
|
||||
<div
|
||||
v-for="action in groupedActions[type]"
|
||||
v-show="action.id === editingActionId"
|
||||
:key="action.id"
|
||||
class="action-item"
|
||||
当前连接模式 (OpenLive) 无法获取用户UID,因此无法执行【发送私信】操作。如需使用私信功能,请考虑切换至直连模式。
|
||||
</NAlert>
|
||||
|
||||
<NSpace
|
||||
justify="end"
|
||||
style="margin-bottom: 12px;"
|
||||
>
|
||||
<NPopconfirm
|
||||
:negative-text="'取消'"
|
||||
:positive-text="'确认测试'"
|
||||
@positive-click="() => handleTestClick(type as TriggerType)"
|
||||
>
|
||||
<AutoActionEditor :action="action" />
|
||||
<template #trigger>
|
||||
<NButton
|
||||
size="small"
|
||||
type="warning"
|
||||
ghost
|
||||
>
|
||||
测试 {{ label }} 类型
|
||||
</NButton>
|
||||
</template>
|
||||
{{ `确认模拟一个 ${label} 事件来测试所有启用的 ${label} 操作吗?\n注意:这可能会发送真实的消息、执行操作,并可能触发B站风控限制。` }}
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
|
||||
<div v-if="activeTab === TriggerType.SCHEDULED">
|
||||
<GlobalScheduledSettings />
|
||||
<div
|
||||
v-if="enabledTriggerTypes && enabledTriggerTypes[TriggerType.SCHEDULED] && autoActionStore.globalSchedulingMode === 'sequential' && autoActionStore.nextScheduledAction"
|
||||
class="next-action-display"
|
||||
>
|
||||
<NDivider style="margin: 12px 0 8px 0;" />
|
||||
<NSpace
|
||||
align="center"
|
||||
justify="space-between"
|
||||
>
|
||||
<NText type="success">
|
||||
<NIcon
|
||||
:component="Target24Filled"
|
||||
style="vertical-align: -0.15em; margin-right: 4px;"
|
||||
/>
|
||||
下一个执行:
|
||||
<NTag
|
||||
type="info"
|
||||
size="small"
|
||||
round
|
||||
>
|
||||
{{ autoActionStore.nextScheduledAction?.name || '未命名操作' }}
|
||||
</NTag>
|
||||
</NText>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
text
|
||||
icon-placement="right"
|
||||
size="small"
|
||||
@click="openSetNextModal"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="Edit16Regular" />
|
||||
</template>
|
||||
手动指定
|
||||
</NButton>
|
||||
</template>
|
||||
手动设置下一个要执行的操作
|
||||
</NTooltip>
|
||||
</NSpace>
|
||||
</div>
|
||||
</transition-group>
|
||||
<NAlert
|
||||
v-else-if="enabledTriggerTypes && !enabledTriggerTypes[TriggerType.SCHEDULED]"
|
||||
type="warning"
|
||||
:bordered="false"
|
||||
style="margin-bottom: 12px;"
|
||||
>
|
||||
定时发送类型已被禁用,所有相关操作不会执行。
|
||||
</NAlert>
|
||||
</div>
|
||||
<NEmpty
|
||||
v-if="groupedActions[type].length === 0"
|
||||
description="暂无自动操作"
|
||||
>
|
||||
<template #extra>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="() => { selectedTriggerType = type as TriggerType; showAddModal = true; }"
|
||||
>
|
||||
添加{{ typeMap[type as TriggerType] }}
|
||||
</NButton>
|
||||
</template>
|
||||
</NEmpty>
|
||||
|
||||
<div
|
||||
v-else-if="editingActionId === null"
|
||||
class="overview-container"
|
||||
>
|
||||
<NDataTable
|
||||
:bordered="false"
|
||||
:single-line="false"
|
||||
:columns="getColumnsForType(type as TriggerType)"
|
||||
:data="groupedActions[type]"
|
||||
:row-key="(row: AutoActionItem) => row.id"
|
||||
>
|
||||
<template #empty>
|
||||
<NEmpty description="暂无数据" />
|
||||
</template>
|
||||
</NDataTable>
|
||||
|
||||
<NButton
|
||||
type="default"
|
||||
style="width: 100%; margin-top: 16px;"
|
||||
class="btn-with-transition"
|
||||
@click="() => { selectedTriggerType = type as TriggerType; showAddModal = true; }"
|
||||
>
|
||||
+ 添加{{ typeMap[type as TriggerType] }}
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="edit-container"
|
||||
>
|
||||
<NSpace vertical>
|
||||
<NButton
|
||||
size="small"
|
||||
style="align-self: flex-start; margin-bottom: 8px"
|
||||
class="back-btn btn-with-transition"
|
||||
@click="backToOverview"
|
||||
>
|
||||
← 返回列表
|
||||
</NButton>
|
||||
|
||||
<transition-group
|
||||
name="fade-slide"
|
||||
tag="div"
|
||||
>
|
||||
<div
|
||||
v-for="action in groupedActions[type]"
|
||||
v-show="action.id === editingActionId"
|
||||
:key="action.id"
|
||||
class="action-item"
|
||||
>
|
||||
<AutoActionEditor :action="action" />
|
||||
</div>
|
||||
</transition-group>
|
||||
</NSpace>
|
||||
</div>
|
||||
</NSpace>
|
||||
</div>
|
||||
</NSpace>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</NTabPane>
|
||||
|
||||
<!-- 新增数据管理标签页 -->
|
||||
<!-- 历史记录标签页 -->
|
||||
<NTabPane
|
||||
name="action-history"
|
||||
tab="执行历史"
|
||||
>
|
||||
<ActionHistoryViewer />
|
||||
</NTabPane>
|
||||
|
||||
<!-- 数据管理标签页 -->
|
||||
<NTabPane
|
||||
name="data-manager"
|
||||
tab="数据管理"
|
||||
>
|
||||
<DataManager />
|
||||
</NTabPane>
|
||||
<!-- 结束新增 -->
|
||||
</NTabs>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
|
||||
278
src/client/components/autoaction/ActionHistoryViewer.vue
Normal file
278
src/client/components/autoaction/ActionHistoryViewer.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, h } from 'vue';
|
||||
import {
|
||||
NSpace, NSelect, NButton, NDataTable, NPopconfirm,
|
||||
NCard, NEmpty, NTag, NTabPane, NTabs, NTime,
|
||||
useMessage, NIcon, NTooltip, NSpin
|
||||
} from 'naive-ui';
|
||||
import { ArrowClockwise16Filled, Delete16Filled, CheckmarkCircle16Filled, DismissCircle16Filled } from '@vicons/fluent';
|
||||
import { HistoryType, HistoryItem, getHistoryByType, clearHistory, clearAllHistory } from '../../store/autoAction/utils/historyLogger';
|
||||
|
||||
const message = useMessage();
|
||||
const loading = ref(true);
|
||||
const activeTab = ref(HistoryType.DANMAKU);
|
||||
const historyData = ref<Record<HistoryType, HistoryItem[]>>({
|
||||
[HistoryType.DANMAKU]: [],
|
||||
[HistoryType.PRIVATE_MSG]: [],
|
||||
[HistoryType.COMMAND]: []
|
||||
});
|
||||
|
||||
// 类型名称映射
|
||||
const typeNameMap = {
|
||||
[HistoryType.DANMAKU]: '弹幕发送',
|
||||
[HistoryType.PRIVATE_MSG]: '私信发送',
|
||||
[HistoryType.COMMAND]: '命令执行'
|
||||
};
|
||||
|
||||
// 刷新间隔(毫秒)
|
||||
const refreshInterval = 10000;
|
||||
let refreshTimer: number | null = null;
|
||||
|
||||
// 列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '时间',
|
||||
key: 'timestamp',
|
||||
width: 180,
|
||||
render: (row: HistoryItem) => {
|
||||
return h(NTime, {
|
||||
time: new Date(row.timestamp),
|
||||
format: 'yyyy-MM-dd HH:mm:ss'
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作名称',
|
||||
key: 'actionName',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
title: '内容',
|
||||
key: 'content',
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '目标',
|
||||
key: 'target',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'success',
|
||||
width: 100,
|
||||
render: (row: HistoryItem) => {
|
||||
if (row.success) {
|
||||
return h(
|
||||
NTooltip,
|
||||
{ trigger: 'hover' },
|
||||
{
|
||||
trigger: () => h(
|
||||
NTag,
|
||||
{ type: 'success', size: 'small', round: true },
|
||||
{ default: () => '成功', icon: () => h(NIcon, { component: CheckmarkCircle16Filled }) }
|
||||
),
|
||||
default: () => '执行成功'
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return h(
|
||||
NTooltip,
|
||||
{ trigger: 'hover' },
|
||||
{
|
||||
trigger: () => h(
|
||||
NTag,
|
||||
{ type: 'error', size: 'small', round: true },
|
||||
{ default: () => '失败', icon: () => h(NIcon, { component: DismissCircle16Filled }) }
|
||||
),
|
||||
default: () => row.error || '执行失败'
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
// 加载历史数据
|
||||
async function loadHistory() {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 并行加载所有类型的历史
|
||||
const [danmakuHistory, privateMsgHistory, commandHistory] = await Promise.all([
|
||||
getHistoryByType(HistoryType.DANMAKU),
|
||||
getHistoryByType(HistoryType.PRIVATE_MSG),
|
||||
getHistoryByType(HistoryType.COMMAND)
|
||||
]);
|
||||
|
||||
historyData.value = {
|
||||
[HistoryType.DANMAKU]: danmakuHistory,
|
||||
[HistoryType.PRIVATE_MSG]: privateMsgHistory,
|
||||
[HistoryType.COMMAND]: commandHistory
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('加载历史数据失败:', error);
|
||||
message.error('加载历史数据失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 清除历史
|
||||
async function handleClearHistory(type: HistoryType) {
|
||||
try {
|
||||
await clearHistory(type);
|
||||
historyData.value[type] = [];
|
||||
message.success(`已清空${typeNameMap[type]}历史`);
|
||||
} catch (error) {
|
||||
console.error('清除历史失败:', error);
|
||||
message.error('清除历史失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 清除所有历史
|
||||
async function handleClearAllHistory() {
|
||||
try {
|
||||
await clearAllHistory();
|
||||
Object.keys(historyData.value).forEach((type) => {
|
||||
historyData.value[type as HistoryType] = [];
|
||||
});
|
||||
message.success('已清空所有历史记录');
|
||||
} catch (error) {
|
||||
console.error('清除所有历史失败:', error);
|
||||
message.error('清除所有历史失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 开始定时刷新
|
||||
function startRefreshTimer() {
|
||||
stopRefreshTimer();
|
||||
refreshTimer = window.setInterval(() => {
|
||||
loadHistory();
|
||||
}, refreshInterval);
|
||||
}
|
||||
|
||||
// 停止定时刷新
|
||||
function stopRefreshTimer() {
|
||||
if (refreshTimer !== null) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadHistory();
|
||||
startRefreshTimer();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopRefreshTimer();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="history-viewer">
|
||||
<NSpace vertical>
|
||||
<NSpace justify="space-between">
|
||||
<div class="history-title">
|
||||
操作执行历史
|
||||
</div>
|
||||
<NSpace>
|
||||
<NButton
|
||||
size="small"
|
||||
:loading="loading"
|
||||
@click="loadHistory"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="ArrowClockwise16Filled" />
|
||||
</template>
|
||||
刷新
|
||||
</NButton>
|
||||
<NPopconfirm
|
||||
placement="bottom"
|
||||
@positive-click="handleClearAllHistory"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
size="small"
|
||||
type="error"
|
||||
ghost
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="Delete16Filled" />
|
||||
</template>
|
||||
清空所有历史
|
||||
</NButton>
|
||||
</template>
|
||||
确定要清空所有类型的历史记录吗?此操作不可恢复。
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
|
||||
<NTabs
|
||||
v-model:value="activeTab"
|
||||
type="line"
|
||||
animated
|
||||
>
|
||||
<NTabPane
|
||||
v-for="(label, type) in typeNameMap"
|
||||
:key="type"
|
||||
:name="type"
|
||||
:tab="label"
|
||||
>
|
||||
<NSpin :show="loading">
|
||||
<NSpace vertical>
|
||||
<NSpace justify="end">
|
||||
<NPopconfirm
|
||||
placement="bottom"
|
||||
@positive-click="() => handleClearHistory(type as HistoryType)"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
size="small"
|
||||
type="warning"
|
||||
ghost
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="Delete16Filled" />
|
||||
</template>
|
||||
清空{{ label }}历史
|
||||
</NButton>
|
||||
</template>
|
||||
确定要清空所有{{ label }}历史记录吗?此操作不可恢复。
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
|
||||
<NDataTable
|
||||
:columns="columns"
|
||||
:data="historyData[type as HistoryType]"
|
||||
:bordered="false"
|
||||
:pagination="{
|
||||
pageSize: 10,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 50],
|
||||
}"
|
||||
:row-key="row => row.id"
|
||||
>
|
||||
<template #empty>
|
||||
<NEmpty description="暂无历史记录" />
|
||||
</template>
|
||||
</NDataTable>
|
||||
</NSpace>
|
||||
</NSpin>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</NSpace>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.history-viewer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.history-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -11,6 +11,7 @@ import { buildExecutionContext, getRandomTemplate } from './utils';
|
||||
import { evaluateTemplateExpressions } from './expressionEvaluator';
|
||||
import { evaluateExpression } from './utils';
|
||||
import { useBiliCookie } from '../useBiliCookie';
|
||||
import { logDanmakuHistory, logPrivateMsgHistory, logCommandHistory } from './utils/historyLogger';
|
||||
|
||||
/**
|
||||
* 过滤有效的自动操作项
|
||||
@@ -223,11 +224,57 @@ export function executeActions(
|
||||
if (action.actionConfig.delaySeconds && action.actionConfig.delaySeconds > 0) {
|
||||
setTimeout(() => {
|
||||
handlers.sendLiveDanmaku!(roomId, message)
|
||||
.catch(err => console.error(`[AutoAction] 发送弹幕失败 (${action.name || action.id}):`, err));
|
||||
.then(success => {
|
||||
// 记录弹幕发送历史
|
||||
logDanmakuHistory(
|
||||
action.id,
|
||||
action.name || '未命名操作',
|
||||
message,
|
||||
roomId,
|
||||
success,
|
||||
success ? undefined : '发送失败'
|
||||
).catch(err => console.error('记录弹幕历史失败:', err));
|
||||
return success;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(`[AutoAction] 发送弹幕失败 (${action.name || action.id}):`, err);
|
||||
// 记录失败的发送
|
||||
logDanmakuHistory(
|
||||
action.id,
|
||||
action.name || '未命名操作',
|
||||
message,
|
||||
roomId,
|
||||
false,
|
||||
err.toString()
|
||||
).catch(e => console.error('记录弹幕历史失败:', e));
|
||||
});
|
||||
}, action.actionConfig.delaySeconds * 1000);
|
||||
} else {
|
||||
handlers.sendLiveDanmaku(roomId, message)
|
||||
.catch(err => console.error(`[AutoAction] 发送弹幕失败 (${action.name || action.id}):`, err));
|
||||
.then(success => {
|
||||
// 记录弹幕发送历史
|
||||
logDanmakuHistory(
|
||||
action.id,
|
||||
action.name || '未命名操作',
|
||||
message,
|
||||
roomId,
|
||||
success,
|
||||
success ? undefined : '发送失败'
|
||||
).catch(err => console.error('记录弹幕历史失败:', err));
|
||||
return success;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(`[AutoAction] 发送弹幕失败 (${action.name || action.id}):`, err);
|
||||
// 记录失败的发送
|
||||
logDanmakuHistory(
|
||||
action.id,
|
||||
action.name || '未命名操作',
|
||||
message,
|
||||
roomId,
|
||||
false,
|
||||
err.toString()
|
||||
).catch(e => console.error('记录弹幕历史失败:', e));
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -249,6 +296,16 @@ export function executeActions(
|
||||
const sendPmPromise = (uid: number, msg: string) => {
|
||||
return handlers.sendPrivateMessage!(uid, msg)
|
||||
.then(success => {
|
||||
// 记录私信发送历史
|
||||
logPrivateMsgHistory(
|
||||
action.id,
|
||||
action.name || '未命名操作',
|
||||
msg,
|
||||
uid,
|
||||
success,
|
||||
success ? undefined : '发送失败'
|
||||
).catch(err => console.error('记录私信历史失败:', err));
|
||||
|
||||
if (success && options?.onSuccess) {
|
||||
// 发送成功后调用 onSuccess 回调
|
||||
options.onSuccess(action, context);
|
||||
@@ -257,6 +314,15 @@ export function executeActions(
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(`[AutoAction] 发送私信失败 (${action.name || action.id}):`, err);
|
||||
// 记录失败的发送
|
||||
logPrivateMsgHistory(
|
||||
action.id,
|
||||
action.name || '未命名操作',
|
||||
msg,
|
||||
uid,
|
||||
false,
|
||||
err.toString()
|
||||
).catch(e => console.error('记录私信历史失败:', e));
|
||||
return false; // 明确返回 false 表示失败
|
||||
});
|
||||
};
|
||||
@@ -276,8 +342,22 @@ export function executeActions(
|
||||
break;
|
||||
|
||||
case ActionType.EXECUTE_COMMAND:
|
||||
// 执行自定义命令(未实现)
|
||||
console.warn(`[AutoAction] 暂不支持执行自定义命令: ${action.name || action.id}`);
|
||||
// 执行自定义命令
|
||||
const command = processTemplate(action, context);
|
||||
if (command) {
|
||||
// 更新冷却时间
|
||||
runtimeState.lastExecutionTime[action.id] = Date.now();
|
||||
|
||||
// 目前只记录执行历史,具体实现可在未来扩展
|
||||
logCommandHistory(
|
||||
action.id,
|
||||
action.name || '未命名操作',
|
||||
command,
|
||||
true
|
||||
).catch(err => console.error('记录命令执行历史失败:', err));
|
||||
|
||||
console.warn(`[AutoAction] 暂不支持执行自定义命令: ${action.name || action.id}`);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
145
src/client/store/autoAction/utils/historyLogger.ts
Normal file
145
src/client/store/autoAction/utils/historyLogger.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { get, set, del, update } from 'idb-keyval';
|
||||
import { ActionType } from '../types';
|
||||
|
||||
// 历史记录类型常量
|
||||
export enum HistoryType {
|
||||
DANMAKU = 'danmaku',
|
||||
PRIVATE_MSG = 'privateMsg',
|
||||
COMMAND = 'command',
|
||||
}
|
||||
|
||||
// 历史记录项结构
|
||||
export interface HistoryItem {
|
||||
id: string; // 唯一ID
|
||||
actionId: string; // 操作ID
|
||||
actionName: string; // 操作名称
|
||||
actionType: ActionType; // 操作类型
|
||||
timestamp: number; // 执行时间戳
|
||||
content: string; // 发送的内容
|
||||
target?: string; // 目标(如UID或房间ID)
|
||||
success: boolean; // 是否成功
|
||||
error?: string; // 错误信息(如果有)
|
||||
}
|
||||
|
||||
// 每种类型的历史记录容量
|
||||
const HISTORY_CAPACITY = 1000;
|
||||
|
||||
// 使用IDB存储的键名
|
||||
const HISTORY_KEYS = {
|
||||
[HistoryType.DANMAKU]: 'autoAction_history_danmaku',
|
||||
[HistoryType.PRIVATE_MSG]: 'autoAction_history_privateMsg',
|
||||
[HistoryType.COMMAND]: 'autoAction_history_command',
|
||||
};
|
||||
|
||||
// 环形队列添加记录
|
||||
async function addToCircularQueue(key: string, item: HistoryItem, capacity: number): Promise<void> {
|
||||
await update<HistoryItem[]>(key, (history = []) => {
|
||||
// 添加到队列末尾
|
||||
history.push(item);
|
||||
|
||||
// 如果超出容量,移除最旧的记录
|
||||
if (history.length > capacity) {
|
||||
history.splice(0, history.length - capacity);
|
||||
}
|
||||
return history;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录弹幕发送历史
|
||||
*/
|
||||
export async function logDanmakuHistory(
|
||||
actionId: string,
|
||||
actionName: string,
|
||||
content: string,
|
||||
roomId: number,
|
||||
success: boolean,
|
||||
error?: string
|
||||
): Promise<void> {
|
||||
const historyItem: HistoryItem = {
|
||||
id: `d_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
|
||||
actionId,
|
||||
actionName,
|
||||
actionType: ActionType.SEND_DANMAKU,
|
||||
timestamp: Date.now(),
|
||||
content,
|
||||
target: roomId.toString(),
|
||||
success,
|
||||
error
|
||||
};
|
||||
|
||||
await addToCircularQueue(HISTORY_KEYS[HistoryType.DANMAKU], historyItem, HISTORY_CAPACITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录私信发送历史
|
||||
*/
|
||||
export async function logPrivateMsgHistory(
|
||||
actionId: string,
|
||||
actionName: string,
|
||||
content: string,
|
||||
userId: number,
|
||||
success: boolean,
|
||||
error?: string
|
||||
): Promise<void> {
|
||||
const historyItem: HistoryItem = {
|
||||
id: `p_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
|
||||
actionId,
|
||||
actionName,
|
||||
actionType: ActionType.SEND_PRIVATE_MSG,
|
||||
timestamp: Date.now(),
|
||||
content,
|
||||
target: userId.toString(),
|
||||
success,
|
||||
error
|
||||
};
|
||||
|
||||
await addToCircularQueue(HISTORY_KEYS[HistoryType.PRIVATE_MSG], historyItem, HISTORY_CAPACITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录命令执行历史
|
||||
*/
|
||||
export async function logCommandHistory(
|
||||
actionId: string,
|
||||
actionName: string,
|
||||
content: string,
|
||||
success: boolean,
|
||||
error?: string
|
||||
): Promise<void> {
|
||||
const historyItem: HistoryItem = {
|
||||
id: `c_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
|
||||
actionId,
|
||||
actionName,
|
||||
actionType: ActionType.EXECUTE_COMMAND,
|
||||
timestamp: Date.now(),
|
||||
content,
|
||||
success,
|
||||
error
|
||||
};
|
||||
|
||||
await addToCircularQueue(HISTORY_KEYS[HistoryType.COMMAND], historyItem, HISTORY_CAPACITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取历史记录
|
||||
*/
|
||||
export async function getHistoryByType(type: HistoryType): Promise<HistoryItem[]> {
|
||||
return await get<HistoryItem[]>(HISTORY_KEYS[type]) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除历史记录
|
||||
*/
|
||||
export async function clearHistory(type: HistoryType): Promise<void> {
|
||||
await del(HISTORY_KEYS[type]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有历史记录
|
||||
*/
|
||||
export async function clearAllHistory(): Promise<void> {
|
||||
await Promise.all(
|
||||
Object.values(HistoryType).map(type => clearHistory(type as HistoryType))
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user