新增 debug 页面

This commit is contained in:
2025-09-23 11:40:12 +08:00
parent 2c106e43df
commit 2d5e334a5c
11 changed files with 279 additions and 30 deletions

10
.env.example Normal file
View File

@@ -0,0 +1,10 @@
# 数据库连接字符串
DATABASE_URL=
# Redis 连接字符串
REDIS_URL=
# 京东网关配置
JD_BASE=https://smart.jdbox.xyz:58001
JD_USERNAME=
JD_PASSWORD=

View File

@@ -19,6 +19,7 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.62.0",
"redis": "^5.8.2",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"zustand": "^5.0.8",
@@ -302,6 +303,16 @@
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
"@redis/bloom": ["@redis/bloom@5.8.2", "https://registry.npmmirror.com/@redis/bloom/-/bloom-5.8.2.tgz", { "peerDependencies": { "@redis/client": "^5.8.2" } }, "sha512-855DR0ChetZLarblio5eM0yLwxA9Dqq50t8StXKp5bAtLT0G+rZ+eRzzqxl37sPqQKjUudSYypz55o6nNhbz0A=="],
"@redis/client": ["@redis/client@5.8.2", "https://registry.npmmirror.com/@redis/client/-/client-5.8.2.tgz", { "dependencies": { "cluster-key-slot": "1.1.2" } }, "sha512-WtMScno3+eBpTac1Uav2zugXEoXqaU23YznwvFgkPwBQVwEHTDgOG7uEAObtZ/Nyn8SmAMbqkEubJaMOvnqdsQ=="],
"@redis/json": ["@redis/json@5.8.2", "https://registry.npmmirror.com/@redis/json/-/json-5.8.2.tgz", { "peerDependencies": { "@redis/client": "^5.8.2" } }, "sha512-uxpVfas3I0LccBX9rIfDgJ0dBrUa3+0Gc8sEwmQQH0vHi7C1Rx1Qn8Nv1QWz5bohoeIXMICFZRcyDONvum2l/w=="],
"@redis/search": ["@redis/search@5.8.2", "https://registry.npmmirror.com/@redis/search/-/search-5.8.2.tgz", { "peerDependencies": { "@redis/client": "^5.8.2" } }, "sha512-cNv7HlgayavCBXqPXgaS97DRPVWFznuzsAmmuemi2TMCx5scwLiP50TeZvUS06h/MG96YNPe6A0Zt57yayfxwA=="],
"@redis/time-series": ["@redis/time-series@5.8.2", "https://registry.npmmirror.com/@redis/time-series/-/time-series-5.8.2.tgz", { "peerDependencies": { "@redis/client": "^5.8.2" } }, "sha512-g2NlHM07fK8H4k+613NBsk3y70R2JIM2dPMSkhIjl2Z17SYvaYKdusz85d7VYOrZBWtDrHV/WD2E3vGu+zni8A=="],
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
"@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.12.0", "", {}, "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw=="],
@@ -490,6 +501,8 @@
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"cluster-key-slot": ["cluster-key-slot@1.1.2", "https://registry.npmmirror.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
@@ -908,6 +921,8 @@
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"redis": ["redis@5.8.2", "https://registry.npmmirror.com/redis/-/redis-5.8.2.tgz", { "dependencies": { "@redis/bloom": "5.8.2", "@redis/client": "5.8.2", "@redis/json": "5.8.2", "@redis/search": "5.8.2", "@redis/time-series": "5.8.2" } }, "sha512-31vunZj07++Y1vcFGcnNWEf5jPoTkGARgfWI4+Tk55vdwHxhAvug8VEtW7Cx+/h47NuJTEg/JL77zAwC6E0OeA=="],
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],

View File

@@ -1,4 +1,5 @@
services:
mariadb:
image: mariadb:10
environment:
@@ -7,4 +8,11 @@ services:
ports:
- "23306:3306"
volumes:
- .volumes/mysql:/var/lib/mysql
- .volumes/mysql:/var/lib/mysql
redis:
image: redis:7
ports:
- "26379:6379"
volumes:
- .volumes/redis:/data

