Update Live-streaming code (auto-daily features)

This commit is contained in:
2026-08-15 17:45:19 +08:00
parent ba39e9743d
commit f623959fd4
19 changed files with 3517 additions and 62 deletions
+410 -49
View File
@@ -15,6 +15,9 @@ BetterGI 弹幕排队系统
上号 - 队首触发扫码上号配置组
执行 <组名> - 已确认账号的队首触发配置组(: 执行 泡泡桔)
<组名> - 兼容旧指令等同于执行
自动每日 [模式] - 启动托管的一条龙每日任务
切换队伍 <名称> - 执行切换队伍配置组
修改队员 <四人> - 执行修改队员配置组
退出 - 退出排队队列(三级队首不可用)
重置 - 一级用户重启原神和BetterGI
积分 - 查询自己的积分
@@ -105,6 +108,42 @@ if str(APP_DIR) not in sys.path:
from admin_auth import AdminAuthManager, SESSION_COOKIE_NAME
from admin_events import AdminEventBus
if __package__:
from .bettergi_current_party import (
CURRENT_PARTY_SCRIPT_NAME,
CurrentPartyReadError,
PreparedCurrentPartyRead,
clear_current_party_status,
prepare_current_party_read,
read_current_party_status,
)
from .bettergi_daily import (
DailyAutomationError,
JsonUpdate,
apply_json_updates,
prepare_commission_current_party_update,
prepare_daily_run,
prepare_edit_party_update,
prepare_switch_party_update,
)
else:
from bettergi_current_party import (
CURRENT_PARTY_SCRIPT_NAME,
CurrentPartyReadError,
PreparedCurrentPartyRead,
clear_current_party_status,
prepare_current_party_read,
read_current_party_status,
)
from bettergi_daily import (
DailyAutomationError,
JsonUpdate,
apply_json_updates,
prepare_commission_current_party_update,
prepare_daily_run,
prepare_edit_party_update,
prepare_switch_party_update,
)
from bilibili_cookie_refresh import BilibiliCookieRefresher, BilibiliCredentialStore, BilibiliQrLogin
from faster_qwen_worker import FasterQwenWorkerClient
from mpv_player import MpvPlayer
@@ -199,6 +238,9 @@ COMMAND_ALLOWED_ROLE_DEFAULTS = {
"confirm_yes": [ROLE_SUPER_ADMIN, ROLE_PENDING_OPERATOR],
"confirm_no": [ROLE_SUPER_ADMIN, ROLE_PENDING_OPERATOR],
"run": [ROLE_SUPER_ADMIN, ROLE_ACTIVE_OPERATOR],
"daily": [ROLE_SUPER_ADMIN, ROLE_ACTIVE_OPERATOR],
"switch_party": [ROLE_SUPER_ADMIN, ROLE_ACTIVE_OPERATOR],
"edit_party": [ROLE_SUPER_ADMIN, ROLE_ACTIVE_OPERATOR],
"leave": [ROLE_SUPER_ADMIN, ROLE_ACTIVE_OPERATOR, ROLE_VIEWER],
"reset": [ROLE_SUPER_ADMIN],
"points": ALL_DANMU_ROLES,
@@ -406,6 +448,13 @@ class Config:
self.data["bilibili"].setdefault("cookie_auto_refresh_enabled", True)
self.data["bilibili"].setdefault("cookie_check_interval_hours", 6)
self.data.setdefault("bettergi", {})
self.data.setdefault("daily", {})
daily_cfg = self.data["daily"]
daily_cfg.setdefault("one_dragon_template", "默认配置")
daily_cfg.setdefault("managed_one_dragon_name", "直播系统自动每日")
daily_cfg.setdefault("ley_line_craft_resin_before", True)
daily_cfg.setdefault("commission_use_current_party", True)
daily_cfg.setdefault("current_party_read_timeout_sec", 45)
self.data.setdefault("global", {})
self.data["global"].pop("admin_uidsText", None)
self.data.setdefault("queue", {})
@@ -574,6 +623,9 @@ class Config:
"confirm_yes": {"enabled": True, "aliases": [""]},
"confirm_no": {"enabled": True, "aliases": ["不是"]},
"run": {"enabled": True, "aliases": ["执行", "", "开始"]},
"daily": {"enabled": True, "aliases": ["自动每日"]},
"switch_party": {"enabled": True, "aliases": ["切换队伍", "更换队伍"]},
"edit_party": {"enabled": True, "aliases": ["修改队员", "更换队员"]},
"leave": {"enabled": True, "aliases": ["退出", "退出排队", "取消排队"]},
"reset": {"enabled": True, "aliases": ["重置"]},
"points": {"enabled": True, "aliases": ["积分"]},
@@ -629,6 +681,10 @@ class Config:
wd = self.data.get("bettergi", {}).get("work_dir", "")
return wd if wd else str(Path(self.bettergi_exe).parent)
@property
def daily_cfg(self) -> dict:
return self.data.get("daily", {})
@property
def default_group(self) -> str:
return self.data.get("queue", {}).get("default_group", "薄荷")
@@ -3050,10 +3106,11 @@ class Broadcaster:
# ============== BetterGI 日志监控 ==============
class BgiLogMonitor:
"""实时监控BetterGI日志,检测配置组完成。"""
"""实时监控 BetterGI 日志,检测配置组或一条龙完成。"""
# 完成关键字: 配置组 "组名" 执行结束
FINISH_PATTERN = "执行结束"
ONE_DRAGON_FINISH_PATTERN = "一条龙和配置组任务结束"
LOG_HEADER_PATTERN = re.compile(
r"^\[(?P<time>\d{2}:\d{2}:\d{2})(?:\.\d+)?\]"
r"\s+\[[A-Z]+\](?:\s+\[[^\]\r\n]+\])*\s+.+$"
@@ -3066,6 +3123,7 @@ class BgiLogMonitor:
self._on_finish_callback = None
self._current_group = None
self._current_run_id = None
self._current_kind = None
self._position = 0
def set_work_dir(self, work_dir: str):
@@ -3080,8 +3138,16 @@ class BgiLogMonitor:
def set_current_group(self, group_name: str | None, run_id: str | None = None):
"""设置当前配置组及任务实例编号,用于精确匹配完成事件。"""
self._current_group = group_name
self._current_run_id = str(run_id) if group_name and run_id else None
self._set_current_task(group_name, run_id, "group")
def set_current_one_dragon(self, task_name: str | None, run_id: str | None = None):
"""设置当前一条龙任务及任务实例编号。"""
self._set_current_task(task_name, run_id, "one_dragon")
def _set_current_task(self, task_name: str | None, run_id: str | None, kind: str):
self._current_group = task_name
self._current_run_id = str(run_id) if task_name and run_id else None
self._current_kind = kind if task_name else None
# 重置读取位置到文件末尾(只监听新日志)
self._position = self._get_current_log_size()
@@ -3159,17 +3225,24 @@ class BgiLogMonitor:
def _process_line(self, line: str):
if not line or not self._current_group:
return
# 匹配: 配置组 "组名" 执行结束
if (self.FINISH_PATTERN in line
and self._current_group in line
and "配置组" in line):
self.logger.info(
f"[日志监控] 检测到配置组 '{self._current_group}' 执行结束"
)
group_finished = (
self._current_kind == "group"
and self.FINISH_PATTERN in line
and self._current_group in line
and "配置组" in line
)
one_dragon_finished = (
self._current_kind == "one_dragon"
and self.ONE_DRAGON_FINISH_PATTERN in line
)
if group_finished or one_dragon_finished:
kind_label = "一条龙" if one_dragon_finished else "配置组"
self.logger.info(f"[日志监控] 检测到{kind_label} '{self._current_group}' 执行结束")
finished = self._current_group
finished_run_id = self._current_run_id
self._current_group = None
self._current_run_id = None
self._current_kind = None
if self._on_finish_callback:
asyncio.create_task(self._on_finish_callback(finished, finished_run_id))
@@ -3243,6 +3316,24 @@ class BetterGIRunner:
self.logger.error(f"[BGI] 启动失败: {e}")
return False
async def start_one_dragon(self, config_name: str) -> bool:
"""通过 BetterGI 0.63.0+ 命令行入口启动一条龙。"""
cmd = [self.exe_path, "startOneDragon", config_name]
self.logger.info(f"[BGI] 调用: {' '.join(cmd)}")
try:
await asyncio.create_subprocess_exec(
*cmd,
cwd=self.work_dir,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
await asyncio.sleep(2)
self.logger.info(f"[BGI] 一条龙启动成功: {config_name}")
return True
except Exception as e:
self.logger.error(f"[BGI] 一条龙启动失败: {e}")
return False
async def start_bgi(self) -> bool:
"""仅启动 BetterGI 主程序,不执行配置组。"""
self.logger.info(f"[BGI] 启动主程序: {self.exe_path}")
@@ -4902,6 +4993,7 @@ class CommandHandler:
self.login_monitor = None # 由QueueSystem设置
self._rule_last_trigger_at: dict[int, float] = {}
self._pending_group_confirmations: dict[int, dict[str, Any]] = {}
self._bgi_operation_lock = asyncio.Lock()
self._tts_category_context = contextvars.ContextVar(
"command_tts_category",
default=None,
@@ -5107,6 +5199,33 @@ class CommandHandler:
return key
return None
def _split_command_text(self, text: str) -> tuple[str, str]:
"""拆分指令与参数,并兼容执行类指令省略命令后的空格。"""
raw_text = str(text or "").strip()
parts = raw_text.split(None, 1)
if not parts:
return "", ""
cmd = parts[0]
arg = parts[1].strip() if len(parts) > 1 else ""
if self._get_command_key(cmd) is not None:
return cmd, arg
aliases: list[str] = []
commands = self.config.data.get("commands", {})
for command_key in ("run", "daily", "switch_party", "edit_party"):
cfg = commands.get(command_key, {})
if not cfg.get("enabled", True):
continue
aliases.extend(
str(alias).strip()
for alias in cfg.get("aliases", [])
if str(alias).strip()
)
for alias in sorted(set(aliases), key=len, reverse=True):
if raw_text.startswith(alias) and len(raw_text) > len(alias):
return alias, raw_text[len(alias):].strip()
return cmd, arg
def get_user_role(self, uid: int) -> str:
if uid in self.config.admin_uids:
return ROLE_SUPER_ADMIN
@@ -5341,19 +5460,7 @@ class CommandHandler:
if len(self.recent_danmu) > self.max_danmu:
self.recent_danmu.pop(0)
parts = text.split(None, 1)
cmd = parts[0]
if len(parts) == 1:
run_cfg = self.config.data.get("commands", {}).get("run", {})
run_aliases = sorted(
(str(alias).strip() for alias in run_cfg.get("aliases", []) if str(alias).strip()),
key=len,
reverse=True,
)
for alias in run_aliases:
if text.startswith(alias) and len(text) > len(alias):
cmd = alias
break
cmd, _ = self._split_command_text(text)
cmd_key = self._get_command_key(cmd)
is_song_command = self._is_song_request_command(cmd)
is_custom_rule_command = self._is_custom_rule_command(text)
@@ -5387,21 +5494,7 @@ class CommandHandler:
# 只有真正发送“上号”进入登录阶段后,才由 login_started_at 接管计时。
# 解析指令。执行类指令兼容省略空格,例如“执行薄荷”“跑清心”。
parts = text.split(None, 1) # 按空格分割,最多2段
cmd = parts[0]
arg = parts[1].strip() if len(parts) > 1 else ""
if not arg:
run_cfg = self.config.data.get("commands", {}).get("run", {})
run_aliases = sorted(
(str(alias).strip() for alias in run_cfg.get("aliases", []) if str(alias).strip()),
key=len,
reverse=True,
)
for alias in run_aliases:
if text.startswith(alias) and len(text) > len(alias):
cmd = alias
arg = text[len(alias):].strip()
break
cmd, arg = self._split_command_text(text)
# 用配置别名匹配内置指令。当前指令类别通过 ContextVar 传给所有
# broadcast 调用,因此无需在每个分支重复传 category。
@@ -5413,6 +5506,9 @@ class CommandHandler:
"confirm_yes": "login",
"confirm_no": "login",
"run": "execution",
"daily": "execution",
"switch_party": "execution",
"edit_party": "execution",
"leave": "queue",
"reset": "reset",
"points": "points",
@@ -5478,6 +5574,30 @@ class CommandHandler:
)
return
await self._cmd_run(uid, uname, arg)
elif cmd_key == "daily":
if not await self._ensure_user_permitted(uid, uname, scope="command", allowed_roles=self._command_allowed_roles("daily")):
return
await self._cmd_daily(uid, uname, arg)
elif cmd_key == "switch_party":
if not await self._ensure_user_permitted(uid, uname, scope="command", allowed_roles=self._command_allowed_roles("switch_party")):
return
if not arg:
await self.broadcast(
f"{uname}」请发送“切换队伍 <队伍名称>”",
tts=True,
)
return
await self._cmd_switch_party(uid, uname, arg)
elif cmd_key == "edit_party":
if not await self._ensure_user_permitted(uid, uname, scope="command", allowed_roles=self._command_allowed_roles("edit_party")):
return
if not arg:
await self.broadcast(
f"{uname}」请发送“修改队员 <队员1> <队员2> <队员3> <队员4>”",
tts=True,
)
return
await self._cmd_edit_party(uid, uname, arg)
elif cmd_key == "leave":
if not await self._ensure_user_permitted(uid, uname, scope="command", allowed_roles=self._command_allowed_roles("leave")):
return
@@ -5768,6 +5888,18 @@ class CommandHandler:
return False
async def _cmd_run(self, uid: int, uname: str, group_name: str):
async with self._bgi_operation_lock:
await self._cmd_run_locked(uid, uname, group_name)
async def _cmd_run_locked(
self,
uid: int,
uname: str,
group_name: str,
*,
updates: tuple[JsonUpdate, ...] = (),
action_description: str | None = None,
):
"""已登录队首发送"执行 组名": taskkill → 启动指定配置组"""
effective_uid = self._effective_queue_admin_uid(uid)
# 校验配置组是否存在,支持模糊匹配/错别字纠正。
@@ -5812,7 +5944,15 @@ class CommandHandler:
if was_running:
self.logger.info(f"[执行] 先停止当前任务,再启动'{group_name}'")
self.queue_mgr.interrupt_group("group_replaced", status="cancelled")
self.log_monitor.set_current_group(None)
await self.runner.kill_bgi(reason="配置组替换")
if updates:
try:
await asyncio.to_thread(apply_json_updates, updates)
except Exception as exc:
self.logger.error(f"[执行失败] 写入 BGI 配置失败: {exc}")
await self.broadcast(f"{uname}」写入 BGI 配置失败:{exc}", tts=True)
return
run_id = uuid.uuid4().hex
result = self.queue_mgr.start_group(effective_uid, group_name, run_id=run_id)
if not result["success"]:
@@ -5824,11 +5964,12 @@ class CommandHandler:
self.log_monitor.set_current_group(group_name, run_id)
ok = await self.runner.start_groups([group_name])
if ok:
action_text = action_description or f"开始执行配置组'{group_name}'"
self.logger.info(
f"[执行] {uname}({uid}) 启动'{group_name}', 每分钟扣{POINTS_PER_MINUTE}积分"
f"[执行] {uname}({uid}) {action_text}, 每分钟扣{POINTS_PER_MINUTE}积分"
)
await self.broadcast(
f"{uname}开始执行配置组'{group_name}',每分钟扣{POINTS_PER_MINUTE}积分"
f"{uname}{action_text},每分钟扣{POINTS_PER_MINUTE}积分"
)
else:
self.log_monitor.set_current_group(None)
@@ -5843,6 +5984,219 @@ class CommandHandler:
else:
await self._perform_reset("配置组启动失败,正在关闭原神并启动扫码上号")
async def _capture_current_party_name(
self,
prepared: PreparedCurrentPartyRead,
timeout_sec: float,
) -> str:
try:
timeout = max(10.0, min(120.0, float(timeout_sec or 45.0)))
except (TypeError, ValueError):
timeout = 45.0
synced = await asyncio.to_thread(
self.runner.sync_js_script,
CURRENT_PARTY_SCRIPT_NAME,
)
if not synced:
raise CurrentPartyReadError("同步当前队伍读取脚本失败")
await asyncio.to_thread(clear_current_party_status, prepared.status_path)
await asyncio.to_thread(apply_json_updates, prepared.updates)
self.logger.info(
f"[自动每日] 启动当前队伍读取配置组: {prepared.group_name}, "
f"request_id={prepared.request_id}"
)
if not await self.runner.start_groups([prepared.group_name]):
raise CurrentPartyReadError("BetterGI 当前队伍读取配置组启动失败")
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
result = await asyncio.to_thread(
read_current_party_status,
prepared.status_path,
prepared.request_id,
)
if result is not None:
await asyncio.sleep(0.75)
self.logger.info(
f"[自动每日] 当前队伍读取成功: {result.party_name}, "
f"candidates={list(result.candidates)}"
)
return result.party_name
await asyncio.sleep(0.5)
raise CurrentPartyReadError(f"读取当前队伍名称超时({timeout:g}秒)")
async def _cmd_daily(self, uid: int, uname: str, argument: str):
async with self._bgi_operation_lock:
effective_uid = self._effective_queue_admin_uid(uid)
if not self.queue_mgr.is_admin(effective_uid):
await self.broadcast(f"{uname}」你不是队首,请先排队", tts=True)
return
state = self.queue_mgr.state
if state.get("login_status") != "logged_in":
await self.broadcast(f"{uname}」请先发\"上号\"完成扫码登录", tts=True)
return
daily_cfg = self.config.daily_cfg
try:
prepared = await asyncio.to_thread(
prepare_daily_run,
self.config.bettergi_work_dir,
CONFIG_DIR / "domain_aliases.json",
argument,
template_name=str(daily_cfg.get("one_dragon_template") or "默认配置"),
managed_name=str(daily_cfg.get("managed_one_dragon_name") or "直播系统自动每日"),
ley_line_craft_resin_before=bool(
daily_cfg.get("ley_line_craft_resin_before", True)
),
commission_use_current_party=bool(
daily_cfg.get("commission_use_current_party", True)
),
)
current_party_read = None
if prepared.requires_current_party:
reader_source = INTEGRATIONS_DIR / CURRENT_PARTY_SCRIPT_NAME
if not reader_source.is_dir():
raise CurrentPartyReadError(
f"项目内缺少当前队伍读取脚本: {reader_source}"
)
current_party_read = await asyncio.to_thread(
prepare_current_party_read,
self.config.bettergi_work_dir,
uuid.uuid4().hex,
)
except DailyAutomationError as exc:
self.logger.info(f"[自动每日] 参数或 BGI 配置校验失败: {exc}")
await self.broadcast(f"{uname}」自动每日无法启动:{exc}", tts=True)
return
except Exception as exc:
self.logger.exception("[自动每日] 准备配置失败")
await self.broadcast(f"{uname}」自动每日配置生成失败:{exc}", tts=True)
return
was_running = bool(state.get("current_group") or state.get("default_running"))
if was_running:
self.queue_mgr.interrupt_group("daily_replaced", status="cancelled")
self.log_monitor.set_current_group(None)
await self.runner.kill_bgi(reason="自动每日配置更新")
daily_updates = prepared.updates
if current_party_read is not None:
try:
party_name = await self._capture_current_party_name(
current_party_read,
daily_cfg.get("current_party_read_timeout_sec", 45),
)
party_update = await asyncio.to_thread(
prepare_commission_current_party_update,
self.config.bettergi_work_dir,
party_name,
)
daily_updates = (party_update, *prepared.updates)
except CurrentPartyReadError as exc:
self.logger.warning(f"[自动每日] 读取当前队伍失败: {exc}")
await self.broadcast(
f"{uname}」自动每日无法启动:读取当前队伍失败,{exc}",
tts=True,
)
return
except Exception as exc:
self.logger.exception("[自动每日] 当前队伍准备阶段异常")
await self.broadcast(
f"{uname}」自动每日无法启动:当前队伍准备失败,{exc}",
tts=True,
)
return
state = self.queue_mgr.state
if (
not self.queue_mgr.is_admin(effective_uid)
or state.get("login_status") != "logged_in"
):
self.logger.info("[自动每日] 读取当前队伍期间队首或登录状态已变化,取消启动")
await self.broadcast(f"{uname}」登录状态已变化,自动每日已取消", tts=True)
return
try:
await asyncio.to_thread(apply_json_updates, daily_updates)
except Exception as exc:
self.logger.error(f"[自动每日] 写入 BGI 配置失败: {exc}")
await self.broadcast(f"{uname}」写入自动每日配置失败:{exc}", tts=True)
return
run_id = uuid.uuid4().hex
result = self.queue_mgr.start_group(
effective_uid,
prepared.task_name,
run_id=run_id,
)
if not result.get("success"):
await self.broadcast(f"{uname}{result.get('msg', '自动每日无法启动')}", tts=True)
return
self.log_monitor.set_current_one_dragon(prepared.task_name, run_id)
ok = await self.runner.start_one_dragon(prepared.config_name)
if ok:
self.logger.info(
f"[自动每日] {uname}({uid}) 启动'{prepared.config_name}'"
f"模式={prepared.request.summary},每分钟扣{POINTS_PER_MINUTE}积分"
)
await self.broadcast(
f"{uname}」开始自动每日({prepared.request.summary}),"
f"每分钟扣{POINTS_PER_MINUTE}积分"
)
return
self.log_monitor.set_current_one_dragon(None)
self.logger.error("[自动每日] BetterGI 一条龙启动失败,进入自动重置流程")
await self.broadcast(f"{uname}」自动每日启动失败,系统将自动重置并重新扫码", tts=True)
if self.system:
await self.system._reset_wait_and_retry_login(
effective_uid,
uname,
"自动每日启动失败,正在关闭原神并启动扫码上号",
)
else:
await self._perform_reset("自动每日启动失败,正在关闭原神并启动扫码上号")
async def _cmd_switch_party(self, uid: int, uname: str, party_name: str):
async with self._bgi_operation_lock:
try:
update = await asyncio.to_thread(
prepare_switch_party_update,
self.config.bettergi_work_dir,
party_name,
)
except DailyAutomationError as exc:
await self.broadcast(f"{uname}」切换队伍无法启动:{exc}", tts=True)
return
await self._cmd_run_locked(
uid,
uname,
"切换队伍",
updates=(update,),
action_description=f"切换队伍为'{party_name.strip()}'",
)
async def _cmd_edit_party(self, uid: int, uname: str, argument: str):
async with self._bgi_operation_lock:
try:
update, members = await asyncio.to_thread(
prepare_edit_party_update,
self.config.bettergi_work_dir,
argument,
)
except DailyAutomationError as exc:
await self.broadcast(f"{uname}」修改队员无法启动:{exc}", tts=True)
return
await self._cmd_run_locked(
uid,
uname,
"修改队员",
updates=(update,),
action_description=f"将当前队员修改为{''.join(members)}",
)
def _is_level_one_user(self, uid: int) -> bool:
"""一级用户:config.json global.admin_uids 中配置的 UID。"""
return uid in self.config.admin_uids
@@ -6167,6 +6521,9 @@ class CommandHandler:
s = self._get_cmd_aliases_str("signin", "签到")
l = self._get_cmd_aliases_str("login", "上号")
r = self._get_cmd_aliases_str("run", "执行")
daily = self._get_cmd_aliases_str("daily", "自动每日")
switch_party = self._get_cmd_aliases_str("switch_party", "切换队伍")
edit_party = self._get_cmd_aliases_str("edit_party", "修改队员")
c_yes = self._get_cmd_aliases_str("confirm_yes", "")
c_no = self._get_cmd_aliases_str("confirm_no", "不是")
leave = self._get_cmd_aliases_str("leave", "退出")
@@ -6182,6 +6539,9 @@ class CommandHandler:
f" {l} - 队首触发扫码上号\n"
f" {c_yes}/{c_no} - 确认扫码账号是否正确\n"
f" {r} <组名> - 已确认账号后执行配置组(1积分=1分钟)\n"
f" {daily} [秘境/地脉/委托] - 执行自动每日一条龙\n"
f" {switch_party} <队伍名> - 切换到指定队伍\n"
f" {edit_party} <四名角色> - 修改当前队伍角色\n"
f" {song_cmd} <歌名> - 发送点歌\n"
f" {leave} - 退出队列(三级队首不可用)\n"
f" {pts} - 查询积分\n"
@@ -6191,7 +6551,8 @@ class CommandHandler:
)
self.logger.info(help_text)
await self.broadcast(
f"指令: {q}/{s}/{l}/{c_yes}/{c_no}/{r} 组名/{song_cmd} 歌名/{leave}/{reset}/{pts}/{ql}/{h}",
f"指令: {q}/{s}/{l}/{c_yes}/{c_no}/{r} 组名/{daily}/"
f"{switch_party} 队伍名/{edit_party} 四名角色/{song_cmd} 歌名/{leave}/{reset}/{pts}/{ql}/{h}",
tts=True
)
@@ -7517,17 +7878,17 @@ class QueueSystem:
await self.broadcast(f"请「{uname}」确认这是你的账号吗,回复是或不是")
async def _on_group_finished(self, group_name: str, run_id: str | None):
"""配置组执行完成回调;仅当前任务实例可结算一次。"""
"""配置组或一条龙执行完成回调;仅当前任务实例可结算一次。"""
state = self.queue_mgr.state
current_group = state.get("current_group")
current_run_id = state.get("current_group_run_id")
if not run_id or run_id != current_run_id or group_name != current_group:
self.logger.info(
f"[完成] 忽略迟到/重复配置组回调: group='{group_name}', run_id={run_id}, "
f"[完成] 忽略迟到/重复任务回调: group='{group_name}', run_id={run_id}, "
f"current_group='{current_group}', current_run_id={current_run_id}"
)
return
self.logger.info(f"[完成] 配置组 '{group_name}' 执行结束, run_id={run_id}")
self.logger.info(f"[完成] 任务 '{group_name}' 执行结束, run_id={run_id}")
start_time = state.get("group_start_time")
run_seconds = 0
if start_time:
@@ -7537,7 +7898,7 @@ class QueueSystem:
run_seconds = 0
admin_uid = state.get("current_admin_uid")
admin_points = self.user_mgr.get_points(admin_uid) if admin_uid else 0
# 积分已耗尽/扣到负分时,配置组自然结束后不再保留队列,直接进入默认薄荷。
# 积分已耗尽/扣到负分时,任务自然结束后不再保留队列,直接进入默认薄荷。
# 积分仍为正且3分钟内完成,才保留当前用户继续二级权限。
keep_current = (
not state.get("default_running")
@@ -7545,12 +7906,12 @@ class QueueSystem:
and run_seconds > 0
and run_seconds < 180
)
# 配置组自然完成后不主动 kill BGI;真正抢占 kill 放到下一个队首发送“上号”时执行。
# 任务自然完成后不主动 kill BGI;真正抢占 kill 放到下一个队首发送“上号”时执行。
self.log_monitor.set_current_group(None)
result = self.queue_mgr.group_finished(expected_run_id=run_id, keep_current=keep_current)
if not result.get("accepted"):
self.logger.info(
f"[完成] 配置组实例已被其他入口结算或替换,忽略: group='{group_name}', run_id={run_id}"
f"[完成] 任务实例已被其他入口结算或替换,忽略: group='{group_name}', run_id={run_id}"
)
return
finished_uid = result.get("finished_uid")
@@ -7561,10 +7922,10 @@ class QueueSystem:
).get("uname", "?")
if result.get("kept"):
self.logger.info(
f"[完成] {finished_uname}({finished_uid}) 配置组跑完,用时{run_seconds}秒,保留队列"
f"[完成] {finished_uname}({finished_uid}) 任务跑完,用时{run_seconds}秒,保留队列"
)
await self.broadcast(
f"{finished_uname}」的配置组'{group_name}'执行完毕,未满3分钟,保留队列"
f"{finished_uname}」的任务'{group_name}'执行完毕,未满3分钟,保留队列"
)
else:
self.logger.info(