feat(governance): unify durable event retention

This commit is contained in:
jxxghp
2026-08-26 12:45:44 +08:00
parent f5dacf79c3
commit 362f606751
29 changed files with 948 additions and 121 deletions
+5
View File
@@ -167,6 +167,11 @@ class ChainRuntimeConfig:
data_cleanup_site_userdata_days: Any = 0
data_cleanup_transfer_history_days: Any = 0
data_cleanup_download_failure_days: Any = 0
data_cleanup_subscribe_history_days: Any = 0
data_cleanup_agent_chat_days: Any = 0
data_cleanup_agent_task_run_days: Any = 0
data_cleanup_outbox_completed_days: Any = 0
data_cleanup_outbox_dead_days: Any = 0
download_subtitle: bool = True
lyrics_batch_timeout: int = 120
music_metadata_to_simplified: bool = True
+107 -1
View File
@@ -6,7 +6,7 @@
import json
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Any, Callable, ContextManager, Dict, Optional, Protocol
from app.application.configuration import get_chain_runtime_config_snapshot
@@ -26,6 +26,11 @@ class CleanupPolicy:
site_userdata_days: int
transfer_history_days: int
download_failure_days: int
subscribe_history_days: int
agent_chat_days: int
agent_task_run_days: int
outbox_completed_days: int
outbox_dead_days: int
@dataclass(frozen=True, slots=True)
@@ -73,6 +78,26 @@ class CleanupRepository(Protocol):
"""删除已经过期的下载失败冷却记录。"""
...
def delete_subscribe_history(self, db: Any, cutoff: str, limit: int) -> int:
"""删除早于截止时间的订阅历史。"""
...
def delete_agent_chats(self, db: Any, cutoff: str, limit: int) -> int:
"""删除早于截止时间且未被任务引用的 Agent 会话。"""
...
def delete_agent_task_runs(self, db: Any, cutoff: str, limit: int) -> int:
"""删除早于截止时间且不再承担恢复语义的 Agent 运行历史。"""
...
def delete_outbox_completed(self, db: Any, cutoff: str, limit: int) -> int:
"""删除早于截止时间的 Outbox 已完成记录。"""
...
def delete_outbox_dead(self, db: Any, cutoff: str, limit: int) -> int:
"""删除早于截止时间的 Outbox 死信记录。"""
...
class CleanupUnitOfWork(Protocol):
"""数据维护每一批删除所需的最小事务能力。"""
@@ -249,6 +274,29 @@ class DataCleanupService:
policy.download_failure_days,
"%Y-%m-%d %H:%M:%S",
)
subscribe_history_cutoff = self._cutoff(
started_at,
policy.subscribe_history_days,
"%Y-%m-%d %H:%M:%S",
)
agent_chat_cutoff = self._cutoff(
started_at,
policy.agent_chat_days,
"%Y-%m-%d %H:%M:%S",
)
agent_task_run_cutoff = self._cutoff(
started_at,
policy.agent_task_run_days,
"%Y-%m-%d %H:%M:%S",
)
outbox_completed_cutoff = self._outbox_cutoff(
started_at,
policy.outbox_completed_days,
)
outbox_dead_cutoff = self._outbox_cutoff(
started_at,
policy.outbox_dead_days,
)
return [
CleanupPlan(
"message",
@@ -296,6 +344,46 @@ class DataCleanupService:
db, download_failure_cutoff, batch_size
),
),
CleanupPlan(
"subscribehistory",
policy.subscribe_history_days,
subscribe_history_cutoff,
lambda db: self._repository.delete_subscribe_history(
db, subscribe_history_cutoff, batch_size
),
),
CleanupPlan(
"agentchat",
policy.agent_chat_days,
agent_chat_cutoff,
lambda db: self._repository.delete_agent_chats(
db, agent_chat_cutoff, batch_size
),
),
CleanupPlan(
"agenttaskrun",
policy.agent_task_run_days,
agent_task_run_cutoff,
lambda db: self._repository.delete_agent_task_runs(
db, agent_task_run_cutoff, batch_size
),
),
CleanupPlan(
"outbox_completed",
policy.outbox_completed_days,
outbox_completed_cutoff,
lambda db: self._repository.delete_outbox_completed(
db, outbox_completed_cutoff, batch_size
),
),
CleanupPlan(
"outbox_dead",
policy.outbox_dead_days,
outbox_dead_cutoff,
lambda db: self._repository.delete_outbox_dead(
db, outbox_dead_cutoff, batch_size
),
),
]
def _cleanup_in_batches(
@@ -326,6 +414,19 @@ class DataCleanupService:
"""按兼容格式计算一个清理截止时间。"""
return (started_at - timedelta(days=retention_days)).strftime(pattern)
@staticmethod
def _outbox_cutoff(started_at: datetime, retention_days: int) -> str:
"""按 Outbox 的 UTC ISO 格式生成可排序截止时间。"""
aware_started_at = (
started_at.astimezone()
if started_at.tzinfo is None
else started_at
)
return (
aware_started_at.astimezone(timezone.utc)
- timedelta(days=retention_days)
).isoformat()
def read_cleanup_policy() -> CleanupPolicy:
"""读取并规范化当前数据清理配置,单次运行期间保持快照一致。"""
@@ -337,6 +438,11 @@ def read_cleanup_policy() -> CleanupPolicy:
site_userdata_days=_normalize_days(config.data_cleanup_site_userdata_days),
transfer_history_days=_normalize_days(config.data_cleanup_transfer_history_days),
download_failure_days=_normalize_days(config.data_cleanup_download_failure_days),
subscribe_history_days=_normalize_days(config.data_cleanup_subscribe_history_days),
agent_chat_days=_normalize_days(config.data_cleanup_agent_chat_days),
agent_task_run_days=_normalize_days(config.data_cleanup_agent_task_run_days),
outbox_completed_days=_normalize_days(config.data_cleanup_outbox_completed_days),
outbox_dead_days=_normalize_days(config.data_cleanup_outbox_dead_days),
)
+10 -2
View File
@@ -17,18 +17,28 @@ T = TypeVar("T")
SUBSCRIBE_ADDED_TOPIC = "subscribe.added"
SUBSCRIBE_MODIFIED_TOPIC = "subscribe.modified"
SUBSCRIBE_DELETED_TOPIC = "subscribe.deleted"
SUBSCRIBE_COMPLETED_TOPIC = "subscribe.complete"
DOWNLOAD_ADDED_TOPIC = "download.added"
TRANSFER_COMPLETED_TOPIC = "transfer.completed"
TRANSFER_FAILED_TOPIC = "transfer.failed"
SUBTITLE_TRANSFER_COMPLETED_TOPIC = "transfer.subtitle.completed"
SUBTITLE_TRANSFER_FAILED_TOPIC = "transfer.subtitle.failed"
AUDIO_TRANSFER_COMPLETED_TOPIC = "transfer.audio.completed"
AUDIO_TRANSFER_FAILED_TOPIC = "transfer.audio.failed"
OUTBOX_LEASE_SECONDS = 60
DURABLE_EVENT_TOPICS: Mapping[EventType, str] = MappingProxyType({
EventType.SubscribeAdded: SUBSCRIBE_ADDED_TOPIC,
EventType.SubscribeModified: SUBSCRIBE_MODIFIED_TOPIC,
EventType.SubscribeDeleted: SUBSCRIBE_DELETED_TOPIC,
EventType.SubscribeComplete: SUBSCRIBE_COMPLETED_TOPIC,
EventType.DownloadAdded: DOWNLOAD_ADDED_TOPIC,
EventType.TransferComplete: TRANSFER_COMPLETED_TOPIC,
EventType.TransferFailed: TRANSFER_FAILED_TOPIC,
EventType.SubtitleTransferComplete: SUBTITLE_TRANSFER_COMPLETED_TOPIC,
EventType.SubtitleTransferFailed: SUBTITLE_TRANSFER_FAILED_TOPIC,
EventType.AudioTransferComplete: AUDIO_TRANSFER_COMPLETED_TOPIC,
EventType.AudioTransferFailed: AUDIO_TRANSFER_FAILED_TOPIC,
})
@@ -96,7 +106,6 @@ class OutboxRepository(Protocol):
) -> None:
"""记录有限退避或 dead-letter 终态。"""
class AsyncOutboxTransaction(Protocol):
"""异步业务事务暂存并收口 durable intent 的最小端口。"""
@@ -236,7 +245,6 @@ class OutboxDispatcher:
"""释放 dispatcher 工厂创建的短生命周期持久化资源。"""
self._close()
_configured_dispatcher: Callable[[], OutboxDispatcher] | None = None
+2 -1
View File
@@ -10,6 +10,7 @@ from typing import Any, Protocol
from app.application.outbox import (
OUTBOX_LEASE_SECONDS,
OutboxIntent,
SUBSCRIBE_COMPLETED_TOPIC,
SyncOutboxTransaction,
SyncUnitOfWork,
)
@@ -77,7 +78,7 @@ class CompleteSubscriptionCommand:
self._outbox.stage(
OutboxIntent(
event_key=event_key,
topic="subscribe.complete",
topic=SUBSCRIBE_COMPLETED_TOPIC,
payload=event_payload,
),
now,
+87 -91
View File
@@ -41,7 +41,14 @@ from app.application.history import (
is_skip_action,
record_transfer_failure,
)
from app.application.outbox import TRANSFER_COMPLETED_TOPIC, TRANSFER_FAILED_TOPIC
from app.application.outbox import (
AUDIO_TRANSFER_COMPLETED_TOPIC,
AUDIO_TRANSFER_FAILED_TOPIC,
SUBTITLE_TRANSFER_COMPLETED_TOPIC,
SUBTITLE_TRANSFER_FAILED_TOPIC,
TRANSFER_COMPLETED_TOPIC,
TRANSFER_FAILED_TOPIC,
)
from app.application.transfer import (
FailedRetryScheduler,
JobManager,
@@ -146,6 +153,53 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
"transfer_history_id": history_id,
}
def _durable_transfer_event(
self,
task: TransferTask,
*,
success: bool,
) -> Optional[tuple[str, EventType]]:
"""返回当前整理结果应持久化的 topic 与兼容事件类型。"""
if success:
if self._is_primary_media_file(task.fileitem, task.mediainfo):
return TRANSFER_COMPLETED_TOPIC, EventType.TransferComplete
if self._is_subtitle_file(task.fileitem):
return (
SUBTITLE_TRANSFER_COMPLETED_TOPIC,
EventType.SubtitleTransferComplete,
)
if self._is_audio_file(task.fileitem):
return AUDIO_TRANSFER_COMPLETED_TOPIC, EventType.AudioTransferComplete
return None
if self._is_media_file(task.fileitem):
return TRANSFER_FAILED_TOPIC, EventType.TransferFailed
if self._is_subtitle_file(task.fileitem):
return SUBTITLE_TRANSFER_FAILED_TOPIC, EventType.SubtitleTransferFailed
if self._is_audio_file(task.fileitem):
return AUDIO_TRANSFER_FAILED_TOPIC, EventType.AudioTransferFailed
return None
def _publish_transfer_result(
self,
event_type: EventType,
payload: dict[str, Any],
) -> None:
"""显式分派整理结果,使运行契约可追踪每种事件的生产者。"""
if event_type is EventType.TransferComplete:
self.eventmanager.send_event(EventType.TransferComplete, payload)
elif event_type is EventType.TransferFailed:
self.eventmanager.send_event(EventType.TransferFailed, payload)
elif event_type is EventType.SubtitleTransferComplete:
self.eventmanager.send_event(EventType.SubtitleTransferComplete, payload)
elif event_type is EventType.SubtitleTransferFailed:
self.eventmanager.send_event(EventType.SubtitleTransferFailed, payload)
elif event_type is EventType.AudioTransferComplete:
self.eventmanager.send_event(EventType.AudioTransferComplete, payload)
elif event_type is EventType.AudioTransferFailed:
self.eventmanager.send_event(EventType.AudioTransferFailed, payload)
else:
raise ValueError(f"不支持的整理结果事件:{event_type}")
def __init__(self) -> None:
"""初始化文件整理处理链。"""
super().__init__()
@@ -479,14 +533,16 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
fileid=task.fileitem.fileid if task.fileitem else None,
)
durable_event = self._durable_transfer_event(task, success=False)
durable_transfer_failed = bool(
getattr(self, "durable_event_writer", None)
and self._is_media_file(task.fileitem)
getattr(self, "durable_event_writer", None) and durable_event
)
if durable_transfer_failed:
assert durable_event is not None
topic, event_type = durable_event
event_payload = self._transfer_result_payload(task, transferinfo)
history = self.durable_event_writer.transfer_result(
topic=TRANSFER_FAILED_TOPIC,
topic=topic,
stage_history=lambda writer: add_transfer_fail(
fileitem=task.fileitem,
mode=transferinfo.transfer_type if transferinfo else "",
@@ -498,9 +554,8 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
transfer_history_oper=writer,
),
event_payload=event_payload,
publish=lambda payload: self.eventmanager.send_event(
EventType.TransferFailed,
payload,
publish=lambda payload: self._publish_transfer_result(
event_type, payload
),
)
else:
@@ -515,45 +570,15 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
transfer_history_oper=transferhis,
)
# 整理失败事件
if self._is_media_file(task.fileitem):
if not durable_transfer_failed:
# 显式旧测试上下文仍走原始发送;正式上下文由 outbox writer 发布。
self.eventmanager.send_event(
EventType.TransferFailed,
self._transfer_result_payload(
task,
transferinfo,
history.id if history else None,
),
)
elif self._is_subtitle_file(task.fileitem):
# 字幕整理失败事件
self.eventmanager.send_event(
EventType.SubtitleTransferFailed,
{
"fileitem": task.fileitem,
"meta": task.meta,
"mediainfo": task.mediainfo,
"transferinfo": transferinfo,
"downloader": task.downloader,
"download_hash": task.download_hash,
"transfer_history_id": history.id if history else None,
},
)
elif self._is_audio_file(task.fileitem):
# 音频文件整理失败事件
self.eventmanager.send_event(
EventType.AudioTransferFailed,
{
"fileitem": task.fileitem,
"meta": task.meta,
"mediainfo": task.mediainfo,
"transferinfo": transferinfo,
"downloader": task.downloader,
"download_hash": task.download_hash,
"transfer_history_id": history.id if history else None,
},
if durable_event and not durable_transfer_failed:
# 显式旧测试上下文仍走原始发送;正式上下文由 outbox writer 发布。
self._publish_transfer_result(
durable_event[1],
self._transfer_result_payload(
task,
transferinfo,
history.id if history else None,
),
)
self.queue_failed_transfer_notification(
@@ -592,14 +617,16 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
task.fileitem.storage if task.fileitem else None,
)
durable_event = self._durable_transfer_event(task, success=True)
durable_transfer_complete = bool(
getattr(self, "durable_event_writer", None)
and self._is_primary_media_file(task.fileitem, task.mediainfo)
getattr(self, "durable_event_writer", None) and durable_event
)
if durable_transfer_complete:
assert durable_event is not None
topic, event_type = durable_event
event_payload = self._transfer_result_payload(task, transferinfo)
history = self.durable_event_writer.transfer_result(
topic=TRANSFER_COMPLETED_TOPIC,
topic=topic,
stage_history=lambda writer: add_transfer_success(
fileitem=task.fileitem,
mode=transferinfo.transfer_type if transferinfo else "",
@@ -611,9 +638,8 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
transfer_history_oper=writer,
),
event_payload=event_payload,
publish=lambda payload: self.eventmanager.send_event(
EventType.TransferComplete,
payload,
publish=lambda payload: self._publish_transfer_result(
event_type, payload
),
)
else:
@@ -628,45 +654,15 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
transfer_history_oper=transferhis,
)
# task整理完成事件
if self._is_primary_media_file(task.fileitem, task.mediainfo):
if not durable_transfer_complete:
# 显式旧测试上下文仍走原始发送;正式上下文由 outbox writer 发布。
self.eventmanager.send_event(
EventType.TransferComplete,
self._transfer_result_payload(
task,
transferinfo,
history.id if history else None,
),
)
elif self._is_subtitle_file(task.fileitem):
# 字幕整理完成事件
self.eventmanager.send_event(
EventType.SubtitleTransferComplete,
{
"fileitem": task.fileitem,
"meta": task.meta,
"mediainfo": task.mediainfo,
"transferinfo": transferinfo,
"downloader": task.downloader,
"download_hash": task.download_hash,
"transfer_history_id": history.id if history else None,
},
)
elif self._is_audio_file(task.fileitem):
# 音频文件整理完成事件
self.eventmanager.send_event(
EventType.AudioTransferComplete,
{
"fileitem": task.fileitem,
"meta": task.meta,
"mediainfo": task.mediainfo,
"transferinfo": transferinfo,
"downloader": task.downloader,
"download_hash": task.download_hash,
"transfer_history_id": history.id if history else None,
},
if durable_event and not durable_transfer_complete:
# 显式旧测试上下文仍走原始发送;正式上下文由 outbox writer 发布。
self._publish_transfer_result(
durable_event[1],
self._transfer_result_payload(
task,
transferinfo,
history.id if history else None,
),
)
# task登记转移成功文件清单
-1
View File
@@ -176,7 +176,6 @@ class SqlAlchemyOutboxRepository:
)
self._session.commit()
class SqlAlchemyAsyncOutboxStager:
"""只负责把 outbox 意图加入调用方异步事务。"""
+121
View File
@@ -2,10 +2,18 @@
from typing import Any, Callable, ContextManager
from sqlalchemy import delete, exists, select
from app.db.base import execute_dml
from app.db.models.agentchat import AgentChat
from app.db.models.agenttask import AgentTask
from app.db.models.agenttaskrun import AgentTaskRun
from app.db.models.downloadfailure import DownloadFailure
from app.db.models.downloadhistory import DownloadFiles, DownloadHistory
from app.db.models.message import Message
from app.db.models.outbox import OutboxMessage
from app.db.models.siteuserdata import SiteUserData
from app.db.models.subscribehistory import SubscribeHistory
from app.db.models.transferhistory import TransferHistory
from app.db.uow import SqlAlchemyUnitOfWork
@@ -67,3 +75,116 @@ class DatabaseCleanupRepository:
before_time=cutoff,
limit=limit,
)
@staticmethod
def delete_subscribe_history(db: Any, cutoff: str, limit: int) -> int:
"""分批删除超过用户保留期的已完成订阅快照。"""
return DatabaseCleanupRepository._delete_selected_ids(
db=db,
model=SubscribeHistory,
condition=SubscribeHistory.date < cutoff,
limit=limit,
)
@staticmethod
def delete_agent_chats(db: Any, cutoff: str, limit: int) -> int:
"""清理旧会话,但保留仍被 Agent 定时任务引用的上下文。"""
task_reference = exists(
select(AgentTask.id).where(AgentTask.session_id == AgentChat.session_id)
)
return DatabaseCleanupRepository._delete_selected_ids(
db=db,
model=AgentChat,
condition=(AgentChat.updated_at < cutoff) & ~task_reference,
limit=limit,
)
@staticmethod
def delete_agent_task_runs(db: Any, cutoff: str, limit: int) -> int:
"""清理旧终态运行,但保留运行中和父任务最后一次运行。"""
latest_run_reference = exists(
select(AgentTask.id).where(AgentTask.last_run_id == AgentTaskRun.run_id)
)
return DatabaseCleanupRepository._delete_selected_ids(
db=db,
model=AgentTaskRun,
condition=(
(AgentTaskRun.started_at < cutoff)
& (AgentTaskRun.status != "running")
& ~latest_run_reference
),
limit=limit,
)
@staticmethod
def delete_outbox_completed(db: Any, cutoff: str, limit: int) -> int:
"""删除过期 completed 记录,事务提交由维护用例统一控制。"""
return DatabaseCleanupRepository._delete_outbox_status(
db=db,
status="completed",
timestamp_column=OutboxMessage.completed_at,
cutoff=cutoff,
limit=limit,
)
@staticmethod
def delete_outbox_dead(db: Any, cutoff: str, limit: int) -> int:
"""删除过期 dead-letter 记录,保留 pending/processing 恢复语义。"""
return DatabaseCleanupRepository._delete_outbox_status(
db=db,
status="dead",
timestamp_column=OutboxMessage.next_retry_at,
cutoff=cutoff,
limit=limit,
)
@staticmethod
def _delete_outbox_status(
*,
db: Any,
status: str,
timestamp_column: Any,
cutoff: str,
limit: int,
) -> int:
"""先锁定有限 ID 再删除,避免一次维护事务无界膨胀。"""
message_ids = db.execute(
select(OutboxMessage.id)
.where(
OutboxMessage.status == status,
timestamp_column.is_not(None),
timestamp_column < cutoff,
)
.order_by(OutboxMessage.id)
.limit(limit)
).scalars().all()
if not message_ids:
return 0
return execute_dml(
db,
delete(OutboxMessage).where(OutboxMessage.id.in_(message_ids)),
execution_options={"synchronize_session": False},
)
@staticmethod
def _delete_selected_ids(
*,
db: Any,
model: Any,
condition: Any,
limit: int,
) -> int:
"""按安全谓词锁定有限主键并暂存删除。"""
record_ids = db.execute(
select(model.id)
.where(condition)
.order_by(model.id)
.limit(limit)
).scalars().all()
if not record_ids:
return 0
return execute_dml(
db,
delete(model).where(model.id.in_(record_ids)),
execution_options={"synchronize_session": False},
)
+1
View File
@@ -46,6 +46,7 @@ class AgentChat(Base):
Index("ix_agentchat_session_user", "session_id", "user_id"),
Index("ix_agentchat_user_updated", "user_id", "updated_at", "id"),
Index("ix_agentchat_channel_updated", "channel", "updated_at", "id"),
Index("ix_agentchat_updated_id", "updated_at", "id"),
)
@classmethod
+1
View File
@@ -37,6 +37,7 @@ class AgentTaskRun(Base):
__table_args__ = (
Index("ix_agenttaskrun_run_id", "run_id", unique=True),
Index("ix_agenttaskrun_task_started", "task_id", "started_at", "id"),
Index("ix_agenttaskrun_status_started_id", "status", "started_at", "id"),
)
@classmethod
+1
View File
@@ -102,6 +102,7 @@ class SubscribeHistory(Base):
__table_args__ = (
media_identity_constraint("subscribehistory"),
Index('ix_subscribehistory_type_date', 'type', 'date'),
Index('ix_subscribehistory_date_id', 'date', 'id'),
Index('ix_subscribehistory_media_identity', 'media_source', 'media_id'),
)
+10
View File
@@ -218,6 +218,16 @@ class ConfigModel(BaseModel):
DATA_CLEANUP_TRANSFER_HISTORY_DAYS: int = 365 * 3
# 下载失败冷却记录保留天数,0为不清理
DATA_CLEANUP_DOWNLOAD_FAILURE_DAYS: int = 7
# 订阅完成历史保留天数,0为不清理
DATA_CLEANUP_SUBSCRIBE_HISTORY_DAYS: int = 365 * 3
# Agent 会话历史保留天数,0为不清理
DATA_CLEANUP_AGENT_CHAT_DAYS: int = 180
# Agent 定时任务运行历史保留天数,0为不清理
DATA_CLEANUP_AGENT_TASK_RUN_DAYS: int = 180
# Outbox 已完成记录保留天数,0为不清理
DATA_CLEANUP_OUTBOX_COMPLETED_DAYS: int = 30
# Outbox 死信记录保留天数,0为不清理
DATA_CLEANUP_OUTBOX_DEAD_DAYS: int = 90
# ==================== 缓存配置 ====================
# 缓存类型,支持 cachetools 和 redis,默认使用 cachetools
+5
View File
@@ -159,9 +159,14 @@ _DURABLE_REQUIRED = {
EventType.SubscribeAdded,
EventType.SubscribeModified,
EventType.SubscribeDeleted,
EventType.SubscribeComplete,
EventType.DownloadAdded,
EventType.TransferComplete,
EventType.TransferFailed,
EventType.SubtitleTransferComplete,
EventType.SubtitleTransferFailed,
EventType.AudioTransferComplete,
EventType.AudioTransferFailed,
}
_TARGET_PLUGIN = {EventType.PluginAction, EventType.PluginTriggered}
_HOST_ONLY = {EventType.SystemError, EventType.ConfigChanged, EventType.ModuleReload}
+6 -1
View File
@@ -126,6 +126,12 @@ class Scheduler(ConfigReloadMixin, metaclass=SingletonClass):
"DATA_CLEANUP_DOWNLOAD_HISTORY_DAYS",
"DATA_CLEANUP_SITE_USERDATA_DAYS",
"DATA_CLEANUP_TRANSFER_HISTORY_DAYS",
"DATA_CLEANUP_DOWNLOAD_FAILURE_DAYS",
"DATA_CLEANUP_SUBSCRIBE_HISTORY_DAYS",
"DATA_CLEANUP_AGENT_CHAT_DAYS",
"DATA_CLEANUP_AGENT_TASK_RUN_DAYS",
"DATA_CLEANUP_OUTBOX_COMPLETED_DAYS",
"DATA_CLEANUP_OUTBOX_DEAD_DAYS",
"DB_BACKUP_ENABLE",
"DB_BACKUP_CRON",
"USAGE_STATISTIC_SHARE",
@@ -591,7 +597,6 @@ class Scheduler(ConfigReloadMixin, metaclass=SingletonClass):
kwargs={"job_id": "outbox_dispatch"},
replace_existing=True,
)
# CookieCloud定时同步
if (
config.cookiecloud_interval
+5
View File
@@ -120,6 +120,11 @@ def build_chain_runtime_config(settings: Settings) -> ChainRuntimeConfig:
data_cleanup_site_userdata_days=settings.DATA_CLEANUP_SITE_USERDATA_DAYS,
data_cleanup_transfer_history_days=settings.DATA_CLEANUP_TRANSFER_HISTORY_DAYS,
data_cleanup_download_failure_days=settings.DATA_CLEANUP_DOWNLOAD_FAILURE_DAYS,
data_cleanup_subscribe_history_days=settings.DATA_CLEANUP_SUBSCRIBE_HISTORY_DAYS,
data_cleanup_agent_chat_days=settings.DATA_CLEANUP_AGENT_CHAT_DAYS,
data_cleanup_agent_task_run_days=settings.DATA_CLEANUP_AGENT_TASK_RUN_DAYS,
data_cleanup_outbox_completed_days=settings.DATA_CLEANUP_OUTBOX_COMPLETED_DAYS,
data_cleanup_outbox_dead_days=settings.DATA_CLEANUP_OUTBOX_DEAD_DAYS,
download_subtitle=settings.DOWNLOAD_SUBTITLE,
music_metadata_to_simplified=settings.MUSIC_METADATA_TO_SIMPLIFIED,
recognize_plugin_first=settings.RECOGNIZE_PLUGIN_FIRST,
+27 -1
View File
@@ -374,7 +374,9 @@ def _build_outbox_dispatcher() -> OutboxDispatcher:
message.payload,
),
"subscribe.deleted.report": dispatch_subscribe_deleted_report,
"subscribe.complete": lambda message: EventManager().send_event(
durable_event_topic(
EventType.SubscribeComplete
): lambda message: EventManager().send_event(
EventType.SubscribeComplete,
message.payload,
),
@@ -398,6 +400,30 @@ def _build_outbox_dispatcher() -> OutboxDispatcher:
EventType.TransferFailed,
restore_transfer_result(message.payload),
),
durable_event_topic(
EventType.SubtitleTransferComplete
): lambda message: EventManager().send_event(
EventType.SubtitleTransferComplete,
restore_transfer_result(message.payload),
),
durable_event_topic(
EventType.SubtitleTransferFailed
): lambda message: EventManager().send_event(
EventType.SubtitleTransferFailed,
restore_transfer_result(message.payload),
),
durable_event_topic(
EventType.AudioTransferComplete
): lambda message: EventManager().send_event(
EventType.AudioTransferComplete,
restore_transfer_result(message.payload),
),
durable_event_topic(
EventType.AudioTransferFailed
): lambda message: EventManager().send_event(
EventType.AudioTransferFailed,
restore_transfer_result(message.payload),
),
}
validate_durable_event_handlers(handlers)
session = SessionFactory()