Update Live-streaming code (auto-daily features)
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
(async function () {
|
||||
setGameMetrics(1920, 1080, 1);
|
||||
|
||||
const STATUS_FILE = "status.json";
|
||||
const requestId = String(settings.requestId || "").trim();
|
||||
let lastCandidates = [];
|
||||
|
||||
function writeStatus(state, data) {
|
||||
const payload = Object.assign({
|
||||
state: state,
|
||||
request_id: requestId,
|
||||
party_name: "",
|
||||
candidates: lastCandidates,
|
||||
message: "",
|
||||
updated_at: new Date().toISOString()
|
||||
}, data || {});
|
||||
file.writeTextSync(STATUS_FILE, JSON.stringify(payload, null, 2));
|
||||
}
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value || "").replace(/\s+/g, "").trim();
|
||||
}
|
||||
|
||||
function isPartyNameCandidate(text) {
|
||||
if (!text || text.length > 20) {
|
||||
return false;
|
||||
}
|
||||
const ignored = [
|
||||
"队伍配置",
|
||||
"快速编队",
|
||||
"元素共鸣",
|
||||
"调整队伍",
|
||||
"当前队伍",
|
||||
"部署",
|
||||
"出战",
|
||||
"详情"
|
||||
];
|
||||
if (ignored.some((label) => text === label || text.includes(label))) {
|
||||
return false;
|
||||
}
|
||||
return !/^(esc|enter|space|l|f\d{1,2})$/i.test(text);
|
||||
}
|
||||
|
||||
function isPartyPageOpen() {
|
||||
const capture = captureGameRegion();
|
||||
try {
|
||||
const results = capture.findMulti(RecognitionObject.ocr(0, 0, 720, 180));
|
||||
for (let index = 0; index < results.count; index++) {
|
||||
const text = normalizeText(results[index].text);
|
||||
if (text.includes("队伍配置")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
capture.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
function recognizePartyNameCandidates() {
|
||||
const capture = captureGameRegion();
|
||||
try {
|
||||
const results = capture.findMulti(RecognitionObject.ocr(0, 940, 520, 140));
|
||||
const candidates = [];
|
||||
for (let index = 0; index < results.count; index++) {
|
||||
const result = results[index];
|
||||
const text = normalizeText(result.text);
|
||||
log.info(
|
||||
"当前队伍名称候选位置:({x},{y},{w},{h}), 识别结果:{text}",
|
||||
result.x,
|
||||
result.y,
|
||||
result.Width,
|
||||
result.Height,
|
||||
text
|
||||
);
|
||||
if (isPartyNameCandidate(text)) {
|
||||
candidates.push({
|
||||
text: text,
|
||||
x: Number(result.x || 0),
|
||||
y: Number(result.y || 0)
|
||||
});
|
||||
}
|
||||
}
|
||||
candidates.sort((left, right) => {
|
||||
if (left.x !== right.x) {
|
||||
return left.x - right.x;
|
||||
}
|
||||
return right.y - left.y;
|
||||
});
|
||||
return candidates.filter(
|
||||
(candidate, index, values) =>
|
||||
values.findIndex((value) => value.text === candidate.text) === index
|
||||
);
|
||||
} finally {
|
||||
capture.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
async function openPartyPageAndRead() {
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
keyPress("VK_L");
|
||||
await sleep(2200);
|
||||
for (let scan = 0; scan < 4; scan++) {
|
||||
if (isPartyPageOpen()) {
|
||||
const candidates = recognizePartyNameCandidates();
|
||||
if (candidates.length > 0) {
|
||||
return candidates;
|
||||
}
|
||||
}
|
||||
await sleep(600);
|
||||
}
|
||||
keyPress("VK_ESCAPE");
|
||||
await sleep(800);
|
||||
await genshin.returnMainUi();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
if (!requestId) {
|
||||
throw new Error("读取请求标识为空");
|
||||
}
|
||||
writeStatus("running");
|
||||
await genshin.returnMainUi();
|
||||
const candidates = await openPartyPageAndRead();
|
||||
lastCandidates = candidates.map((candidate) => candidate.text);
|
||||
if (candidates.length === 0) {
|
||||
throw new Error("未识别到当前队伍名称,请确认游戏处于可打开配队界面的状态");
|
||||
}
|
||||
if (candidates.length > 1) {
|
||||
throw new Error("识别到多个队伍名称候选:" + lastCandidates.join("、"));
|
||||
}
|
||||
|
||||
const partyName = candidates[0].text;
|
||||
await genshin.returnMainUi();
|
||||
await sleep(500);
|
||||
writeStatus("success", {
|
||||
party_name: partyName,
|
||||
candidates: lastCandidates
|
||||
});
|
||||
log.info("当前队伍名称读取成功: {name}", partyName);
|
||||
} catch (error) {
|
||||
try {
|
||||
await genshin.returnMainUi();
|
||||
} catch (_) {
|
||||
}
|
||||
const message = String(error && error.message ? error.message : error);
|
||||
writeStatus("error", {
|
||||
candidates: lastCandidates,
|
||||
message: message
|
||||
});
|
||||
log.error("当前队伍名称读取失败: {message}", message);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "直播系统读取当前队伍",
|
||||
"version": "1.0.0",
|
||||
"bgi_version": "0.63.0",
|
||||
"description": "读取游戏当前队伍预设名称并通过状态文件返回给直播联动系统",
|
||||
"authors": [
|
||||
{
|
||||
"name": "BGI直播项目"
|
||||
}
|
||||
],
|
||||
"settings_ui": "settings.json",
|
||||
"main": "main.js",
|
||||
"saved_files": [
|
||||
"status.json"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"name": "requestId",
|
||||
"type": "input-text",
|
||||
"label": "读取请求标识",
|
||||
"default": ""
|
||||
}
|
||||
]
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 764 B |
Binary file not shown.
|
After Width: | Height: | Size: 503 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 608 B |
@@ -0,0 +1,284 @@
|
||||
(async function () {
|
||||
// ========================================
|
||||
// 扫码上号 v3
|
||||
// 流程: 状态判定 → (已登录则先退出) → A0检测tap → A1选号 → A2等登录+点中心 → A3完成
|
||||
// 状态文件: status.txt (登录中 / 已登录)
|
||||
// ========================================
|
||||
|
||||
setGameMetrics(1920, 1080, 1);
|
||||
|
||||
// ---------- 状态文件 ----------
|
||||
const STATUS_FILE = "status.txt";
|
||||
function writeStatus(text) {
|
||||
try {
|
||||
file.writeTextSync(STATUS_FILE, text);
|
||||
} catch (e) {
|
||||
log.warn("写入状态文件失败: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
// 脚本启动 → 写入"登录中"
|
||||
writeStatus("登录中");
|
||||
|
||||
// ---------- 加载图像资源 ----------
|
||||
const tapMat = file.readImageMatSync("assets/tap.png");
|
||||
const a0PhoneMat = file.readImageMatSync("assets/a0_phone.png");
|
||||
const loggedInMat = file.readImageMatSync("assets/btn_logged_in_real.png");
|
||||
const paimonMat = file.readImageMatSync("assets/paimon_menu.png");
|
||||
const exitDoorMat = file.readImageMatSync("assets/btn_exit_door.png");
|
||||
const preLoginNoticeMat = file.readImageMatSync("assets/pre_login_notice.png");
|
||||
|
||||
// ---------- 工具函数 ----------
|
||||
|
||||
/**
|
||||
* 在截屏中匹配指定图片, 返回识别结果(含 x/y/中心坐标)
|
||||
*/
|
||||
function findImageMatch(mat, x, y, w, h) {
|
||||
const cap = captureGameRegion();
|
||||
try {
|
||||
const ro = RecognitionObject.TemplateMatch(mat, x || 0, y || 0, w || 1920, h || 1080);
|
||||
const r = cap.find(ro);
|
||||
if (r.isExist()) {
|
||||
return { x: r.x, y: r.y, w: r.width, h: r.height };
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
log.error("图像识别失败: " + e);
|
||||
return null;
|
||||
} finally {
|
||||
cap.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 严格匹配:用于 A0 入口判定,避免地图页面/传送点/圆形 UI 被低阈值误识别。
|
||||
*/
|
||||
function findImageMatchStrict(mat, x, y, w, h) {
|
||||
const thresholds = [0.88, 0.84];
|
||||
for (const t of thresholds) {
|
||||
const cap = captureGameRegion();
|
||||
try {
|
||||
const ro = RecognitionObject.TemplateMatch(mat, x || 0, y || 0, w || 1920, h || 1080);
|
||||
ro.threshold = t;
|
||||
ro.Use3Channels = true;
|
||||
const r = cap.find(ro);
|
||||
if (r.isExist()) {
|
||||
log.info(`严格找到图标 (阈值=${t}, x=${r.x}, y=${r.y})`);
|
||||
return { x: r.x, y: r.y, w: r.width, h: r.height };
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略, 继续下一个阈值
|
||||
} finally {
|
||||
cap.dispose();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 多阈值尝试匹配, 提高识别率
|
||||
*/
|
||||
function findImageMatchRobust(mat, x, y, w, h) {
|
||||
const thresholds = [0.7, 0.6, 0.5];
|
||||
for (const t of thresholds) {
|
||||
const cap = captureGameRegion();
|
||||
try {
|
||||
const ro = RecognitionObject.TemplateMatch(mat, x || 0, y || 0, w || 1920, h || 1080);
|
||||
ro.threshold = t;
|
||||
ro.Use3Channels = true;
|
||||
const r = cap.find(ro);
|
||||
if (r.isExist()) {
|
||||
log.info(`找到图标 (阈值=${t}, x=${r.x}, y=${r.y})`);
|
||||
return { x: r.x, y: r.y, w: r.width, h: r.height };
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略, 继续下一个阈值
|
||||
} finally {
|
||||
cap.dispose();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function clickImage(found) {
|
||||
click(Math.round(found.x + found.w / 2), Math.round(found.y + found.h / 2));
|
||||
}
|
||||
|
||||
async function waitForImage(mat, timeout, interval) {
|
||||
interval = interval || 500;
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
const found = findImageMatch(mat);
|
||||
if (found) return found;
|
||||
await sleep(interval);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 前置处理:检测登录前的提示图标
|
||||
// 检测到则点击 (1830, 985),1秒后点击 (1100, 675),再1秒后进入初始判定;
|
||||
// 未检测到则直接进入初始判定,不影响原流程。
|
||||
// ========================================
|
||||
log.info("===== 前置检测: pre_login_notice 图标 =====");
|
||||
const preNoticeFound = findImageMatchRobust(preLoginNoticeMat);
|
||||
if (preNoticeFound) {
|
||||
log.info(`检测到 pre_login_notice 图标 (x=${preNoticeFound.x}, y=${preNoticeFound.y})`);
|
||||
log.info("点击 (1830, 985)...");
|
||||
click(1830, 985);
|
||||
await sleep(1000);
|
||||
log.info("点击 (1100, 675)...");
|
||||
click(1100, 675);
|
||||
await sleep(1000);
|
||||
} else {
|
||||
log.info("未检测到 pre_login_notice 图标,跳过前置处理");
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 初始状态判定:先检测 tap + 手机图标
|
||||
// 1. 同时存在:已经在开门页,直接进入 A0 选号
|
||||
// 2. 不同时存在:检测派蒙头像;若无派蒙,则按 ESC 后重检,最多 8 次
|
||||
// 3. 8 次仍无派蒙:再严格检测一次开门页;开门页也不存在才判定登录失败
|
||||
// 4. 找到派蒙:说明已在游戏内,先退出到开门页,再进入 A0
|
||||
// ========================================
|
||||
log.info("===== 初始状态判定: 严格检测 tap 图标 + 手机图标 =====");
|
||||
// A0 入口图标不能全屏低阈值搜索,否则地图/传送点等 UI 容易误判。
|
||||
// 这里限定在开门页中下区域,并使用高阈值严格匹配。
|
||||
const A0_X = 420;
|
||||
const A0_Y = 120;
|
||||
const A0_W = 1080;
|
||||
const A0_H = 820;
|
||||
let tapFound = findImageMatchStrict(tapMat, A0_X, A0_Y, A0_W, A0_H);
|
||||
let phoneFound = findImageMatchStrict(a0PhoneMat, A0_X, A0_Y, A0_W, A0_H);
|
||||
|
||||
if (!tapFound || !phoneFound) {
|
||||
log.info("未同时检测到 tap 图标和手机图标 → 开始检测派蒙头像");
|
||||
let paimonFound = findImageMatchRobust(paimonMat);
|
||||
|
||||
for (let i = 0; !paimonFound && i < 8; i++) {
|
||||
log.info(`第 ${i + 1}/8 次未检测到派蒙头像,按 ESC 后重试...`);
|
||||
keyPress("Escape");
|
||||
await sleep(1000);
|
||||
paimonFound = findImageMatchRobust(paimonMat);
|
||||
}
|
||||
|
||||
if (!paimonFound) {
|
||||
log.info("按 ESC 检测 8 次后仍未检测到派蒙头像 → 再严格检测一次开门页");
|
||||
tapFound = findImageMatchStrict(tapMat, A0_X, A0_Y, A0_W, A0_H);
|
||||
phoneFound = findImageMatchStrict(a0PhoneMat, A0_X, A0_Y, A0_W, A0_H);
|
||||
if (!tapFound || !phoneFound) {
|
||||
log.error("未检测到派蒙头像,最终也未同时检测到 tap 图标和手机图标,判定登录失败");
|
||||
writeStatus("登录失败");
|
||||
return;
|
||||
}
|
||||
log.info("最终严格检测到 tap 图标和手机图标 → 当前已在开门页,继续扫码流程");
|
||||
}
|
||||
|
||||
if (paimonFound) {
|
||||
// ========================================
|
||||
// 已登录流程: 退出到开门页
|
||||
// 找到派蒙→ESC关菜单→点55,1010→找exit_door→点击→等10s
|
||||
// ========================================
|
||||
log.info("检测到派蒙头像 → 已登录状态, 执行退出流程");
|
||||
log.info(`找到派蒙头像 (x=${paimonFound.x}, y=${paimonFound.y}), 按 ESC 打开菜单...`);
|
||||
keyPress("Escape");
|
||||
await sleep(1000);
|
||||
|
||||
log.info("点击 (55, 1010) 打开派蒙菜单...");
|
||||
click(55, 1010);
|
||||
await sleep(1500);
|
||||
|
||||
log.info("查找退出图标 btn_exit_door...");
|
||||
const doorFound = await waitForImage(exitDoorMat, 10 * 1000, 500);
|
||||
if (doorFound) {
|
||||
log.info("找到退出图标, 点击退出...");
|
||||
clickImage(doorFound);
|
||||
} else {
|
||||
log.warn("未找到退出图标, 继续后续流程");
|
||||
}
|
||||
|
||||
log.info("等待 8 秒, 等待退回到开门页...");
|
||||
await sleep(8 * 1000);
|
||||
|
||||
log.info("退出流程完成, 重新严格检测 tap 图标 + 手机图标");
|
||||
tapFound = findImageMatchStrict(tapMat, A0_X, A0_Y, A0_W, A0_H);
|
||||
phoneFound = findImageMatchStrict(a0PhoneMat, A0_X, A0_Y, A0_W, A0_H);
|
||||
if (!tapFound || !phoneFound) {
|
||||
log.error("退出后仍未同时检测到 tap 图标和手机图标,判定登录失败");
|
||||
writeStatus("登录失败");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("同时检测到 tap 图标和手机图标 → 准备进入扫码流程");
|
||||
|
||||
// ========================================
|
||||
// A1: 进入扫码页
|
||||
// 点击选号坐标 → 确认进入
|
||||
// ========================================
|
||||
log.info("===== A1: 进入扫码页 =====");
|
||||
log.info("点击 (660, 250) 选择米游社账号...");
|
||||
click(660, 250);
|
||||
await sleep(750);
|
||||
|
||||
log.info("点击 (828, 691) 确认进入...");
|
||||
click(828, 691);
|
||||
|
||||
log.info("A1 完成, 进入等待登录...");
|
||||
|
||||
// ========================================
|
||||
// A2: 等待登录完成 → 点击画面进入游戏
|
||||
// 等待 btn_logged_in_real 出现 (80秒超时)
|
||||
// 找到后每秒点击 (960, 800), 每3秒检测派蒙头像
|
||||
// ========================================
|
||||
log.info("===== A2: 等待登录完成 =====");
|
||||
log.info("等待扫码登录... 超时 80 秒");
|
||||
|
||||
const loginResult = await waitForImage(loggedInMat, 80 * 1000, 1000);
|
||||
|
||||
if (!loginResult) {
|
||||
log.error("登录超时! 80 秒内未检测到登录完成图标");
|
||||
writeStatus("登录失败");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(`检测到登录完成图标! (x=${loginResult.x}, y=${loginResult.y})`);
|
||||
log.info("点击画面进入游戏...");
|
||||
|
||||
const CENTER_X = 960;
|
||||
const CENTER_Y = 800;
|
||||
let enteredGame = false;
|
||||
for (let i = 0; i < 60; i++) {
|
||||
click(CENTER_X, CENTER_Y);
|
||||
await sleep(1000);
|
||||
|
||||
// 每 3 秒检测一次是否进入游戏 (匹配派蒙头像)
|
||||
if (i % 3 === 2) {
|
||||
const found = findImageMatch(paimonMat);
|
||||
if (found) {
|
||||
log.info(`检测到派蒙头像! 已进入游戏 (x=${found.x}, y=${found.y})`);
|
||||
enteredGame = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!enteredGame) {
|
||||
log.error("60秒内未检测到最后的派蒙头像, 判定登录失败");
|
||||
writeStatus("登录失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// A3: 登录流程完成, 退出脚本
|
||||
// ========================================
|
||||
log.info("===== A3: 登录流程完成! =====");
|
||||
log.info("扫码上号成功, 脚本退出");
|
||||
|
||||
// 写入"已登录"后按一次 ESC,收起菜单/退出可能残留的界面
|
||||
writeStatus("已登录");
|
||||
await sleep(500);
|
||||
keyPress("Escape");
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "扫码上号",
|
||||
"version": "1.0",
|
||||
"bgi_version": "0.48.0",
|
||||
"description": "直播用扫码上号脚本:判断在大世界则退出到开门页面→点击扫码登录→等待扫码→检测验证码",
|
||||
"authors": [
|
||||
{
|
||||
"name": "BGI直播项目",
|
||||
"links": "https://github.com/"
|
||||
}
|
||||
],
|
||||
"settings_ui": "settings.json",
|
||||
"main": "main.js",
|
||||
"saved_files": []
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
[
|
||||
{
|
||||
"name": "timeoutEsc",
|
||||
"type": "input-text",
|
||||
"label": "步骤1: ESC循环超时(秒)",
|
||||
"default": "60"
|
||||
},
|
||||
{
|
||||
"name": "escInterval",
|
||||
"type": "input-text",
|
||||
"label": "步骤1: ESC按键间隔(毫秒)",
|
||||
"default": "2000"
|
||||
},
|
||||
{
|
||||
"name": "paimonBtnX",
|
||||
"type": "input-text",
|
||||
"label": "步骤3: 派蒙按钮 X(默认55)",
|
||||
"default": "55"
|
||||
},
|
||||
{
|
||||
"name": "paimonBtnY",
|
||||
"type": "input-text",
|
||||
"label": "步骤3: 派蒙按钮 Y(默认1010)",
|
||||
"default": "1010"
|
||||
},
|
||||
{
|
||||
"name": "timeoutMenu",
|
||||
"type": "input-text",
|
||||
"label": "步骤3: 派蒙菜单展开超时(秒)",
|
||||
"default": "10"
|
||||
},
|
||||
{
|
||||
"name": "timeoutExit",
|
||||
"type": "input-text",
|
||||
"label": "步骤4: 退出图标超时(秒)",
|
||||
"default": "10"
|
||||
},
|
||||
{
|
||||
"name": "timeoutGate",
|
||||
"type": "input-text",
|
||||
"label": "步骤6: 开门页面等待超时(秒)",
|
||||
"default": "30"
|
||||
},
|
||||
{
|
||||
"name": "useOcr",
|
||||
"type": "checkbox",
|
||||
"label": "使用OCR识别文本(否则用固定坐标)",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"name": "confirmExitX",
|
||||
"type": "input-text",
|
||||
"label": "确认退出按钮 X(固定坐标模式)",
|
||||
"default": "830"
|
||||
},
|
||||
{
|
||||
"name": "confirmExitY",
|
||||
"type": "input-text",
|
||||
"label": "确认退出按钮 Y(固定坐标模式)",
|
||||
"default": "600"
|
||||
},
|
||||
{
|
||||
"name": "qrLoginX",
|
||||
"type": "input-text",
|
||||
"label": "扫码登录按钮 X(固定坐标模式)",
|
||||
"default": "1720"
|
||||
},
|
||||
{
|
||||
"name": "qrLoginY",
|
||||
"type": "input-text",
|
||||
"label": "扫码登录按钮 Y(固定坐标模式)",
|
||||
"default": "970"
|
||||
},
|
||||
{
|
||||
"name": "timeoutQr",
|
||||
"type": "input-text",
|
||||
"label": "扫码等待超时(秒)",
|
||||
"default": "120"
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user