feat: 设备池管理面板(新增/停用/删除/一键重连,调度数据源规范化入口)+ STF 设备管理增删桥接同步设备池(迁移期)
This commit is contained in:
@@ -111,6 +111,7 @@ function showSubTab(tabId, name){
|
||||
tab.querySelectorAll('.sub-tab').forEach(b=>b.classList.toggle('active', b.dataset.sub===name));
|
||||
tab.querySelectorAll('.sub-panel').forEach(p=>p.classList.toggle('active', p.id===tabId+'-sub-'+name));
|
||||
if(name==='screen' && typeof loadScreenDevices==='function') loadScreenDevices();
|
||||
if(name==='devpool' && typeof loadDevPool==='function') loadDevPool();
|
||||
}
|
||||
|
||||
// ================== 模态框 ==================
|
||||
|
||||
@@ -546,3 +546,71 @@ function screenSendText(){
|
||||
onUp({clientX:t.clientX,clientY:t.clientY,preventDefault(){}});
|
||||
},{passive:false});
|
||||
})();
|
||||
|
||||
// ================== Tab: 工具 - 设备池管理(本地 SQLite 清单) ==================
|
||||
async function loadDevPool(){
|
||||
const tb=document.getElementById('tb-devpool');
|
||||
const status=document.getElementById('devpool-status');
|
||||
if(!tb)return;
|
||||
tb.innerHTML='<tr><td colspan="6" class="empty">加载中...</td></tr>';
|
||||
const r=await apiGet('/api/devices/pool');
|
||||
if(!r||!r.ok){tb.innerHTML='<tr><td colspan="6" class="empty">加载失败</td></tr>';return;}
|
||||
const rows=r.devices||[];
|
||||
const onlineCnt=rows.filter(x=>x.online).length;
|
||||
status.textContent='共 '+rows.length+' 台,在线 '+onlineCnt+' 台';
|
||||
tb.innerHTML=rows.map(d=>{
|
||||
const st= d.enabled
|
||||
? '<span class="label label-success">启用</span>'
|
||||
: '<span class="label label-default">停用</span>';
|
||||
const online= d.online
|
||||
? '<span class="label label-success">在线</span>'
|
||||
: '<span class="label label-warning">离线</span>';
|
||||
return '<tr>'+
|
||||
'<td style="font-family:monospace">'+esc(d.serial)+'</td>'+
|
||||
'<td>'+esc(d.name||'')+'</td>'+
|
||||
'<td>'+online+'</td>'+
|
||||
'<td>'+st+'</td>'+
|
||||
'<td style="color:var(--text-light)">'+esc(d.created_at||'')+'</td>'+
|
||||
'<td style="white-space:nowrap">'+
|
||||
'<button class="btn btn-xs" onclick="togglePoolDev(\''+esc(d.serial)+'\','+(!d.enabled)+')">'+(d.enabled?'停用':'启用')+'</button> '+
|
||||
'<button class="btn btn-xs btn-danger" onclick="removePoolDev(\''+esc(d.serial)+'\')">删除</button>'+
|
||||
'</td></tr>';
|
||||
}).join('')||'<tr><td colspan="6" class="empty">设备池为空,添加第一台设备开始</td></tr>';
|
||||
}
|
||||
|
||||
async function addPoolDev(){
|
||||
const serial=document.getElementById('devpool-serial').value.trim();
|
||||
const name=document.getElementById('devpool-name').value.trim();
|
||||
if(!serial){showToast('请输入 serial','error');return;}
|
||||
const r=await apiPost('/api/devices/pool/add',{serial,name});
|
||||
if(!r)return;
|
||||
if(r.ok){
|
||||
showToast(r.msg+(serial.includes(':')?'(已尝试连接)':''),'success');
|
||||
document.getElementById('devpool-serial').value='';
|
||||
document.getElementById('devpool-name').value='';
|
||||
loadDevPool();
|
||||
}else showToast(r.error,'error');
|
||||
}
|
||||
|
||||
async function removePoolDev(serial){
|
||||
if(!confirm('从设备池删除 '+serial+'?\n\n删除后不再参与任务调度(不影响设备本身与其他功能)。'))return;
|
||||
const r=await apiPost('/api/devices/pool/remove',{serial});
|
||||
if(!r)return;
|
||||
if(r.ok){showToast(r.msg,'success');loadDevPool();}
|
||||
else showToast(r.error,'error');
|
||||
}
|
||||
|
||||
async function togglePoolDev(serial,enabled){
|
||||
const r=await apiPost('/api/devices/pool/toggle',{serial,enabled});
|
||||
if(!r)return;
|
||||
if(r.ok){showToast(r.msg,'success');loadDevPool();}
|
||||
else showToast(r.error,'error');
|
||||
}
|
||||
|
||||
async function reconnectPoolDevices(){
|
||||
if(!confirm('并发 adb connect 设备池内全部设备?\n\n已连接的自动跳过。'))return;
|
||||
const r=await apiPost('/api/devices/pool/reconnect',{});
|
||||
if(!r)return;
|
||||
if(r.ok){showToast(r.msg,'success');setTimeout(loadDevPool,12000);}
|
||||
else showToast(r.error,'error');
|
||||
}
|
||||
|
||||
@@ -481,6 +481,7 @@ select.form-control{cursor:pointer}
|
||||
<button class="sub-tab" data-sub="apks" onclick="showSubTab('tools','apks')">应用管理</button>
|
||||
<button class="sub-tab" data-sub="appver" onclick="showSubTab('tools','appver')">应用版本管理</button>
|
||||
<button class="sub-tab" data-sub="devapps" onclick="showSubTab('tools','devapps')">设备已装应用</button>
|
||||
<button class="sub-tab" data-sub="devpool" onclick="showSubTab('tools','devpool')">设备池管理</button>
|
||||
<button class="sub-tab" data-sub="stfdev" onclick="showSubTab('tools','stfdev')">STF 设备管理</button>
|
||||
<button class="sub-tab" data-sub="screen" onclick="showSubTab('tools','screen')">远程看屏</button>
|
||||
</div>
|
||||
@@ -638,6 +639,32 @@ select.form-control{cursor:pointer}
|
||||
<div class="list-pager" id="pager-devapps"></div>
|
||||
</div>
|
||||
|
||||
<!-- 子分栏 6:设备池管理(本地 SQLite 清单,任务调度的数据源) -->
|
||||
<div class="sub-panel" id="tools-sub-devpool">
|
||||
<div class="toolbar">
|
||||
<input type="text" id="devpool-serial" class="form-control" style="width:220px;font-family:monospace"
|
||||
placeholder="serial,如 100.100.10.20:5555" onkeydown="if(event.key==='Enter')addPoolDev()">
|
||||
<input type="text" id="devpool-name" class="form-control" style="width:150px"
|
||||
placeholder="备注名(可选)" onkeydown="if(event.key==='Enter')addPoolDev()">
|
||||
<button class="btn btn-primary" onclick="addPoolDev()">添加设备</button>
|
||||
<button class="btn" onclick="reconnectPoolDevices()" title="并发 adb connect 池内全部设备">一键重连</button>
|
||||
<button class="btn" onclick="loadDevPool()">刷新</button>
|
||||
<span id="devpool-status" class="text-muted"></span>
|
||||
</div>
|
||||
<table class="table table-hover table-sm" style="margin-top:8px">
|
||||
<thead><tr>
|
||||
<th>serial</th><th>名称</th><th>在线</th><th>状态</th><th>添加时间</th><th>操作</th>
|
||||
</tr></thead>
|
||||
<tbody id="tb-devpool"></tbody>
|
||||
</table>
|
||||
<div class="help">
|
||||
新增设备流程:手机开 USB 调试 → 连上后「adb 终端」执行 <code>tcpip 5555</code> 转网络调试 →
|
||||
Tailscale 上线(同一账号)→ 在此添加 <code>IP:5555</code>(自动尝试连接)→
|
||||
任务运行时 u2 自动推送 atx-agent。<b>停用 = 不参与任务调度</b>,删除 = 从池中移除(不影响其他系统)。
|
||||
迁移期如需 STF 侧也接入,再到「STF 设备管理」添加同一 IP。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 子分栏 6:STF 设备管理 -->
|
||||
<div class="sub-panel" id="tools-sub-stfdev">
|
||||
<div class="toolbar">
|
||||
|
||||
+118
-6
@@ -1333,29 +1333,45 @@ def api_stf_devmgmt_status():
|
||||
@app.route("/api/stf/devmgmt/add", methods=["POST"])
|
||||
@admin_required
|
||||
def api_stf_devmgmt_add():
|
||||
"""STF 设备管理:添加设备(写入脚本 + 立即 connect)。"""
|
||||
"""STF 设备管理:添加设备(写入脚本 + 立即 connect)。
|
||||
|
||||
迁移期桥接:同步加入本地设备池(SQLite),保证新设备可被调度。
|
||||
阶段 3 摘除 STF 后此接口随设备池管理面板退役。
|
||||
"""
|
||||
ip = (request.json or {}).get("ip", "").strip()
|
||||
if not ip:
|
||||
return jsonify({"ok": False, "error": "请输入设备 IP"}), 400
|
||||
try:
|
||||
msgs = stf_device_mgmt.add_device(ip)
|
||||
return jsonify({"ok": True, "msgs": msgs})
|
||||
except StfDevError as e:
|
||||
return jsonify({"ok": False, "error": str(e)}), 502
|
||||
# 桥接:同步本地设备池(serial 统一 IP:5555)
|
||||
try:
|
||||
device_pool.add_device(f"{ip}:5555" if ":" not in ip else ip)
|
||||
except Exception as e:
|
||||
_log.warning(f"STF 添加设备桥接设备池失败: {e}")
|
||||
return jsonify({"ok": True, "msgs": msgs})
|
||||
|
||||
|
||||
@app.route("/api/stf/devmgmt/remove", methods=["POST"])
|
||||
@admin_required
|
||||
def api_stf_devmgmt_remove():
|
||||
"""STF 设备管理:移除设备(脚本删除 + disconnect)。"""
|
||||
"""STF 设备管理:移除设备(脚本删除 + disconnect)。
|
||||
|
||||
迁移期桥接:同步从本地设备池删除。阶段 3 此接口退役。
|
||||
"""
|
||||
ip = (request.json or {}).get("ip", "").strip()
|
||||
if not ip:
|
||||
return jsonify({"ok": False, "error": "请输入设备 IP"}), 400
|
||||
try:
|
||||
msgs = stf_device_mgmt.remove_device(ip)
|
||||
return jsonify({"ok": True, "msgs": msgs})
|
||||
except StfDevError as e:
|
||||
return jsonify({"ok": False, "error": str(e)}), 502
|
||||
try:
|
||||
device_pool.remove_device(f"{ip}:5555" if ":" not in ip else ip)
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify({"ok": True, "msgs": msgs})
|
||||
|
||||
|
||||
def _app_ver_on_device(serial, pkg):
|
||||
@@ -1419,15 +1435,22 @@ def api_tools_appver():
|
||||
@app.route("/api/stf/devmgmt/delete", methods=["POST"])
|
||||
@admin_required
|
||||
def api_stf_devmgmt_delete():
|
||||
"""STF 设备管理:彻底删除(脚本移除 + disconnect + 清 STF 池记录)。"""
|
||||
"""STF 设备管理:彻底删除(脚本移除 + disconnect + 清 STF 池记录)。
|
||||
|
||||
迁移期桥接:同步从本地设备池删除。阶段 3 此接口退役。
|
||||
"""
|
||||
ip = (request.json or {}).get("ip", "").strip()
|
||||
if not ip:
|
||||
return jsonify({"ok": False, "error": "请输入设备 IP"}), 400
|
||||
try:
|
||||
msgs = stf_device_mgmt.delete_device(ip, stf=stf)
|
||||
return jsonify({"ok": True, "msgs": msgs})
|
||||
except StfDevError as e:
|
||||
return jsonify({"ok": False, "error": str(e)}), 502
|
||||
try:
|
||||
device_pool.remove_device(f"{ip}:5555" if ":" not in ip else ip)
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify({"ok": True, "msgs": msgs})
|
||||
|
||||
|
||||
@app.route("/api/stf/devmgmt/reconnect", methods=["POST"])
|
||||
@@ -1441,6 +1464,95 @@ def api_stf_devmgmt_reconnect():
|
||||
return jsonify({"ok": False, "error": str(e)}), 502
|
||||
|
||||
|
||||
# ================== API:设备池管理(本地 SQLite 清单,新增设备的规范化入口) ==================
|
||||
@app.route("/api/devices/pool", methods=["GET"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_devices_pool_list():
|
||||
"""设备池清单(SQLite,含实时在线状态)。"""
|
||||
try:
|
||||
online = set(device_pool.list_online())
|
||||
except Exception:
|
||||
online = set()
|
||||
rows = device_pool.list_devices()
|
||||
for r in rows:
|
||||
r["online"] = r["serial"] in online
|
||||
return jsonify({"ok": True, "devices": rows})
|
||||
|
||||
|
||||
@app.route("/api/devices/pool/add", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_devices_pool_add():
|
||||
"""添加/更新设备:{serial, name?, note?}。
|
||||
|
||||
IP:5555 设备添加后立即尝试 adb connect(不可达显示离线,不影响其他设备)。
|
||||
"""
|
||||
data = request.json or {}
|
||||
serial = (data.get("serial") or "").strip()
|
||||
if not serial:
|
||||
return jsonify({"ok": False, "error": "请输入设备 serial(如 100.100.10.20:5555)"}), 400
|
||||
is_new = device_pool.add_device(serial,
|
||||
name=(data.get("name") or "").strip(),
|
||||
note=(data.get("note") or "").strip())
|
||||
msg = "已添加" if is_new else "已更新"
|
||||
if ":" in serial:
|
||||
try:
|
||||
adb_connect(serial)
|
||||
except Exception:
|
||||
pass
|
||||
_log.info(f"设备池管理: {msg} {serial}")
|
||||
return jsonify({"ok": True, "msg": msg, "is_new": is_new})
|
||||
|
||||
|
||||
@app.route("/api/devices/pool/remove", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_devices_pool_remove():
|
||||
"""从设备池删除(不再参与调度;不影响其他系统)。"""
|
||||
serial = (request.json or {}).get("serial", "").strip()
|
||||
if not serial:
|
||||
return jsonify({"ok": False, "error": "缺少 serial"}), 400
|
||||
ok = device_pool.remove_device(serial)
|
||||
if not ok:
|
||||
return jsonify({"ok": False, "error": "设备不存在"}), 404
|
||||
return jsonify({"ok": True, "msg": "已删除"})
|
||||
|
||||
|
||||
@app.route("/api/devices/pool/toggle", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_devices_pool_toggle():
|
||||
"""启用/停用设备(停用后不参与调度)。"""
|
||||
data = request.json or {}
|
||||
serial = (data.get("serial") or "").strip()
|
||||
enabled = data.get("enabled")
|
||||
if not serial or enabled is None:
|
||||
return jsonify({"ok": False, "error": "缺少参数"}), 400
|
||||
ok = device_pool.set_enabled(serial, bool(enabled))
|
||||
if not ok:
|
||||
return jsonify({"ok": False, "error": "设备不存在"}), 404
|
||||
return jsonify({"ok": True, "msg": "已" + ("启用" if enabled else "停用")})
|
||||
|
||||
|
||||
@app.route("/api/devices/pool/reconnect", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_devices_pool_reconnect():
|
||||
"""一键重连:并发 adb connect 池内全部 IP:5555 设备(后台执行,不阻塞)。"""
|
||||
def _run():
|
||||
try:
|
||||
serials = [s for s in device_pool.list_configured() if ":" in s]
|
||||
except Exception:
|
||||
return
|
||||
if not serials:
|
||||
return
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
with ThreadPoolExecutor(max_workers=10) as pool:
|
||||
futures = {pool.submit(adb_connect, s): s for s in serials}
|
||||
for _ in as_completed(futures):
|
||||
pass
|
||||
_log.info(f"设备池一键重连完成({len(serials)} 台)")
|
||||
import threading
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
return jsonify({"ok": True, "msg": "重连已启动(后台并发,约 10-20 秒)"})
|
||||
|
||||
|
||||
@app.route("/api/stf/restart", methods=["POST"])
|
||||
@admin_required
|
||||
def api_stf_restart():
|
||||
|
||||
Reference in New Issue
Block a user