@walkhi/code-relax 0.1.0-beta.65 → 0.1.0-beta.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/codex-remote.mjs +4 -2
- package/dist/bin/manage-relay-admin.mjs +20 -20
- package/dist/bin/migrate-relay-db.mjs +109 -109
- package/dist/bin/publish-harmony-release.mjs +46 -46
- package/dist/bin/self-relay-admin.mjs +8 -8
- package/dist/bin/self-relay-demo.mjs +33 -33
- package/dist/bin/self-relay-server.mjs +66 -66
- package/dist/bin/set-relay-admin-password.mjs +11 -11
- package/dist/build-platform.json +1 -1
- package/dist/get-lan-networks.ps1 +40 -40
- package/dist/shared/app-server-events.cjs +511 -511
- package/dist/shared/p2p-data-channel.cjs +72 -72
- package/dist/shared/p2p-diagnostics.cjs +28 -28
- package/dist/shared/p2p-ice-policy.cjs +22 -22
- package/dist/shared/plan-pricing.cjs +32 -32
- package/dist/shared/question-attention.cjs +69 -69
- package/dist/shared/release-version.cjs +31 -31
- package/dist/src/archived-threads.mjs +22 -22
- package/dist/src/background-process.mjs +18 -18
- package/dist/src/code-browser.mjs +100 -100
- package/dist/src/desktop-launcher.mjs +62 -62
- package/dist/src/dots-task-files.mjs +52 -52
- package/dist/src/execution-health.mjs +26 -26
- package/dist/src/message-images.mjs +9 -9
- package/dist/src/plan-pricing.mjs +47 -47
- package/dist/src/platform/codex-runtime.mjs +3 -3
- package/dist/src/platform/credential-store.mjs +19 -19
- package/dist/src/platform/desktop-host.mjs +3 -3
- package/dist/src/platform/desktop-permission-mode.mjs +17 -17
- package/dist/src/platform/desktop-project-registration.mjs +3 -3
- package/dist/src/platform/file-opener.mjs +9 -9
- package/dist/src/platform/linux/README.md +3 -3
- package/dist/src/platform/linux/codex-runtime.mjs +56 -56
- package/dist/src/platform/linux/desktop-installation.mjs +36 -36
- package/dist/src/platform/linux/desktop-project-registration.mjs +11 -11
- package/dist/src/platform/linux/network-info.mjs +11 -11
- package/dist/src/platform/linux/project-locations.mjs +17 -17
- package/dist/src/platform/linux/resident-startup.mjs +52 -52
- package/dist/src/platform/linux/retained-runtime.mjs +76 -76
- package/dist/src/platform/linux/service-host.mjs +18 -18
- package/dist/src/platform/macos/README.md +3 -3
- package/dist/src/platform/macos/codex-runtime.mjs +0 -4
- package/dist/src/platform/macos/desktop-project-registration.mjs +11 -11
- package/dist/src/platform/macos/network-info.mjs +8 -8
- package/dist/src/platform/macos/project-locations.mjs +8 -8
- package/dist/src/platform/macos/resident-startup.mjs +84 -84
- package/dist/src/platform/macos/retained-runtime.mjs +127 -127
- package/dist/src/platform/macos/service-host.mjs +18 -18
- package/dist/src/platform/macos/system.mjs +29 -29
- package/dist/src/platform/network-info.mjs +3 -3
- package/dist/src/platform/path-identity.mjs +5 -5
- package/dist/src/platform/posix/network-candidates.mjs +23 -23
- package/dist/src/platform/posix/runtime-paths.mjs +25 -25
- package/dist/src/platform/power-inhibitor.mjs +22 -22
- package/dist/src/platform/project-locations.mjs +3 -3
- package/dist/src/platform/runtime-paths.mjs +8 -8
- package/dist/src/platform/select.mjs +5 -5
- package/dist/src/platform/service-host.mjs +3 -3
- package/dist/src/platform/user-environment.mjs +3 -3
- package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
- package/dist/src/platform/windows/background-process.mjs +30 -30
- package/dist/src/platform/windows/desktop-helper.mjs +179 -179
- package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -7
- package/dist/src/platform/windows/keep-awake.ps1 +20 -20
- package/dist/src/platform/windows/network-info.mjs +34 -34
- package/dist/src/platform/windows/resident-start.js +20 -20
- package/dist/src/platform/windows/resident-start.ps1 +7 -7
- package/dist/src/platform/windows/resident-startup.ps1 +83 -83
- package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
- package/dist/src/platform/windows/runtime-paths.mjs +20 -20
- package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
- package/dist/src/platform/windows/start-hidden-console.ps1 +58 -58
- package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
- package/dist/src/platform/windows/user-environment.ps1 +41 -41
- package/dist/src/project-create.mjs +121 -121
- package/dist/src/project-management.mjs +1 -1
- package/dist/src/project-remove.mjs +55 -55
- package/dist/src/project-update.mjs +59 -59
- package/dist/src/relay-config-import.mjs +45 -45
- package/dist/src/self-relay/account-connections.mjs +52 -52
- package/dist/src/self-relay/admin-auth.mjs +59 -59
- package/dist/src/self-relay/admin-cloud.mjs +101 -101
- package/dist/src/self-relay/admin-login-page.mjs +7 -7
- package/dist/src/self-relay/admin-server.mjs +105 -105
- package/dist/src/self-relay/admin-state.mjs +127 -127
- package/dist/src/self-relay/admin-workstations-page.mjs +95 -95
- package/dist/src/self-relay/admin-workstations.mjs +143 -143
- package/dist/src/self-relay/android-releases.mjs +61 -61
- package/dist/src/self-relay/client.mjs +61 -61
- package/dist/src/self-relay/connection-reporting.mjs +38 -38
- package/dist/src/self-relay/debug-allowlist.mjs +33 -33
- package/dist/src/self-relay/demo.mjs +6 -6
- package/dist/src/self-relay/device-info.mjs +27 -27
- package/dist/src/self-relay/device-store.mjs +92 -92
- package/dist/src/self-relay/lan-authorizations.mjs +71 -71
- package/dist/src/self-relay/lifecycle.mjs +278 -278
- package/dist/src/self-relay/long-pairing.mjs +40 -40
- package/dist/src/self-relay/mysql-state.mjs +118 -118
- package/dist/src/self-relay/phone-connections.mjs +55 -55
- package/dist/src/self-relay/server.mjs +1066 -1066
- package/dist/src/self-relay/usage-daily.mjs +55 -55
- package/dist/src/self-relay/windows-route-helper.mjs +75 -75
- package/dist/src/self-relay/wire.mjs +36 -36
- package/dist/src/server.mjs +1947 -1947
- package/dist/src/service-control.mjs +82 -82
- package/dist/src/service-health.mjs +33 -33
- package/dist/src/service-power.mjs +58 -58
- package/dist/src/service-restart.mjs +81 -81
- package/dist/src/service-update-worker.mjs +42 -42
- package/dist/src/service-update.mjs +80 -80
- package/dist/src/shared-recovery.mjs +152 -152
- package/dist/src/skill-install.mjs +51 -51
- package/dist/src/thread-title-generation.mjs +150 -150
- package/dist/src/turn-model-history.mjs +47 -47
- package/dist/tools/rtc-route-helper.ps1 +99 -99
- package/dist/tools/start-rtc-route-helper.ps1 +15 -15
- package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
- package/dist/web/vendor/markdown-it.umd.min.js +13 -13
- package/package.json +4 -4
- package/tools/postinstall.mjs +1 -1
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
import { digest } from './wire.mjs';
|
|
2
|
-
|
|
3
|
-
// Register before the asynchronous database check so logout also invalidates
|
|
4
|
-
// handshakes whose successful authentication response has not arrived yet.
|
|
5
|
-
export function accountConnections() {
|
|
6
|
-
const sessions = new Map();
|
|
7
|
-
return {
|
|
8
|
-
track(token, onInvalid) {
|
|
9
|
-
const key = digest(token);
|
|
10
|
-
const entries = sessions.get(key) || new Set();
|
|
11
|
-
sessions.set(key, entries);
|
|
12
|
-
let active = true, expires = Infinity, timer;
|
|
13
|
-
function dispose() {
|
|
14
|
-
active = false;
|
|
15
|
-
clearTimeout(timer);
|
|
16
|
-
entries.delete(entry);
|
|
17
|
-
if (!entries.size && sessions.get(key) === entries) sessions.delete(key);
|
|
18
|
-
}
|
|
19
|
-
function invalidate() {
|
|
20
|
-
if (!active) return;
|
|
21
|
-
dispose();
|
|
22
|
-
onInvalid();
|
|
23
|
-
}
|
|
24
|
-
function valid() {
|
|
25
|
-
if (active && Date.now() >= expires) invalidate();
|
|
26
|
-
return active;
|
|
27
|
-
}
|
|
28
|
-
function schedule() {
|
|
29
|
-
if (!valid()) return;
|
|
30
|
-
// Login lasts 30 days, longer than Node's maximum timer delay.
|
|
31
|
-
timer = setTimeout(schedule, Math.min(expires - Date.now(), 0x7fffffff));
|
|
32
|
-
timer.unref();
|
|
33
|
-
}
|
|
34
|
-
const entry = {
|
|
35
|
-
activate(expiresAt) {
|
|
36
|
-
if (!active) return false;
|
|
37
|
-
expires = Date.parse(expiresAt);
|
|
38
|
-
if (!Number.isFinite(expires)) { invalidate(); return false; }
|
|
39
|
-
schedule();
|
|
40
|
-
return active;
|
|
41
|
-
},
|
|
42
|
-
valid, dispose, invalidate,
|
|
43
|
-
};
|
|
44
|
-
entries.add(entry);
|
|
45
|
-
return entry;
|
|
46
|
-
},
|
|
47
|
-
revoke(token) {
|
|
48
|
-
const entries = sessions.get(digest(token));
|
|
49
|
-
if (entries) for (const entry of [...entries]) entry.invalidate();
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
}
|
|
1
|
+
import { digest } from './wire.mjs';
|
|
2
|
+
|
|
3
|
+
// Register before the asynchronous database check so logout also invalidates
|
|
4
|
+
// handshakes whose successful authentication response has not arrived yet.
|
|
5
|
+
export function accountConnections() {
|
|
6
|
+
const sessions = new Map();
|
|
7
|
+
return {
|
|
8
|
+
track(token, onInvalid) {
|
|
9
|
+
const key = digest(token);
|
|
10
|
+
const entries = sessions.get(key) || new Set();
|
|
11
|
+
sessions.set(key, entries);
|
|
12
|
+
let active = true, expires = Infinity, timer;
|
|
13
|
+
function dispose() {
|
|
14
|
+
active = false;
|
|
15
|
+
clearTimeout(timer);
|
|
16
|
+
entries.delete(entry);
|
|
17
|
+
if (!entries.size && sessions.get(key) === entries) sessions.delete(key);
|
|
18
|
+
}
|
|
19
|
+
function invalidate() {
|
|
20
|
+
if (!active) return;
|
|
21
|
+
dispose();
|
|
22
|
+
onInvalid();
|
|
23
|
+
}
|
|
24
|
+
function valid() {
|
|
25
|
+
if (active && Date.now() >= expires) invalidate();
|
|
26
|
+
return active;
|
|
27
|
+
}
|
|
28
|
+
function schedule() {
|
|
29
|
+
if (!valid()) return;
|
|
30
|
+
// Login lasts 30 days, longer than Node's maximum timer delay.
|
|
31
|
+
timer = setTimeout(schedule, Math.min(expires - Date.now(), 0x7fffffff));
|
|
32
|
+
timer.unref();
|
|
33
|
+
}
|
|
34
|
+
const entry = {
|
|
35
|
+
activate(expiresAt) {
|
|
36
|
+
if (!active) return false;
|
|
37
|
+
expires = Date.parse(expiresAt);
|
|
38
|
+
if (!Number.isFinite(expires)) { invalidate(); return false; }
|
|
39
|
+
schedule();
|
|
40
|
+
return active;
|
|
41
|
+
},
|
|
42
|
+
valid, dispose, invalidate,
|
|
43
|
+
};
|
|
44
|
+
entries.add(entry);
|
|
45
|
+
return entry;
|
|
46
|
+
},
|
|
47
|
+
revoke(token) {
|
|
48
|
+
const entries = sessions.get(digest(token));
|
|
49
|
+
if (entries) for (const entry of [...entries]) entry.invalidate();
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import { randomBytes, scrypt, timingSafeEqual } from 'node:crypto';
|
|
3
|
-
import { promisify } from 'node:util';
|
|
4
|
-
|
|
5
|
-
const derive = promisify(scrypt);
|
|
6
|
-
const COOKIE = '__Host-code-relax-admin';
|
|
7
|
-
const SESSION_MS = 12 * 60 * 60 * 1000;
|
|
8
|
-
const cookie = (value, seconds) => `${COOKIE}=${value}; Path=/; Secure; HttpOnly; SameSite=Strict; Max-Age=${seconds}`;
|
|
9
|
-
|
|
10
|
-
export async function passwordRecord(password) {
|
|
11
|
-
if (typeof password !== 'string' || password.length < 10 || Buffer.byteLength(password) > 256) {
|
|
12
|
-
throw new Error('Admin password must have at least 10 characters and at most 256 bytes');
|
|
13
|
-
}
|
|
14
|
-
const salt = randomBytes(16).toString('hex');
|
|
15
|
-
return { salt, hash: (await derive(password, salt, 64)).toString('hex') };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function createAdminAuth(passwordFile) {
|
|
19
|
-
const { salt, hash } = JSON.parse(fs.readFileSync(passwordFile, 'utf8'));
|
|
20
|
-
if (!/^[a-f0-9]{32}$/.test(salt) || !/^[a-f0-9]{128}$/.test(hash)) throw new Error('Invalid admin password record');
|
|
21
|
-
const expected = Buffer.from(hash, 'hex');
|
|
22
|
-
const sessions = new Map();
|
|
23
|
-
let windowStart = 0, attempts = 0, verifying = false;
|
|
24
|
-
const token = request => (request.headers.cookie || '').split(';').map(value => value.trim())
|
|
25
|
-
.find(value => value.startsWith(`${COOKIE}=`))?.slice(COOKIE.length + 1);
|
|
26
|
-
function purge() {
|
|
27
|
-
for (const [id, expires] of sessions) if (expires <= Date.now()) sessions.delete(id);
|
|
28
|
-
}
|
|
29
|
-
return {
|
|
30
|
-
authenticated(request) { purge(); return sessions.has(token(request)); },
|
|
31
|
-
async login(password, request, response) {
|
|
32
|
-
const now = Date.now();
|
|
33
|
-
if (now - windowStart >= 60000) { windowStart = now; attempts = 0; }
|
|
34
|
-
// This is a single-owner console: bound both guessing and concurrent scrypt memory.
|
|
35
|
-
if (attempts >= 10 || verifying) {
|
|
36
|
-
response.setHeader('Retry-After', '60');
|
|
37
|
-
return { status: 429, error: '尝试过于频繁,请一分钟后重试' };
|
|
38
|
-
}
|
|
39
|
-
attempts++;
|
|
40
|
-
if (typeof password !== 'string' || Buffer.byteLength(password) > 256) return { status: 400, error: '请输入有效密码' };
|
|
41
|
-
verifying = true;
|
|
42
|
-
try {
|
|
43
|
-
const actual = await derive(password, salt, 64);
|
|
44
|
-
if (!timingSafeEqual(actual, expected)) return { status: 401, error: '密码不正确' };
|
|
45
|
-
purge();
|
|
46
|
-
sessions.delete(token(request));
|
|
47
|
-
if (sessions.size >= 32) sessions.delete(sessions.keys().next().value);
|
|
48
|
-
const id = randomBytes(32).toString('base64url');
|
|
49
|
-
sessions.set(id, Date.now() + SESSION_MS);
|
|
50
|
-
response.setHeader('Set-Cookie', cookie(id, SESSION_MS / 1000));
|
|
51
|
-
return { status: 200 };
|
|
52
|
-
} finally { verifying = false; }
|
|
53
|
-
},
|
|
54
|
-
logout(request, response) {
|
|
55
|
-
sessions.delete(token(request));
|
|
56
|
-
response.setHeader('Set-Cookie', cookie('', 0));
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { randomBytes, scrypt, timingSafeEqual } from 'node:crypto';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
|
|
5
|
+
const derive = promisify(scrypt);
|
|
6
|
+
const COOKIE = '__Host-code-relax-admin';
|
|
7
|
+
const SESSION_MS = 12 * 60 * 60 * 1000;
|
|
8
|
+
const cookie = (value, seconds) => `${COOKIE}=${value}; Path=/; Secure; HttpOnly; SameSite=Strict; Max-Age=${seconds}`;
|
|
9
|
+
|
|
10
|
+
export async function passwordRecord(password) {
|
|
11
|
+
if (typeof password !== 'string' || password.length < 10 || Buffer.byteLength(password) > 256) {
|
|
12
|
+
throw new Error('Admin password must have at least 10 characters and at most 256 bytes');
|
|
13
|
+
}
|
|
14
|
+
const salt = randomBytes(16).toString('hex');
|
|
15
|
+
return { salt, hash: (await derive(password, salt, 64)).toString('hex') };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createAdminAuth(passwordFile) {
|
|
19
|
+
const { salt, hash } = JSON.parse(fs.readFileSync(passwordFile, 'utf8'));
|
|
20
|
+
if (!/^[a-f0-9]{32}$/.test(salt) || !/^[a-f0-9]{128}$/.test(hash)) throw new Error('Invalid admin password record');
|
|
21
|
+
const expected = Buffer.from(hash, 'hex');
|
|
22
|
+
const sessions = new Map();
|
|
23
|
+
let windowStart = 0, attempts = 0, verifying = false;
|
|
24
|
+
const token = request => (request.headers.cookie || '').split(';').map(value => value.trim())
|
|
25
|
+
.find(value => value.startsWith(`${COOKIE}=`))?.slice(COOKIE.length + 1);
|
|
26
|
+
function purge() {
|
|
27
|
+
for (const [id, expires] of sessions) if (expires <= Date.now()) sessions.delete(id);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
authenticated(request) { purge(); return sessions.has(token(request)); },
|
|
31
|
+
async login(password, request, response) {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
if (now - windowStart >= 60000) { windowStart = now; attempts = 0; }
|
|
34
|
+
// This is a single-owner console: bound both guessing and concurrent scrypt memory.
|
|
35
|
+
if (attempts >= 10 || verifying) {
|
|
36
|
+
response.setHeader('Retry-After', '60');
|
|
37
|
+
return { status: 429, error: '尝试过于频繁,请一分钟后重试' };
|
|
38
|
+
}
|
|
39
|
+
attempts++;
|
|
40
|
+
if (typeof password !== 'string' || Buffer.byteLength(password) > 256) return { status: 400, error: '请输入有效密码' };
|
|
41
|
+
verifying = true;
|
|
42
|
+
try {
|
|
43
|
+
const actual = await derive(password, salt, 64);
|
|
44
|
+
if (!timingSafeEqual(actual, expected)) return { status: 401, error: '密码不正确' };
|
|
45
|
+
purge();
|
|
46
|
+
sessions.delete(token(request));
|
|
47
|
+
if (sessions.size >= 32) sessions.delete(sessions.keys().next().value);
|
|
48
|
+
const id = randomBytes(32).toString('base64url');
|
|
49
|
+
sessions.set(id, Date.now() + SESSION_MS);
|
|
50
|
+
response.setHeader('Set-Cookie', cookie(id, SESSION_MS / 1000));
|
|
51
|
+
return { status: 200 };
|
|
52
|
+
} finally { verifying = false; }
|
|
53
|
+
},
|
|
54
|
+
logout(request, response) {
|
|
55
|
+
sessions.delete(token(request));
|
|
56
|
+
response.setHeader('Set-Cookie', cookie('', 0));
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import tencentcloud from 'tencentcloud-sdk-nodejs-common';
|
|
3
|
-
|
|
4
|
-
const INSTANCE_ID = 'lhins-byrs7n8o';
|
|
5
|
-
const REGION = 'ap-beijing';
|
|
6
|
-
const { CommonClient } = tencentcloud;
|
|
7
|
-
const METRICS = [
|
|
8
|
-
['LighthouseOutratio', 'outRatioPercent', 10],
|
|
9
|
-
['LighthouseOuttraffic', 'outMbps', 10],
|
|
10
|
-
['LighthouseIntraffic', 'inMbps', 10],
|
|
11
|
-
['CpuUsage', 'cpuPercent', 10],
|
|
12
|
-
['MemUsage', 'memoryPercent', 10],
|
|
13
|
-
['DiskUsage', 'diskPercent', 60],
|
|
14
|
-
['CpuLoad1', 'cpuLoad1', 10],
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
function latestPoint(result) {
|
|
18
|
-
const point = result?.DataPoints?.[0];
|
|
19
|
-
if (!point?.Values?.length || !point?.Timestamps?.length) return null;
|
|
20
|
-
const index = point.Values.length - 1;
|
|
21
|
-
const value = Number(point.Values[index]);
|
|
22
|
-
const timestamp = Number(point.Timestamps[index]);
|
|
23
|
-
return Number.isFinite(value) && Number.isFinite(timestamp)
|
|
24
|
-
? { value, sampledAt: new Date(timestamp * 1000).toISOString() } : null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function startAdminCloud({ credentialFile = process.env.CREDENTIALS_DIRECTORY
|
|
28
|
-
? `${process.env.CREDENTIALS_DIRECTORY}/tencent-monitor.json` : null,
|
|
29
|
-
metricIntervalMs = 60000, trafficIntervalMs = 300000 } = {}) {
|
|
30
|
-
if (!credentialFile) return { read: () => ({ configured: false }), close() {} };
|
|
31
|
-
const { secretId, secretKey } = JSON.parse(fs.readFileSync(credentialFile, 'utf8'));
|
|
32
|
-
if (!secretId || !secretKey) throw new Error('Tencent monitor credentials are incomplete');
|
|
33
|
-
const credential = { secretId, secretKey };
|
|
34
|
-
const profile = { httpProfile: { reqTimeout: 8 } };
|
|
35
|
-
const monitor = new CommonClient('monitor.tencentcloudapi.com', '2018-07-24',
|
|
36
|
-
{ credential, region: REGION, profile });
|
|
37
|
-
const lighthouse = new CommonClient('lighthouse.tencentcloudapi.com', '2020-03-24',
|
|
38
|
-
{ credential, region: REGION, profile });
|
|
39
|
-
let closed = false;
|
|
40
|
-
let metricBusy = false;
|
|
41
|
-
let trafficBusy = false;
|
|
42
|
-
const current = { configured: true, metricAvailable: false, trafficAvailable: false,
|
|
43
|
-
outRatioPercent: null, outMbps: null, inMbps: null, metricSampledAt: null,
|
|
44
|
-
cpuPercent: null, memoryPercent: null, diskPercent: null, cpuLoad1: null,
|
|
45
|
-
trafficUsedBytes: null, trafficTotalBytes: null, trafficRemainingBytes: null,
|
|
46
|
-
trafficStartAt: null, trafficEndAt: null, trafficSampledAt: null };
|
|
47
|
-
|
|
48
|
-
async function sampleMetrics() {
|
|
49
|
-
if (metricBusy || closed) return;
|
|
50
|
-
metricBusy = true;
|
|
51
|
-
const end = new Date();
|
|
52
|
-
const base = { Namespace: 'QCE/LIGHTHOUSE',
|
|
53
|
-
StartTime: new Date(end.getTime() - 180000).toISOString(), EndTime: end.toISOString(),
|
|
54
|
-
Instances: [{ Dimensions: [{ Name: 'InstanceId', Value: INSTANCE_ID }] }] };
|
|
55
|
-
try {
|
|
56
|
-
const values = await Promise.allSettled(METRICS.map(([MetricName, , Period]) =>
|
|
57
|
-
monitor.request('GetMonitorData', { ...base, MetricName, Period })));
|
|
58
|
-
if (closed) return;
|
|
59
|
-
const samples = values.map(result => result.status === 'fulfilled' ? latestPoint(result.value) : null);
|
|
60
|
-
for (let index = 0; index < METRICS.length; index++) {
|
|
61
|
-
const [, field] = METRICS[index];
|
|
62
|
-
const value = samples[index]?.value ?? null;
|
|
63
|
-
current[field] = value !== null && (field === 'outMbps' || field === 'inMbps') ? value * 8 : value;
|
|
64
|
-
}
|
|
65
|
-
current.metricSampledAt = samples.reduce((latest, sample) => sample?.sampledAt > latest
|
|
66
|
-
? sample.sampledAt : latest, '') || null;
|
|
67
|
-
current.metricAvailable = Boolean(current.metricSampledAt);
|
|
68
|
-
} catch {
|
|
69
|
-
current.metricAvailable = false;
|
|
70
|
-
} finally { metricBusy = false; }
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function sampleTraffic() {
|
|
74
|
-
if (trafficBusy || closed) return;
|
|
75
|
-
trafficBusy = true;
|
|
76
|
-
try {
|
|
77
|
-
const data = await lighthouse.request('DescribeInstancesTrafficPackages', { InstanceIds: [INSTANCE_ID] });
|
|
78
|
-
if (closed) return;
|
|
79
|
-
const packages = data?.InstanceTrafficPackageSet?.find(item => item.InstanceId === INSTANCE_ID)?.TrafficPackageSet || [];
|
|
80
|
-
const active = packages.find(item => Date.parse(item.StartTime) <= Date.now()
|
|
81
|
-
&& Date.parse(item.EndTime) >= Date.now()) || packages[0];
|
|
82
|
-
current.trafficUsedBytes = active?.TrafficUsed ?? null;
|
|
83
|
-
current.trafficTotalBytes = active?.TrafficPackageTotal ?? null;
|
|
84
|
-
current.trafficRemainingBytes = active?.TrafficPackageRemaining ?? null;
|
|
85
|
-
current.trafficStartAt = active?.StartTime ?? null;
|
|
86
|
-
current.trafficEndAt = active?.EndTime ?? null;
|
|
87
|
-
current.trafficSampledAt = active ? new Date().toISOString() : null;
|
|
88
|
-
current.trafficAvailable = Boolean(active);
|
|
89
|
-
} catch {
|
|
90
|
-
current.trafficAvailable = false;
|
|
91
|
-
} finally { trafficBusy = false; }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
void sampleMetrics();
|
|
95
|
-
void sampleTraffic();
|
|
96
|
-
const metricTimer = setInterval(sampleMetrics, metricIntervalMs);
|
|
97
|
-
const trafficTimer = setInterval(sampleTraffic, trafficIntervalMs);
|
|
98
|
-
metricTimer.unref();
|
|
99
|
-
trafficTimer.unref();
|
|
100
|
-
return { read: () => ({ ...current }), close() { closed = true; clearInterval(metricTimer); clearInterval(trafficTimer); } };
|
|
101
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import tencentcloud from 'tencentcloud-sdk-nodejs-common';
|
|
3
|
+
|
|
4
|
+
const INSTANCE_ID = 'lhins-byrs7n8o';
|
|
5
|
+
const REGION = 'ap-beijing';
|
|
6
|
+
const { CommonClient } = tencentcloud;
|
|
7
|
+
const METRICS = [
|
|
8
|
+
['LighthouseOutratio', 'outRatioPercent', 10],
|
|
9
|
+
['LighthouseOuttraffic', 'outMbps', 10],
|
|
10
|
+
['LighthouseIntraffic', 'inMbps', 10],
|
|
11
|
+
['CpuUsage', 'cpuPercent', 10],
|
|
12
|
+
['MemUsage', 'memoryPercent', 10],
|
|
13
|
+
['DiskUsage', 'diskPercent', 60],
|
|
14
|
+
['CpuLoad1', 'cpuLoad1', 10],
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function latestPoint(result) {
|
|
18
|
+
const point = result?.DataPoints?.[0];
|
|
19
|
+
if (!point?.Values?.length || !point?.Timestamps?.length) return null;
|
|
20
|
+
const index = point.Values.length - 1;
|
|
21
|
+
const value = Number(point.Values[index]);
|
|
22
|
+
const timestamp = Number(point.Timestamps[index]);
|
|
23
|
+
return Number.isFinite(value) && Number.isFinite(timestamp)
|
|
24
|
+
? { value, sampledAt: new Date(timestamp * 1000).toISOString() } : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function startAdminCloud({ credentialFile = process.env.CREDENTIALS_DIRECTORY
|
|
28
|
+
? `${process.env.CREDENTIALS_DIRECTORY}/tencent-monitor.json` : null,
|
|
29
|
+
metricIntervalMs = 60000, trafficIntervalMs = 300000 } = {}) {
|
|
30
|
+
if (!credentialFile) return { read: () => ({ configured: false }), close() {} };
|
|
31
|
+
const { secretId, secretKey } = JSON.parse(fs.readFileSync(credentialFile, 'utf8'));
|
|
32
|
+
if (!secretId || !secretKey) throw new Error('Tencent monitor credentials are incomplete');
|
|
33
|
+
const credential = { secretId, secretKey };
|
|
34
|
+
const profile = { httpProfile: { reqTimeout: 8 } };
|
|
35
|
+
const monitor = new CommonClient('monitor.tencentcloudapi.com', '2018-07-24',
|
|
36
|
+
{ credential, region: REGION, profile });
|
|
37
|
+
const lighthouse = new CommonClient('lighthouse.tencentcloudapi.com', '2020-03-24',
|
|
38
|
+
{ credential, region: REGION, profile });
|
|
39
|
+
let closed = false;
|
|
40
|
+
let metricBusy = false;
|
|
41
|
+
let trafficBusy = false;
|
|
42
|
+
const current = { configured: true, metricAvailable: false, trafficAvailable: false,
|
|
43
|
+
outRatioPercent: null, outMbps: null, inMbps: null, metricSampledAt: null,
|
|
44
|
+
cpuPercent: null, memoryPercent: null, diskPercent: null, cpuLoad1: null,
|
|
45
|
+
trafficUsedBytes: null, trafficTotalBytes: null, trafficRemainingBytes: null,
|
|
46
|
+
trafficStartAt: null, trafficEndAt: null, trafficSampledAt: null };
|
|
47
|
+
|
|
48
|
+
async function sampleMetrics() {
|
|
49
|
+
if (metricBusy || closed) return;
|
|
50
|
+
metricBusy = true;
|
|
51
|
+
const end = new Date();
|
|
52
|
+
const base = { Namespace: 'QCE/LIGHTHOUSE',
|
|
53
|
+
StartTime: new Date(end.getTime() - 180000).toISOString(), EndTime: end.toISOString(),
|
|
54
|
+
Instances: [{ Dimensions: [{ Name: 'InstanceId', Value: INSTANCE_ID }] }] };
|
|
55
|
+
try {
|
|
56
|
+
const values = await Promise.allSettled(METRICS.map(([MetricName, , Period]) =>
|
|
57
|
+
monitor.request('GetMonitorData', { ...base, MetricName, Period })));
|
|
58
|
+
if (closed) return;
|
|
59
|
+
const samples = values.map(result => result.status === 'fulfilled' ? latestPoint(result.value) : null);
|
|
60
|
+
for (let index = 0; index < METRICS.length; index++) {
|
|
61
|
+
const [, field] = METRICS[index];
|
|
62
|
+
const value = samples[index]?.value ?? null;
|
|
63
|
+
current[field] = value !== null && (field === 'outMbps' || field === 'inMbps') ? value * 8 : value;
|
|
64
|
+
}
|
|
65
|
+
current.metricSampledAt = samples.reduce((latest, sample) => sample?.sampledAt > latest
|
|
66
|
+
? sample.sampledAt : latest, '') || null;
|
|
67
|
+
current.metricAvailable = Boolean(current.metricSampledAt);
|
|
68
|
+
} catch {
|
|
69
|
+
current.metricAvailable = false;
|
|
70
|
+
} finally { metricBusy = false; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function sampleTraffic() {
|
|
74
|
+
if (trafficBusy || closed) return;
|
|
75
|
+
trafficBusy = true;
|
|
76
|
+
try {
|
|
77
|
+
const data = await lighthouse.request('DescribeInstancesTrafficPackages', { InstanceIds: [INSTANCE_ID] });
|
|
78
|
+
if (closed) return;
|
|
79
|
+
const packages = data?.InstanceTrafficPackageSet?.find(item => item.InstanceId === INSTANCE_ID)?.TrafficPackageSet || [];
|
|
80
|
+
const active = packages.find(item => Date.parse(item.StartTime) <= Date.now()
|
|
81
|
+
&& Date.parse(item.EndTime) >= Date.now()) || packages[0];
|
|
82
|
+
current.trafficUsedBytes = active?.TrafficUsed ?? null;
|
|
83
|
+
current.trafficTotalBytes = active?.TrafficPackageTotal ?? null;
|
|
84
|
+
current.trafficRemainingBytes = active?.TrafficPackageRemaining ?? null;
|
|
85
|
+
current.trafficStartAt = active?.StartTime ?? null;
|
|
86
|
+
current.trafficEndAt = active?.EndTime ?? null;
|
|
87
|
+
current.trafficSampledAt = active ? new Date().toISOString() : null;
|
|
88
|
+
current.trafficAvailable = Boolean(active);
|
|
89
|
+
} catch {
|
|
90
|
+
current.trafficAvailable = false;
|
|
91
|
+
} finally { trafficBusy = false; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
void sampleMetrics();
|
|
95
|
+
void sampleTraffic();
|
|
96
|
+
const metricTimer = setInterval(sampleMetrics, metricIntervalMs);
|
|
97
|
+
const trafficTimer = setInterval(sampleTraffic, trafficIntervalMs);
|
|
98
|
+
metricTimer.unref();
|
|
99
|
+
trafficTimer.unref();
|
|
100
|
+
return { read: () => ({ ...current }), close() { closed = true; clearInterval(metricTimer); clearInterval(trafficTimer); } };
|
|
101
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { adminUiTokens, adminFooterStyles, adminAuthStyles } from './admin-style.mjs';
|
|
2
|
-
|
|
3
|
-
export function adminLoginPage() {
|
|
4
|
-
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>登录 · Code Relax 管理</title><style>${adminUiTokens}${adminFooterStyles}${adminAuthStyles}</style></head>
|
|
5
|
-
<body class="admin-login"><main><form id="login" class="login-card"><h1>Code Relax 管理</h1><p>输入管理密码,进入后台。</p><label for="password">管理密码</label><input id="password" name="password" type="password" autocomplete="current-password" required maxlength="256" autofocus><button id="submit" type="submit">登录</button><p id="result" role="alert" aria-live="polite"></p></form></main><footer class="admin-footer">京ICP备2026027643号-2</footer>
|
|
6
|
-
<script>const form=document.getElementById('login'),button=document.getElementById('submit'),result=document.getElementById('result');form.addEventListener('submit',async event=>{event.preventDefault();if(button.disabled)return;button.disabled=true;result.textContent='正在登录…';try{const response=await fetch('/admin/login',{method:'POST',headers:{'Content-Type':'application/json','X-Code-Relax-Admin':'1'},body:JSON.stringify({password:form.password.value})});const body=await response.json();if(!response.ok)throw new Error(body.error||'登录失败');form.reset();location.replace('/')}catch(error){result.textContent=error.message}finally{button.disabled=false}});</script></body></html>`;
|
|
7
|
-
}
|
|
1
|
+
import { adminUiTokens, adminFooterStyles, adminAuthStyles } from './admin-style.mjs';
|
|
2
|
+
|
|
3
|
+
export function adminLoginPage() {
|
|
4
|
+
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>登录 · Code Relax 管理</title><style>${adminUiTokens}${adminFooterStyles}${adminAuthStyles}</style></head>
|
|
5
|
+
<body class="admin-login"><main><form id="login" class="login-card"><h1>Code Relax 管理</h1><p>输入管理密码,进入后台。</p><label for="password">管理密码</label><input id="password" name="password" type="password" autocomplete="current-password" required maxlength="256" autofocus><button id="submit" type="submit">登录</button><p id="result" role="alert" aria-live="polite"></p></form></main><footer class="admin-footer">京ICP备2026027643号-2</footer>
|
|
6
|
+
<script>const form=document.getElementById('login'),button=document.getElementById('submit'),result=document.getElementById('result');form.addEventListener('submit',async event=>{event.preventDefault();if(button.disabled)return;button.disabled=true;result.textContent='正在登录…';try{const response=await fetch('/admin/login',{method:'POST',headers:{'Content-Type':'application/json','X-Code-Relax-Admin':'1'},body:JSON.stringify({password:form.password.value})});const body=await response.json();if(!response.ok)throw new Error(body.error||'登录失败');form.reset();location.replace('/')}catch(error){result.textContent=error.message}finally{button.disabled=false}});</script></body></html>`;
|
|
7
|
+
}
|