import { Hono } from 'hono'; import { zValidator } from '@hono/zod-validator'; import { asc, eq, sql } from 'drizzle-orm'; import { nanoid } from 'nanoid'; import { z } from 'zod'; import { BUILTIN_POLICIES, ruleInputSchema, type RuleDto } from '@proxy-station/shared'; import { db } from '../db/index.js'; import { policyGroups, rules, rulesets } from '../db/schema.js'; import { seedRulesFromTemplate } from '../db/seed.js'; function loadRules(): RuleDto[] { const rows = db.select().from(rules).orderBy(asc(rules.seq)).all(); const sets = new Map(db.select().from(rulesets).all().map((r) => [r.id, r])); return rows.map((r) => { const rs = r.value ? sets.get(r.value) : undefined; return { ...r, params: JSON.parse(r.params), rulesetName: rs?.name ?? null, rulesetUrl: rs?.surgeUrl ?? null, }; }); } /** 引用不存在的策略组会被 IR 静默剔除,必须在写入前拦下 */ function policyError(policy: string): string | null { if ((BUILTIN_POLICIES as readonly string[]).includes(policy)) return null; const group = db.select().from(policyGroups).where(eq(policyGroups.name, policy)).get(); return group?.enabled ? null : `策略「${policy}」不存在或已停用:请选择内置策略或启用的策略组`; } function inputError(input: z.output, id?: string): string | null { const err = policyError(input.policy); if (err) return err; if (input.type === 'FINAL' && db.select().from(rules).all().some((r) => r.type === 'FINAL' && r.id !== id)) return '只能有一条 FINAL 规则'; if (['RULE-SET', 'DOMAIN-SET'].includes(input.type) && !db.select().from(rulesets).where(eq(rulesets.id, input.value!)).get()) return '规则集不存在'; return null; } export const rulesRoute = new Hono() .get('/', (c) => c.json(loadRules())) .post('/', zValidator('json', ruleInputSchema), (c) => { const input = c.req.valid('json'); const err = inputError(input); if (err) return c.json({ error: err }, 400); // 规则首条匹配即生效,自定义规则必须压过模板里的宽泛规则集(如 download.conf),插到最前 const id = nanoid(10); db.transaction(() => { if (input.type !== 'FINAL') db.update(rules).set({ seq: sql`${rules.seq} + 1` }).run(); const seq = input.type === 'FINAL' ? Math.max(-1, ...db.select().from(rules).all().map((r) => r.seq)) + 1 : 0; db.insert(rules) .values({ id, seq, type: input.type, value: input.value ?? null, policy: input.policy, params: JSON.stringify(input.params), enabled: input.enabled, source: 'custom', }) .run(); }); return c.json({ id }, 201); }) .put('/reorder', zValidator('json', z.object({ ids: z.array(z.string()) })), (c) => { const { ids } = c.req.valid('json'); const rows = db.select().from(rules).all(); const known = new Set(rows.map((r) => r.id)); if (ids.length !== rows.length || new Set(ids).size !== ids.length || ids.some((id) => !known.has(id))) return c.json({ error: '排序必须包含全部规则且每条仅出现一次' }, 400); const finals = rows.filter((r) => r.type === 'FINAL'); if (finals.length !== 1 || !finals[0].enabled || ids.at(-1) !== finals[0].id) return c.json({ error: '启用的唯一 FINAL 必须位于末尾' }, 400); db.transaction(() => ids.forEach((id, i) => db.update(rules).set({ seq: i }).where(eq(rules.id, id)).run())); return c.json({ ok: true }); }) .post('/reset', (c) => { seedRulesFromTemplate(db, { keepCustom: true }); return c.json({ ok: true, rules: loadRules().length }); }) .put('/:id', zValidator('json', ruleInputSchema), (c) => { const id = c.req.param('id'); const existing = db.select().from(rules).where(eq(rules.id, id)).get(); if (!existing) return c.json({ error: '规则不存在' }, 404); const input = c.req.valid('json'); if (existing.type === 'FINAL' && input.type !== 'FINAL') return c.json({ error: '不能将 FINAL 改成其他规则类型' }, 400); if (existing.type !== 'FINAL' && input.type === 'FINAL') return c.json({ error: '请编辑已有 FINAL,不能改变普通规则的类型为 FINAL' }, 400); const err = inputError(input, id); if (err) return c.json({ error: err }, 400); db.update(rules) .set({ type: input.type, value: input.value ?? null, policy: input.policy, params: JSON.stringify(input.params), enabled: input.enabled, }) .where(eq(rules.id, id)) .run(); return c.json({ ok: true }); }) .patch('/:id/enabled', zValidator('json', z.object({ enabled: z.boolean() })), (c) => { const row = db.select().from(rules).where(eq(rules.id, c.req.param('id'))).get(); if (!row) return c.json({ error: '规则不存在' }, 404); if (row.type === 'FINAL' && !c.req.valid('json').enabled) return c.json({ error: '不能停用 FINAL' }, 400); const res = db .update(rules) .set({ enabled: c.req.valid('json').enabled }) .where(eq(rules.id, c.req.param('id'))) .run(); if (res.changes === 0) return c.json({ error: '规则不存在' }, 404); return c.json({ ok: true }); }) .delete('/:id', (c) => { const row = db.select().from(rules).where(eq(rules.id, c.req.param('id'))).get(); if (!row) return c.json({ error: '规则不存在' }, 404); if (row.type === 'FINAL') return c.json({ error: '不能删除 FINAL' }, 400); const res = db.delete(rules).where(eq(rules.id, c.req.param('id'))).run(); if (res.changes === 0) return c.json({ error: '规则不存在' }, 404); return c.json({ ok: true }); }); export const rulesetsRoute = new Hono() .get('/', (c) => { const rows = db.select().from(rulesets).all(); return c.json(rows); }) .get('/:id/content', async (c) => { const rs = db.select().from(rulesets).where(eq(rulesets.id, c.req.param('id'))).get(); if (!rs) return c.json({ error: '规则集不存在' }, 404); const refresh = c.req.query('refresh') === '1'; try { const { fetchRulesetContent } = await import('../services/rulesets/fetch.js'); const { content, fetchedAt, stale } = await fetchRulesetContent(rs.surgeUrl, { refresh }); const lineCount = content.split(/\r?\n/).filter((l) => l.trim() && !l.trim().startsWith('#')).length; return c.json({ content, fetchedAt, stale, lineCount }); } catch (e: any) { return c.json({ error: `拉取失败:${e.message}` }, 502); } });