You've already forked pruchase_jhw
Docker打包
This commit is contained in:
+160
-1
@@ -4,13 +4,18 @@
|
||||
启动后与主守护进程共享 asyncio 事件循环,不阻塞 Redis 消费。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
from aiohttp import web
|
||||
from loguru import logger
|
||||
|
||||
from src.config.accounts import MultiAccountConfig
|
||||
from src.config.settings import get_settings
|
||||
|
||||
# 默认只展示最近 N 天的截图
|
||||
@@ -117,6 +122,76 @@ async def handle_index(request: web.Request) -> web.Response:
|
||||
return web.Response(text=_INDEX_HTML, content_type="text/html")
|
||||
|
||||
|
||||
async def handle_accounts_get(request: web.Request) -> web.Response:
|
||||
"""GET /api/accounts — 返回 accounts.yaml 原始文本。"""
|
||||
settings = get_settings()
|
||||
config_path = Path(settings.accounts_config_path)
|
||||
|
||||
if not config_path.exists():
|
||||
return web.Response(text="", content_type="text/plain", charset="utf-8")
|
||||
|
||||
return web.Response(
|
||||
text=config_path.read_text(encoding="utf-8"),
|
||||
content_type="text/plain",
|
||||
charset="utf-8",
|
||||
)
|
||||
|
||||
|
||||
async def handle_accounts_put(request: web.Request) -> web.Response:
|
||||
"""PUT /api/accounts — 校验并保存 accounts.yaml(Pydantic 结构校验通过才写入)。"""
|
||||
raw_text = await request.text()
|
||||
|
||||
try:
|
||||
raw_data = yaml.safe_load(raw_text) or {}
|
||||
config = MultiAccountConfig.model_validate(raw_data)
|
||||
except Exception as e:
|
||||
return web.json_response({"ok": False, "error": f"配置校验失败: {e}"}, status=400)
|
||||
|
||||
settings = get_settings()
|
||||
config_path = Path(settings.accounts_config_path)
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 先写临时文件再原子替换,避免写入中途崩溃损坏配置
|
||||
tmp_path = config_path.with_name(config_path.name + ".tmp")
|
||||
tmp_path.write_text(raw_text, encoding="utf-8")
|
||||
os.replace(tmp_path, config_path)
|
||||
|
||||
account_count = len(config.sites.surugaya)
|
||||
logger.warning(f"accounts.yaml 已通过 API 更新(骏河屋账号 {account_count} 个),重启进程后生效。")
|
||||
return web.json_response({
|
||||
"ok": True,
|
||||
"message": f"配置已保存(骏河屋账号 {account_count} 个),需重启进程后生效。",
|
||||
})
|
||||
|
||||
|
||||
async def handle_restart(request: web.Request) -> web.Response:
|
||||
"""
|
||||
POST /api/restart — 触发进程优雅重启。
|
||||
|
||||
向自身发送 SIGTERM,复用 main.py 的优雅停机流程(停监听、关浏览器);
|
||||
进程退出后由 supervisord(容器内 autorestart=true)拉起新进程,
|
||||
重新加载 accounts.yaml 并预热浏览器。
|
||||
"""
|
||||
if sys.platform == "win32":
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "Windows 环境不支持 API 重启,请手动重启进程。"},
|
||||
status=501,
|
||||
)
|
||||
|
||||
logger.warning("收到 API 重启请求,0.5 秒后向自身发送 SIGTERM 触发优雅重启...")
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.call_later(0.5, os.kill, os.getpid(), signal.SIGTERM)
|
||||
return web.json_response({
|
||||
"ok": True,
|
||||
"message": "进程即将优雅退出,由 supervisord 自动重启并加载最新配置。",
|
||||
})
|
||||
|
||||
|
||||
async def handle_accounts_page(request: web.Request) -> web.Response:
|
||||
"""GET /accounts — accounts.yaml 在线编辑 HTML 页面。"""
|
||||
return web.Response(text=_ACCOUNTS_HTML, content_type="text/html")
|
||||
|
||||
|
||||
# 简易 HTML 页面,运维人员可直接在浏览器中查看
|
||||
_INDEX_HTML = """<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
@@ -147,7 +222,7 @@ _INDEX_HTML = """<!DOCTYPE html>
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>截图查看器</h1>
|
||||
<h1>截图查看器 <a href="/accounts" style="font-size:0.6em;font-weight:normal;margin-left:12px;">账号配置 →</a></h1>
|
||||
<div class="filters">
|
||||
<label>最近 <input type="number" id="days" value="3" min="1" max="30" style="width:50px"> 天</label>
|
||||
<label>场景 <input type="text" id="scene" placeholder="如:任务执行出错"></label>
|
||||
@@ -196,12 +271,96 @@ load();
|
||||
</html>"""
|
||||
|
||||
|
||||
# accounts.yaml 在线编辑页面,供运维人员修改账号配置后触发优雅重启
|
||||
_ACCOUNTS_HTML = """<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>账号配置 — JP Purchase Bot</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 20px; background: #f5f5f5; }
|
||||
h1 { color: #333; font-size: 1.4em; }
|
||||
textarea { width: 100%; height: 60vh; font-family: Consolas, Menlo, monospace; font-size: 13px;
|
||||
padding: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;
|
||||
background: #fff; white-space: pre; }
|
||||
.actions { margin: 12px 0; display: flex; gap: 10px; align-items: center; }
|
||||
.actions button { padding: 8px 20px; border: none; border-radius: 4px; cursor: pointer; color: #fff; }
|
||||
#btnSave { background: #4a90d9; }
|
||||
#btnSave:hover { background: #357abd; }
|
||||
#btnSaveRestart { background: #d9534f; }
|
||||
#btnSaveRestart:hover { background: #c9302c; }
|
||||
.actions button:disabled { background: #aaa; cursor: not-allowed; }
|
||||
#msg { font-size: 0.9em; }
|
||||
#msg.ok { color: #2a7a2a; }
|
||||
#msg.err { color: #c00; white-space: pre-wrap; }
|
||||
.tip { font-size: 0.85em; color: #888; margin-bottom: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>账号配置 (accounts.yaml) <a href="/" style="font-size:0.6em;font-weight:normal;margin-left:12px;">← 截图查看器</a></h1>
|
||||
<div class="tip">保存时会做 YAML 结构校验;修改仅在进程重启后生效。"保存并重启"会优雅停机后由 supervisord 自动拉起进程并按新配置重新预热浏览器。</div>
|
||||
<textarea id="editor" spellcheck="false"></textarea>
|
||||
<div class="actions">
|
||||
<button id="btnSave" onclick="save(false)">保存</button>
|
||||
<button id="btnSaveRestart" onclick="save(true)">保存并重启</button>
|
||||
<span id="msg"></span>
|
||||
</div>
|
||||
<script>
|
||||
function setMsg(text, isErr) {
|
||||
const el = document.getElementById('msg');
|
||||
el.textContent = text;
|
||||
el.className = isErr ? 'err' : 'ok';
|
||||
}
|
||||
function setBusy(busy) {
|
||||
document.getElementById('btnSave').disabled = busy;
|
||||
document.getElementById('btnSaveRestart').disabled = busy;
|
||||
}
|
||||
async function load() {
|
||||
const resp = await fetch('/api/accounts');
|
||||
document.getElementById('editor').value = await resp.text();
|
||||
}
|
||||
async function save(restart) {
|
||||
if (restart && !confirm('确认保存并重启进程?进行中的任务会被优雅停机打断。')) return;
|
||||
setBusy(true);
|
||||
setMsg('保存中...', false);
|
||||
try {
|
||||
const resp = await fetch('/api/accounts', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
||||
body: document.getElementById('editor').value,
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) { setMsg(data.error, true); return; }
|
||||
if (!restart) { setMsg(data.message, false); return; }
|
||||
|
||||
setMsg('已保存,正在触发重启...', false);
|
||||
const r = await fetch('/api/restart', { method: 'POST' });
|
||||
const rd = await r.json();
|
||||
if (!rd.ok) { setMsg(rd.error, true); return; }
|
||||
setMsg('重启已触发,进程将在优雅停机后自动拉起(约数十秒),期间 API 短暂不可用。', false);
|
||||
} catch (e) {
|
||||
setMsg('请求失败: ' + e, true);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
def create_app() -> web.Application:
|
||||
"""创建 aiohttp Application 实例。"""
|
||||
app = web.Application()
|
||||
app.router.add_get("/", handle_index)
|
||||
app.router.add_get("/accounts", handle_accounts_page)
|
||||
app.router.add_get("/api/screenshots", handle_screenshots)
|
||||
app.router.add_get("/api/screenshots/{date}/{topic}/{filename}", handle_screenshot_image)
|
||||
app.router.add_get("/api/accounts", handle_accounts_get)
|
||||
app.router.add_put("/api/accounts", handle_accounts_put)
|
||||
app.router.add_post("/api/restart", handle_restart)
|
||||
return app
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user