Files
MediaCrawler/mac-agent-os-main/05_tools/10_dashboard/frontend/src/views/matrix-c2.js
T
butubb 2112c1a870
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s
feat(monitor): 抖音 Web 接口客户端 —— 绕开爬虫子进程,直接发 HTTP
移植自 mac-agent-os 的 mediacrawler_adapter:不起子进程、不开页面,用浏览器里那份
登录态直接调抖音 Web 接口。产物键名照抄 store/douyin,所以 ingest 那条链路一个字不用改。

**目前能用的(真环境实测,非推断)**:

    profile/other : 200, 7075 字节  —— 博主主页指标(粉丝/获赞/作品数/昵称)
    aweme/detail  : 200, 45425 字节 —— 单条作品详情(含点赞/评论/收藏/分享)

**目前不能用的:作品列表 `aweme/post`。** 两个互相独立的原因:
  1. 这个接口被抖音单独升级成了真校验:不带 x-tt-argus 回 403「Uifid Not Found」,
     带上 dummy 值回 200 + **空 body**。也就是说「头在不在」骗得过,「真校验」过不了。
     同一套头打 profile/other 和 aweme/detail 都是通的 —— 抖音是挑着接口加保护的,
     挑中的恰好是「批量拉作品列表」这个最敏感的动作。
  2. 改走页面截获也不行:CDP 浏览器打开博主主页会落到「验证码中间页」(当天大量探测的
     代价,过几小时要重测)。

  所以现在的边界是:**已知作品的指标刷新能做,自动发现新作品做不了**。

**排查中控住变量后得到的两条事实**(都写进注释了):
  · `Accept` / `Accept-Language` / `Referer` 才是主页接口能返回真数据的原因 —— 只有
    UA+client hints+Cookie 时是 200 但仅 121 字节的空壳,补上这三个头变 7074 字节。
    (我先前猜的 sec-ch-ua 不是关键。)
  · 因此 UA 与 client hints 必须**成套地取自同一个浏览器**,所以 BrowserIdentity 一次
    从 CDP 取齐 cookie + UA + hints,而不是各自写死。

「200 + 空 body 必须当场报错」也是刻意写死的:放过去它会在下游变成「这个博主没作品」,
把一次失败伪装成一条正常结果 —— 爬虫那条路正是这么栽的,还被翻译成「账号被封」。

测试 +11:cookie 解析、请求头成套性(含 uifid 缺失/回退)、产物键名与 store 对齐、
以及 _get 的三条失败路径(空 body / 403 带网关原话 / 正常返回)。
2026-10-10 17:12:25 +08:00

133 lines
7.8 KiB
Python

