跳到主要内容

原神日常自动化方案

· 阅读需 14 分钟
Versed_sine
Website Developer

依据 BetterGI、Python 以及 Windows 计划任务实现的原神日常自动化方案

需要有一台开机的 Windows 系统电脑

基本配置

BetterGI 一条龙设置

先去 BetterGI 下载页 下载最新版本(截止本文落笔,最新版为 v0.46.2

下载完打开后,在启动页将“同时启动原神”选项勾上,如有需要可以往“启动参数”中填写 -screen-width 1920 -screen-height 1080,如图:

BetterGI启动页

然后可以根据自身情况配置左侧“一条龙”任务列表,记得将“完成后操作-任务完成后执行的操作”设置为“关闭游戏和软件”,以便下一次启动(因为笔者没找到在 BetterGI 运行时怎么启动一条龙,干脆每次从命令行启动 BetterGI 顺便开启一条龙),如下图:

BetterGI一条龙任务列表

当然可以用左侧“全自动-调度器”,但是笔者找不到怎么在最后关闭 BetterGI 窗口,需要手动关闭来确保下一次正常启动

有需要可以去左侧“独立任务-自动战斗”里设置战斗策略

至此,你已经完成了基本配置,此时去“一条龙”中点击“任务列表”右侧的启动按钮就可以自动清日常了

可选配置

远程本地多用户桌面

由于 BetterGI 的工作原理是 ORC,需要原神窗口居于最顶层,会和你抢键鼠,而且电脑锁屏时也不能正常工作,要是你不希望这样,可以尝试远程本地多用户桌面

请参考 b 站凜若無音的图文“远程本地多用户桌面1.3(一种不让电脑跟你抢键鼠的思路)”

显卡欺骗器

要是折腾不起上面远程本地多用户桌面方案的话,可以去拼夕夕上买一个显卡欺骗器,用来假装自己有一个显示屏,然后 Win+P 选择“仅第二屏幕”就可以做到电脑屏幕上不显示任何图像

但是一般这类设置会先有 15s 的测试时间,如果没有点击确定的话会切换回原状态,此时可以找一个远程控制软件来在第二屏幕上点击确定

确定之后,拔出显卡欺骗器就可以恢复原屏幕的显示,而且重插回去也不用重新设置

Windows 任务计划

通常情况下,我们都希望能够每天自动进行一条龙,那不妨通过 Windows 任务计划程序来订定时任务

按 Win 键输入“任务计划程序”,回车打开

笔者建议新建一个文件夹(右侧按钮),以后点开文件夹比一条一条找方便得多,如图:

Windows任务计划程序

下面是一个计划任务例子:

ys.xml
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<!--按需更改-->
<Date>2025-06-25T20:18:17.4805159</Date>
<Author> <!--按需替换--> </Author>
<URI>\MihoyoOneDragon\ys</URI>
</RegistrationInfo>
<Triggers>
<CalendarTrigger>
<StartBoundary>2025-06-25T13:00:00</StartBoundary>
<Enabled>true</Enabled>
<ScheduleByDay>
<DaysInterval>1</DaysInterval>
</ScheduleByDay>
</CalendarTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<UserId> <!--导入后在 安全选项-更改用户或组 中修改后生成,如下图--> </UserId>
<LogonType>InteractiveToken</LogonType>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>false</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession>
<UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>
<WakeToRun>true</WakeToRun>
<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
<Priority>7</Priority>
<RestartOnFailure>
<Interval>PT1M</Interval>
<Count>3</Count>
</RestartOnFailure>
</Settings>
<Actions Context="Author">
<Exec>
<!--按需更改-->
<Command>D:\BetterGI\BetterGI.exe</Command>
<Arguments>--startOneDragon</Arguments>
</Exec>
</Actions>
</Task>

复制下来保存到文件中并按需修改,再在 Windows 计划任务程序右侧点击“导入任务...”

如果配置了远程本地多用户桌面,可以将运行用户改为远程用户,这样可以在远程用户处执行了,如图:

Windows任务计划程序属性

如果需要手动启动任务,请点击右侧的“运行”按钮

通知

BetterGI 支持多种通知方式,可前往 官网 查看

BetterGI通知页

失败重试

我们可以创建一个本地 Webhook 端点,当接收到“每日奖励未领取”时,就让 Windows 计划任务重新执行

以下为一份支持网页查看并启动一条龙、日志文件保存的 Python 代码,大部分出自 DeepSeek 之手,有点小 Bug:

ysOneDragonRestart.py
import os
import sys
import subprocess
import json
import logging
import time
import threading
import ctypes
from collections import deque
from http.server import HTTPServer, SimpleHTTPRequestHandler

# 检查并请求管理员权限
def require_admin():
try:
# 检查当前是否已经是管理员权限
if ctypes.windll.shell32.IsUserAnAdmin():
return True

# 如果不是管理员,则请求管理员权限
script = os.path.abspath(sys.argv[0])
params = ' '.join([script] + sys.argv[1:])
ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, params, None, 1)
return False
except Exception as e:
print(f"请求管理员权限失败: {e}")
return False

