diff --git a/README.md b/README.md
index 20ef2f4..078ae34 100644
--- a/README.md
+++ b/README.md
@@ -522,6 +522,27 @@ node prober/node/index.js
> ⚠️ 注意:打开后同一端口上的公开面板(`/` 与 `/api/status`)也会跟着对外可达。它们本身是只读的,但目标名称、地址与可用率会一并暴露。如果不希望这样,请在你自己那层(反向代理/隧道)按路径收窄。
+### 面板部署在别处(跨域只读)
+
+面板与探针可以放在两台机器上:面板放 GitHub Pages / Cloudflare(静态托管),探针放你家里或内网的机器上持续探测。两项配置:
+
+| 配置 | 作用 |
+| --- | --- |
+| `settings.dataUrl` | 面板去哪里取数据,例 `https://monitor.example.com`。留空表示与探针同源 |
+| `settings.corsOrigins` | 允许跨域读取的**页面地址**,逗号分隔的完整 origin,如 `https://you.github.io` |
+
+两者都在管理界面里填(改完立即生效)。
+
+**硬限制(务必先看)**:GitHub Pages 强制 HTTPS,而浏览器的**混合内容**策略禁止 HTTPS 页面 `fetch` HTTP 资源 —— 这与 CORS 无关,改设置也没用。**因此 `dataUrl` 必须与面板页面同为 HTTPS**(你自己搭隧道给本地探针套 HTTPS 即可)。
+
+安全设计:
+
+- **绝不使用 `Access-Control-Allow-Origin: *`** —— 那等于让任意网站都能读走你的监控数据。默认只放行本机回环地址(`localhost` / `127.0.0.1`,**忽略端口**,因为本地前端常跑在 5173 这类端口)
+- 白名单里的 origin 必须**精确匹配**,`https://you.github.io.evil.com` 这种前缀伪造会被拒
+- 回显具体 origin 并带 `Vary: Origin`,避免中间缓存串味
+- **管理端永不放行跨域**:`/admin` 与 `/api/admin/*` 不带任何 `Access-Control-*` 头,外部页面无法读取也无法写入
+- 跨域读到的配置同样经过密钥剥离
+
### 部署形态与可写能力
| 部署形态 | 面板可读 | 探测 | 通知/维护窗口/事件 | 管理端(2FA) |
@@ -814,7 +835,7 @@ NODE_PATH= node scripts/verify-views.cjs # 视
NODE_PATH= node scripts/verify-layout.cjs # 13 视口 × 2 视图零横向溢出 + 行高一致(26 项)
NODE_PATH= node scripts/verify-admin.cjs # 管理端 2FA 全流程(102 项:RFC 向量 / 绑定 / 会话 / 限流 / 接口 / 页面)
node scripts/verify-worker.cjs # Cloudflare Worker(40 项:UR 映射 / 探测范围 / 失败降级 / 管理端拒绝 / 密钥泄露)
-node scripts/verify-security.cjs # banner 探测 / 脱敏 / 私有分层 / 刷新限流 / 配置接口密钥泄露(25 项)
+node scripts/verify-security.cjs # banner 探测 / 脱敏 / 私有分层 / 刷新限流 / 密钥泄露 / CORS 白名单(40 项)
node scripts/verify-notify.cjs # 通知与证书(44 项:假 SMTP / 假 MeoW / 去抖 / 恢复 / 维护窗口 / 真实证书)
UPTIMEROBOT_API_KEY=ur... node scripts/verify-uptimerobot.cjs # UR 映射/兼容接口/失败路径/冒烟(18 项)
```
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
index 1074b2b..a2d5af7 100644
--- a/docs/SECURITY.md
+++ b/docs/SECURITY.md
@@ -180,6 +180,23 @@ v0.12.0 起,所有会改配置的操作都放在 `/admin`,**必须绑定并
`/api/monitors` 与 Worker 上的同名接口都**无鉴权**,因此对外响应一律剥离 `uptimeRobot.apiKey`(保留 `enabled` 等非敏感字段供面板读取)。探针内部同步 UptimeRobot 仍使用完整配置,剥离只作用于 HTTP 出口。`/api/status` 与静态 `monitors.json` 同样不含私有层内容。
+### 跨域只读白名单(CORS)
+
+面板部署在别处(GitHub Pages / Cloudflare)、探针在另一台机器时,需要让指定页面跨域读取状态与配置。配置项:`settings.dataUrl`(面板去哪里取数据)与 `settings.corsOrigins`(允许哪些页面读取)。
+
+核心风险是「任意网站都能读走监控数据」,措施:
+
+| 风险 | 措施 |
+| --- | --- |
+| 开放成 `*` 导致任意网站可读 | **代码里不存在 `*` 分支**:白名单里写 `*` 不会放行任何来源(已加断言) |
+| 本机回环被任意端口冒用 | 默认只放行 `localhost` / `127.0.0.1` 且**忽略端口**(本地前端常跑 5173);其余必须精确匹配完整 origin |
+| 前缀伪造域名(`https://you.github.io.evil.com`) | 精确字符串匹配,不做子串判断(已加断言) |
+| 协议降级(`https://localhost` 冒用 http 的默认项) | 协议必须一致才走回环豁免(已加断言) |
+| 中间缓存串味(把 A 的响应给 B) | 回显具体 origin 并带 `Vary: Origin` |
+| **外部页面读取或改写管理端** | `/admin` 与 `/api/admin/*` **一律不带 `Access-Control-*` 头**,预检直接 403(已加断言) |
+| 外部页面借 CORS 触发有副作用的调用 | `/api/admin/*` 完全不放行;`/api/refresh` 另有 `refreshDecision` 的限流与 `refreshLocalOnly` 兜底 |
+| 跨域读到密钥 | 跨域响应与同源响应走同一套剥离逻辑,`uptimeRobot.apiKey` 不会回吐(已加断言) |
+
### 威胁模型与对应措施
| 威胁 | 措施 |
diff --git a/docs/screenshot-desktop.png b/docs/screenshot-desktop.png
index 86be7b3..fa341b1 100644
Binary files a/docs/screenshot-desktop.png and b/docs/screenshot-desktop.png differ
diff --git a/docs/screenshot-mobile.png b/docs/screenshot-mobile.png
index db98130..8138a33 100644
Binary files a/docs/screenshot-mobile.png and b/docs/screenshot-mobile.png differ
diff --git a/docs/screenshot-view-cards.png b/docs/screenshot-view-cards.png
index b4a808d..17ba3bb 100644
Binary files a/docs/screenshot-view-cards.png and b/docs/screenshot-view-cards.png differ
diff --git a/docs/screenshot-view-table.png b/docs/screenshot-view-table.png
index d18269c..b968171 100644
Binary files a/docs/screenshot-view-table.png and b/docs/screenshot-view-table.png differ
diff --git a/edge/cloudflare/worker.js b/edge/cloudflare/worker.js
index 5ed7ccd..8525dbb 100644
--- a/edge/cloudflare/worker.js
+++ b/edge/cloudflare/worker.js
@@ -16,7 +16,7 @@
import { connect } from 'cloudflare:sockets';
import { fetchUptimeRobotMonitors } from './uptimerobot.js';
-const VERSION = '0.12.1';
+const VERSION = '0.12.2';
const HISTORY_LIMIT = 48;
const UPTIME_DAYS = 90;
const USER_AGENT = 'shiroko-monitor/0.3 (lightweight uptime panel)';
diff --git a/prober/node/index.js b/prober/node/index.js
index 6e59017..14e39a2 100644
--- a/prober/node/index.js
+++ b/prober/node/index.js
@@ -39,7 +39,7 @@ const CONCURRENCY = 8;
const WRITE_DEBOUNCE_MS = 300;
const DEFAULT_INTERVAL_SEC = 60;
const PORT = Number(process.env.PORT || 8787);
-const VERSION = '0.12.1';
+const VERSION = '0.12.2';
const UR_DEFAULT_POLL_SEC = 300;
const UR_MIN_REFRESH_GAP_MS = 60000;
@@ -875,6 +875,56 @@ function sendJson(res, data, { status = 200, headers = {} } = {}) {
res.end(JSON.stringify(data));
}
+/* ---------- 跨域只读(前端可部署在别处,数据源指向本机) ---------- */
+
+// 允许跨域读取的前端来源。默认放行本机:本地开发时前端页面与探针常常跑在不同端口上
+// (例如 Vite 的 5173),而 origin 是「协议 + 主机 + 端口」的完整组合,
+// 因此默认项按「忽略端口」匹配,主机相同即放行。
+// 部署在 GitHub Pages / Cloudflare 等其它域名时,在 settings.corsOrigins 里显式列出
+// 完整 origin(含协议与端口,如 https://you.github.io)。绝不使用 '*':
+// 那等于让任意网站都能读走监控数据。
+function corsOrigins(config) {
+ const extra = Array.isArray(config.settings?.corsOrigins)
+ ? config.settings.corsOrigins.map((s) => String(s).trim()).filter(Boolean)
+ : [];
+ return { exact: new Set(extra), loopbackHosts: new Set(['127.0.0.1', 'localhost', '[::1]']) };
+}
+
+// 请求的 origin 是否在白名单内。主机是本机回环地址时忽略端口(本地开发常见形态),
+// 其余情况要求完整 origin 精确匹配。
+function originAllowed(reqOrigin, config) {
+ let u;
+ try { u = new URL(reqOrigin); } catch { return false; }
+ const { exact, loopbackHosts } = corsOrigins(config);
+ if (exact.has(reqOrigin)) return true;
+ return u.protocol === 'http:' && loopbackHosts.has(u.hostname);
+}
+
+// 返回跨域响应头;origin 不在白名单时返回空对象(请求本身照常处理,只是浏览器读不到响应)
+function corsHeaders(req, config) {
+ const origin = req.headers.origin;
+ if (!origin || !originAllowed(origin, config)) return {};
+ return {
+ 'access-control-allow-origin': origin, // 回显具体 origin,由白名单保证安全
+ 'access-control-allow-methods': 'GET, POST, OPTIONS',
+ 'access-control-allow-headers': 'content-type',
+ 'access-control-max-age': '86400',
+ vary: 'Origin', // 缓存必须区分 Origin,否则会串味
+ };
+}
+
+// 只读接口的预检响应。写接口(/api/refresh、/api/admin/*)不放行 OPTIONS,
+// 因此外部页面无法用简单请求之外的方式触发有副作用的调用。
+function handlePreflight(req, res, config) {
+ const headers = corsHeaders(req, config);
+ if (!headers['access-control-allow-origin']) {
+ res.writeHead(403);
+ return res.end('origin not allowed');
+ }
+ res.writeHead(204, headers);
+ res.end();
+}
+
/* ---------- /api/refresh 滥用防护 ---------- */
// 仅信任 socket 的对端地址,不解析 X-Forwarded-For(可被伪造);若前方挂了反向代理,请改用 allowRefresh:false
@@ -985,15 +1035,22 @@ function createServer(runtime) {
res.writeHead(200, { 'content-type': MIME['.html'], 'cache-control': 'no-store' });
return void fs.createReadStream(file).pipe(res);
}
- if (url.pathname === '/api/status') return sendJson(res, snapshot(runtime));
- if (url.pathname === '/api/monitors') return sendJson(res, publicConfig(config));
+ // 预检:只对只读接口与 /api/refresh 放行(/api/admin/* 一律不放行,管理端必须同源访问)。
+// /api/refresh 虽然有副作用,但已有 refreshDecision 的限流与 refreshLocalOnly 兜底,
+// 且它不带自定义头时属于「简单请求」——浏览器无论如何都会把它发出去,
+// 加跨域头只影响响应能否被 JS 读到(前端据此退回只读),不影响安全性。
+ if (req.method === 'OPTIONS' && (url.pathname === '/api/status' || url.pathname === '/api/monitors' || url.pathname === '/api/events' || url.pathname === '/api/refresh' || url.pathname === '/api/v2/getMonitors' || url.pathname === '/v2/getMonitors')) {
+ return handlePreflight(req, res, config);
+ }
+ if (url.pathname === '/api/status') return sendJson(res, snapshot(runtime), { headers: corsHeaders(req, config) });
+ if (url.pathname === '/api/monitors') return sendJson(res, publicConfig(config), { headers: corsHeaders(req, config) });
if (url.pathname === '/api/refresh') {
const decide = refreshDecision(config, req.socket.remoteAddress);
if (!decide.ok) {
// 只回错误码,不回显目标地址与内部细节
return sendJson(res, { error: decide.error }, {
status: decide.status,
- headers: decide.retryAfter ? { 'retry-after': String(decide.retryAfter) } : {},
+ headers: { ...(decide.retryAfter ? { 'retry-after': String(decide.retryAfter) } : {}), ...corsHeaders(req, config) },
});
}
// scope=external:只同步 UptimeRobot,不跑本地探测轮次(面板在「只看 UR」模式下用)
@@ -1010,19 +1067,19 @@ function createServer(runtime) {
]);
// 浏览器中断请求(切页/关标签)是常态:等待期间连接可能已断开,此时不能再写响应
if (res.writableEnded || res.destroyed) return;
- return sendJson(res, snapshot(runtime));
+ return sendJson(res, snapshot(runtime), { headers: corsHeaders(req, config) });
}
// UptimeRobot 兼容的只读接口(GET/POST 均可):已有脚本/看板可直接消费本服务数据。
// 不做鉴权,因为其暴露内容与公开面板一致;如需鉴权请在其前方加代理。
if (url.pathname === '/api/v2/getMonitors' || url.pathname === '/v2/getMonitors') {
- return sendJson(res, toUptimeRobotResponse(runtime));
+ return sendJson(res, toUptimeRobotResponse(runtime), { headers: corsHeaders(req, config) });
}
// 事件时间线:面板「事件」视图取这里(含通知去抖前的全部状态翻转记录)
if (url.pathname === '/api/events') {
const list = [...(runtime.events ?? [])].reverse();
- return sendJson(res, { total: list.length, events: list.slice(0, 200) });
+ return sendJson(res, { total: list.length, events: list.slice(0, 200) }, { headers: corsHeaders(req, config) });
}
let rel = decodeURIComponent(url.pathname);
diff --git a/prober/node/lib/admin-routes.js b/prober/node/lib/admin-routes.js
index 6d8496c..1b736e9 100644
--- a/prober/node/lib/admin-routes.js
+++ b/prober/node/lib/admin-routes.js
@@ -139,6 +139,8 @@ export function createAdminRouter({ auth, localConfigPath, reloadConfig, runtime
notifyEnabled: notifyConfig(cfg).enabled,
tlsEnabled: tlsConfig(cfg).enabled,
adminUrl: s.adminUrl ?? '',
+ dataUrl: s.dataUrl ?? '',
+ corsOrigins: Array.isArray(s.corsOrigins) ? s.corsOrigins : [],
},
maintenance: Array.isArray(s.maintenance) ? s.maintenance : [],
monitors: cfg.monitors.map((m) => ({
@@ -325,6 +327,41 @@ export function createAdminRouter({ auth, localConfigPath, reloadConfig, runtime
return sendJson(res, { ok: true, adminUrl: raw });
}
+ if (p === '/api/admin/remote' && req.method === 'POST') {
+ const parsed = await readJsonBody(req, res);
+ if (!parsed.ok) return;
+ const body = parsed.value;
+ // 跨域数据源:允许省略协议(默认 https),省得使用者每次都写全
+ const rawUrl = String(body.dataUrl ?? '').trim();
+ let dataUrl = '';
+ if (rawUrl) {
+ let u;
+ try { u = new URL(/^https?:\/\//i.test(rawUrl) ? rawUrl : `https://${rawUrl}`); } catch {
+ return sendJson(res, { error: 'invalid-url', hint: '不是合法的地址' }, { status: 400 });
+ }
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') {
+ return sendJson(res, { error: 'invalid-url', hint: '只支持 http/https' }, { status: 400 });
+ }
+ dataUrl = u.origin + u.pathname.replace(/\/+$/, '');
+ }
+ // CORS 白名单:只收完整的 origin,且拒绝 '*'
+ const origins = Array.isArray(body.corsOrigins)
+ ? body.corsOrigins.map((s) => String(s).trim()).filter(Boolean)
+ : String(body.corsOrigins ?? '').split(',').map((s) => s.trim()).filter(Boolean);
+ const bad = origins.filter((o) => o === '*' || !/^https?:\/\/[^\s/]+(:\d+)?$/.test(o));
+ if (bad.length) {
+ return sendJson(res, { error: 'invalid-origin', hint: `这些写法不合法(必须是完整来源,且不允许 *):${bad.join(', ')}` }, { status: 400 });
+ }
+ savePrivateConfig(localConfigPath, (local) => {
+ local.settings = { ...(local.settings ?? {}), dataUrl, corsOrigins: origins };
+ return local;
+ });
+ reloadConfig();
+ scheduleWrite(runtime);
+ console.log(`[admin] 跨域设置已保存:dataUrl=${dataUrl || '(空)'},corsOrigins=${origins.length ? origins.join(' ') : '(默认)'}`);
+ return sendJson(res, { ok: true, dataUrl, corsOrigins: origins });
+ }
+
if (p === '/api/admin/probe-now' && req.method === 'POST') {
await Promise.race([forceRound(runtime), new Promise((r) => setTimeout(r, forcedMaxWaitMs))]);
if (res.writableEnded || res.destroyed) return;
diff --git a/public/admin.html b/public/admin.html
index 96de905..ff71424 100644
--- a/public/admin.html
+++ b/public/admin.html
@@ -134,6 +134,23 @@
面板不做任何假设。
+
+
+
+
+ 仅当面板部署在别处(GitHub Pages / Cloudflare)而探针在另一台机器时才需要填。
+ 填了之后面板会去这个地址取数据,因此该地址必须与页面同为 HTTPS ——
+ HTTPS 页面读 HTTP 会被浏览器以「混合内容」为由直接拦截,与跨域设置无关。
+
+
+
+
+
+
+ 逗号分隔,必须是完整来源(含协议与端口)。只有这里列出的页面才能跨域读走监控数据;
+ 默认只放行 http://127.0.0.1 与 http://localhost。管理端本身永远不同源开放。
+
+
diff --git a/public/assets/admin.js b/public/assets/admin.js
index 5fdcbdf..ee10ffb 100644
--- a/public/assets/admin.js
+++ b/public/assets/admin.js
@@ -382,6 +382,8 @@ function renderConsole(d) {
$('data-filter').value = d.settings?.dataFilter ?? 'both';
$('admin-url').value = d.settings?.adminUrl ?? '';
$('admin-url').dataset.saved = $('admin-url').value;
+ $('data-url').value = d.settings?.dataUrl ?? '';
+ $('cors-origins').value = (d.settings?.corsOrigins ?? []).join(', ');
// 维护窗口目标下拉
const sel = $('mw-id');
@@ -559,6 +561,21 @@ $('admin-url').addEventListener('change', async (e) => {
}
});
+async function saveRemoteSettings() {
+ const dataUrl = $('data-url').value.trim();
+ const corsOrigins = $('cors-origins').value.split(',').map((s) => s.trim()).filter(Boolean);
+ const r = await api('/remote', { method: 'POST', body: { dataUrl, corsOrigins } });
+ if (r.ok) {
+ msg($('action-msg'), dataUrl ? `跨域数据源:${r.data.dataUrl};已放行 ${r.data.corsOrigins.length} 个来源` : '已清空跨域数据源', 'ok');
+ refresh();
+ } else {
+ msg($('action-msg'), errText(r.data?.error, r.data?.hint), 'err');
+ }
+}
+
+$('data-url').addEventListener('change', saveRemoteSettings);
+$('cors-origins').addEventListener('change', saveRemoteSettings);
+
$('probe-now').addEventListener('click', async () => {
const btn = $('probe-now');
btn.disabled = true;
diff --git a/public/assets/app.js b/public/assets/app.js
index 098e24d..dc55f75 100644
--- a/public/assets/app.js
+++ b/public/assets/app.js
@@ -1,4 +1,4 @@
-const VERSION = '0.12.1';
+const VERSION = '0.12.2';
const HB_LIMIT = 48;
const UPTIME_DAYS = 90;
const BROWSER_CONCURRENCY = 6;
@@ -28,6 +28,7 @@ const $ = (id) => document.getElementById(id);
const SOURCE_LABEL = {
api: '边缘 API(实时)',
+ remote: '远程探针(跨域)',
static: '静态快照',
browser: '浏览器直连',
};
@@ -36,7 +37,10 @@ const ESC = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''
const escapeHtml = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ESC[c]);
async function fetchJson(url, timeoutMs = 8000) {
- const res = await fetch(url, { cache: 'no-store', signal: AbortSignal.timeout(timeoutMs) });
+ // 相对路径 + 存在远程数据源时,自动改写到该地址(跨域读取由对方开放 CORS)
+ const base = remoteBase();
+ const full = base && url.startsWith('api/') ? `${base}/${url}` : url;
+ const res = await fetch(full, { cache: 'no-store', signal: AbortSignal.timeout(timeoutMs) });
if (!res.ok) {
const err = new Error(`HTTP ${res.status}`);
err.status = res.status;
@@ -83,9 +87,26 @@ function setSource(s) {
$('source-badge').textContent = `数据源:${SOURCE_LABEL[s] ?? s}`;
}
+// 跨域数据源:settings.dataUrl 指向另一台机器上的探针(通常是本机,但页面部署在别处)。
+// 例:"https://monitor.example.com" —— 前端会从 https://monitor.example.com/api/status 取数据。
+// 该地址必须与本页同为 HTTPS,否则浏览器会以「混合内容」为由拦截(与 CORS 无关)。
+function remoteBase() {
+ const raw = String(state.config?.settings?.dataUrl ?? '').trim();
+ if (!raw) return '';
+ try {
+ const u = new URL(raw);
+ return u.protocol === 'http:' || u.protocol === 'https:' ? u.origin + u.pathname.replace(/\/+$/, '') : '';
+ } catch {
+ return '';
+ }
+}
+
async function resolveSource() {
const forced = new URLSearchParams(location.search).get('source');
const mode = forced || state.config.settings?.dataSource || 'auto';
+ // 配置了远程数据源时,「api」与「auto」都优先尝试远程地址
+ const remote = remoteBase();
+ const apiUrl = remote ? `${remote}/api/status` : 'api/status';
if (mode === 'browser') return setSource('browser');
if (mode === 'static') {
try {
@@ -97,17 +118,17 @@ async function resolveSource() {
}
if (mode === 'api') {
try {
- await fetchJson('api/status', 5000);
- return setSource('api');
+ await fetchJson(apiUrl, 6000);
+ return setSource(remote ? 'remote' : 'api');
} catch {
return setSource('browser');
}
}
- // auto:优先边缘 API,其次静态快照,最后浏览器直连
+ // auto:优先远程/边缘 API,其次静态快照,最后浏览器直连
try {
- await fetchJson('api/status', 3000);
- return setSource('api');
- } catch { /* 无边缘 API */ }
+ await fetchJson(apiUrl, 4000);
+ return setSource(remote ? 'remote' : 'api');
+ } catch { /* 无边缘/远程 API */ }
try {
await fetchJson('status.json', 3000);
return setSource('static');
diff --git a/public/index.html b/public/index.html
index 42a1311..b166006 100644
--- a/public/index.html
+++ b/public/index.html
@@ -76,7 +76,7 @@
-
+
diff --git a/public/monitors.json b/public/monitors.json
index fc3040e..e9d34fc 100644
--- a/public/monitors.json
+++ b/public/monitors.json
@@ -10,6 +10,8 @@
"refreshInterval": 300,
"dataSource": "auto",
"adminUrl": "",
+ "dataUrl": "",
+ "corsOrigins": [],
"uptimeRobot": {
"enabled": false,
"apiKey": "",
diff --git a/public/status.json b/public/status.json
index 2c302d2..5c25649 100644
--- a/public/status.json
+++ b/public/status.json
@@ -1,8 +1,8 @@
{
"prober": "shiroko-monitor-node/0.12.1",
"demo": false,
- "generatedAt": "2026-10-14T07:22:36.648Z",
- "dataUpdatedAt": "2026-10-14T07:22:28.178Z",
+ "generatedAt": "2026-10-14T08:19:10.335Z",
+ "dataUpdatedAt": "2026-10-14T08:19:01.199Z",
"updateIntervalSec": 300,
"dataFilter": "uptimerobot",
"probeScope": "all",
@@ -11,12 +11,12 @@
"up": 5,
"down": 1,
"pending": 0,
- "avgLatency": 805,
+ "avgLatency": 865,
"avgUptime90": 83.3,
"memory": {
- "rssMB": 75.5,
- "heapUsedMB": 13.7,
- "heapTotalMB": 14.7
+ "rssMB": 73.7,
+ "heapUsedMB": 13.2,
+ "heapTotalMB": 14.5
}
},
"sources": {
@@ -24,10 +24,10 @@
"enabled": true,
"ok": true,
"error": null,
- "lastSyncAt": "2026-10-14T07:20:45.501Z",
+ "lastSyncAt": "2026-10-14T08:19:01.199Z",
"count": 6,
"syncing": false,
- "lastSyncMs": 1791962444262
+ "lastSyncMs": 1791965938931
}
},
"notify": {
@@ -55,189 +55,189 @@
"monitors": {
"ur-792059439": {
"status": "up",
- "latency": 433,
+ "latency": 325,
"code": null,
"detail": "UptimeRobot:运行正常",
- "lastCheck": "2026-10-14T07:15:00.000Z",
+ "lastCheck": "2026-10-14T08:16:00.000Z",
"history": [
{
- "t": "2026-10-13T08:35:00.000Z",
+ "t": "2026-10-13T09:30:00.000Z",
"ok": true,
- "ms": 434
+ "ms": 263
},
{
- "t": "2026-10-13T09:15:00.000Z",
- "ok": true,
- "ms": 333
- },
- {
- "t": "2026-10-13T09:55:00.000Z",
- "ok": true,
- "ms": 427
- },
- {
- "t": "2026-10-13T10:35:00.000Z",
- "ok": true,
- "ms": 233
- },
- {
- "t": "2026-10-13T11:15:00.000Z",
- "ok": true,
- "ms": 388
- },
- {
- "t": "2026-10-13T11:55:00.000Z",
- "ok": true,
- "ms": 329
- },
- {
- "t": "2026-10-13T12:35:00.000Z",
- "ok": true,
- "ms": 422
- },
- {
- "t": "2026-10-13T13:15:00.000Z",
- "ok": true,
- "ms": 271
- },
- {
- "t": "2026-10-13T13:55:00.000Z",
+ "t": "2026-10-13T10:15:00.000Z",
"ok": true,
"ms": 307
},
{
- "t": "2026-10-13T14:35:00.000Z",
+ "t": "2026-10-13T10:55:00.000Z",
"ok": true,
- "ms": 417
+ "ms": 379
},
{
- "t": "2026-10-13T15:15:00.000Z",
+ "t": "2026-10-13T11:35:00.000Z",
"ok": true,
- "ms": 283
+ "ms": 244
},
{
- "t": "2026-10-13T15:55:00.000Z",
+ "t": "2026-10-13T12:15:00.000Z",
"ok": true,
- "ms": 308
+ "ms": 337
},
{
- "t": "2026-10-13T16:35:00.000Z",
+ "t": "2026-10-13T12:55:00.000Z",
"ok": true,
- "ms": 279
+ "ms": 392
},
{
- "t": "2026-10-13T17:15:00.000Z",
+ "t": "2026-10-13T13:35:00.000Z",
"ok": true,
- "ms": 239
+ "ms": 249
},
{
- "t": "2026-10-13T17:55:00.000Z",
+ "t": "2026-10-13T14:15:00.000Z",
"ok": true,
- "ms": 299
+ "ms": 549
},
{
- "t": "2026-10-13T18:35:00.000Z",
+ "t": "2026-10-13T14:55:00.000Z",
"ok": true,
- "ms": 240
+ "ms": 231
},
{
- "t": "2026-10-13T19:15:00.000Z",
+ "t": "2026-10-13T15:40:00.000Z",
"ok": true,
- "ms": 526
+ "ms": 251
},
{
- "t": "2026-10-13T19:55:00.000Z",
+ "t": "2026-10-13T16:20:00.000Z",
"ok": true,
- "ms": 367
+ "ms": 482
},
{
- "t": "2026-10-13T20:35:00.000Z",
+ "t": "2026-10-13T17:00:00.000Z",
"ok": true,
- "ms": 369
+ "ms": 266
},
{
- "t": "2026-10-13T21:15:00.000Z",
+ "t": "2026-10-13T17:40:00.000Z",
"ok": true,
- "ms": 281
+ "ms": 249
},
{
- "t": "2026-10-13T21:55:00.000Z",
- "ok": true,
- "ms": 321
- },
- {
- "t": "2026-10-13T22:35:00.000Z",
- "ok": true,
- "ms": 150
- },
- {
- "t": "2026-10-13T23:15:00.000Z",
- "ok": true,
- "ms": 298
- },
- {
- "t": "2026-10-13T23:55:00.000Z",
+ "t": "2026-10-13T18:20:00.000Z",
"ok": true,
"ms": 305
},
{
- "t": "2026-10-14T00:35:00.000Z",
+ "t": "2026-10-13T19:00:00.000Z",
"ok": true,
- "ms": 208
+ "ms": 479
},
{
- "t": "2026-10-14T01:15:00.000Z",
+ "t": "2026-10-13T19:40:00.000Z",
+ "ok": true,
+ "ms": 378
+ },
+ {
+ "t": "2026-10-13T20:20:00.000Z",
+ "ok": true,
+ "ms": 372
+ },
+ {
+ "t": "2026-10-13T21:00:00.000Z",
+ "ok": true,
+ "ms": 276
+ },
+ {
+ "t": "2026-10-13T21:40:00.000Z",
+ "ok": true,
+ "ms": 304
+ },
+ {
+ "t": "2026-10-13T22:20:00.000Z",
+ "ok": true,
+ "ms": 202
+ },
+ {
+ "t": "2026-10-13T23:00:00.000Z",
+ "ok": true,
+ "ms": 228
+ },
+ {
+ "t": "2026-10-13T23:40:00.000Z",
+ "ok": true,
+ "ms": 316
+ },
+ {
+ "t": "2026-10-14T00:20:00.000Z",
+ "ok": true,
+ "ms": 356
+ },
+ {
+ "t": "2026-10-14T01:00:00.000Z",
+ "ok": true,
+ "ms": 264
+ },
+ {
+ "t": "2026-10-14T01:40:00.000Z",
+ "ok": true,
+ "ms": 260
+ },
+ {
+ "t": "2026-10-14T02:20:00.000Z",
+ "ok": true,
+ "ms": 286
+ },
+ {
+ "t": "2026-10-14T03:00:00.000Z",
+ "ok": true,
+ "ms": 204
+ },
+ {
+ "t": "2026-10-14T03:40:00.000Z",
+ "ok": true,
+ "ms": 256
+ },
+ {
+ "t": "2026-10-14T04:20:00.000Z",
+ "ok": true,
+ "ms": 197
+ },
+ {
+ "t": "2026-10-14T05:00:00.000Z",
+ "ok": true,
+ "ms": 261
+ },
+ {
+ "t": "2026-10-14T05:40:00.000Z",
+ "ok": true,
+ "ms": 374
+ },
+ {
+ "t": "2026-10-14T06:20:00.000Z",
+ "ok": true,
+ "ms": 268
+ },
+ {
+ "t": "2026-10-14T07:35:00.000Z",
"ok": true,
"ms": 421
},
{
- "t": "2026-10-14T02:00:00.000Z",
+ "t": "2026-10-14T08:16:00.000Z",
"ok": true,
- "ms": 191
- },
- {
- "t": "2026-10-14T02:40:00.000Z",
- "ok": true,
- "ms": 187
- },
- {
- "t": "2026-10-14T03:20:00.000Z",
- "ok": true,
- "ms": 282
- },
- {
- "t": "2026-10-14T04:00:00.000Z",
- "ok": true,
- "ms": 289
- },
- {
- "t": "2026-10-14T04:40:00.000Z",
- "ok": true,
- "ms": 171
- },
- {
- "t": "2026-10-14T05:20:00.000Z",
- "ok": true,
- "ms": 311
- },
- {
- "t": "2026-10-14T06:34:00.000Z",
- "ok": true,
- "ms": 177
- },
- {
- "t": "2026-10-14T07:15:00.000Z",
- "ok": true,
- "ms": 433
+ "ms": 325
}
],
"daily": {
"2026-10-14": {
"total": 4,
"up": 4,
- "latSum": 1744,
+ "latSum": 1903,
"latCount": 4,
- "latMax": 439
+ "latMax": 546
}
},
"meta": {
@@ -262,211 +262,211 @@
"90": 0
},
"lat": {
- "avg": 436,
- "max": 439,
+ "avg": 476,
+ "max": 546,
"days": [
{
"key": "2026-10-14",
- "avg": 436
+ "avg": 476
}
]
}
},
"ur-795497022": {
"status": "up",
- "latency": 672,
+ "latency": 719,
"code": null,
"detail": "UptimeRobot:运行正常",
- "lastCheck": "2026-10-14T07:18:00.000Z",
+ "lastCheck": "2026-10-14T08:16:00.000Z",
"history": [
{
- "t": "2026-10-13T08:00:00.000Z",
+ "t": "2026-10-13T08:55:00.000Z",
"ok": true,
- "ms": 758
+ "ms": 939
},
{
- "t": "2026-10-13T08:40:00.000Z",
+ "t": "2026-10-13T09:35:00.000Z",
"ok": true,
- "ms": 826
+ "ms": 663
},
{
- "t": "2026-10-13T09:20:00.000Z",
+ "t": "2026-10-13T10:15:00.000Z",
"ok": true,
- "ms": 749
+ "ms": 837
},
{
- "t": "2026-10-13T10:00:00.000Z",
+ "t": "2026-10-13T10:55:00.000Z",
"ok": true,
- "ms": 791
+ "ms": 723
},
{
- "t": "2026-10-13T10:40:00.000Z",
+ "t": "2026-10-13T11:35:00.000Z",
"ok": true,
- "ms": 678
+ "ms": 688
},
{
- "t": "2026-10-13T11:20:00.000Z",
+ "t": "2026-10-13T12:15:00.000Z",
"ok": true,
- "ms": 740
+ "ms": 824
},
{
- "t": "2026-10-13T12:00:00.000Z",
+ "t": "2026-10-13T12:55:00.000Z",
"ok": true,
- "ms": 828
+ "ms": 799
},
{
- "t": "2026-10-13T12:40:00.000Z",
+ "t": "2026-10-13T13:35:00.000Z",
+ "ok": true,
+ "ms": 943
+ },
+ {
+ "t": "2026-10-13T14:20:00.000Z",
+ "ok": true,
+ "ms": 808
+ },
+ {
+ "t": "2026-10-13T15:00:00.000Z",
+ "ok": true,
+ "ms": 842
+ },
+ {
+ "t": "2026-10-13T15:40:00.000Z",
+ "ok": true,
+ "ms": 918
+ },
+ {
+ "t": "2026-10-13T16:20:00.000Z",
+ "ok": true,
+ "ms": 646
+ },
+ {
+ "t": "2026-10-13T17:00:00.000Z",
"ok": true,
"ms": 722
},
{
- "t": "2026-10-13T13:20:00.000Z",
- "ok": true,
- "ms": 849
- },
- {
- "t": "2026-10-13T14:00:00.000Z",
- "ok": true,
- "ms": 1071
- },
- {
- "t": "2026-10-13T14:40:00.000Z",
- "ok": true,
- "ms": 863
- },
- {
- "t": "2026-10-13T15:20:00.000Z",
- "ok": true,
- "ms": 893
- },
- {
- "t": "2026-10-13T16:00:00.000Z",
- "ok": true,
- "ms": 742
- },
- {
- "t": "2026-10-13T16:40:00.000Z",
- "ok": true,
- "ms": 769
- },
- {
- "t": "2026-10-13T17:20:00.000Z",
- "ok": true,
- "ms": 769
- },
- {
- "t": "2026-10-13T18:00:00.000Z",
- "ok": true,
- "ms": 738
- },
- {
- "t": "2026-10-13T18:40:00.000Z",
- "ok": true,
- "ms": 888
- },
- {
- "t": "2026-10-13T19:20:00.000Z",
- "ok": true,
- "ms": 810
- },
- {
- "t": "2026-10-13T20:00:00.000Z",
- "ok": true,
- "ms": 754
- },
- {
- "t": "2026-10-13T20:40:00.000Z",
- "ok": true,
- "ms": 1721
- },
- {
- "t": "2026-10-13T21:20:00.000Z",
+ "t": "2026-10-13T17:40:00.000Z",
"ok": true,
"ms": 749
},
{
- "t": "2026-10-13T22:00:00.000Z",
+ "t": "2026-10-13T18:20:00.000Z",
"ok": true,
- "ms": 1288
+ "ms": 750
},
{
- "t": "2026-10-13T22:40:00.000Z",
+ "t": "2026-10-13T19:00:00.000Z",
"ok": true,
- "ms": 694
+ "ms": 920
},
{
- "t": "2026-10-13T23:20:00.000Z",
+ "t": "2026-10-13T19:40:00.000Z",
"ok": true,
- "ms": 728
+ "ms": 715
},
{
- "t": "2026-10-14T00:00:00.000Z",
+ "t": "2026-10-13T20:20:00.000Z",
"ok": true,
- "ms": 611
+ "ms": 835
},
{
- "t": "2026-10-14T00:40:00.000Z",
+ "t": "2026-10-13T21:00:00.000Z",
"ok": true,
- "ms": 752
+ "ms": 1038
},
{
- "t": "2026-10-14T01:20:00.000Z",
+ "t": "2026-10-13T21:40:00.000Z",
"ok": true,
- "ms": 734
+ "ms": 1235
},
{
- "t": "2026-10-14T02:00:00.000Z",
+ "t": "2026-10-13T22:20:00.000Z",
"ok": true,
- "ms": 635
+ "ms": 731
},
{
- "t": "2026-10-14T02:40:00.000Z",
+ "t": "2026-10-13T23:00:00.000Z",
"ok": true,
- "ms": 781
+ "ms": 685
},
{
- "t": "2026-10-14T03:20:00.000Z",
+ "t": "2026-10-13T23:40:00.000Z",
+ "ok": true,
+ "ms": 597
+ },
+ {
+ "t": "2026-10-14T00:20:00.000Z",
+ "ok": true,
+ "ms": 676
+ },
+ {
+ "t": "2026-10-14T01:00:00.000Z",
+ "ok": true,
+ "ms": 755
+ },
+ {
+ "t": "2026-10-14T01:40:00.000Z",
+ "ok": true,
+ "ms": 658
+ },
+ {
+ "t": "2026-10-14T02:20:00.000Z",
+ "ok": true,
+ "ms": 740
+ },
+ {
+ "t": "2026-10-14T03:00:00.000Z",
+ "ok": true,
+ "ms": 718
+ },
+ {
+ "t": "2026-10-14T03:40:00.000Z",
+ "ok": true,
+ "ms": 593
+ },
+ {
+ "t": "2026-10-14T04:20:00.000Z",
+ "ok": true,
+ "ms": 627
+ },
+ {
+ "t": "2026-10-14T05:00:00.000Z",
+ "ok": true,
+ "ms": 674
+ },
+ {
+ "t": "2026-10-14T05:40:00.000Z",
+ "ok": true,
+ "ms": 700
+ },
+ {
+ "t": "2026-10-14T06:20:00.000Z",
+ "ok": true,
+ "ms": 707
+ },
+ {
+ "t": "2026-10-14T07:02:00.000Z",
"ok": true,
"ms": 710
},
{
- "t": "2026-10-14T04:00:00.000Z",
+ "t": "2026-10-14T07:39:00.000Z",
"ok": true,
- "ms": 645
+ "ms": 718
},
{
- "t": "2026-10-14T04:40:00.000Z",
+ "t": "2026-10-14T08:16:00.000Z",
"ok": true,
- "ms": 668
- },
- {
- "t": "2026-10-14T05:20:00.000Z",
- "ok": true,
- "ms": 683
- },
- {
- "t": "2026-10-14T06:13:00.000Z",
- "ok": true,
- "ms": 711
- },
- {
- "t": "2026-10-14T06:45:00.000Z",
- "ok": true,
- "ms": 692
- },
- {
- "t": "2026-10-14T07:18:00.000Z",
- "ok": true,
- "ms": 672
+ "ms": 719
}
],
"daily": {
"2026-10-14": {
"total": 4,
"up": 4,
- "latSum": 2889,
+ "latSum": 2884,
"latCount": 4,
- "latMax": 751
+ "latMax": 745
}
},
"meta": {
@@ -491,201 +491,206 @@
"90": 5432
},
"lat": {
- "avg": 722,
- "max": 751,
+ "avg": 721,
+ "max": 745,
"days": [
{
"key": "2026-10-14",
- "avg": 722
+ "avg": 721
}
]
}
},
"ur-795556828": {
"status": "up",
- "latency": 435,
+ "latency": 369,
"code": null,
"detail": "UptimeRobot:运行正常",
- "lastCheck": "2026-10-14T07:15:00.000Z",
+ "lastCheck": "2026-10-14T08:16:00.000Z",
"history": [
{
- "t": "2026-10-13T08:35:00.000Z",
+ "t": "2026-10-13T09:00:00.000Z",
"ok": true,
- "ms": 224
+ "ms": 264
},
{
- "t": "2026-10-13T09:15:00.000Z",
+ "t": "2026-10-13T09:40:00.000Z",
"ok": true,
- "ms": 247
+ "ms": 305
},
{
- "t": "2026-10-13T10:00:00.000Z",
+ "t": "2026-10-13T10:20:00.000Z",
"ok": true,
- "ms": 455
+ "ms": 420
},
{
- "t": "2026-10-13T10:40:00.000Z",
+ "t": "2026-10-13T11:00:00.000Z",
"ok": true,
- "ms": 384
+ "ms": 227
},
{
- "t": "2026-10-13T11:20:00.000Z",
+ "t": "2026-10-13T11:40:00.000Z",
"ok": true,
- "ms": 334
+ "ms": 389
},
{
- "t": "2026-10-13T12:00:00.000Z",
+ "t": "2026-10-13T12:20:00.000Z",
"ok": true,
- "ms": 234
+ "ms": 302
},
{
- "t": "2026-10-13T12:40:00.000Z",
+ "t": "2026-10-13T13:00:00.000Z",
"ok": true,
- "ms": 335
+ "ms": 375
},
{
- "t": "2026-10-13T13:20:00.000Z",
+ "t": "2026-10-13T13:40:00.000Z",
"ok": true,
- "ms": 379
+ "ms": 287
},
{
- "t": "2026-10-13T14:00:00.000Z",
+ "t": "2026-10-13T14:20:00.000Z",
"ok": true,
- "ms": 304
+ "ms": 352
},
{
- "t": "2026-10-13T14:40:00.000Z",
- "ok": true,
- "ms": 341
- },
- {
- "t": "2026-10-13T15:25:00.000Z",
- "ok": true,
- "ms": 417
- },
- {
- "t": "2026-10-13T16:05:00.000Z",
- "ok": true,
- "ms": 241
- },
- {
- "t": "2026-10-13T16:45:00.000Z",
- "ok": true,
- "ms": 366
- },
- {
- "t": "2026-10-13T17:25:00.000Z",
- "ok": true,
- "ms": 244
- },
- {
- "t": "2026-10-13T18:05:00.000Z",
- "ok": true,
- "ms": 378
- },
- {
- "t": "2026-10-13T18:45:00.000Z",
- "ok": true,
- "ms": 310
- },
- {
- "t": "2026-10-13T19:25:00.000Z",
- "ok": true,
- "ms": 335
- },
- {
- "t": "2026-10-13T20:05:00.000Z",
- "ok": true,
- "ms": 351
- },
- {
- "t": "2026-10-13T20:45:00.000Z",
- "ok": true,
- "ms": 523
- },
- {
- "t": "2026-10-13T21:25:00.000Z",
- "ok": true,
- "ms": 400
- },
- {
- "t": "2026-10-13T22:05:00.000Z",
- "ok": true,
- "ms": 222
- },
- {
- "t": "2026-10-13T22:45:00.000Z",
- "ok": true,
- "ms": 169
- },
- {
- "t": "2026-10-13T23:25:00.000Z",
- "ok": true,
- "ms": 225
- },
- {
- "t": "2026-10-14T00:05:00.000Z",
- "ok": true,
- "ms": 209
- },
- {
- "t": "2026-10-14T00:45:00.000Z",
- "ok": true,
- "ms": 272
- },
- {
- "t": "2026-10-14T01:25:00.000Z",
- "ok": true,
- "ms": 431
- },
- {
- "t": "2026-10-14T02:05:00.000Z",
- "ok": true,
- "ms": 241
- },
- {
- "t": "2026-10-14T02:45:00.000Z",
- "ok": true,
- "ms": 444
- },
- {
- "t": "2026-10-14T03:25:00.000Z",
- "ok": true,
- "ms": 359
- },
- {
- "t": "2026-10-14T04:05:00.000Z",
- "ok": true,
- "ms": 400
- },
- {
- "t": "2026-10-14T04:45:00.000Z",
- "ok": true,
- "ms": 214
- },
- {
- "t": "2026-10-14T05:25:00.000Z",
+ "t": "2026-10-13T15:00:00.000Z",
"ok": true,
"ms": 404
},
{
- "t": "2026-10-14T06:35:00.000Z",
+ "t": "2026-10-13T15:40:00.000Z",
+ "ok": true,
+ "ms": 323
+ },
+ {
+ "t": "2026-10-13T16:20:00.000Z",
+ "ok": true,
+ "ms": 311
+ },
+ {
+ "t": "2026-10-13T17:00:00.000Z",
+ "ok": true,
+ "ms": 376
+ },
+ {
+ "t": "2026-10-13T17:40:00.000Z",
+ "ok": true,
+ "ms": 413
+ },
+ {
+ "t": "2026-10-13T18:20:00.000Z",
+ "ok": true,
+ "ms": 386
+ },
+ {
+ "t": "2026-10-13T19:00:00.000Z",
+ "ok": true,
+ "ms": 302
+ },
+ {
+ "t": "2026-10-13T19:40:00.000Z",
+ "ok": true,
+ "ms": 376
+ },
+ {
+ "t": "2026-10-13T20:20:00.000Z",
+ "ok": true,
+ "ms": 266
+ },
+ {
+ "t": "2026-10-13T21:00:00.000Z",
+ "ok": true,
+ "ms": 475
+ },
+ {
+ "t": "2026-10-13T21:40:00.000Z",
+ "ok": true,
+ "ms": 354
+ },
+ {
+ "t": "2026-10-13T22:20:00.000Z",
+ "ok": true,
+ "ms": 239
+ },
+ {
+ "t": "2026-10-13T23:00:00.000Z",
+ "ok": true,
+ "ms": 225
+ },
+ {
+ "t": "2026-10-13T23:40:00.000Z",
+ "ok": true,
+ "ms": 243
+ },
+ {
+ "t": "2026-10-14T00:20:00.000Z",
+ "ok": true,
+ "ms": 319
+ },
+ {
+ "t": "2026-10-14T01:00:00.000Z",
+ "ok": true,
+ "ms": 396
+ },
+ {
+ "t": "2026-10-14T01:45:00.000Z",
+ "ok": true,
+ "ms": 475
+ },
+ {
+ "t": "2026-10-14T02:25:00.000Z",
+ "ok": true,
+ "ms": 128
+ },
+ {
+ "t": "2026-10-14T03:05:00.000Z",
+ "ok": true,
+ "ms": 351
+ },
+ {
+ "t": "2026-10-14T03:45:00.000Z",
+ "ok": true,
+ "ms": 429
+ },
+ {
+ "t": "2026-10-14T04:25:00.000Z",
+ "ok": true,
+ "ms": 303
+ },
+ {
+ "t": "2026-10-14T05:05:00.000Z",
+ "ok": true,
+ "ms": 276
+ },
+ {
+ "t": "2026-10-14T05:45:00.000Z",
+ "ok": true,
+ "ms": 555
+ },
+ {
+ "t": "2026-10-14T06:25:00.000Z",
"ok": true,
"ms": 278
},
{
- "t": "2026-10-14T07:15:00.000Z",
+ "t": "2026-10-14T07:36:00.000Z",
"ok": true,
- "ms": 435
+ "ms": 204
+ },
+ {
+ "t": "2026-10-14T08:16:00.000Z",
+ "ok": true,
+ "ms": 369
}
],
"daily": {
"2026-10-14": {
"total": 4,
"up": 4,
- "latSum": 1749,
+ "latSum": 1530,
"latCount": 4,
- "latMax": 438
+ "latMax": 387
}
},
"meta": {
@@ -710,28 +715,23 @@
"90": 0
},
"lat": {
- "avg": 437,
- "max": 438,
+ "avg": 383,
+ "max": 387,
"days": [
{
"key": "2026-10-14",
- "avg": 437
+ "avg": 383
}
]
}
},
"ur-792059414": {
"status": "up",
- "latency": 2007,
+ "latency": 2428,
"code": null,
"detail": "UptimeRobot:运行正常",
- "lastCheck": "2026-10-14T07:16:00.000Z",
+ "lastCheck": "2026-10-14T08:17:00.000Z",
"history": [
- {
- "t": "2026-10-13T08:25:00.000Z",
- "ok": true,
- "ms": 1861
- },
{
"t": "2026-10-13T09:10:00.000Z",
"ok": true,
@@ -783,118 +783,123 @@
"ms": 2097
},
{
- "t": "2026-10-13T16:30:00.000Z",
+ "t": "2026-10-13T16:35:00.000Z",
"ok": true,
- "ms": 2060
+ "ms": 2206
},
{
- "t": "2026-10-13T17:15:00.000Z",
+ "t": "2026-10-13T17:20:00.000Z",
"ok": true,
- "ms": 2264
+ "ms": 2293
},
{
- "t": "2026-10-13T17:55:00.000Z",
+ "t": "2026-10-13T18:05:00.000Z",
"ok": true,
- "ms": 1246
+ "ms": 1347
},
{
- "t": "2026-10-13T18:35:00.000Z",
+ "t": "2026-10-13T18:50:00.000Z",
"ok": true,
- "ms": 1451
+ "ms": 1656
},
{
- "t": "2026-10-13T19:15:00.000Z",
+ "t": "2026-10-13T19:35:00.000Z",
"ok": true,
- "ms": 3225
+ "ms": 2883
},
{
- "t": "2026-10-13T19:55:00.000Z",
+ "t": "2026-10-13T20:20:00.000Z",
"ok": true,
- "ms": 2138
+ "ms": 2649
},
{
- "t": "2026-10-13T20:35:00.000Z",
+ "t": "2026-10-13T21:05:00.000Z",
"ok": true,
- "ms": 2633
+ "ms": 2923
},
{
- "t": "2026-10-13T21:15:00.000Z",
+ "t": "2026-10-13T21:50:00.000Z",
"ok": true,
- "ms": 3605
+ "ms": 1964
},
{
- "t": "2026-10-13T21:55:00.000Z",
+ "t": "2026-10-13T22:35:00.000Z",
"ok": true,
- "ms": 1785
+ "ms": 2335
},
{
- "t": "2026-10-13T22:40:00.000Z",
+ "t": "2026-10-13T23:20:00.000Z",
"ok": true,
- "ms": 1904
+ "ms": 2418
},
{
- "t": "2026-10-13T23:25:00.000Z",
+ "t": "2026-10-14T00:05:00.000Z",
"ok": true,
- "ms": 2527
+ "ms": 2098
},
{
- "t": "2026-10-14T00:10:00.000Z",
+ "t": "2026-10-14T00:50:00.000Z",
"ok": true,
- "ms": 2372
+ "ms": 1921
},
{
- "t": "2026-10-14T00:55:00.000Z",
+ "t": "2026-10-14T01:35:00.000Z",
"ok": true,
- "ms": 1934
+ "ms": 3110
},
{
- "t": "2026-10-14T01:40:00.000Z",
+ "t": "2026-10-14T02:20:00.000Z",
"ok": true,
- "ms": 3003
+ "ms": 1719
},
{
- "t": "2026-10-14T02:25:00.000Z",
+ "t": "2026-10-14T03:05:00.000Z",
"ok": true,
- "ms": 1655
+ "ms": 1623
},
{
- "t": "2026-10-14T03:10:00.000Z",
+ "t": "2026-10-14T03:45:00.000Z",
"ok": true,
- "ms": 1873
+ "ms": 3219
},
{
- "t": "2026-10-14T03:55:00.000Z",
+ "t": "2026-10-14T04:25:00.000Z",
"ok": true,
- "ms": 3017
+ "ms": 2408
},
{
- "t": "2026-10-14T04:40:00.000Z",
+ "t": "2026-10-14T05:05:00.000Z",
"ok": true,
- "ms": 1955
+ "ms": 2619
},
{
- "t": "2026-10-14T05:25:00.000Z",
+ "t": "2026-10-14T05:45:00.000Z",
"ok": true,
- "ms": 3612
+ "ms": 2976
},
{
- "t": "2026-10-14T06:36:00.000Z",
+ "t": "2026-10-14T06:25:00.000Z",
"ok": true,
- "ms": 1548
+ "ms": 1855
},
{
- "t": "2026-10-14T07:16:00.000Z",
+ "t": "2026-10-14T07:38:00.000Z",
"ok": true,
- "ms": 2007
+ "ms": 2010
+ },
+ {
+ "t": "2026-10-14T08:17:00.000Z",
+ "ok": true,
+ "ms": 2428
}
],
"daily": {
"2026-10-14": {
"total": 4,
"up": 4,
- "latSum": 7923,
+ "latSum": 8517,
"latCount": 4,
- "latMax": 2007
+ "latMax": 2428
}
},
"meta": {
@@ -919,206 +924,201 @@
"90": 7593
},
"lat": {
- "avg": 1981,
- "max": 2007,
+ "avg": 2129,
+ "max": 2428,
"days": [
{
"key": "2026-10-14",
- "avg": 1981
+ "avg": 2129
}
]
}
},
"ur-798788113": {
"status": "up",
- "latency": 480,
+ "latency": 486,
"code": null,
"detail": "UptimeRobot:运行正常",
- "lastCheck": "2026-10-14T07:16:00.000Z",
+ "lastCheck": "2026-10-14T08:17:00.000Z",
"history": [
{
- "t": "2026-10-13T08:00:00.000Z",
+ "t": "2026-10-13T09:30:00.000Z",
"ok": true,
- "ms": 279
+ "ms": 545
},
{
- "t": "2026-10-13T08:40:00.000Z",
+ "t": "2026-10-13T10:10:00.000Z",
"ok": true,
- "ms": 407
+ "ms": 456
},
{
- "t": "2026-10-13T09:20:00.000Z",
+ "t": "2026-10-13T10:50:00.000Z",
"ok": true,
- "ms": 317
+ "ms": 394
},
{
- "t": "2026-10-13T10:00:00.000Z",
+ "t": "2026-10-13T11:30:00.000Z",
"ok": true,
- "ms": 627
+ "ms": 398
},
{
- "t": "2026-10-13T10:40:00.000Z",
+ "t": "2026-10-13T12:10:00.000Z",
"ok": true,
- "ms": 505
+ "ms": 588
},
{
- "t": "2026-10-13T11:20:00.000Z",
+ "t": "2026-10-13T12:50:00.000Z",
"ok": true,
- "ms": 424
+ "ms": 430
},
{
- "t": "2026-10-13T12:00:00.000Z",
+ "t": "2026-10-13T13:30:00.000Z",
"ok": true,
- "ms": 1135
+ "ms": 502
},
{
- "t": "2026-10-13T12:40:00.000Z",
+ "t": "2026-10-13T14:15:00.000Z",
"ok": true,
- "ms": 462
+ "ms": 355
},
{
- "t": "2026-10-13T13:20:00.000Z",
+ "t": "2026-10-13T14:55:00.000Z",
"ok": true,
- "ms": 436
+ "ms": 358
},
{
- "t": "2026-10-13T14:00:00.000Z",
+ "t": "2026-10-13T15:35:00.000Z",
"ok": true,
- "ms": 521
+ "ms": 385
},
{
- "t": "2026-10-13T14:40:00.000Z",
+ "t": "2026-10-13T16:15:00.000Z",
"ok": true,
- "ms": 480
+ "ms": 342
},
{
- "t": "2026-10-13T15:20:00.000Z",
+ "t": "2026-10-13T16:55:00.000Z",
"ok": true,
- "ms": 462
+ "ms": 428
},
{
- "t": "2026-10-13T16:00:00.000Z",
+ "t": "2026-10-13T17:35:00.000Z",
"ok": true,
- "ms": 331
+ "ms": 508
},
{
- "t": "2026-10-13T16:40:00.000Z",
+ "t": "2026-10-13T18:15:00.000Z",
"ok": true,
- "ms": 373
+ "ms": 602
},
{
- "t": "2026-10-13T17:20:00.000Z",
- "ok": true,
- "ms": 429
- },
- {
- "t": "2026-10-13T18:00:00.000Z",
- "ok": true,
- "ms": 558
- },
- {
- "t": "2026-10-13T18:40:00.000Z",
- "ok": true,
- "ms": 370
- },
- {
- "t": "2026-10-13T19:20:00.000Z",
- "ok": true,
- "ms": 1519
- },
- {
- "t": "2026-10-13T20:00:00.000Z",
- "ok": true,
- "ms": 483
- },
- {
- "t": "2026-10-13T20:40:00.000Z",
- "ok": true,
- "ms": 388
- },
- {
- "t": "2026-10-13T21:20:00.000Z",
- "ok": true,
- "ms": 367
- },
- {
- "t": "2026-10-13T22:00:00.000Z",
- "ok": true,
- "ms": 273
- },
- {
- "t": "2026-10-13T22:40:00.000Z",
- "ok": true,
- "ms": 261
- },
- {
- "t": "2026-10-13T23:20:00.000Z",
- "ok": true,
- "ms": 263
- },
- {
- "t": "2026-10-14T00:00:00.000Z",
- "ok": true,
- "ms": 229
- },
- {
- "t": "2026-10-14T00:40:00.000Z",
+ "t": "2026-10-13T18:55:00.000Z",
"ok": true,
"ms": 363
},
{
- "t": "2026-10-14T01:20:00.000Z",
+ "t": "2026-10-13T19:40:00.000Z",
"ok": true,
- "ms": 329
+ "ms": 1380
},
{
- "t": "2026-10-14T02:00:00.000Z",
+ "t": "2026-10-13T20:20:00.000Z",
"ok": true,
- "ms": 302
+ "ms": 512
},
{
- "t": "2026-10-14T02:40:00.000Z",
+ "t": "2026-10-13T21:00:00.000Z",
"ok": true,
- "ms": 425
+ "ms": 394
},
{
- "t": "2026-10-14T03:20:00.000Z",
+ "t": "2026-10-13T21:40:00.000Z",
"ok": true,
- "ms": 328
+ "ms": 238
},
{
- "t": "2026-10-14T04:00:00.000Z",
+ "t": "2026-10-13T22:20:00.000Z",
"ok": true,
- "ms": 319
+ "ms": 323
},
{
- "t": "2026-10-14T04:40:00.000Z",
+ "t": "2026-10-13T23:00:00.000Z",
"ok": true,
- "ms": 272
+ "ms": 353
},
{
- "t": "2026-10-14T05:20:00.000Z",
+ "t": "2026-10-13T23:40:00.000Z",
"ok": true,
- "ms": 347
+ "ms": 179
},
{
- "t": "2026-10-14T06:36:00.000Z",
+ "t": "2026-10-14T00:20:00.000Z",
"ok": true,
- "ms": 484
+ "ms": 263
},
{
- "t": "2026-10-14T07:16:00.000Z",
+ "t": "2026-10-14T01:00:00.000Z",
"ok": true,
- "ms": 480
+ "ms": 381
+ },
+ {
+ "t": "2026-10-14T01:40:00.000Z",
+ "ok": true,
+ "ms": 378
+ },
+ {
+ "t": "2026-10-14T02:20:00.000Z",
+ "ok": true,
+ "ms": 278
+ },
+ {
+ "t": "2026-10-14T03:00:00.000Z",
+ "ok": true,
+ "ms": 280
+ },
+ {
+ "t": "2026-10-14T03:40:00.000Z",
+ "ok": true,
+ "ms": 322
+ },
+ {
+ "t": "2026-10-14T04:20:00.000Z",
+ "ok": true,
+ "ms": 323
+ },
+ {
+ "t": "2026-10-14T05:00:00.000Z",
+ "ok": true,
+ "ms": 321
+ },
+ {
+ "t": "2026-10-14T05:40:00.000Z",
+ "ok": true,
+ "ms": 642
+ },
+ {
+ "t": "2026-10-14T06:20:00.000Z",
+ "ok": true,
+ "ms": 315
+ },
+ {
+ "t": "2026-10-14T07:36:00.000Z",
+ "ok": true,
+ "ms": 363
+ },
+ {
+ "t": "2026-10-14T08:17:00.000Z",
+ "ok": true,
+ "ms": 486
}
],
"daily": {
"2026-10-14": {
"total": 4,
"up": 4,
- "latSum": 1875,
+ "latSum": 1756,
"latCount": 4,
- "latMax": 480
+ "latMax": 486
}
},
"meta": {
@@ -1143,12 +1143,12 @@
"90": 0
},
"lat": {
- "avg": 469,
- "max": 480,
+ "avg": 439,
+ "max": 486,
"days": [
{
"key": "2026-10-14",
- "avg": 469
+ "avg": 439
}
]
}
diff --git a/scripts/verify-security.cjs b/scripts/verify-security.cjs
index cb3d1d9..64a0b8c 100644
--- a/scripts/verify-security.cjs
+++ b/scripts/verify-security.cjs
@@ -172,6 +172,77 @@ async function main() {
check('静态 monitors.json 不含私有层内容', !pubText.includes(SECRET), '');
srvC.kill();
+ // ---------- D3:跨域只读(CORS 白名单) ----------
+ // 场景:前端部署在 GitHub Pages / Cloudflare,探针在另一台机器,
+ // 需要让指定页面能跨域读状态与配置。核心风险是「任意网站都能读走监控数据」。
+ console.log('\n[D3] 跨域只读白名单');
+ const corsCfg = path.join(TMP, 'cors.json');
+ fs.writeFileSync(corsCfg, JSON.stringify({
+ settings: {
+ title: 't',
+ corsOrigins: ['https://you.github.io', 'https://monitor.example.com'],
+ uptimeRobot: { enabled: true, apiKey: SECRET },
+ },
+ monitors: [],
+ }, null, 2));
+ const srvD = startProber([`--config=${corsCfg}`, `--local-config=${corsCfg}`, `--out=${path.join(TMP, 'd.json')}`], { PORT: '8895' });
+ await waitForHttp('http://127.0.0.1:8895/api/status');
+
+ const acaoOf = async (pathname, origin, method = 'GET') => {
+ const res = await fetch(`http://127.0.0.1:8895${pathname}`, { method, headers: { Origin: origin } });
+ return res.headers.get('access-control-allow-origin');
+ };
+
+ check('白名单内的 GitHub Pages 来源被放行', await acaoOf('/api/status', 'https://you.github.io') === 'https://you.github.io');
+ check('白名单内的自定义域名被放行', await acaoOf('/api/status', 'https://monitor.example.com') === 'https://monitor.example.com');
+ check('白名单外的站点被拒(无跨域头)', await acaoOf('/api/status', 'https://evil.example') === null);
+ check('前缀伪造的域名被拒(如 https://you.github.io.evil.com)', await acaoOf('/api/status', 'https://you.github.io.evil.com') === null);
+ check('协议不符的同域被拒(https 的 localhost 不匹配 http 默认项)', await acaoOf('/api/status', 'https://localhost') === null);
+
+ // 本机回环:默认放行且忽略端口(本地前端与探针常跑在不同端口)
+ check('本机 localhost 放行(任意端口)', await acaoOf('/api/status', 'http://localhost:5173') === 'http://localhost:5173');
+ check('本机 127.0.0.1 放行(任意端口)', await acaoOf('/api/status', 'http://127.0.0.1:3000') === 'http://127.0.0.1:3000');
+ check('局域网 IP 未列入白名单时被拒', await acaoOf('/api/status', 'http://192.168.1.5:5173') === null);
+
+ // 预检
+ const pre = await fetch('http://127.0.0.1:8895/api/status', { method: 'OPTIONS', headers: { Origin: 'https://you.github.io', 'Access-Control-Request-Method': 'GET' } });
+ check('合法来源的预检返回 204 且带跨域头', pre.status === 204 && pre.headers.get('access-control-allow-origin') === 'https://you.github.io', `HTTP ${pre.status}`);
+ const preBad = await fetch('http://127.0.0.1:8895/api/status', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'GET' } });
+ check('非法来源的预检被拒 403', preBad.status === 403, `HTTP ${preBad.status}`);
+
+ // 各只读接口都放行
+ const readOnly = ['/api/status', '/api/monitors', '/api/events', '/api/v2/getMonitors'];
+ const allowed = [];
+ for (const p of readOnly) allowed.push((await acaoOf(p, 'https://you.github.io')) === 'https://you.github.io');
+ check('四个只读接口都支持跨域', allowed.every(Boolean), readOnly.filter((_, i) => !allowed[i]).join(',') || '全部放行');
+
+ // 关键:管理端绝不能跨域
+ const adminAcao = [];
+ for (const p of ['/api/admin/status', '/api/admin/overview', '/api/admin/login']) {
+ const r = await fetch(`http://127.0.0.1:8895${p}`, { method: 'GET', headers: { Origin: 'https://you.github.io' } });
+ adminAcao.push(r.headers.get('access-control-allow-origin'));
+ }
+ check('管理端接口一律不带跨域头(必须同源访问)', adminAcao.every((v) => v === null), JSON.stringify(adminAcao));
+ const adminPre = await fetch('http://127.0.0.1:8895/api/admin/overview', { method: 'OPTIONS', headers: { Origin: 'https://you.github.io', 'Access-Control-Request-Method': 'POST' } });
+ check('管理端的预检被拒 403(外部页面无法发写操作)', adminPre.status === 403, `HTTP ${adminPre.status}`);
+
+ // 跨域读到的配置同样不得含密钥
+ const corsMonitors = await (await fetch('http://127.0.0.1:8895/api/monitors', { headers: { Origin: 'https://you.github.io' } })).text();
+ check('跨域读取配置时不泄露 API Key', !corsMonitors.includes(SECRET), corsMonitors.includes(SECRET) ? '泄露了!' : '未泄露');
+ srvD.kill();
+
+ // corsOrigins 里不允许 '*'
+ console.log('\n[D4] corsOrigins 的写法校验');
+ const starCfg = path.join(TMP, 'star.json');
+ fs.writeFileSync(starCfg, JSON.stringify({ settings: { title: 't', corsOrigins: ['*'] }, monitors: [] }, null, 2));
+ const { corsOrigins: corsFn } = await import(`file://${PROBER.replace(/\\/g, '/')}`).catch(() => ({}));
+ // corsOrigins 未导出时以 HTTP 层验证:配置 '*' 后不应变成任意来源可读
+ const srvE = startProber([`--config=${starCfg}`, `--local-config=${starCfg}`, `--out=${path.join(TMP, 'e.json')}`], { PORT: '8894' });
+ await waitForHttp('http://127.0.0.1:8894/api/status');
+ const starRes = await fetch('http://127.0.0.1:8894/api/status', { headers: { Origin: 'https://anything.example' } });
+ check("corsOrigins 配成 '*' 不会放行任意来源(必须写完整 origin)", starRes.headers.get('access-control-allow-origin') === null, starRes.headers.get('access-control-allow-origin') ?? '无跨域头');
+ srvE.kill();
+
// ---------- E:来源判定 ----------
console.log('\n[E] 来源判定');
const { isPrivateAddress } = await import(`file://${PROBER.replace(/\\/g, '/')}`);