优化自动每日指令与扫码退出秘境

This commit is contained in:
2026-08-30 00:40:18 +08:00
parent da8a82bfd5
commit 2d81615f78
10 changed files with 447 additions and 38 deletions
+3 -3
View File
@@ -106,9 +106,9 @@ npm run build
| `confirm_yes` | 是 | 确认账号正确 | | `confirm_yes` | 是 | 确认账号正确 |
| `confirm_no` | 不是 | 确认账号不正确,重新扫码 | | `confirm_no` | 不是 | 确认账号不正确,重新扫码 |
| `run` | 执行, 跑, 开始 | 执行配置组(需带参数) | | `run` | 执行, 跑, 开始 | 执行配置组(需带参数) |
| `daily` | 自动每日 | 启动托管的一条龙每日任务,可选秘境、地脉或委托模式 | | `daily` | 自动每日, 每日, 日常, 做每日, 做日常 | 启动托管的一条龙每日任务,可选秘境、地脉或委托模式 |
| `switch_party` | 切换队伍, 更换队伍 | 修改并执行配置组“切换队伍” | | `switch_party` | 切换队伍, 更换队伍, 换队伍, 换队 | 修改并执行配置组“切换队伍” |
| `edit_party` | 修改队员, 更换队员 | 校验四名角色后修改并执行配置组“修改队员” | | `edit_party` | 修改队员, 更换队员, 换队员, 换角色 | 校验四名角色后修改并执行配置组“修改队员” |
| `leave` | 退出 | 退出队列 | | `leave` | 退出 | 退出队列 |
| `reset` | 重置 | 一级用户重启原神和 BGI | | `reset` | 重置 | 一级用户重启原神和 BGI |
| `points` | 积分 | 查询积分 | | `points` | 积分 | 查询积分 |
+89 -11
View File
@@ -251,6 +251,72 @@ COMMAND_ALLOWED_ROLE_DEFAULTS = {
"queue_list": ALL_DANMU_ROLES, "queue_list": ALL_DANMU_ROLES,
"help": ALL_DANMU_ROLES, "help": ALL_DANMU_ROLES,
} }
NEW_COMMAND_ALIAS_DEFAULTS = {
"daily": ["自动每日", "每日", "日常", "做每日", "做日常"],
"switch_party": ["切换队伍", "更换队伍", "换队伍", "换队"],
"edit_party": ["修改队员", "更换队员", "换队员", "换角色"],
}
NEW_COMMAND_PREFERRED_ALIASES = {
"daily": "每日",
"switch_party": "换队",
"edit_party": "换角色",
}
FRONTEND_COMMAND_HINT_KEYS = tuple(NEW_COMMAND_ALIAS_DEFAULTS)
def configured_command_aliases(
config_data: dict[str, Any],
command_key: str,
fallback: list[str] | tuple[str, ...] = (),
) -> list[str]:
commands = config_data.get("commands", {})
cfg = commands.get(command_key, {}) if isinstance(commands, dict) else {}
if not isinstance(cfg, dict) or not cfg.get("enabled", True):
return []
source = cfg.get("aliases")
if not isinstance(source, list):
source = list(fallback)
aliases: list[str] = []
for item in source:
alias = str(item).strip()
if alias and alias not in aliases:
aliases.append(alias)
return aliases
def preferred_command_alias(
config_data: dict[str, Any],
command_key: str,
fallback: str,
) -> str:
aliases = configured_command_aliases(config_data, command_key, (fallback,))
preferred = NEW_COMMAND_PREFERRED_ALIASES.get(command_key, "")
if preferred in aliases:
return preferred
return min(aliases, key=len) if aliases else fallback
def build_public_command_hints(config_data: dict[str, Any]) -> dict[str, dict[str, Any]]:
commands = config_data.get("commands", {})
hints: dict[str, dict[str, Any]] = {}
for command_key in FRONTEND_COMMAND_HINT_KEYS:
cfg = commands.get(command_key, {}) if isinstance(commands, dict) else {}
enabled = isinstance(cfg, dict) and bool(cfg.get("enabled", True))
aliases = configured_command_aliases(
config_data,
command_key,
NEW_COMMAND_ALIAS_DEFAULTS[command_key],
)
hints[command_key] = {
"enabled": enabled,
"aliases": aliases,
"preferred_alias": preferred_command_alias(
config_data,
command_key,
NEW_COMMAND_PREFERRED_ALIASES[command_key],
) if aliases else "",
}
return hints
def normalize_allowed_roles(value: Any, default: list[str]) -> list[str]: def normalize_allowed_roles(value: Any, default: list[str]) -> list[str]:
@@ -628,9 +694,9 @@ class Config:
"confirm_yes": {"enabled": True, "aliases": [""]}, "confirm_yes": {"enabled": True, "aliases": [""]},
"confirm_no": {"enabled": True, "aliases": ["不是"]}, "confirm_no": {"enabled": True, "aliases": ["不是"]},
"run": {"enabled": True, "aliases": ["执行", "", "开始"]}, "run": {"enabled": True, "aliases": ["执行", "", "开始"]},
"daily": {"enabled": True, "aliases": ["自动每日"]}, "daily": {"enabled": True, "aliases": list(NEW_COMMAND_ALIAS_DEFAULTS["daily"])},
"switch_party": {"enabled": True, "aliases": ["切换队伍", "更换队伍"]}, "switch_party": {"enabled": True, "aliases": list(NEW_COMMAND_ALIAS_DEFAULTS["switch_party"])},
"edit_party": {"enabled": True, "aliases": ["修改队员", "更换队员"]}, "edit_party": {"enabled": True, "aliases": list(NEW_COMMAND_ALIAS_DEFAULTS["edit_party"])},
"leave": {"enabled": True, "aliases": ["退出", "退出排队", "取消排队"]}, "leave": {"enabled": True, "aliases": ["退出", "退出排队", "取消排队"]},
"reset": {"enabled": True, "aliases": ["重置"]}, "reset": {"enabled": True, "aliases": ["重置"]},
"points": {"enabled": True, "aliases": ["积分"]}, "points": {"enabled": True, "aliases": ["积分"]},
@@ -5817,7 +5883,22 @@ class CommandHandler:
if answer == "": if answer == "":
self.queue_mgr.set_login_status("logged_in") self.queue_mgr.set_login_status("logged_in")
self.logger.info(f"[登录确认] {uname}({uid}) 确认账号正确,升为二级") self.logger.info(f"[登录确认] {uname}({uid}) 确认账号正确,升为二级")
await self.broadcast(f"{uname}」账号已确认,发送\"执行 组名\"开始(如: 执行 泡泡桔)") daily_alias = preferred_command_alias(self.config.data, "daily", "自动每日")
switch_alias = preferred_command_alias(self.config.data, "switch_party", "切换队伍")
edit_alias = preferred_command_alias(self.config.data, "edit_party", "修改队员")
run_aliases = configured_command_aliases(self.config.data, "run", ("执行",))
run_alias = run_aliases[0] if run_aliases else "执行"
await self.broadcast(
f"{uname}」账号已确认,可发送“{daily_alias}”完成基础每日",
tts=True,
)
await self.broadcast(
f"其他模式:{daily_alias} 秘境 风本 / {daily_alias} 地脉 经验 蒙德 / "
f"{daily_alias} 委托;{switch_alias} 永冻队 / {edit_alias} 四名角色;"
f"普通任务仍发送“{run_alias} 组名”",
tts=False,
danmu=True,
)
return return
self.logger.info(f"[登录确认] {uname}({uid}) 确认不是本人账号,重新扫码上号") self.logger.info(f"[登录确认] {uname}({uid}) 确认不是本人账号,重新扫码上号")
await self.broadcast(f"{uname}」账号不正确,正在关闭当前账号并重新扫码上号", tts=True) await self.broadcast(f"{uname}」账号不正确,正在关闭当前账号并重新扫码上号", tts=True)
@@ -6536,12 +6617,8 @@ class CommandHandler:
def _get_cmd_aliases_str(self, key: str, fallback: str = "") -> str: def _get_cmd_aliases_str(self, key: str, fallback: str = "") -> str:
"""获取指令的别名展示字符串,用于帮助信息。""" """获取指令的别名展示字符串,用于帮助信息。"""
cmds = self.config.data.get("commands", {}) aliases = configured_command_aliases(self.config.data, key, (fallback,))
cfg = cmds.get(key, {}) return "/".join(aliases) if aliases else fallback
if cfg.get("enabled", True):
aliases = cfg.get("aliases", [])
return "/".join(str(a).strip() for a in aliases if str(a).strip())
return fallback
async def _cmd_help(self, uid: int, uname: str): async def _cmd_help(self, uid: int, uname: str):
q = self._get_cmd_aliases_str("queue", "排队") q = self._get_cmd_aliases_str("queue", "排队")
@@ -10082,7 +10159,7 @@ class WebServer:
return web.Response(status=404, text="no upload") return web.Response(status=404, text="no upload")
async def _api_frontend_config(self, request): async def _api_frontend_config(self, request):
"""返回前台视觉配置。""" """返回前台视觉配置和可公开展示的指令提示"""
cfg = self.config.data.setdefault("frontend", {}) cfg = self.config.data.setdefault("frontend", {})
return web.json_response({ return web.json_response({
"background_image": cfg.get("background_image", ""), "background_image": cfg.get("background_image", ""),
@@ -10090,6 +10167,7 @@ class WebServer:
"background_blur": float(cfg.get("background_blur", 0)), "background_blur": float(cfg.get("background_blur", 0)),
"background_fit": cfg.get("background_fit", "cover"), "background_fit": cfg.get("background_fit", "cover"),
"theme": cfg.get("theme", "classic"), "theme": cfg.get("theme", "classic"),
"command_hints": build_public_command_hints(self.config.data),
}) })
async def _api_music_monitor_config(self, request): async def _api_music_monitor_config(self, request):
+11 -3
View File
@@ -260,7 +260,11 @@
"daily": { "daily": {
"enabled": true, "enabled": true,
"aliases": [ "aliases": [
"自动每日" "自动每日",
"每日",
"日常",
"做每日",
"做日常"
], ],
"allowed_roles": [ "allowed_roles": [
"super_admin", "super_admin",
@@ -271,7 +275,9 @@
"enabled": true, "enabled": true,
"aliases": [ "aliases": [
"切换队伍", "切换队伍",
"更换队伍" "更换队伍",
"换队伍",
"换队"
], ],
"allowed_roles": [ "allowed_roles": [
"super_admin", "super_admin",
@@ -282,7 +288,9 @@
"enabled": true, "enabled": true,
"aliases": [ "aliases": [
"修改队员", "修改队员",
"更换队员" "更换队员",
"换队员",
"换角色"
], ],
"allowed_roles": [ "allowed_roles": [
"super_admin", "super_admin",
+15
View File
@@ -10,32 +10,46 @@ BetterGI.exe startOneDragon <配置名称>
```text ```text
自动每日 自动每日
每日
日常
做每日
做日常
自动每日 秘境 <秘境正式名称或俗称> 自动每日 秘境 <秘境正式名称或俗称>
自动每日 地脉 <经验|摩拉> <国家> 自动每日 地脉 <经验|摩拉> <国家>
自动每日 委托 自动每日 委托
切换队伍 <队伍名称> 切换队伍 <队伍名称>
更换队伍 <队伍名称> 更换队伍 <队伍名称>
换队伍 <队伍名称>
换队 <队伍名称>
修改队员 <队员1> <队员2> <队员3> <队员4> 修改队员 <队员1> <队员2> <队员3> <队员4>
更换队员 <队员1> <队员2> <队员3> <队员4> 更换队员 <队员1> <队员2> <队员3> <队员4>
换队员 <队员1> <队员2> <队员3> <队员4>
换角色 <队员1> <队员2> <队员3> <队员4>
``` ```
示例: 示例:
```text ```text
自动每日 自动每日
每日
日常 秘境 风本
自动每日 秘境 风本 自动每日 秘境 风本
自动每日 秘境 少女套 自动每日 秘境 少女套
自动每日 地脉 经验 蒙德 自动每日 地脉 经验 蒙德
自动每日 地脉 摩拉 枫丹 自动每日 地脉 摩拉 枫丹
自动每日 委托 自动每日 委托
切换队伍 永冻队 切换队伍 永冻队
换队 永冻队
修改队员 神里绫华 申鹤 枫原万叶 珊瑚宫心海 修改队员 神里绫华 申鹤 枫原万叶 珊瑚宫心海
换角色 神里绫华 申鹤 枫原万叶 珊瑚宫心海
修改队员 神里绫华、申鹤、枫原万叶、珊瑚宫心海 修改队员 神里绫华、申鹤、枫原万叶、珊瑚宫心海
``` ```
队员支持空格、英文或中文逗号、顿号、斜杠分隔。连续输入四个角色名时,只有能够唯一拆分为四名已知角色才会执行。角色必须存在于 BGI 的 `AutoSwitchRoles` 角色数据中,且四人不能重复。新版脚本同时支持 `combat_avatar.json` 中维护的角色别名。 队员支持空格、英文或中文逗号、顿号、斜杠分隔。连续输入四个角色名时,只有能够唯一拆分为四名已知角色才会执行。角色必须存在于 BGI 的 `AutoSwitchRoles` 角色数据中,且四人不能重复。新版脚本同时支持 `combat_avatar.json` 中维护的角色别名。
执行类指令可以省略指令后的空格,例如 `每日秘境 风本``换队永冻队``换角色芙宁娜纳西妲钟离雷电将军`。系统按最长别名优先解析,因此“换队员”不会被误识别为“换队”。
## 每日流程 ## 每日流程
每日流程固定为: 每日流程固定为:
@@ -177,3 +191,4 @@ User/JsScript/AutoSwitchRoles/combat_avatar.json
- 提示角色未知或有歧义:检查 `AutoSwitchRoles/settings.json``combat_avatar.json`,使用其中唯一对应的名称或别名,并用空格明确分隔四人。 - 提示角色未知或有歧义:检查 `AutoSwitchRoles/settings.json``combat_avatar.json`,使用其中唯一对应的名称或别名,并用空格明确分隔四人。
- BGI 启动失败:确认版本为 `0.63.0+``bettergi.exe_path``bettergi.work_dir` 正确,并查看 BetterGI 日志。 - BGI 启动失败:确认版本为 `0.63.0+``bettergi.exe_path``bettergi.work_dir` 正确,并查看 BetterGI 日志。
- 一条龙执行中子任务报错:查看 BetterGI 日志确认后续“领取尘歌壶奖励”和“领取每日奖励”是否继续;本项目只在整条一条龙完成标记出现后结算。 - 一条龙执行中子任务报错:查看 BetterGI 日志确认后续“领取尘歌壶奖励”和“领取每日奖励”是否继续;本项目只在整条一条龙完成标记出现后结算。
- 自动秘境异常后扫码上号卡在“退出秘境”:确认项目维护的 `integrations/扫码上号` 已同步。脚本会 OCR 识别“退出秘境”并点击右侧确认,OCR 按钮定位失败时使用 1920×1080 坐标 `(1164, 757)` 兜底。
+77 -1
View File
@@ -1,6 +1,6 @@
(async function () { (async function () {
// ======================================== // ========================================
// 扫码上号 v3 // 扫码上号 v3.1
// 流程: 状态判定 → (已登录则先退出) → A0检测tap → A1选号 → A2等登录+点中心 → A3完成 // 流程: 状态判定 → (已登录则先退出) → A0检测tap → A1选号 → A2等登录+点中心 → A3完成
// 状态文件: status.txt (登录中 / 已登录) // 状态文件: status.txt (登录中 / 已登录)
// ======================================== // ========================================
@@ -20,6 +20,25 @@
// 脚本启动 → 写入"登录中" // 脚本启动 → 写入"登录中"
writeStatus("登录中"); writeStatus("登录中");
function settingNumber(name, fallback) {
const value = typeof settings !== "undefined" && settings ? Number(settings[name]) : NaN;
return Number.isFinite(value) ? value : fallback;
}
function settingBoolean(name, fallback) {
if (typeof settings === "undefined" || !settings || settings[name] === undefined) return fallback;
return settings[name] !== false && String(settings[name]).toLowerCase() !== "false";
}
const useExitDomainOcr = settingBoolean("useOcr", true);
let confirmExitX = settingNumber("confirmExitX", 1164);
let confirmExitY = settingNumber("confirmExitY", 757);
if (confirmExitX === 830 && confirmExitY === 600) {
confirmExitX = 1164;
confirmExitY = 757;
log.info("检测到旧版退出秘境坐标,已自动修正为 (1164, 757)");
}
// ---------- 加载图像资源 ---------- // ---------- 加载图像资源 ----------
const tapMat = file.readImageMatSync("assets/tap.png"); const tapMat = file.readImageMatSync("assets/tap.png");
const a0PhoneMat = file.readImageMatSync("assets/a0_phone.png"); const a0PhoneMat = file.readImageMatSync("assets/a0_phone.png");
@@ -115,6 +134,54 @@
return null; return null;
} }
function normalizeOcrText(text) {
return String(text || "").replace(/\s+/g, "");
}
function readOcrResults(x, y, w, h) {
const cap = captureGameRegion();
try {
const results = cap.findMulti(RecognitionObject.ocr(x, y, w, h));
return Array.from(results).map((result) => ({
text: normalizeOcrText(result.text),
x: Number(result.x || 0),
y: Number(result.y || 0),
w: Number(result.width || result.Width || 0),
h: Number(result.height || result.Height || 0)
}));
} catch (e) {
log.warn("退出秘境弹窗 OCR 失败: " + e);
return [];
} finally {
cap.dispose();
}
}
async function handleExitDomainDialog() {
const results = readOcrResults(480, 220, 960, 620);
const combinedText = results.map((result) => result.text).join("");
if (!combinedText.includes("退出秘境")) return false;
log.info("检测到退出秘境确认弹窗");
const confirmResult = results
.filter((result) => result.text.includes("确认") && result.x >= 900)
.sort((left, right) => right.x - left.x)[0];
if (useExitDomainOcr && confirmResult) {
const x = Math.round(confirmResult.x + confirmResult.w / 2);
const y = Math.round(confirmResult.y + confirmResult.h / 2);
log.info(`OCR 定位确认按钮,点击 (${x}, ${y})`);
click(x, y);
} else {
const reason = useExitDomainOcr ? "OCR 未定位到确认按钮" : "已关闭 OCR 按钮定位";
log.warn(`${reason},使用固定坐标 (${confirmExitX}, ${confirmExitY})`);
click(confirmExitX, confirmExitY);
}
log.info("已点击退出秘境确认,等待返回主界面...");
await sleep(6000);
return true;
}
// ======================================== // ========================================
// 前置处理:检测登录前的提示图标 // 前置处理:检测登录前的提示图标
// 检测到则点击 (1830, 985)1秒后点击 (1100, 675),再1秒后进入初始判定; // 检测到则点击 (1830, 985)1秒后点击 (1100, 675),再1秒后进入初始判定;
@@ -155,11 +222,20 @@
log.info("未同时检测到 tap 图标和手机图标 → 开始检测派蒙头像"); log.info("未同时检测到 tap 图标和手机图标 → 开始检测派蒙头像");
let paimonFound = findImageMatchRobust(paimonMat); let paimonFound = findImageMatchRobust(paimonMat);
if (!paimonFound && await handleExitDomainDialog()) {
paimonFound = findImageMatchRobust(paimonMat);
if (paimonFound) log.info("退出秘境后已检测到派蒙头像");
}
for (let i = 0; !paimonFound && i < 8; i++) { for (let i = 0; !paimonFound && i < 8; i++) {
log.info(`${i + 1}/8 次未检测到派蒙头像,按 ESC 后重试...`); log.info(`${i + 1}/8 次未检测到派蒙头像,按 ESC 后重试...`);
keyPress("Escape"); keyPress("Escape");
await sleep(1000); await sleep(1000);
if (await handleExitDomainDialog()) {
log.info("退出秘境弹窗已处理,重新检测主界面");
}
paimonFound = findImageMatchRobust(paimonMat); paimonFound = findImageMatchRobust(paimonMat);
if (paimonFound) log.info("已检测到派蒙头像,继续退出账号流程");
} }
if (!paimonFound) { if (!paimonFound) {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 1, "manifest_version": 1,
"name": "扫码上号", "name": "扫码上号",
"version": "1.0", "version": "1.1",
"bgi_version": "0.48.0", "bgi_version": "0.48.0",
"description": "直播用扫码上号脚本:判断在大世界则退出到开门页面→点击扫码登录→等待扫码→检测验证码", "description": "直播用扫码上号脚本:判断在大世界则退出到开门页面→点击扫码登录→等待扫码→检测验证码",
"authors": [ "authors": [
+5 -5
View File
@@ -44,7 +44,7 @@
{ {
"name": "useOcr", "name": "useOcr",
"type": "checkbox", "type": "checkbox",
"label": "使用OCR识别文本(否则用固定坐标)", "label": "退出秘境确认按钮使用 OCR 定位(关闭后使用固定坐标",
"default": true "default": true
}, },
{ {
@@ -53,14 +53,14 @@
{ {
"name": "confirmExitX", "name": "confirmExitX",
"type": "input-text", "type": "input-text",
"label": "确认退出按钮 X(固定坐标模式)", "label": "退出秘境确认按钮 X(1920×1080 固定坐标",
"default": "830" "default": "1164"
}, },
{ {
"name": "confirmExitY", "name": "confirmExitY",
"type": "input-text", "type": "input-text",
"label": "确认退出按钮 Y(固定坐标模式)", "label": "退出秘境确认按钮 Y(1920×1080 固定坐标",
"default": "600" "default": "757"
}, },
{ {
"name": "qrLoginX", "name": "qrLoginX",
+89 -3
View File
@@ -1,4 +1,5 @@
import asyncio import asyncio
import json
import logging import logging
import tempfile import tempfile
import unittest import unittest
@@ -21,6 +22,8 @@ from app.danmu_queue import (
COMMAND_ALLOWED_ROLE_DEFAULTS, COMMAND_ALLOWED_ROLE_DEFAULTS,
BetterGIRunner, BetterGIRunner,
CommandHandler, CommandHandler,
Config,
build_public_command_hints,
) )
@@ -32,14 +35,17 @@ class DailyCommandIntegrationTests(unittest.IsolatedAsyncioTestCase):
data={ data={
"commands": { "commands": {
"run": {"enabled": True, "aliases": ["执行", ""]}, "run": {"enabled": True, "aliases": ["执行", ""]},
"daily": {"enabled": True, "aliases": ["自动每日"]}, "daily": {
"enabled": True,
"aliases": ["自动每日", "每日", "日常", "做每日", "做日常"],
},
"switch_party": { "switch_party": {
"enabled": True, "enabled": True,
"aliases": ["切换队伍", "更换队伍"], "aliases": ["切换队伍", "更换队伍", "换队伍", "换队"],
}, },
"edit_party": { "edit_party": {
"enabled": True, "enabled": True,
"aliases": ["修改队员", "更换队员"], "aliases": ["修改队员", "更换队员", "换队员", "换角色"],
}, },
} }
}, },
@@ -315,6 +321,86 @@ class DailyCommandIntegrationTests(unittest.IsolatedAsyncioTestCase):
self.handler._split_command_text("修改队员芙宁娜纳西妲钟离雷电将军"), self.handler._split_command_text("修改队员芙宁娜纳西妲钟离雷电将军"),
("修改队员", "芙宁娜纳西妲钟离雷电将军"), ("修改队员", "芙宁娜纳西妲钟离雷电将军"),
) )
self.assertEqual(
self.handler._split_command_text("日常秘境 风本"),
("日常", "秘境 风本"),
)
self.assertEqual(
self.handler._split_command_text("换队永冻队"),
("换队", "永冻队"),
)
self.assertEqual(
self.handler._split_command_text("换角色芙宁娜纳西妲钟离雷电将军"),
("换角色", "芙宁娜纳西妲钟离雷电将军"),
)
def test_longest_new_alias_wins_for_contiguous_arguments(self):
self.assertEqual(
self.handler._split_command_text("换队员芙宁娜纳西妲钟离雷电将军"),
("换队员", "芙宁娜纳西妲钟离雷电将军"),
)
async def test_confirmed_login_announces_daily_and_party_usage(self):
self.queue_mgr.state["login_status"] = "confirming"
await self.handler._cmd_confirm_login(123, "测试用户", "")
self.assertEqual(self.broadcaster.broadcast.await_count, 2)
first, second = self.broadcaster.broadcast.await_args_list
self.assertIn("发送“每日”完成基础每日", first.args[0])
self.assertTrue(first.kwargs["tts"])
self.assertIn("每日 秘境 风本", second.args[0])
self.assertIn("换队 永冻队", second.args[0])
self.assertIn("换角色 四名角色", second.args[0])
self.assertIn("执行 组名", second.args[0])
self.assertFalse(second.kwargs["tts"])
self.assertTrue(second.kwargs["danmu"])
def test_public_command_hints_are_sanitized_and_use_shortest_alias(self):
config_data = {
"commands": self.config.data["commands"],
"bilibili": {"cookie": "secret"},
}
hints = build_public_command_hints(config_data)
self.assertEqual(set(hints), {"daily", "switch_party", "edit_party"})
self.assertEqual(hints["daily"]["preferred_alias"], "每日")
self.assertEqual(hints["switch_party"]["preferred_alias"], "换队")
self.assertEqual(hints["edit_party"]["preferred_alias"], "换角色")
self.assertNotIn("bilibili", hints)
def test_new_command_alias_defaults_are_complete(self):
config_path = self.root / "config.json"
config_path.write_text(json.dumps({}), encoding="utf-8")
config = Config(str(config_path))
self.assertEqual(
config.data["commands"]["daily"]["aliases"],
["自动每日", "每日", "日常", "做每日", "做日常"],
)
self.assertEqual(
config.data["commands"]["switch_party"]["aliases"],
["切换队伍", "更换队伍", "换队伍", "换队"],
)
self.assertEqual(
config.data["commands"]["edit_party"]["aliases"],
["修改队员", "更换队员", "换队员", "换角色"],
)
def test_disabled_command_hint_has_no_public_alias(self):
config_data = {
"commands": {
"daily": {"enabled": False, "aliases": ["每日"]},
}
}
hints = build_public_command_hints(config_data)
self.assertFalse(hints["daily"]["enabled"])
self.assertEqual(hints["daily"]["aliases"], [])
self.assertEqual(hints["daily"]["preferred_alias"], "")
def test_new_command_permissions_match_run(self): def test_new_command_permissions_match_run(self):
for key in ("daily", "switch_party", "edit_party"): for key in ("daily", "switch_party", "edit_party"):
+33
View File
@@ -0,0 +1,33 @@
import json
import unittest
from pathlib import Path
class LoginScriptDomainExitTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.script_dir = Path(__file__).resolve().parents[1] / "integrations" / "扫码上号"
cls.source = (cls.script_dir / "main.js").read_text(encoding="utf-8")
cls.settings = json.loads((cls.script_dir / "settings.json").read_text(encoding="utf-8"))
cls.manifest = json.loads((cls.script_dir / "manifest.json").read_text(encoding="utf-8"))
def test_domain_exit_requires_specific_ocr_text_before_click(self):
self.assertIn('combinedText.includes("退出秘境")', self.source)
self.assertIn('result.text.includes("确认") && result.x >= 900', self.source)
def test_legacy_and_default_confirm_coordinates_are_corrected(self):
defaults = {
item.get("name"): item.get("default")
for item in self.settings
if isinstance(item, dict) and item.get("name")
}
self.assertEqual(defaults["confirmExitX"], "1164")
self.assertEqual(defaults["confirmExitY"], "757")
self.assertIn("confirmExitX === 830 && confirmExitY === 600", self.source)
def test_private_login_script_version_is_updated(self):
self.assertEqual(self.manifest["version"], "1.1")
if __name__ == "__main__":
unittest.main()
+124 -11
View File
@@ -47,9 +47,10 @@
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
display: flex; display: flex;
align-items: center; align-items: flex-start;
justify-content: center; justify-content: flex-start;
background: transparent; background: transparent;
overflow: hidden;
} }
body.preview #viewport { body.preview #viewport {
@@ -1315,6 +1316,34 @@
border: 1px solid rgba(255, 242, 201, 0.13); border: 1px solid rgba(255, 242, 201, 0.13);
} }
.command-quick-reference {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 2px 12px;
margin: 0 12px 2px;
padding: 3px 8px;
border-top: 1px solid rgba(255, 242, 201, 0.15);
border-bottom: 1px solid rgba(255, 242, 201, 0.15);
color: rgba(244, 255, 247, 0.86);
font-size: 12px;
font-weight: 720;
line-height: 1.25;
}
.command-quick-reference span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.command-quick-reference strong {
color: #fff0b5;
font-weight: 900;
}
.tutorial-stage { .tutorial-stage {
position: relative; position: relative;
z-index: 1; z-index: 1;
@@ -1322,19 +1351,20 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 0 14px 3px; min-height: 32px;
padding: 0 10px 1px;
overflow: hidden; overflow: hidden;
} }
.tutorial-text { .tutorial-text {
width: 100%; width: 100%;
color: var(--text); color: var(--text);
font-size: 26px; font-size: 20px;
font-weight: 900; font-weight: 900;
line-height: 1.35; line-height: 1.25;
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
letter-spacing: 0.7px; letter-spacing: 0;
text-shadow: 0 2px 5px rgba(0, 0, 0, 0.48), 0 0 14px rgba(255, 240, 181, 0.18); text-shadow: 0 2px 5px rgba(0, 0, 0, 0.48), 0 0 14px rgba(255, 240, 181, 0.18);
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
@@ -1342,12 +1372,12 @@
} }
.tutorial-text.long { .tutorial-text.long {
font-size: 23px; font-size: 18px;
letter-spacing: 0.35px; letter-spacing: 0;
} }
.tutorial-text.extra-long { .tutorial-text.extra-long {
font-size: 21px; font-size: 16px;
letter-spacing: 0; letter-spacing: 0;
} }
@@ -1852,6 +1882,7 @@
<!-- 右侧中部:教学轮播 --> <!-- 右侧中部:教学轮播 -->
<section class="panel tutorial-panel"> <section class="panel tutorial-panel">
<div class="panel-title tutorial-title">直播间使用说明</div> <div class="panel-title tutorial-title">直播间使用说明</div>
<div id="command-quick-reference" class="command-quick-reference"></div>
<div class="tutorial-stage"> <div class="tutorial-stage">
<div id="tutorial-text" class="tutorial-text">发送“签到”领取积分,发送“排队”加入队伍</div> <div id="tutorial-text" class="tutorial-text">发送“签到”领取积分,发送“排队”加入队伍</div>
</div> </div>
@@ -1898,7 +1929,7 @@
return m + ":" + String(sec % 60).padStart(2, "0"); return m + ":" + String(sec % 60).padStart(2, "0");
} }
const tutorialSlides = [ const baseTutorialSlides = [
"发送“签到”领取积分,发送“排队”加入队伍", "发送“签到”领取积分,发送“排队”加入队伍",
"成为队首后,请在90秒内发送“上号”", "成为队首后,请在90秒内发送“上号”",
"发送“上号”后,请在4分钟内完成扫码登录", "发送“上号”后,请在4分钟内完成扫码登录",
@@ -1907,8 +1938,85 @@
"发送“积分”查询积分,发送“队列”查看排队情况", "发送“积分”查询积分,发送“队列”查看排队情况",
"发送“退出”离开队伍,发送“点歌 歌名”点歌" "发送“退出”离开队伍,发送“点歌 歌名”点歌"
]; ];
const fallbackCommandHints = {
daily: { enabled: true, aliases: ["自动每日", "每日", "日常"], preferred_alias: "每日" },
switch_party: { enabled: true, aliases: ["切换队伍", "换队"], preferred_alias: "换队" },
edit_party: { enabled: true, aliases: ["修改队员", "换角色"], preferred_alias: "换角色" }
};
let commandHints = fallbackCommandHints;
let tutorialSlides = baseTutorialSlides.slice();
let tutorialIndex = 0; let tutorialIndex = 0;
function normalizeCommandHint(value, fallback) {
const source = value && typeof value === "object" ? value : fallback;
const aliases = Array.isArray(source.aliases)
? source.aliases.map((item) => String(item || "").trim()).filter(Boolean)
: fallback.aliases.slice();
const preferred = String(source.preferred_alias || "").trim()
|| aliases.reduce((best, item) => !best || item.length < best.length ? item : best, "")
|| fallback.preferred_alias;
return {
enabled: source.enabled !== false,
aliases,
preferred_alias: preferred
};
}
function renderCommandQuickReference() {
const el = document.getElementById("command-quick-reference");
if (!el) return;
const daily = commandHints.daily;
if (!daily || !daily.enabled) {
el.hidden = true;
el.innerHTML = "";
return;
}
const cmd = esc(daily.preferred_alias || "每日");
el.hidden = false;
el.innerHTML = [
`<span><strong>${cmd}</strong>:基础流程</span>`,
`<span><strong>${cmd} 秘境</strong> &lt;名称&gt;</span>`,
`<span><strong>${cmd} 地脉</strong> &lt;类型&gt; &lt;国家&gt;</span>`,
`<span><strong>${cmd} 委托</strong>:每日委托</span>`
].join("");
}
function rebuildTutorialSlides() {
const slides = baseTutorialSlides.slice();
const daily = commandHints.daily;
const switchParty = commandHints.switch_party;
const editParty = commandHints.edit_party;
if (daily && daily.enabled) {
const cmd = daily.preferred_alias || "每日";
slides.push(`发送“${cmd}”完成邮件、树脂、尘歌壶和每日奖励`);
slides.push(`发送“${cmd} 秘境 风本”刷到树脂耗尽`);
slides.push(`发送“${cmd} 地脉 经验 蒙德”刷经验地脉`);
slides.push(`发送“${cmd} 委托”执行每日委托配置`);
}
const partyParts = [];
if (switchParty && switchParty.enabled) {
partyParts.push(`${switchParty.preferred_alias || "换队"} 永冻队”切换队伍`);
}
if (editParty && editParty.enabled) {
partyParts.push(`${editParty.preferred_alias || "换角色"} 四名角色”修改队员`);
}
if (partyParts.length) slides.push(`发送${partyParts.join(",发送")}`);
tutorialSlides = slides;
tutorialIndex %= tutorialSlides.length;
renderCommandQuickReference();
showTutorialSlide(tutorialIndex, false);
}
function applyCommandHints(value) {
const source = value && typeof value === "object" ? value : {};
commandHints = {
daily: normalizeCommandHint(source.daily, fallbackCommandHints.daily),
switch_party: normalizeCommandHint(source.switch_party, fallbackCommandHints.switch_party),
edit_party: normalizeCommandHint(source.edit_party, fallbackCommandHints.edit_party)
};
rebuildTutorialSlides();
}
function renderTutorialDots() { function renderTutorialDots() {
const dots = document.getElementById("tutorial-dots"); const dots = document.getElementById("tutorial-dots");
if (!dots) return; if (!dots) return;
@@ -2344,8 +2452,10 @@
themeClasses.forEach((c) => document.body.classList.remove(c)); themeClasses.forEach((c) => document.body.classList.remove(c));
const theme = cfg.theme || "classic"; const theme = cfg.theme || "classic";
if (theme && theme !== "classic") document.body.classList.add("theme-" + theme); if (theme && theme !== "classic") document.body.classList.add("theme-" + theme);
applyCommandHints(cfg.command_hints);
} catch (e) { } catch (e) {
console.error("frontend-config", e); console.error("frontend-config", e);
applyCommandHints(fallbackCommandHints);
} }
} }
@@ -2647,9 +2757,12 @@
function scaleCanvas() { function scaleCanvas() {
const canvas = document.getElementById("canvas"); const canvas = document.getElementById("canvas");
const scale = Math.min(window.innerWidth / 1920, window.innerHeight / 1080); const scale = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
canvas.style.transform = "scale(" + scale + ")"; const offsetX = Math.max(0, (window.innerWidth - 1920 * scale) / 2);
const offsetY = Math.max(0, (window.innerHeight - 1080 * scale) / 2);
canvas.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
} }
applyCommandHints(fallbackCommandHints);
startTutorialCarousel(); startTutorialCarousel();
fetchFrontendConfig(); fetchFrontendConfig();
fetchState(); fetchState();