# 配置区域
PORT = 41210
FAILURE_KEYWORDS = ["每日奖励未领取"] # 修改为您的关键词
TASK_NAME = r"\MihoyoOneDragon\ys" # 计划任务名称
SAVE_TO_LOG = True # 控制是否保存日志到文件
LOG_LEVEL = logging.DEBUG # 控制日志详细程度 (DEBUG, INFO, WARNING, ERROR)
MAX_LOG_LINES = 300 # 内存中保留的最大日志行数

# 创建日志缓冲区
log_buffer = deque(maxlen=MAX_LOG_LINES)

# 自定义日志处理器 - 捕获日志到缓冲区
class BufferLogHandler(logging.Handler):
def __init__(self):
super().__init__()
self.setFormatter(logging.Formatter(
r'[%(asctime)s][%(levelname)s] %(message)s', # 使用原始字符串解决转义问题
datefmt='%Y-%m-%d %H:%M:%S'
))

def emit(self, record):
try:
log_line = self.format(record)
log_buffer.append(log_line)
except Exception:
self.handleError(record)

# 配置日志系统
logger = logging.getLogger('WebhookLogger')
logger.setLevel(LOG_LEVEL)

# 创建控制台处理器 - 移除彩色输出
console_handler = logging.StreamHandler()
console_handler.setLevel(LOG_LEVEL)
console_formatter = logging.Formatter(
r'[%(asctime)s][%(levelname)s] %(message)s', # 使用原始字符串
datefmt='%Y-%m-%d %H:%M:%S'
)
console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler)

# 添加缓冲区处理器
buffer_handler = BufferLogHandler()
buffer_handler.setLevel(LOG_LEVEL)
logger.addHandler(buffer_handler)

# 根据配置决定是否添加文件处理器
if SAVE_TO_LOG:
file_handler = logging.FileHandler('webhook_server.log', encoding='utf-8')
file_handler.setLevel(LOG_LEVEL)
file_formatter = logging.Formatter(
r'[%(asctime)s][%(levelname)-7s] %(message)s', # 使用原始字符串
datefmt='%Y-%m-%d %H:%M:%S'
)
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)

class WebhookHandler(SimpleHTTPRequestHandler):
def do_GET(self):
"""处理GET请求 - 返回日志查看页面"""
if self.path == '/':
try:
# 返回日志查看页面
self.send_response(200)
self.send_header('Content-type', 'text/html; charset=utf-8')
self.end_headers()

