新增 debug 页面
This commit is contained in:
10
.env.example
Normal file
10
.env.example
Normal file
@@ -0,0 +1,10 @@
|
||||
# 数据库连接字符串
|
||||
DATABASE_URL=
|
||||
|
||||
# Redis 连接字符串
|
||||
REDIS_URL=
|
||||
|
||||
# 京东网关配置
|
||||
JD_BASE=https://smart.jdbox.xyz:58001
|
||||
JD_USERNAME=
|
||||
JD_PASSWORD=
|
||||
15
bun.lock
15
bun.lock
@@ -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=="],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
services:
|
||||
|
||||
mariadb:
|
||||
image: mariadb:10
|
||||
environment:
|
||||
@@ -8,3 +9,10 @@ services:
|
||||
- "23306:3306"
|
||||
volumes:
|
||||
- .volumes/mysql:/var/lib/mysql
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
ports:
|
||||
- "26379:6379"
|
||||
volumes:
|
||||
- .volumes/redis:/data
|
||||
@@ -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"
|
||||
|
||||
@@ -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
18
src/actions/config.ts
Normal 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
96
src/actions/remote.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
108
src/app/debug/config/page.tsx
Normal file
108
src/app/debug/config/page.tsx
Normal 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
11
src/app/debug/layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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
9
src/lib/redis.ts
Normal 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
|
||||
Reference in New Issue
Block a user