View File

@@ -24,6 +24,7 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.62.0",
"redis": "^5.8.2",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"zustand": "^5.0.8"

View File

@@ -1,21 +1,3 @@
-- jdbox.accounts definition
CREATE TABLE `accounts` (
`id` varchar(191) NOT NULL,
`user_id` int(11) NOT NULL,
`type` varchar(191) NOT NULL,
`provider` varchar(191) NOT NULL,
`provider_account_id` varchar(191) NOT NULL,
`refresh_token` text DEFAULT NULL,
`access_token` text DEFAULT NULL,
`expires_at` int(11) DEFAULT NULL,
`token_type` varchar(191) DEFAULT NULL,
`scope` varchar(191) DEFAULT NULL,
`id_token` text DEFAULT NULL,
`session_state` varchar(191) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `accounts_provider_provider_account_id_key` (`provider`, `provider_account_id`),
KEY `accounts_user_id_fkey` (`user_id`) USING BTREE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
-- jdbox.sessions definition
CREATE TABLE `sessions` (
`id` varchar(191) NOT NULL,
@@ -25,6 +7,7 @@ CREATE TABLE `sessions` (
PRIMARY KEY (`id`),
KEY `sessions_userId_idx` (`userId`) USING BTREE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
-- jdbox.users definition
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
@@ -36,17 +19,6 @@ CREATE TABLE `users` (
PRIMARY KEY (`id`),
UNIQUE KEY `users_phone_key` (`phone`)
) ENGINE = InnoDB AUTO_INCREMENT = 2 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
-- jdbox.verification_codes definition
CREATE TABLE `verification_codes` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`phone` varchar(191) NOT NULL,
`code` varchar(191) NOT NULL,
`type` varchar(191) NOT NULL,
`expiresAt` datetime(3) NOT NULL,
`createdAt` datetime(3) NOT NULL DEFAULT current_timestamp(3),
PRIMARY KEY (`id`),
KEY `verification_codes_phone_type_idx` (`phone`, `type`) USING BTREE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
-- 插入初始用户
INSERT INTO users(phone, password, name)

18
src/actions/config.ts Normal file
View File

@@ -0,0 +1,18 @@
'use server'
import prisma from '@/lib/prisma'
import { log } from 'console'
export async function findConfigs(params: {
macaddr: string
}) {
try {
return await prisma.gateway.findMany({
where: {
macaddr: params.macaddr,
},
})
}
catch (e) {
throw new Error('查询配置失败: ' + (e as Error).message)
}
}

96
src/actions/remote.ts Normal file
View File

@@ -0,0 +1,96 @@
'use server'
import redis from '@/lib/redis'
const base = process.env.JD_BASE
const username = process.env.JD_USERNAME
const password = process.env.JD_PASSWORD
type JdResp<T> = {
code: number
meta: string
data: T
}
async function post<O>(path: string, data: unknown) {
try {
if (!base) throw new Error('JD_BASE 环境变量未设置')
if (!username) throw new Error('JD_USERNAME 环境变量未设置')
if (!password) throw new Error('JD_PASSWORD 环境变量未设置')
// 获取令牌
let token = await redis.get('token')
if (!token) {
const resp = await fetch(`${base}/client/auth`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username,
password,
}),
})
const json = await resp.json()
if (json.code !== 0) {
throw new Error('响应失败: ' + json.meta)
}
token = json.data
if (!token) {
throw new Error('响应中缺少 token')
}
await redis.set('token', token, {
expiration: { type: 'EX', value: 6 * 24 * 3600 },
})
}
// 发起请求
const resp = await fetch(base + path, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Token': token,
},
body: JSON.stringify(data),
})
if (resp.status === 401) {
await redis.del('token')
throw new Error('令牌无效,已删除缓存,请重试')
}
return await resp.json() as JdResp<O>
}
catch (e) {
throw new Error('请求失败: ' + (e as Error).message)
}
}
export async function gatewayConfigGet(params: {
macaddr: string
}) {
try {
const resp = await post<string>('/gateway/config/get', params)
if (resp.code !== 0) {
throw new Error('响应失败: ' + resp.meta)
}
if (!resp.data) {
throw new Error('响应中缺少 data')
}
return JSON.parse(atob(resp.data)) as {
id: number
rules: {
table: number
enable: boolean
edge: string[]
network: string[]
cityhash: string
}[]
}
}
catch (e) {
throw new Error('获取远程配置失败: ' + (e as Error).message)
}
}