# 构建简洁的HTML页面 - 使用字符串拼接避免转义问题
html_content = r"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebHook 日志查看器</title>
<style>
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
background-color: #1e1e1e;
color: #e0e0e0;
margin: 0;
padding: 0;
line-height: 1.6;
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
text-align: center;
padding: 15px 0;
background-color: #252526;
border-bottom: 1px solid #444;
}
.header h1 {
margin: 0;
color: #4a90e2;
font-size: 24px;
}
.header .info {
margin-top: 8px;
font-size: 14px;
color: #9e9e9e;
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
padding: 15px;
background-color: #252526;
}
.control-btn {
background-color: #4a90e2;
color: white;
border: none;
padding: 8px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.2s;
}
.control-btn:hover {
background-color: #357abD;
}
.log-container {
flex: 1;
background-color: #1e1e1e;
overflow-y: auto;
padding: 15px;
font-family: 'Consolas', 'Courier New', monospace;
white-space: pre-wrap;
}
.log-line {
margin-bottom: 6px;
padding: 4px 8px;
border-radius: 4px;
transition: background-color 0.2s;
color: #e0e0e0; /* 默认文本颜色 */
}
.log-line:hover {
background-color: #2a2a2a;
}
.log-debug { color: #9e9e9e; } /* 灰色 - DEBUG */
.log-info { color: #4ec9b0; } /* 青绿色 - INFO */
.log-warning { color: #d7ba7d; } /* 沙黄色 - WARNING */
.log-error { color: #f48771; } /* 珊瑚红 - ERROR */
.log-critical { color: #ff0000; } /* 红色 - CRITICAL */
.timestamp { color: #6a9955; }
.level { color: #c586c0; }
.status-bar {
display: flex;
justify-content: space-between;
padding: 10px 15px;
background-color: #252526;
border-top: 1px solid #444;
font-size: 14px;
color: #9e9e9e;
}
.auto-scroll {
background-color: #4a90e2;
color: white;
padding: 5px 12px;
border-radius: 4px;
cursor: pointer;
user-select: none;
}
.auto-scroll.off {
background-color: #d32f2f;
}
</style>
</head>
<body>
<div class="header">
<h1>WebHook 日志查看器</h1>
<div class="info">
端口: """ + str(PORT) + r""" | 日志级别: """ + logging.getLevelName(LOG_LEVEL) + r""" | 最后更新: <span id="last-update">-</span>
</div>
</div>

<div class="controls">
<button class="control-btn" id="trigger-btn">模拟每日奖励未领取</button>
<button class="control-btn" id="clear-logs">清空日志</button>
</div>

<div class="log-container" id="log-container">
<!-- 日志内容将通过JavaScript动态填充 -->
</div>

<div class="status-bar">
<div>日志行数: <span id="log-count">0</span></div>
<div class="auto-scroll" id="auto-scroll-btn">自动滚动</div>
</div>

<script>
let autoScroll = true;
let lastLogCount = 0;

// HTML转义函数
function escapeHtml(text) {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}

// 初始化页面
document.addEventListener('DOMContentLoaded', function() {
loadLogs();
setInterval(loadLogs, 1000); // 每秒更新一次日志
updateAutoScrollButton();

// 绑定按钮事件
document.getElementById('trigger-btn').addEventListener('click', triggerDailyReward);
document.getElementById('clear-logs').addEventListener('click', clearLogs);
});

// 加载日志内容
function loadLogs() {
fetch('/logs')
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(data => {
if (data.logs.length === lastLogCount) return;

const container = document.getElementById('log-container');
const fragment = document.createDocumentFragment();

// 只添加新的日志行
for (let i = lastLogCount; i < data.logs.length; i++) {
const logLine = document.createElement('div');
logLine.className = 'log-line';

const logText = data.logs[i];

// 添加日志级别样式 - 更精确的匹配
if (logText.includes('[DEBUG]')) {
logLine.classList.add('log-debug');
} else if (logText.includes('[INFO]')) {
logLine.classList.add('log-info');
} else if (logText.includes('[WARNING]')) {
logLine.classList.add('log-warning');
} else if (logText.includes('[ERROR]')) {
logLine.classList.add('log-error');
} else if (logText.includes('[CRITICAL]')) {
logLine.classList.add('log-critical');
}

// 处理日志内容 - 添加HTML标签
let processedText = escapeHtml(logText);

// 高亮时间戳和日志级别
processedText = processedText
// 时间戳格式: [2023-01-01 12:00:00]
.replace(
/\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/g,
'<span class="timestamp">[$1]</span>'
)
// 日志级别格式: [DEBUG], [INFO] 等
.replace(
/\[(DEBUG|INFO|WARNING|ERROR|CRITICAL)\]/g,
'<span class="level">[$1]</span>'
);

logLine.innerHTML = processedText;
fragment.appendChild(logLine);
}

container.appendChild(fragment);
lastLogCount = data.logs.length;

// 更新日志计数
document.getElementById('log-count').textContent = lastLogCount;
document.getElementById('last-update').textContent = new Date().toLocaleTimeString();

if (autoScroll) {
container.scrollTop = container.scrollHeight;
}
})
.catch(error => {
console.error('获取日志失败:', error);
});
}

// 切换自动滚动
function toggleAutoScroll() {
autoScroll = !autoScroll;
updateAutoScrollButton();
}

// 更新自动滚动按钮状态
function updateAutoScrollButton() {
const btn = document.getElementById('auto-scroll-btn');
btn.textContent = autoScroll ? '自动滚动' : '手动滚动';
btn.className = autoScroll ? 'auto-scroll' : 'auto-scroll off';
}

// 绑定自动滚动事件
document.getElementById('auto-scroll-btn').addEventListener('click', toggleAutoScroll);

// 模拟每日奖励未领取
function triggerDailyReward() {
fetch('/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ message: '每日奖励未领取' })
})
.then(response => response.text())
.then(data => {
console.log('模拟请求结果:', data);
})
.catch(error => {
console.error('模拟请求失败:', error);
});

alert('请求已发送');
}

// 清空日志
function clearLogs() {
if (confirm('确定要清空日志吗?')) {
fetch('/clear-logs', { method: 'POST' })
.then(() => {
// 重置日志计数
lastLogCount = 0;
document.getElementById('log-count').textContent = '0';
document.getElementById('log-container').innerHTML = '';
});
}
}
</script>
</body>
</html>
"""
self.wfile.write(html_content.encode('utf-8'))
except (BrokenPipeError, ConnectionAbortedError):
# 忽略客户端断开连接的异常
logger.debug("客户端在页面加载完成前关闭了连接")

elif self.path == '/logs':
# 返回JSON格式的日志数据
self.send_response(200)
self.send_header('Content-type', 'application/json; charset=utf-8')
self.end_headers()
response = json.dumps({
"timestamp": time.time(),
"log_count": len(log_buffer),
"logs": list(log_buffer)
}, ensure_ascii=False)
try:
self.wfile.write(response.encode('utf-8'))
except (BrokenPipeError, ConnectionAbortedError):
logger.debug("客户端在日志数据发送完成前关闭了连接")

else:
self.send_error(404, "Page not found")

def do_POST(self):
"""处理POST请求 - 包括WebHook和模拟请求"""
# 读取数据
content_length = int(self.headers.get('Content-Length', 0))
if content_length:
post_data = self.rfile.read(content_length)
else:
post_data = b''

# 处理清空日志请求
if self.path == '/clear-logs':
log_buffer.clear()
logger.info("🗑️ 日志缓冲区已清空")
self.send_response(200)
self.send_header('Content-type', 'text/plain; charset=utf-8')
self.end_headers()
try:
self.wfile.write(b'Logs cleared')
except (BrokenPipeError, ConnectionAbortedError):
logger.debug("客户端在清空日志响应发送完成前关闭了连接")
return

try:
# 解析 JSON
data = json.loads(post_data.decode('utf-8')) if post_data else {}
message = data.get('message', '')

# 详细记录收到的消息
logger.info(f"📩 收到新消息: {message}")
logger.debug(f"完整请求数据:\n{json.dumps(data, indent=2, ensure_ascii=False)}")
logger.debug(f"请求头:\n{self.headers}")

# 检查是否包含失败关键词
if any(keyword in message for keyword in FAILURE_KEYWORDS):
logger.warning(f"⚠️ 检测到失败关键词: {message}")
logger.debug(f"等待 20 秒")
time.sleep(20) # 等待原任务关闭
self.trigger_task()
response = "任务已触发"
else:
logger.info("✅ 消息未包含失败关键词")
response = "消息未包含失败关键词"

# 发送响应
self.send_response(200)
self.send_header('Content-type', 'text/plain; charset=utf-8')
self.end_headers()
try:
self.wfile.write(response.encode('utf-8'))
except (BrokenPipeError, ConnectionAbortedError):
logger.debug("客户端在响应发送完成前关闭了连接")

except json.JSONDecodeError:
logger.error("❌ 无法解析JSON数据")
if post_data:
logger.debug(f"原始数据: {post_data.decode('utf-8', errors='replace')}")
self.send_error(400, "无效的JSON数据")
except Exception as e:
logger.error(f"🔥 处理错误: {str(e)}")
logger.debug("请求详情:", exc_info=True)
self.send_error(500, f"处理错误: {str(e)}")

# 禁用默认的日志记录
def log_message(self, format, *args):
pass

def trigger_task(self):
"""触发计划任务"""
try:
logger.info(f"🚀 尝试触发任务: {TASK_NAME}")

# 使用 schtasks 命令触发任务
result = subprocess.run(
['schtasks', '/run', '/tn', TASK_NAME],
capture_output=True,
text=True,
timeout=10
)

if result.returncode == 0:
logger.info(f"🎉 成功触发任务: {TASK_NAME}")
logger.debug(f"任务输出:\n{result.stdout.strip()}")
else:
logger.error(f"❌ 触发任务失败: {result.stderr.strip()}")
logger.debug(f"返回码: {result.returncode}")
logger.debug(f"完整错误输出:\n{result.stderr.strip()}")

except subprocess.TimeoutExpired:
logger.error("⌛ 任务触发超时")
except Exception as e:
logger.error(f"💥 任务触发异常: {str(e)}")
logger.debug("任务触发异常详情:", exc_info=True)

# 修复编码问题
def send_error(self, code, message=None, explain=None):
try:
self.send_response(code, message)
self.send_header('Content-type', 'text/html; charset=utf-8')
self.end_headers()

content = f"""
<!DOCTYPE html>
<html><head>
<meta charset="utf-8">
<title>Error {code}</title>
</head><body>
<h1>Error {code}</h1>
<p>{message}</p>
</body></html>
"""
try:
self.wfile.write(content.encode('utf-8'))
except (BrokenPipeError, ConnectionAbortedError):
logger.debug("客户端在错误响应发送完成前关闭了连接")
except Exception as e:
logger.error(f"发送错误响应失败: {str(e)}")

if __name__ == '__main__':
# 请求管理员权限
if not require_admin():
print("🛑 正在请求管理员权限...")
sys.exit(0)

# 添加启动横幅
logger.info("=" * 60)
logger.info(f"🚀 WebHook 接收器已启动 | 管理员权限已获取")
logger.info(f"🛰️ 服务端口: {PORT}")
logger.info(f"📝 日志保存: {'✅ 已启用' if SAVE_TO_LOG else '❌ 已禁用'}")
logger.info(f"🔍 日志级别: {logging.getLevelName(LOG_LEVEL)}")
logger.info(f"🔍 监控关键词: {', '.join(FAILURE_KEYWORDS)}")
logger.info(f"📋 任务名称: {TASK_NAME}")
logger.info(f"🌐 日志查看: http://localhost:{PORT}")
logger.info("=" * 60)

# 启动主WebHook服务器
server = HTTPServer(('0.0.0.0', PORT), WebhookHandler)
try:
logger.info("🛜 服务器已启动,等待WebHook请求...")
server.serve_forever()
except KeyboardInterrupt:
logger.info("🛑 服务器已手动停止")
except Exception as e:
logger.error(f"💥 服务器异常停止: {str(e)}")
logger.debug("服务器停止详情:", exc_info=True)

部署说明

  1. 安装 Python 3.8+
  2. 安装依赖:pip install flask ctypes
  3. 运行:python ysOneDragonRestart.py

上面这份代码运行后会申请以管理员身份运行,默认端口为 41210,浏览器打开 http://localhost:41210 可以在网页上查看输出

WebHook日志查看器