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;
|
|||
|
|
});
|
|||
|
|
})();
|