优化自动每日指令与扫码退出秘境
This commit is contained in:
@@ -106,9 +106,9 @@ npm run build
|
||||
| `confirm_yes` | 是 | 确认账号正确 |
|
||||
| `confirm_no` | 不是 | 确认账号不正确,重新扫码 |
|
||||
| `run` | 执行, 跑, 开始 | 执行配置组(需带参数) |
|
||||
| `daily` | 自动每日 | 启动托管的一条龙每日任务,可选秘境、地脉或委托模式 |
|
||||
| `switch_party` | 切换队伍, 更换队伍 | 修改并执行配置组“切换队伍” |
|
||||
| `edit_party` | 修改队员, 更换队员 | 校验四名角色后修改并执行配置组“修改队员” |
|
||||
| `daily` | 自动每日, 每日, 日常, 做每日, 做日常 | 启动托管的一条龙每日任务,可选秘境、地脉或委托模式 |
|
||||
| `switch_party` | 切换队伍, 更换队伍, 换队伍, 换队 | 修改并执行配置组“切换队伍” |
|
||||
| `edit_party` | 修改队员, 更换队员, 换队员, 换角色 | 校验四名角色后修改并执行配置组“修改队员” |
|
||||
| `leave` | 退出 | 退出队列 |
|
||||
| `reset` | 重置 | 一级用户重启原神和 BGI |
|
||||
| `points` | 积分 | 查询积分 |
|
||||
|
||||
+89
-11
@@ -251,6 +251,72 @@ COMMAND_ALLOWED_ROLE_DEFAULTS = {
|
||||
"queue_list": 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]:
|
||||
@@ -628,9 +694,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": ["修改队员", "更换队员"]},
|
||||
"daily": {"enabled": True, "aliases": list(NEW_COMMAND_ALIAS_DEFAULTS["daily"])},
|
||||
"switch_party": {"enabled": True, "aliases": list(NEW_COMMAND_ALIAS_DEFAULTS["switch_party"])},
|
||||
"edit_party": {"enabled": True, "aliases": list(NEW_COMMAND_ALIAS_DEFAULTS["edit_party"])},
|
||||
"leave": {"enabled": True, "aliases": ["退出", "退出排队", "取消排队"]},
|
||||
"reset": {"enabled": True, "aliases": ["重置"]},
|
||||
"points": {"enabled": True, "aliases": ["积分"]},
|
||||
@@ -5817,7 +5883,22 @@ class CommandHandler:
|
||||
if answer == "是":
|
||||
self.queue_mgr.set_login_status("logged_in")
|
||||
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
|
||||
self.logger.info(f"[登录确认] {uname}({uid}) 确认不是本人账号,重新扫码上号")
|
||||
await self.broadcast(f"「{uname}」账号不正确,正在关闭当前账号并重新扫码上号", tts=True)
|
||||
@@ -6536,12 +6617,8 @@ class CommandHandler:
|
||||
|
||||
def _get_cmd_aliases_str(self, key: str, fallback: str = "") -> str:
|
||||
"""获取指令的别名展示字符串,用于帮助信息。"""
|
||||
cmds = self.config.data.get("commands", {})
|
||||
cfg = cmds.get(key, {})
|
||||
if cfg.get("enabled", True):
|
||||
aliases = cfg.get("aliases", [])
|
||||
return "/".join(str(a).strip() for a in aliases if str(a).strip())
|
||||
return fallback
|
||||
aliases = configured_command_aliases(self.config.data, key, (fallback,))
|
||||
return "/".join(aliases) if aliases else fallback
|
||||
|
||||
async def _cmd_help(self, uid: int, uname: str):
|
||||
q = self._get_cmd_aliases_str("queue", "排队")
|
||||
@@ -10082,7 +10159,7 @@ class WebServer:
|
||||
return web.Response(status=404, text="no upload")
|
||||
|
||||
async def _api_frontend_config(self, request):
|
||||
"""返回前台视觉配置。"""
|
||||
"""返回前台视觉配置和可公开展示的指令提示。"""
|
||||
cfg = self.config.data.setdefault("frontend", {})
|
||||
return web.json_response({
|
||||
"background_image": cfg.get("background_image", ""),
|
||||
@@ -10090,6 +10167,7 @@ class WebServer:
|
||||
"background_blur": float(cfg.get("background_blur", 0)),
|
||||
"background_fit": cfg.get("background_fit", "cover"),
|
||||
"theme": cfg.get("theme", "classic"),
|
||||
"command_hints": build_public_command_hints(self.config.data),
|
||||
})
|
||||
|
||||
async def _api_music_monitor_config(self, request):
|
||||
|
||||
+11
-3
@@ -260,7 +260,11 @@
|
||||
"daily": {
|
||||
"enabled": true,
|
||||
"aliases": [
|
||||
"自动每日"
|
||||
"自动每日",
|
||||
"每日",
|
||||
"日常",
|
||||
"做每日",
|
||||
"做日常"
|
||||
],
|
||||
"allowed_roles": [
|
||||
"super_admin",
|
||||
@@ -271,7 +275,9 @@
|
||||
"enabled": true,
|
||||
"aliases": [
|
||||
"切换队伍",
|
||||
"更换队伍"
|
||||
"更换队伍",
|
||||
"换队伍",
|
||||
"换队"
|
||||
],
|
||||
"allowed_roles": [
|
||||
"super_admin",
|
||||
@@ -282,7 +288,9 @@
|
||||
"enabled": true,
|
||||
"aliases": [
|
||||
"修改队员",
|
||||
"更换队员"
|
||||
"更换队员",
|
||||
"换队员",
|
||||
"换角色"
|
||||
],
|
||||
"allowed_roles": [
|
||||
"super_admin",
|
||||
|
||||
@@ -10,32 +10,46 @@ BetterGI.exe startOneDragon <配置名称>
|
||||
|
||||
```text
|
||||
自动每日
|
||||
每日
|
||||
日常
|
||||
做每日
|
||||
做日常
|
||||
自动每日 秘境 <秘境正式名称或俗称>
|
||||
自动每日 地脉 <经验|摩拉> <国家>
|
||||
自动每日 委托
|
||||
|
||||
切换队伍 <队伍名称>
|
||||
更换队伍 <队伍名称>
|
||||
换队伍 <队伍名称>
|
||||
换队 <队伍名称>
|
||||
修改队员 <队员1> <队员2> <队员3> <队员4>
|
||||
更换队员 <队员1> <队员2> <队员3> <队员4>
|
||||
换队员 <队员1> <队员2> <队员3> <队员4>
|
||||
换角色 <队员1> <队员2> <队员3> <队员4>
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```text
|
||||
自动每日
|
||||
每日
|
||||
日常 秘境 风本
|
||||
自动每日 秘境 风本
|
||||
自动每日 秘境 少女套
|
||||
自动每日 地脉 经验 蒙德
|
||||
自动每日 地脉 摩拉 枫丹
|
||||
自动每日 委托
|
||||
切换队伍 永冻队
|
||||
换队 永冻队
|
||||
修改队员 神里绫华 申鹤 枫原万叶 珊瑚宫心海
|
||||
换角色 神里绫华 申鹤 枫原万叶 珊瑚宫心海
|
||||
修改队员 神里绫华、申鹤、枫原万叶、珊瑚宫心海
|
||||
```
|
||||
|
||||
队员支持空格、英文或中文逗号、顿号、斜杠分隔。连续输入四个角色名时,只有能够唯一拆分为四名已知角色才会执行。角色必须存在于 BGI 的 `AutoSwitchRoles` 角色数据中,且四人不能重复。新版脚本同时支持 `combat_avatar.json` 中维护的角色别名。
|
||||
|
||||
执行类指令可以省略指令后的空格,例如 `每日秘境 风本`、`换队永冻队`、`换角色芙宁娜纳西妲钟离雷电将军`。系统按最长别名优先解析,因此“换队员”不会被误识别为“换队”。
|
||||
|
||||
## 每日流程
|
||||
|
||||
每日流程固定为:
|
||||
@@ -177,3 +191,4 @@ User/JsScript/AutoSwitchRoles/combat_avatar.json
|
||||
- 提示角色未知或有歧义:检查 `AutoSwitchRoles/settings.json` 或 `combat_avatar.json`,使用其中唯一对应的名称或别名,并用空格明确分隔四人。
|
||||
- BGI 启动失败:确认版本为 `0.63.0+`、`bettergi.exe_path` 和 `bettergi.work_dir` 正确,并查看 BetterGI 日志。
|
||||
- 一条龙执行中子任务报错:查看 BetterGI 日志确认后续“领取尘歌壶奖励”和“领取每日奖励”是否继续;本项目只在整条一条龙完成标记出现后结算。
|
||||
- 自动秘境异常后扫码上号卡在“退出秘境”:确认项目维护的 `integrations/扫码上号` 已同步。脚本会 OCR 识别“退出秘境”并点击右侧确认,OCR 按钮定位失败时使用 1920×1080 坐标 `(1164, 757)` 兜底。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
(async function () {
|
||||
// ========================================
|
||||
// 扫码上号 v3
|
||||
// 扫码上号 v3.1
|
||||
// 流程: 状态判定 → (已登录则先退出) → A0检测tap → A1选号 → A2等登录+点中心 → A3完成
|
||||
// 状态文件: status.txt (登录中 / 已登录)
|
||||
// ========================================
|
||||
@@ -20,6 +20,25 @@
|
||||
// 脚本启动 → 写入"登录中"
|
||||
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 a0PhoneMat = file.readImageMatSync("assets/a0_phone.png");
|
||||
@@ -115,6 +134,54 @@
|
||||
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秒后进入初始判定;
|
||||
@@ -155,11 +222,20 @@
|
||||
log.info("未同时检测到 tap 图标和手机图标 → 开始检测派蒙头像");
|
||||
let paimonFound = findImageMatchRobust(paimonMat);
|
||||
|
||||
if (!paimonFound && await handleExitDomainDialog()) {
|
||||
paimonFound = findImageMatchRobust(paimonMat);
|
||||
if (paimonFound) log.info("退出秘境后已检测到派蒙头像");
|
||||
}
|
||||
|
||||
for (let i = 0; !paimonFound && i < 8; i++) {
|
||||
log.info(`第 ${i + 1}/8 次未检测到派蒙头像,按 ESC 后重试...`);
|
||||
keyPress("Escape");
|
||||
await sleep(1000);
|
||||
if (await handleExitDomainDialog()) {
|
||||
log.info("退出秘境弹窗已处理,重新检测主界面");
|
||||
}
|
||||
paimonFound = findImageMatchRobust(paimonMat);
|
||||
if (paimonFound) log.info("已检测到派蒙头像,继续退出账号流程");
|
||||
}
|
||||
|
||||
if (!paimonFound) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "扫码上号",
|
||||
"version": "1.0",
|
||||
"version": "1.1",
|
||||
"bgi_version": "0.48.0",
|
||||
"description": "直播用扫码上号脚本:判断在大世界则退出到开门页面→点击扫码登录→等待扫码→检测验证码",
|
||||
"authors": [
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
{
|
||||
"name": "useOcr",
|
||||
"type": "checkbox",
|
||||
"label": "使用OCR识别文本(否则用固定坐标)",
|
||||
"label": "退出秘境确认按钮使用 OCR 定位(关闭后使用固定坐标)",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
@@ -53,14 +53,14 @@
|
||||
{
|
||||
"name": "confirmExitX",
|
||||
"type": "input-text",
|
||||
"label": "确认退出按钮 X(固定坐标模式)",
|
||||
"default": "830"
|
||||
"label": "退出秘境确认按钮 X(1920×1080 固定坐标)",
|
||||
"default": "1164"
|
||||
},
|
||||
{
|
||||
"name": "confirmExitY",
|
||||
"type": "input-text",
|
||||
"label": "确认退出按钮 Y(固定坐标模式)",
|
||||
"default": "600"
|
||||
"label": "退出秘境确认按钮 Y(1920×1080 固定坐标)",
|
||||
"default": "757"
|
||||
},
|
||||
{
|
||||
"name": "qrLoginX",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import tempfile
|
||||
import unittest
|
||||
@@ -21,6 +22,8 @@ from app.danmu_queue import (
|
||||
COMMAND_ALLOWED_ROLE_DEFAULTS,
|
||||
BetterGIRunner,
|
||||
CommandHandler,
|
||||
Config,
|
||||
build_public_command_hints,
|
||||
)
|
||||
|
||||
|
||||
@@ -32,14 +35,17 @@ class DailyCommandIntegrationTests(unittest.IsolatedAsyncioTestCase):
|
||||
data={
|
||||
"commands": {
|
||||
"run": {"enabled": True, "aliases": ["执行", "跑"]},
|
||||
"daily": {"enabled": True, "aliases": ["自动每日"]},
|
||||
"daily": {
|
||||
"enabled": True,
|
||||
"aliases": ["自动每日", "每日", "日常", "做每日", "做日常"],
|
||||
},
|
||||
"switch_party": {
|
||||
"enabled": True,
|
||||
"aliases": ["切换队伍", "更换队伍"],
|
||||
"aliases": ["切换队伍", "更换队伍", "换队伍", "换队"],
|
||||
},
|
||||
"edit_party": {
|
||||
"enabled": True,
|
||||
"aliases": ["修改队员", "更换队员"],
|
||||
"aliases": ["修改队员", "更换队员", "换队员", "换角色"],
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -315,6 +321,86 @@ class DailyCommandIntegrationTests(unittest.IsolatedAsyncioTestCase):
|
||||
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):
|
||||
for key in ("daily", "switch_party", "edit_party"):
|
||||
|
||||
@@ -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
@@ -47,9 +47,10 @@
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body.preview #viewport {
|
||||
@@ -1315,6 +1316,34 @@
|
||||
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 {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -1322,19 +1351,20 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 14px 3px;
|
||||
min-height: 32px;
|
||||
padding: 0 10px 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tutorial-text {
|
||||
width: 100%;
|
||||
color: var(--text);
|
||||
font-size: 26px;
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
line-height: 1.35;
|
||||
line-height: 1.25;
|
||||
text-align: center;
|
||||
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);
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
@@ -1342,12 +1372,12 @@
|
||||
}
|
||||
|
||||
.tutorial-text.long {
|
||||
font-size: 23px;
|
||||
letter-spacing: 0.35px;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.tutorial-text.extra-long {
|
||||
font-size: 21px;
|
||||
font-size: 16px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
@@ -1852,6 +1882,7 @@
|
||||
<!-- 右侧中部:教学轮播 -->
|
||||
<section class="panel tutorial-panel">
|
||||
<div class="panel-title tutorial-title">直播间使用说明</div>
|
||||
<div id="command-quick-reference" class="command-quick-reference"></div>
|
||||
<div class="tutorial-stage">
|
||||
<div id="tutorial-text" class="tutorial-text">发送“签到”领取积分,发送“排队”加入队伍</div>
|
||||
</div>
|
||||
@@ -1898,7 +1929,7 @@
|
||||
return m + ":" + String(sec % 60).padStart(2, "0");
|
||||
}
|
||||
|
||||
const tutorialSlides = [
|
||||
const baseTutorialSlides = [
|
||||
"发送“签到”领取积分,发送“排队”加入队伍",
|
||||
"成为队首后,请在90秒内发送“上号”",
|
||||
"发送“上号”后,请在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;
|
||||
|
||||
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> <名称></span>`,
|
||||
`<span><strong>${cmd} 地脉</strong> <类型> <国家></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() {
|
||||
const dots = document.getElementById("tutorial-dots");
|
||||
if (!dots) return;
|
||||
@@ -2344,8 +2452,10 @@
|
||||
themeClasses.forEach((c) => document.body.classList.remove(c));
|
||||
const theme = cfg.theme || "classic";
|
||||
if (theme && theme !== "classic") document.body.classList.add("theme-" + theme);
|
||||
applyCommandHints(cfg.command_hints);
|
||||
} catch (e) {
|
||||
console.error("frontend-config", e);
|
||||
applyCommandHints(fallbackCommandHints);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2647,9 +2757,12 @@
|
||||
function scaleCanvas() {
|
||||
const canvas = document.getElementById("canvas");
|
||||
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();
|
||||
fetchFrontendConfig();
|
||||
fetchState();
|
||||
|
||||
Reference in New Issue
Block a user