/**
* 远程控制视图 — C2 机器列表与命令下发
*/
export async function loadView(container) {
container.innerHTML = '<div class="loading">⏳ 加载中...</div>';
try {
const uid = Date.now().toString(36);
const res = await fetch('/api/c2/machines');
const data = res.ok ? await res.json() : { machines: [] };
const machines = Array.isArray(data) ? data : (data.machines || []);
container.innerHTML = `
<div style="padding:16px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<h2 style="font-size:18px;margin:0">🖥️ 远程控制</h2>
<span style="font-size:12px;color:var(--text2)">
<strong>${machines.length}</strong> 台机器
<span style="margin-left:8px;font-size:11px">🟢在线 <strong id="onlineCount_${uid}">0</strong></span>
<span style="margin-left:4px;font-size:11px">🔴离线 <strong id="offlineCount_${uid}">0</strong></span>
</span>
</div>
<div id="machineGrid_${uid}" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:10px"></div>
</div>`;
const gridEl = document.getElementById('machineGrid_' + uid);
if (!gridEl) { container.innerHTML = '<div class="error">❌ 渲染错误: 容器丢失</div>'; return; }
function renderMachines(list) {
const online = list.filter(m => m.online !== false);
const onlineEl = document.getElementById('onlineCount_' + uid);
const offlineEl = document.getElementById('offlineCount_' + uid);
if (onlineEl) onlineEl.textContent = online.length;
if (offlineEl) offlineEl.textContent = list.length - online.length;
if (!list.length) {
gridEl.innerHTML = '<div style="font-size:12px;color:var(--text2);padding:40px;text-align:center">暂无机器数据</div>';
return;
}
gridEl.innerHTML = list.map(m => {
const isOnline = m.online !== false;
return `<div style="background:var(--bg2);border-radius:var(--radius);padding:14px;border:1px solid var(--border)">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<div style="display:flex;align-items:center;gap:6px">
<span style="width:8px;height:8px;border-radius:50%;background:${isOnline ? 'var(--green, #22c55e)' : 'var(--red, #ef4444)'};display:inline-block"></span>
<span style="font-weight:600;font-size:13px">${m.hostname || m.name || m.id}</span>
</div>
<span style="font-size:10px;color:var(--text2)">${m.ip || m.address || ''}</span>
</div>
<div style="font-size:11px;color:var(--text2);margin-bottom:8px">
${m.os ? `<span style="margin-right:8px">${m.os}</span>` : ''}
${m.platform ? `<span>${m.platform}</span>` : ''}
${m.version ? `<span style="margin-left:8px">v${m.version}</span>` : ''}
</div>
<div style="display:flex;gap:4px;flex-wrap:wrap">
<button onclick="window._sendC2('${m.id || m.hostname || m.name}','shell','')" style="background:var(--bg3);border:1px solid var(--border);border-radius:4px;padding:3px 10px;font-size:10px;cursor:pointer">💻 Shell</button>
<button onclick="window._sendC2('${m.id || m.hostname || m.name}','exec','')" style="background:var(--bg3);border:1px solid var(--border);border-radius:4px;padding:3px 10px;font-size:10px;cursor:pointer">⚡ 执行</button>
<button onclick="window._sendC2('${m.id || m.hostname || m.name}','file','')" style="background:var(--bg3);border:1px solid var(--border);border-radius:4px;padding:3px 10px;font-size:10px;cursor:pointer">📁 文件</button>
<button onclick="window._sendC2('${m.id || m.hostname || m.name}','ping','')" style="background:var(--bg3);border:1px solid var(--border);border-radius:4px;padding:3px 10px;font-size:10px;cursor:pointer">🏓 Ping</button>
<button onclick="window._showCustomC2('${m.id || m.hostname || m.name}')" style="background:var(--primary);color:#fff;border:none;border-radius:4px;padding:3px 10px;font-size:10px;cursor:pointer">✏️ 自定义</button>
</div>
<div id="c2Result_${(m.id || m.hostname || m.name).replace(/[^a-zA-Z0-9]/g, '_')}" style="margin-top:6px;font-size:10px;color:var(--text2);min-height:0"></div>
</div>`;
}).join('');
}
renderMachines(machines);
// 通用 C2 发送函数
window._sendC2 = async (target, type, params) => {
const resultId = 'c2Result_' + target.replace(/[^a-zA-Z0-9]/g, '_');
const el = document.getElementById(resultId);
if (el) el.innerHTML = '<span style="color:var(--amber)">⏳ 发送中...</span>';
try {
const r = await fetch('/api/c2/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target, type, params }),
});
const d = await r.json();
if (el) {
const output = d.output || d.result || d.message || JSON.stringify(d);
el.innerHTML = `<span style="color:${r.ok ? 'var(--green)' : 'var(--red)'}">${r.ok ? '✅' : '❌'} ${output}</span>`;
}
} catch (e) {
if (el) el.innerHTML = `<span style="color:var(--red)">❌ ${e.message}</span>`;
}
};
// 自定义命令弹窗
window._showCustomC2 = (target) => {
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:99999;display:flex;align-items:center;justify-content:center';
overlay.innerHTML = `
<div style="background:var(--bg2);border-radius:12px;padding:20px;max-width:400px;width:90%">
<div style="font-size:15px;font-weight:600;margin-bottom:12px">✏️ 发送命令 → ${target}</div>
<div style="display:grid;gap:8px;font-size:12px">
<label>类型
<select id="customC2Type" style="width:100%;padding:4px;background:var(--bg3);border:1px solid var(--border);color:var(--text);border-radius:4px">
<option value="shell">💻 Shell</option>
<option value="exec">⚡ 执行</option>
<option value="file">📁 文件</option>
<option value="ping">🏓 Ping</option>
<option value="custom">🔧 自定义</option>
</select>
</label>
<label>参数
<textarea id="customC2Params" rows="4" placeholder='JSON 格式或文本参数' style="width:100%;padding:4px;background:var(--bg3);border:1px solid var(--border);color:var(--text);border-radius:4px;font-size:11px;resize:vertical"></textarea>
</label>
</div>
<div style="display:flex;gap:6px;justify-content:flex-end;margin-top:12px">
<button onclick="this.closest('div[style]').parentElement.remove()" style="background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:6px 16px;font-size:12px;cursor:pointer">取消</button>
<button onclick="window._doCustomC2('${target}')" style="background:var(--primary);color:#fff;border:none;border-radius:6px;padding:6px 16px;font-size:12px;cursor:pointer">🚀 发送</button>
</div>
</div>`;
document.body.appendChild(overlay);
};
window._doCustomC2 = (target) => {
const type = document.getElementById('customC2Type')?.value || 'shell';
const paramsRaw = document.getElementById('customC2Params')?.value || '';
let params = paramsRaw;
// 尝试解析 JSON
try { params = JSON.parse(paramsRaw); } catch {}
window._sendC2(target, type, params);
document.querySelector('div[style*="position:fixed"]')?.remove();
};
} catch (e) {
container.innerHTML = `<div class="error">❌ ${e.message || e}</div>`;
}
}