init: init proj

This commit is contained in:
2026-08-25 11:35:05 +08:00
commit bf7f2df67e
80 changed files with 9936 additions and 0 deletions

107
server/src/routes/groups.ts Normal file
View File

@@ -0,0 +1,107 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { asc, eq } from 'drizzle-orm';
import { nanoid } from 'nanoid';
import { z } from 'zod';
import { groupInputSchema, type GroupDto } from '@proxy-station/shared';
import { db } from '../db/index.js';
import { groupMembers, policyGroups, rules } from '../db/schema.js';
function loadGroups(): GroupDto[] {
const groups = db.select().from(policyGroups).orderBy(asc(policyGroups.sortOrder)).all();
const members = db.select().from(groupMembers).orderBy(asc(groupMembers.sortOrder)).all();
return groups.map((g) => ({
...g,
members: members
.filter((m) => m.groupId === g.id)
.map((m) => ({ id: m.id, kind: m.memberKind, nodeId: m.nodeId, refName: m.refName, sortOrder: m.sortOrder })),
}));
}
function replaceMembers(groupId: string, members: z.output<typeof groupInputSchema>['members']) {
db.delete(groupMembers).where(eq(groupMembers.groupId, groupId)).run();
members.forEach((m, i) => {
db.insert(groupMembers)
.values({
groupId,
memberKind: m.kind,
nodeId: m.kind === 'node' ? (m.nodeId ?? null) : null,
refName: m.kind === 'node' ? null : (m.refName ?? null),
sortOrder: i,
})
.run();
});
}
export const groupsRoute = new Hono()
.get('/', (c) => c.json(loadGroups()))
.post('/', zValidator('json', groupInputSchema), (c) => {
const input = c.req.valid('json');
const id = nanoid(10);
try {
db.insert(policyGroups)
.values({
id,
name: input.name,
type: input.type,
testUrl: input.testUrl ?? null,
interval: input.interval ?? null,
tolerance: input.tolerance ?? null,
filterRegex: input.filterRegex || null,
includeAllNodes: input.includeAllNodes,
sortOrder: input.sortOrder,
source: 'custom',
})
.run();
} catch (e: any) {
if (String(e.message).includes('UNIQUE')) return c.json({ error: `策略组「${input.name}」已存在` }, 409);
throw e;
}
replaceMembers(id, input.members);
return c.json({ id }, 201);
})
.put('/reorder', zValidator('json', z.object({ ids: z.array(z.string()) })), (c) => {
const { ids } = c.req.valid('json');
ids.forEach((id, i) => db.update(policyGroups).set({ sortOrder: i }).where(eq(policyGroups.id, id)).run());
return c.json({ ok: true });
})
.patch('/:id/enabled', zValidator('json', z.object({ enabled: z.boolean() })), (c) => {
const res = db
.update(policyGroups)
.set({ enabled: c.req.valid('json').enabled })
.where(eq(policyGroups.id, c.req.param('id')))
.run();
if (res.changes === 0) return c.json({ error: '策略组不存在' }, 404);
return c.json({ ok: true });
})
.put('/:id', zValidator('json', groupInputSchema), (c) => {
const id = c.req.param('id');
const existing = db.select().from(policyGroups).where(eq(policyGroups.id, id)).get();
if (!existing) return c.json({ error: '策略组不存在' }, 404);
const input = c.req.valid('json');
const oldName = existing.name;
db.update(policyGroups)
.set({
name: input.name,
type: input.type,
testUrl: input.testUrl ?? null,
interval: input.interval ?? null,
tolerance: input.tolerance ?? null,
filterRegex: input.filterRegex || null,
includeAllNodes: input.includeAllNodes,
})
.where(eq(policyGroups.id, id))
.run();
replaceMembers(id, input.members);
// 组名变更时同步引用它的成员与规则策略
if (oldName !== input.name) {
db.update(groupMembers).set({ refName: input.name }).where(eq(groupMembers.refName, oldName)).run();
db.update(rules).set({ policy: input.name }).where(eq(rules.policy, oldName)).run();
}
return c.json({ ok: true });
})
.delete('/:id', (c) => {
const res = db.delete(policyGroups).where(eq(policyGroups.id, c.req.param('id'))).run();
if (res.changes === 0) return c.json({ error: '策略组不存在' }, 404);
return c.json({ ok: true });
});