Files
2026-05-28 17:04:29 +08:00

251 lines
8.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
const state = {
meta: null,
logs: [],
currentRel: null,
currentDetail: null,
loadSource: 'outbound',
bodyBase64: '',
};
function $(id) { return document.getElementById(id); }
async function api(path, opts) {
const r = await fetch(path, opts);
const t = await r.text();
let j;
try { j = JSON.parse(t); } catch { j = { error: t }; }
if (!r.ok) throw new Error(j.error || r.statusText);
return j;
}
function esc(s) {
const d = document.createElement('div');
d.textContent = s == null ? '' : String(s);
return d.innerHTML;
}
async function loadMeta() {
state.meta = await api('/t/api/meta');
$('meta-hint').innerHTML =
'28212: <code>' + esc(state.meta.superviseTarget) + '</code><br>' +
'28211: <code>' + esc(state.meta.fileTarget) + '</code>';
}
async function loadLogs() {
state.logs = await api('/t/api/logs');
const ul = $('log-list');
ul.innerHTML = '';
if (!state.logs.length) {
ul.innerHTML = '<li style="cursor:default;color:var(--muted)">暂无 file 日志</li>';
return;
}
state.logs.forEach((e) => {
const li = document.createElement('li');
li.dataset.rel = e.rel;
li.innerHTML =
'<div class="name">' + esc(e.name) + '</div>' +
'<div class="sub">' + esc(e.hourDir) + ' · ' + esc(e.modified) + '</div>';
li.onclick = () => selectLog(e.rel, li);
ul.appendChild(li);
});
}
async function selectLog(rel, liEl) {
state.currentRel = rel;
document.querySelectorAll('.log-list li').forEach((el) => el.classList.remove('active'));
if (liEl) liEl.classList.add('active');
state.currentDetail = await api('/t/api/logs/detail?rel=' + encodeURIComponent(rel));
loadSection(state.loadSource === 'inbound' ? state.currentDetail.inbound : state.currentDetail.outbound);
showLogResponse();
$('logmeta-view').textContent = JSON.stringify({
meta: state.currentDetail.meta,
parseWarnings: state.currentDetail.parseWarnings,
outboundAbsent: state.currentDetail.outboundAbsent,
}, null, 2);
}
function showLogResponse() {
const r = state.currentDetail.response;
let text = '';
if (r.bodyJSON) text = r.bodyJSON;
else if (r.body) text = r.body;
else text = '(日志块3 无响应体)';
if (r.status) text = 'HTTP ' + r.status + '\n\n' + text;
$('response-view').textContent = text;
}
function loadSection(sec) {
if (!sec) return;
$('url').value = sec.url || '';
renderHeaders(sec.headers || {});
const info = [];
if (sec.bodyLen) info.push('body_len=' + sec.bodyLen);
if (sec.bodyPlain) info.push('明文');
else if (sec.bodyBase64) info.push('base64');
$('body-info').textContent = info.join(' · ') || '';
state.bodyBase64 = sec.bodyBase64 || '';
if (sec.bodyPlain && sec.bodyRaw) {
$('body-raw').value = sec.bodyRaw;
} else if (sec.bodyBase64) {
try {
$('body-raw').value = '[binary ' + sec.bodyLen + ' bytes — 发送时将使用 bodyBase64 回放]';
} catch (e) {
$('body-raw').value = '';
}
} else {
$('body-raw').value = sec.bodyRaw || '';
}
$('body-base64').value = state.bodyBase64;
}
function renderHeaders(hdrs) {
const tbody = $('headers-table').querySelector('tbody');
tbody.innerHTML = '';
Object.entries(hdrs).forEach(([k, v]) => addHeaderRow(k, v));
if (!tbody.children.length) addHeaderRow('', '');
}
function addHeaderRow(k, v) {
const tbody = $('headers-table').querySelector('tbody');
const tr = document.createElement('tr');
tr.innerHTML =
'<td><input class="hk" value="' + esc(k).replace(/"/g, '&quot;') + '"></td>' +
'<td><input class="hv" value="' + esc(v).replace(/"/g, '&quot;') + '"></td>' +
'<td><button type="button" class="btn sm del">删</button></td>';
tr.querySelector('.del').onclick = () => tr.remove();
tbody.appendChild(tr);
}
function collectHeaders() {
const h = {};
$('headers-table').querySelectorAll('tbody tr').forEach((tr) => {
const k = tr.querySelector('.hk').value.trim();
const v = tr.querySelector('.hv').value.trim();
if (k) h[k] = v;
});
return h;
}
async function runConnect(channel) {
const box = $('connect-results');
box.innerHTML = '<span class="badge">测试中…</span>';
try {
const res = await api('/t/api/test/connect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channel }),
});
box.innerHTML = res.results.map((r) => {
const cls = r.ok ? 'ok' : 'err';
const detail = r.ok
? 'http=' + r.httpStatus + ' ' + r.elapsedMs + 'ms'
: (r.phase || '') + ' ' + (r.error || '');
return '<span class="badge ' + cls + '" title="' + esc(r.url) + '">' +
esc(r.channel) + ' ' + esc(detail) + '</span>';
}).join('');
} catch (e) {
box.innerHTML = '<span class="badge err">' + esc(e.message) + '</span>';
}
}
async function sendRequest() {
const payload = {
url: $('url').value.trim(),
headers: collectHeaders(),
useOutbound: state.loadSource === 'outbound',
};
if (state.currentRel) payload.rel = state.currentRel;
const b64 = $('body-base64').value.trim();
if (b64 && !$('body-raw').value.startsWith('[binary')) {
payload.bodyBase64 = b64;
} else if (b64) {
payload.bodyBase64 = b64;
} else {
const raw = $('body-raw').value;
if (raw && !raw.startsWith('[binary')) {
payload.bodyBase64 = btoa(unescape(encodeURIComponent(raw)));
}
}
$('response-view').textContent = '请求中…';
try {
const res = await api('/t/api/test/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
let text = 'HTTP ' + res.httpStatus + ' (' + res.elapsedMs + 'ms)\n\n';
if (res.error) text += 'Error: ' + res.error;
else if (res.bodyJSON) text += res.bodyJSON;
else text += res.body || '';
$('response-view').textContent = text;
} catch (e) {
$('response-view').textContent = '失败: ' + e.message;
}
}
function exportLog(source) {
if (!state.currentRel) {
alert('请先选择日志');
return;
}
window.location.href =
'/t/api/logs/export?rel=' + encodeURIComponent(state.currentRel) +
'&source=' + (source === 'inbound' ? 'inbound' : 'outbound');
}
function copyCurl() {
const url = $('url').value.trim();
const hdrs = collectHeaders();
let parts = ['curl -X POST "' + url + '"'];
Object.entries(hdrs).forEach(([k, v]) => {
parts.push('-H "' + k + ': ' + v.replace(/"/g, '\\"') + '"');
});
const b64 = $('body-base64').value.trim();
if (b64) {
parts.push('--data-binary @<(echo ' + b64 + ' | base64 -d)');
} else {
const raw = $('body-raw').value;
if (raw && !raw.startsWith('[binary')) {
parts.push("-d '" + raw.replace(/'/g, "'\\''") + "'");
}
}
navigator.clipboard.writeText(parts.join(' \\\n ')).then(
() => alert('已复制 cURLmultipart 二进制请用导出 ApiPost'),
() => alert('复制失败')
);
}
document.querySelectorAll('.tab').forEach((tab) => {
tab.onclick = () => {
document.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'));
document.querySelectorAll('.panel').forEach((p) => p.classList.remove('active'));
tab.classList.add('active');
$('panel-' + tab.dataset.tab).classList.add('active');
};
});
document.querySelectorAll('[data-connect]').forEach((btn) => {
btn.onclick = () => runConnect(btn.dataset.connect);
});
$('btn-refresh-logs').onclick = loadLogs;
$('btn-load-outbound').onclick = () => {
state.loadSource = 'outbound';
if (state.currentDetail) loadSection(state.currentDetail.outbound);
};
$('btn-load-inbound').onclick = () => {
state.loadSource = 'inbound';
if (state.currentDetail) loadSection(state.currentDetail.inbound);
};
$('btn-export-outbound').onclick = () => exportLog('outbound');
$('btn-export-inbound').onclick = () => exportLog('inbound');
$('btn-curl').onclick = copyCurl;
$('btn-send').onclick = sendRequest;
$('btn-add-header').onclick = () => addHeaderRow('', '');
loadMeta().then(loadLogs).catch((e) => {
$('meta-hint').textContent = '加载失败: ' + e.message;
});
})();