251 lines
8.4 KiB
JavaScript
251 lines
8.4 KiB
JavaScript
(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, '"') + '"></td>' +
|
||
'<td><input class="hv" value="' + esc(v).replace(/"/g, '"') + '"></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('已复制 cURL(multipart 二进制请用导出 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;
|
||
});
|
||
})();
|