From 6e7854e8f67890b94d178bc89d247ecfd81e5377 Mon Sep 17 00:00:00 2001 From: ShukeBta <272197458+ShukeBta@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:52:04 +0800 Subject: [PATCH] Harden Telegram bot cleanup commands --- internal/service/bot_features_test.go | 102 +++++++++++++++++++++ internal/service/device_service.go | 46 ++++++++-- internal/service/telegram_api_test.go | 18 ++++ internal/service/telegram_bot.go | 14 ++- internal/service/telegram_bot_user_test.go | 17 +++- internal/service/telegram_commands.go | 20 ++-- internal/service/telegram_menu.go | 65 +++++++++++-- 7 files changed, 252 insertions(+), 30 deletions(-) diff --git a/internal/service/bot_features_test.go b/internal/service/bot_features_test.go index 7fd5614..67fe01b 100644 --- a/internal/service/bot_features_test.go +++ b/internal/service/bot_features_test.go @@ -418,6 +418,108 @@ func TestBotCleanupRulesCanBeDeletedUntilEmpty(t *testing.T) { } } +func TestBotCleanupRunPreviewsBeforeConfirm(t *testing.T) { + ctx := context.Background() + repos, bot := newBotTestService(t) + admin := &model.User{Username: "root", PasswordHash: "x", Role: "admin", IsActive: true} + if err := repos.User.Create(ctx, admin); err != nil { + t.Fatal(err) + } + now := time.Now() + old := now.Add(-30 * 24 * time.Hour) + stale := &model.User{Username: "stale", PasswordHash: "x", Role: "user", IsActive: true} + stale.CreatedAt = old + stale.LastLoginAt = &old + recent := &model.User{Username: "recent", PasswordHash: "x", Role: "user", IsActive: true} + recent.CreatedAt = old + recent.LastLoginAt = &now + newUser := &model.User{Username: "newbie", PasswordHash: "x", Role: "user", IsActive: true} + newUser.CreatedAt = now + for _, user := range []*model.User{stale, recent, newUser} { + if err := repos.User.Create(ctx, user); err != nil { + t.Fatal(err) + } + } + if err := repos.Setting.Set(ctx, SettingAccountCleanupEnabled, "true"); err != nil { + t.Fatal(err) + } + if err := repos.Setting.Set(ctx, SettingAccountCleanupKeepMode, "any"); err != nil { + t.Fatal(err) + } + if err := repos.Setting.Set(ctx, SettingAccountCleanupRules, `[ + {"id":"login_7d","name":"最近登录","type":"recent_login","enabled":true,"window_days_max":7}, + {"id":"new_7d","name":"新号宽限","type":"account_age_grace","enabled":true,"min_count":7} + ]`); err != nil { + t.Fatal(err) + } + channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"admin_user_ids":"9001"}`} + msg := &TelegramMessage{From: TelegramUser{ID: 9001, Username: "root"}, Chat: TelegramChat{ID: 9001, Type: "private"}} + + reply, err := bot.executeCommand(ctx, channel, msg, "/cleanup run") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(reply.Text, "当前只是预览") || !strings.Contains(reply.Text, "stale") || !strings.Contains(reply.Text, "/cleanup run confirm") { + t.Fatalf("cleanup run should preview candidates and confirmation command, got %q", reply.Text) + } + if got, _ := repos.User.FindByID(ctx, stale.ID); got == nil { + t.Fatal("cleanup preview must not delete the stale user") + } + + reply, err = bot.executeCommand(ctx, channel, msg, "/deleted") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(reply.Text, "当前只是预览") { + t.Fatalf("/deleted alias should preview only, got %q", reply.Text) + } + if got, _ := repos.User.FindByID(ctx, stale.ID); got == nil { + t.Fatal("/deleted preview alias must not delete users") + } + + reply, err = bot.executeCommand(ctx, channel, msg, "/cleanup run confirm") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(reply.Text, "已清理 1") { + t.Fatalf("cleanup confirm should delete exactly one stale user, got %q", reply.Text) + } + if got, _ := repos.User.FindByID(ctx, stale.ID); got != nil { + t.Fatal("stale user should be deleted after explicit confirmation") + } + for _, user := range []*model.User{recent, newUser, admin} { + if got, _ := repos.User.FindByID(ctx, user.ID); got == nil { + t.Fatalf("%s should be kept by保号 rules/protection", user.Username) + } + } +} + +func TestBotCleanupConfirmRequiresEnabledRules(t *testing.T) { + ctx := context.Background() + repos, bot := newBotTestService(t) + user := &model.User{Username: "viewer", PasswordHash: "x", Role: "user", IsActive: true} + user.CreatedAt = time.Now().Add(-30 * 24 * time.Hour) + if err := repos.User.Create(ctx, user); err != nil { + t.Fatal(err) + } + if err := repos.Setting.Set(ctx, SettingAccountCleanupEnabled, "true"); err != nil { + t.Fatal(err) + } + channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"admin_user_ids":"9001"}`} + msg := &TelegramMessage{From: TelegramUser{ID: 9001, Username: "root"}, Chat: TelegramChat{ID: 9001, Type: "private"}} + + reply, err := bot.executeCommand(ctx, channel, msg, "/cleanup run confirm") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(reply.Text, "没有启用的保号规则") { + t.Fatalf("cleanup confirm without rules should be blocked, got %q", reply.Text) + } + if got, _ := repos.User.FindByID(ctx, user.ID); got == nil { + t.Fatal("cleanup confirm without enabled rules must not delete users") + } +} + func TestBotCleanupRuleListInfersDaysAndHidesDuplicateNames(t *testing.T) { ctx := context.Background() repos, bot := newBotTestService(t) diff --git a/internal/service/device_service.go b/internal/service/device_service.go index b856b9a..349d703 100644 --- a/internal/service/device_service.go +++ b/internal/service/device_service.go @@ -219,11 +219,42 @@ func (s *DeviceService) SweepAccountCleanup(ctx context.Context) (int, error) { if !cfg.AccountCleanupEnabled { return 0, nil } - users, err := s.repo.User.List(ctx) + candidates, err := s.accountCleanupCandidates(ctx, cfg) if err != nil { return 0, err } removed := 0 + for _, candidate := range candidates { + s.notify(ctx, candidate.UserID, fmt.Sprintf("⛔️ 账号 %s 未满足保号规则,已被清理。\n规则结果:%s\n如需恢复请联系管理员。", candidate.Username, candidate.Details)) + s.log.Warn("account cleanup: deleting account", zap.String("user", candidate.Username), zap.String("details", candidate.Details)) + _ = s.repo.UserDevice.DeleteByUser(ctx, candidate.UserID) + if err := s.repo.User.Delete(ctx, candidate.UserID); err == nil { + removed++ + } + } + return removed, nil +} + +type accountCleanupCandidate struct { + UserID string + Username string + Details string +} + +func (s *DeviceService) PreviewAccountCleanup(ctx context.Context) ([]accountCleanupCandidate, error) { + cfg := loadBotConfig(ctx, s.repo) + if !cfg.AccountCleanupEnabled { + return nil, nil + } + return s.accountCleanupCandidates(ctx, cfg) +} + +func (s *DeviceService) accountCleanupCandidates(ctx context.Context, cfg botConfig) ([]accountCleanupCandidate, error) { + users, err := s.repo.User.List(ctx) + if err != nil { + return nil, err + } + candidates := make([]accountCleanupCandidate, 0) for i := range users { u := &users[i] if s.isProtected(ctx, u) || !u.IsActive { @@ -233,14 +264,13 @@ func (s *DeviceService) SweepAccountCleanup(ctx context.Context) (int, error) { if keep { continue } - s.notify(ctx, u.ID, fmt.Sprintf("⛔️ 账号 %s 未满足保号规则,已被清理。\n规则结果:%s\n如需恢复请联系管理员。", u.Username, details)) - s.log.Warn("account cleanup: deleting account", zap.String("user", u.Username), zap.String("details", details)) - _ = s.repo.UserDevice.DeleteByUser(ctx, u.ID) - if err := s.repo.User.Delete(ctx, u.ID); err == nil { - removed++ - } + candidates = append(candidates, accountCleanupCandidate{ + UserID: u.ID, + Username: u.Username, + Details: details, + }) } - return removed, nil + return candidates, nil } // KickDevice marks a device as kicked so the next request from it is rejected diff --git a/internal/service/telegram_api_test.go b/internal/service/telegram_api_test.go index fc0e4ff..55784c5 100644 --- a/internal/service/telegram_api_test.go +++ b/internal/service/telegram_api_test.go @@ -449,4 +449,22 @@ func TestTelegramCommandFiltering(t *testing.T) { t.Fatalf("%s should be supported so group slash commands get feedback", cmd) } } + for _, cmd := range []string{"/restart", "/update_bot", "/coins", "/red", "/white_channel", "/config"} { + if telegramSupportedCommand(cmd) { + t.Fatalf("%s should not be treated as supported until it has a real Mgo implementation", cmd) + } + } +} + +func TestTelegramSupportedCommandSetMatchesRegistry(t *testing.T) { + _, bot := newBotTestService(t) + channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"admin_user_ids":"9001"}`} + msg := &TelegramMessage{From: TelegramUser{ID: 9001, Username: "admin"}, Chat: TelegramChat{ID: 9001, Type: "private"}} + for _, def := range bot.telegramCommandDefinitions(t.Context(), channel, msg) { + for _, alias := range def.Aliases { + if !telegramSupportedCommand(alias) { + t.Fatalf("registered command %s must be in telegramSupportedCommandSet", alias) + } + } + } } diff --git a/internal/service/telegram_bot.go b/internal/service/telegram_bot.go index 1d7993f..7751ad0 100644 --- a/internal/service/telegram_bot.go +++ b/internal/service/telegram_bot.go @@ -282,7 +282,7 @@ func telegramPrivateMessageForUser(msg *TelegramMessage) *TelegramMessage { } func telegramGroupPrivateAdminHint() string { - return "管理命令请私聊 Bot 使用 /menu 或对应管理员命令,避免在群组公开管理面板。" + return "群组内不展示管理面板;管理员可在已绑定群组直接发送文本管理命令,涉及账号凭据的操作仍请私聊 Bot。" } func telegramGroupPrivateUserHint(action string) string { @@ -479,7 +479,7 @@ func (s *TelegramBotService) cmdHelp(ctx context.Context, msg *TelegramMessage) if telegramIsGroupChat(msg.Chat.Type) { adminHint := "" if s.telegramUserIsAdmin(ctx, channel, msg.From.ID) { - adminHint = "\n\n管理员命令和管理面板请私聊 Bot 使用,避免在群组公开。" + adminHint = "\n\n管理员可在已绑定群组直接发送文本管理命令;管理面板和账号凭据操作请私聊 Bot。" } return "MediaStationGo 群组可用命令\n\n" + "/menu — 打开群组自助菜单\n" + @@ -523,7 +523,9 @@ func (s *TelegramBotService) cmdHelp(ctx context.Context, msg *TelegramMessage) "/unbind 用户1 用户2 — 批量解绑 Telegram 绑定(管理员)\n" + "/unbind_duplicates / /unbind_inactive 天数 — 清理重复/无效绑定或久未登录绑定(管理员)\n" + "/antishare on play=3 login=3 warn=2 — 防共享策略(管理员)\n" + - "/cleanup on|off|run — 保号规则开关/巡检(管理员)\n" + + "/cleanup run — 预览保号清理候选(管理员)\n" + + "/cleanup run confirm — 确认清理候选账号(管理员)\n" + + "/cleanup on|off — 保号规则开关(管理员)\n" + "/cleanup_mode any|all|count 2 — 保号模式(管理员)\n" + "/cleanup_rule list|add|edit|修改|del|enable|disable — Mgo 保号规则(管理员)\n" + "/ban 用户名 / /unban 用户名 — 禁用/解禁用户(管理员)\n" + @@ -532,6 +534,12 @@ func (s *TelegramBotService) cmdHelp(ctx context.Context, msg *TelegramMessage) "/search 关键词 — 搜索媒体库\n" + "/downloads — 下载列表\n" + "/stats — 媒体库统计\n\n" + + "Mgo 兼容 Sakura 管理命令:\n" + + "用户:/ucr /uinfo /rmemby /only_rm_record /renewall\n" + + "审计:/userip /auditip /auditdevice /auditclient /udeviceid\n" + + "清理:/syncunbound /syncgroupm /check_ex /deleted /low_activity\n" + + "权限:/embyadmin /banall /unbanall /prouser /revuser /embylibs_blockall /embylibs_unblockall\n" + + "运维:/proadmin /revadmin /backup_db /restore_from_db\n\n" + "自动推送事件:\n" + "• 订阅命中新资源\n" + "• 下载任务完成\n" + diff --git a/internal/service/telegram_bot_user_test.go b/internal/service/telegram_bot_user_test.go index a354b9b..d4a59d1 100644 --- a/internal/service/telegram_bot_user_test.go +++ b/internal/service/telegram_bot_user_test.go @@ -167,7 +167,11 @@ func TestTelegramGroupHidesAdminPanelFromRegularUsers(t *testing.T) { func TestTelegramGroupAdminMenuDoesNotExposeButtonsInGroup(t *testing.T) { ctx := t.Context() - _, bot := newBotTestService(t) + repos, bot := newBotTestService(t) + admin := &model.User{Username: "root", PasswordHash: "x", Role: "admin", IsActive: true} + if err := repos.User.Create(ctx, admin); err != nil { + t.Fatal(err) + } channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"group_chat_id":"-100123","admin_user_ids":"9001"}`} msg := &TelegramMessage{ From: TelegramUser{ID: 9001, Username: "admin", FirstName: "Admin"}, @@ -189,6 +193,17 @@ func TestTelegramGroupAdminMenuDoesNotExposeButtonsInGroup(t *testing.T) { if !strings.Contains(reply.Text, "请私聊 Bot") || telegramReplyHasButtonPrefix(reply, "adm_") { t.Fatalf("group admin callback should not render admin panel publicly: %#v", reply) } + + reply, err := bot.executeCommand(ctx, channel, msg, "/users") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(reply.Text, "用户管理") { + t.Fatalf("bound group admin text command should run, got %q", reply.Text) + } + if len(reply.Buttons) != 0 { + t.Fatalf("group admin text command must not expose inline buttons publicly: %#v", reply.Buttons) + } } func TestTelegramPollingChannelHintWinsForPrivateMessages(t *testing.T) { diff --git a/internal/service/telegram_commands.go b/internal/service/telegram_commands.go index fb328d4..8beb424 100644 --- a/internal/service/telegram_commands.go +++ b/internal/service/telegram_commands.go @@ -134,9 +134,6 @@ func (s *TelegramBotService) telegramCommandDefinitions(ctx context.Context, cha {Aliases: []string{"/revuser"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdSakuraProtectedUser(ctx, args, false), nil }}, - {Aliases: []string{"/restart", "/update_bot", "/paolu", "/bindall_id", "/sync_favorites", "/coins", "/score", "/coinsall", "/coinsclear", "/red", "/srank", "/white_channel", "/rev_white_channel", "/unban_channel", "/config"}, AdminOnly: true, AdminOnlyText: adminOnly, Handle: func(args []string) (telegramCommandReply, error) { - return s.cmdSakuraUnsupported("Mgo 兼容命令", "这些命令涉及外部 Bot 自身运维/积分红包/皮套人管理,已在 MediaStationGo 中由权限、通知渠道、设备策略替代。"), nil - }}, } } @@ -170,15 +167,18 @@ func (s *TelegramBotService) executeCommand(ctx context.Context, channel *model. return telegramCommandReply{Text: fmt.Sprintf("未知命令: %s\n\n输入 /help 查看可用命令列表。", cmd)}, nil } if telegramIsGroupChat(msg.Chat.Type) && !def.GroupAllowed { - if def.AdminOnly && s.telegramUserIsAdmin(ctx, channel, msg.From.ID) { - return telegramCommandReply{Text: telegramGroupPrivateAdminHint()}, nil + if !def.AdminOnly || !s.telegramUserIsAdmin(ctx, channel, msg.From.ID) { + return telegramCommandReply{}, nil } - return telegramCommandReply{}, nil } if def.AdminOnly && !s.telegramUserIsAdmin(ctx, channel, msg.From.ID) { return telegramCommandReply{Text: def.AdminOnlyText}, nil } - return def.Handle(args) + reply, err := def.Handle(args) + if telegramIsGroupChat(msg.Chat.Type) && def.AdminOnly && !def.GroupAllowed { + reply.Buttons = nil + } + return reply, err } func telegramSupportedCommand(cmd string) bool { @@ -204,9 +204,7 @@ var telegramSupportedCommandSet = map[string]struct{}{ "/check_ex": {}, "/deleted": {}, "/low_activity": {}, "/uranks": {}, "/days_ranks": {}, "/week_ranks": {}, "/embyadmin": {}, "/unbanall": {}, "/banall": {}, "/embylibs_unblockall": {}, "/embylibs_blockall": {}, "/extraembylibs_unblockall": {}, "/extraembylibs_blockall": {}, "/proadmin": {}, "/revadmin": {}, - "/backup_db": {}, "/restore_from_db": {}, "/restart": {}, "/update_bot": {}, "/paolu": {}, "/bindall_id": {}, "/sync_favorites": {}, "/config": {}, - "/coins": {}, "/score": {}, "/coinsall": {}, "/coinsclear": {}, "/red": {}, "/srank": {}, "/prouser": {}, "/revuser": {}, - "/white_channel": {}, "/rev_white_channel": {}, "/unban_channel": {}, + "/backup_db": {}, "/restore_from_db": {}, "/prouser": {}, "/revuser": {}, } type telegramBotCommand struct { @@ -254,7 +252,7 @@ func telegramAdminBotCommandMenu() []telegramBotCommand { telegramBotCommand{Command: "downloads", Description: "下载列表(管理员)"}, telegramBotCommand{Command: "stats", Description: "媒体库统计(管理员)"}, telegramBotCommand{Command: "users", Description: "用户管理(管理员)"}, - telegramBotCommand{Command: "cleanup", Description: "开启关闭或执行保号巡检(管理员)"}, + telegramBotCommand{Command: "cleanup", Description: "保号清理预览/确认(管理员)"}, telegramBotCommand{Command: "cleanup_mode", Description: "设置保号规则匹配模式(管理员)"}, telegramBotCommand{Command: "cleanup_rule", Description: "Mgo保号规则管理(管理员)"}, ) diff --git a/internal/service/telegram_menu.go b/internal/service/telegram_menu.go index 87682bc..ae1cfd9 100644 --- a/internal/service/telegram_menu.go +++ b/internal/service/telegram_menu.go @@ -1125,7 +1125,7 @@ func (s *TelegramBotService) protectReason(ctx context.Context, userID string) s func (s *TelegramBotService) replyDevicePolicy(ctx context.Context) telegramCommandReply { cfg := loadBotConfig(ctx, s.repo) text := fmt.Sprintf( - "设备策略\n\n① 防共享:%s\n 并发播放上限 %d / 登录客户端上限 %d;超限会禁用账号,管理员可解禁。\n 设备指纹异常警告 %d 次后禁用账号。\n\n② Mgo 保号规则:%s\n 保号模式:%s;需要满足 %d 条;启用规则 %d 条。\n\n命令:\n/antishare on play=3 login=3 warn=2\n/cleanup on|off|run\n/cleanup_mode any|all|count 2\n/cleanup_rule list|add|edit|修改|del|enable|disable\n\n策略默认关闭;清理前会先通过 Bot 通知用户;管理员/受保护账号永不自动处理。", + "设备策略\n\n① 防共享:%s\n 并发播放上限 %d / 登录客户端上限 %d;超限会禁用账号,管理员可解禁。\n 设备指纹异常警告 %d 次后禁用账号。\n\n② Mgo 保号规则:%s\n 保号模式:%s;需要满足 %d 条;启用规则 %d 条。\n\n命令:\n/antishare on play=3 login=3 warn=2\n/cleanup run 预览候选\n/cleanup run confirm 确认清理\n/cleanup on|off\n/cleanup_mode any|all|count 2\n/cleanup_rule list|add|edit|修改|del|enable|disable\n\n策略默认关闭;清理前会先预览候选;管理员/受保护账号永不自动处理。", onOff(cfg.AntiShareEnabled), cfg.MaxConcurrentPlay, cfg.MaxLoggedClients, cfg.WarnThreshold, onOff(cfg.AccountCleanupEnabled), cleanupModeLabel(cfg.AccountCleanupKeepMode), cfg.AccountCleanupRequiredCount, countEnabledCleanupRules(cfg.AccountCleanupRules)) return telegramCommandReply{ @@ -1197,21 +1197,72 @@ func (s *TelegramBotService) cmdCleanup(ctx context.Context, args []string) tele return telegramCommandReply{Text: "关闭失败:" + err.Error()} } return s.replyDevicePolicy(ctx) - case "run", "sweep", "巡检": + case "run", "sweep", "巡检", "preview", "预览": device := s.device if device == nil { device = NewDeviceService(s.log, s.repo) } - removed, err := device.SweepAccountCleanup(ctx) - if err != nil { - return telegramCommandReply{Text: "巡检失败:" + err.Error()} + if len(args) > 1 && isCleanupConfirmArg(args[1]) { + cfg := loadBotConfig(ctx, s.repo) + if !cfg.AccountCleanupEnabled { + return telegramCommandReply{Text: "保号规则未开启,不会清理账号。"} + } + if countEnabledCleanupRules(cfg.AccountCleanupRules) == 0 { + return telegramCommandReply{Text: "没有启用的保号规则,不会清理账号。"} + } + removed, err := device.SweepAccountCleanup(ctx) + if err != nil { + return telegramCommandReply{Text: "确认清理失败:" + err.Error()} + } + return telegramCommandReply{Text: fmt.Sprintf("保号规则确认清理完成,已清理 %d 个账号。", removed)} } - return telegramCommandReply{Text: fmt.Sprintf("保号规则巡检完成,清理 %d 个账号。", removed)} + candidates, err := device.PreviewAccountCleanup(ctx) + if err != nil { + return telegramCommandReply{Text: "巡检预览失败:" + err.Error()} + } + return telegramCommandReply{Text: s.formatCleanupPreview(ctx, candidates)} default: - return telegramCommandReply{Text: "用法:/cleanup on|off|run"} + return telegramCommandReply{Text: "用法:/cleanup on|off、/cleanup run 预览、/cleanup run confirm 确认清理"} } } +func isCleanupConfirmArg(arg string) bool { + switch strings.ToLower(strings.TrimSpace(arg)) { + case "confirm", "yes", "delete", "确认", "清理", "删除": + return true + default: + return false + } +} + +func (s *TelegramBotService) formatCleanupPreview(ctx context.Context, candidates []accountCleanupCandidate) string { + cfg := loadBotConfig(ctx, s.repo) + if !cfg.AccountCleanupEnabled { + return "保号规则未开启,不会清理账号。" + } + if countEnabledCleanupRules(cfg.AccountCleanupRules) == 0 { + return "没有启用的保号规则,不会清理账号。" + } + if len(candidates) == 0 { + return "保号规则预览完成:没有需要清理的账号。" + } + var sb strings.Builder + sb.WriteString(fmt.Sprintf("保号规则预览\n\n将清理候选:%d 个账号。\n当前只是预览,未删除任何账号。\n\n", len(candidates))) + limit := len(candidates) + if limit > 10 { + limit = 10 + } + for i := 0; i < limit; i++ { + candidate := candidates[i] + sb.WriteString(fmt.Sprintf("%d. %s\n%s\n", i+1, escapeHTML(candidate.Username), escapeHTML(candidate.Details))) + } + if len(candidates) > limit { + sb.WriteString(fmt.Sprintf("……另有 %d 个候选未展示。\n", len(candidates)-limit)) + } + sb.WriteString("\n确认无误后再执行:/cleanup run confirm") + return sb.String() +} + func (s *TelegramBotService) cmdCleanupMode(ctx context.Context, args []string) telegramCommandReply { if len(args) == 0 { return telegramCommandReply{Text: "用法:/cleanup_mode any、/cleanup_mode all 或 /cleanup_mode count 2"}