View File

@@ -0,0 +1,108 @@
'use client'
import { findConfigs } from '@/actions/config'
import { gatewayConfigGet } from '@/actions/remote'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { useState } from 'react'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
type EdgeConfig = {
port?: string
edge?: string
city?: string
_index: number
}
export default function DebugConfigPage() {
const [macaddr, setMacaddr] = useState('')
const [remotes, setRemotes] = useState<EdgeConfig[]>([])
const [locals, setLocals] = useState<EdgeConfig[]>([])
const fetch = async (macaddr: string) => {
try {
console.log('fetch', macaddr)
if (!macaddr) return
const rawLocal = await findConfigs({ macaddr })
console.log('raw local', rawLocal)
const rawRemote = await gatewayConfigGet({ macaddr })
console.log('raw remote', rawRemote)
setLocals(rawLocal.map(rule => ({
port: rule.network,
edge: rule.edge,
city: rule.cityhash,
_index: parseInt(rule.network.split('.')[3] || '0'),
})).sort((a, b) => a._index - b._index))
setRemotes(rawRemote.rules.map((rule) => {
const port = rule.network.find(n => !!n)
return ({
port: port,
edge: rule.edge.find(n => !!n),
city: rule.cityhash,
_index: port ? parseInt(port.split('.')[3]) : 0,
})
}).sort((a, b) => a._index - b._index))
}
catch (e) {
console.error('数据获取失败', e)
}
}
return (
<div className="flex-auto overflow-hidden flex flex-col p-6 gap-4.5">
<div className="flex-none flex gap-3">
<Input type="text" name="macaddr" value={macaddr} onChange={e => setMacaddr(e.target.value)} className="flex-none basis-60" />
<Button onClick={() => fetch(macaddr)}></Button>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{!locals.length || !remotes.length ? (
<TableRow>
<TableCell colSpan={3} className="text-center"></TableCell>
</TableRow>
) : locals.length !== remotes.length ? (
<TableRow>
<TableCell colSpan={3} className="text-center"></TableCell>
</TableRow>
) : (
locals.map((item, index) => (
<TableRow key={index}>
<TableCell>
{item.port === remotes[index].port ? (
<span className="text-green-500">{item.port}</span>
) : (
<span className="text-red-500">{item.port} : {remotes[index].port}</span>
)}
</TableCell>
<TableCell>
{item.edge === remotes[index].edge ? (
<span className="text-green-500">{item.edge}</span>
) : (
<span className="text-red-500">{item.edge} : {remotes[index].edge}</span>
)}
</TableCell>
<TableCell>
{item.city === remotes[index].city ? (
<span className="text-green-500">{item.city}</span>
) : (
<span className="text-red-500">{item.city} : {remotes[index].city}</span>
)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
)
}

11
src/app/debug/layout.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { ReactNode } from 'react'
export default async function DebugLayout(props: {
children: ReactNode
}) {
return (
<div className="w-screen h-screen flex flex-col">
{props.children}
</div>
)
}

View File

@@ -1,3 +1,4 @@
import 'server-only'
import { PrismaClient } from '@/generated/prisma/client'
const globalForPrisma = global as unknown as {

9
src/lib/redis.ts Normal file
View File

@@ -0,0 +1,9 @@
import 'server-only'
import { createClient } from 'redis'
const client = createClient({
url: process.env.REDIS_URL,
})
const redis = await client.connect()
export default redis