init: init proj
This commit is contained in:
21
shared/package.json
Normal file
21
shared/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@proxy-station/shared",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
119
shared/src/dto.ts
Normal file
119
shared/src/dto.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type { ClientType, Egress, Protocol } from './protocols.js';
|
||||
|
||||
export interface NodeDto {
|
||||
id: string;
|
||||
name: string;
|
||||
protocol: Protocol;
|
||||
server: string;
|
||||
port: number;
|
||||
network: 'tcp' | 'ws';
|
||||
wsPath: string | null;
|
||||
wsHost: string | null;
|
||||
tls: boolean;
|
||||
sni: string | null;
|
||||
skipCertVerify: boolean;
|
||||
uuid: string | null;
|
||||
alterId: number;
|
||||
security: string;
|
||||
password: string | null;
|
||||
method: string | null;
|
||||
obfsType: string | null;
|
||||
obfsPassword: string | null;
|
||||
upMbps: number | null;
|
||||
downMbps: number | null;
|
||||
region: string | null;
|
||||
tags: string[];
|
||||
enabled: boolean;
|
||||
sortOrder: number;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
/** 从节点 tags/名称推断的出口类型,仅前端展示用 */
|
||||
export function nodeEgress(node: Pick<NodeDto, 'tags' | 'name'>): Egress {
|
||||
const haystack = [...node.tags, node.name].join(' ').toLowerCase();
|
||||
if (haystack.includes('warp')) return 'warp';
|
||||
if (haystack.includes('direct')) return 'direct';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
export interface ImportPreviewItem {
|
||||
ok: boolean;
|
||||
uri: string;
|
||||
error?: string;
|
||||
node?: Partial<NodeDto> & { name: string; protocol: Protocol };
|
||||
}
|
||||
|
||||
export interface GroupMemberDto {
|
||||
id: number;
|
||||
kind: 'node' | 'group' | 'builtin';
|
||||
nodeId: string | null;
|
||||
refName: string | null;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export interface GroupDto {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'select' | 'url-test' | 'fallback';
|
||||
testUrl: string | null;
|
||||
interval: number | null;
|
||||
tolerance: number | null;
|
||||
filterRegex: string | null;
|
||||
includeAllNodes: boolean;
|
||||
enabled: boolean;
|
||||
sortOrder: number;
|
||||
source: 'template' | 'custom';
|
||||
members: GroupMemberDto[];
|
||||
}
|
||||
|
||||
export interface RuleDto {
|
||||
id: string;
|
||||
seq: number;
|
||||
type: string;
|
||||
value: string | null;
|
||||
policy: string;
|
||||
params: string[];
|
||||
enabled: boolean;
|
||||
source: 'template' | 'custom';
|
||||
rulesetName?: string | null;
|
||||
rulesetUrl?: string | null;
|
||||
}
|
||||
|
||||
export interface RulesetDto {
|
||||
id: string;
|
||||
name: string;
|
||||
surgeUrl: string;
|
||||
clashUrl: string | null;
|
||||
clashBehavior: string;
|
||||
needsConvert: boolean;
|
||||
}
|
||||
|
||||
export interface TokenDto {
|
||||
id: string;
|
||||
token: string;
|
||||
name: string;
|
||||
/** 分配给该订阅的节点 id;空数组 = 全部节点 */
|
||||
nodeIds: string[];
|
||||
enabled: boolean;
|
||||
createdAt: number;
|
||||
lastAccessAt: number | null;
|
||||
accessCount: number;
|
||||
/** 各客户端订阅路径;ShadowRocket 额外有 shadowrocketConf(策略组与规则) */
|
||||
urls: Record<ClientType, string> & { shadowrocketConf: string };
|
||||
}
|
||||
|
||||
export interface ExcludedNode {
|
||||
name: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface PreviewResult {
|
||||
client: ClientType;
|
||||
content: string;
|
||||
excluded: ExcludedNode[];
|
||||
}
|
||||
|
||||
export interface SettingsDto {
|
||||
surgeSections: Record<string, string>;
|
||||
}
|
||||
3
shared/src/index.ts
Normal file
3
shared/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './protocols.js';
|
||||
export * from './schemas.js';
|
||||
export * from './dto.js';
|
||||
50
shared/src/protocols.ts
Normal file
50
shared/src/protocols.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
export const PROTOCOLS = ['vmess', 'trojan', 'shadowsocks', 'hysteria2'] as const;
|
||||
export type Protocol = (typeof PROTOCOLS)[number];
|
||||
|
||||
export const CLIENTS = ['surge', 'shadowrocket', 'clash'] as const;
|
||||
export type ClientType = (typeof CLIENTS)[number];
|
||||
|
||||
export const CLIENT_LABELS: Record<ClientType, string> = {
|
||||
surge: 'Surge (macOS)',
|
||||
shadowrocket: 'ShadowRocket (iOS)',
|
||||
clash: 'ClashMeta (Android)',
|
||||
};
|
||||
|
||||
/**
|
||||
* ShadowRocket 分两步导入:节点走订阅、策略组与规则走配置文件。
|
||||
* 配置文件里的策略组用 `use=true` 引用订阅,因此 App 内的订阅名必须与此一致。
|
||||
*/
|
||||
export const SHADOWROCKET_SUB_NAME = 'Proxy Station';
|
||||
|
||||
export type Egress = 'direct' | 'warp' | 'unknown';
|
||||
|
||||
/** Surge 与 Clash 使用 IETF 命名,Xray 分享链接使用简名 */
|
||||
export const SS_CIPHER_ALIASES: Record<string, string> = {
|
||||
'chacha20-poly1305': 'chacha20-ietf-poly1305',
|
||||
'xchacha20-poly1305': 'xchacha20-ietf-poly1305',
|
||||
};
|
||||
|
||||
export function normalizeSsCipher(method: string): string {
|
||||
return SS_CIPHER_ALIASES[method] ?? method;
|
||||
}
|
||||
|
||||
export interface NodeCapabilityInput {
|
||||
protocol: Protocol;
|
||||
network: 'tcp' | 'ws';
|
||||
}
|
||||
|
||||
export interface CapabilityVerdict {
|
||||
ok: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端能力矩阵:不支持的组合在下发订阅时自动屏蔽。
|
||||
* Surge 的 ss 类型没有 WebSocket 传输(仅 simple-obfs),SS over WS 线路必须排除。
|
||||
*/
|
||||
export function clientSupports(client: ClientType, node: NodeCapabilityInput): CapabilityVerdict {
|
||||
if (client === 'surge' && node.protocol === 'shadowsocks' && node.network === 'ws') {
|
||||
return { ok: false, reason: 'Surge 不支持 Shadowsocks over WebSocket' };
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
99
shared/src/schemas.ts
Normal file
99
shared/src/schemas.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { z } from 'zod';
|
||||
import { PROTOCOLS } from './protocols.js';
|
||||
|
||||
export const nodeInputSchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(64),
|
||||
protocol: z.enum(PROTOCOLS),
|
||||
server: z.string().min(1),
|
||||
port: z.number().int().min(1).max(65535),
|
||||
network: z.enum(['tcp', 'ws']).default('tcp'),
|
||||
wsPath: z.string().startsWith('/').optional().nullable(),
|
||||
wsHost: z.string().optional().nullable(),
|
||||
tls: z.boolean().default(false),
|
||||
sni: z.string().optional().nullable(),
|
||||
skipCertVerify: z.boolean().default(false),
|
||||
uuid: z.string().uuid().optional().nullable(),
|
||||
alterId: z.number().int().min(0).default(0),
|
||||
security: z.string().default('auto'),
|
||||
password: z.string().optional().nullable(),
|
||||
method: z.string().optional().nullable(),
|
||||
obfsType: z.enum(['salamander']).optional().nullable(),
|
||||
obfsPassword: z.string().optional().nullable(),
|
||||
upMbps: z.number().int().positive().optional().nullable(),
|
||||
downMbps: z.number().int().positive().optional().nullable(),
|
||||
region: z.string().max(8).optional().nullable(),
|
||||
tags: z.array(z.string()).default([]),
|
||||
enabled: z.boolean().default(true),
|
||||
sortOrder: z.number().int().default(0),
|
||||
})
|
||||
.superRefine((v, ctx) => {
|
||||
if (v.protocol === 'vmess' && !v.uuid) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['uuid'], message: 'VMess 需要 UUID' });
|
||||
}
|
||||
if ((v.protocol === 'trojan' || v.protocol === 'shadowsocks' || v.protocol === 'hysteria2') && !v.password) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['password'], message: '该协议需要密码' });
|
||||
}
|
||||
if (v.protocol === 'shadowsocks' && !v.method) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['method'], message: 'Shadowsocks 需要加密方式' });
|
||||
}
|
||||
if (v.network === 'ws' && !v.wsPath) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['wsPath'], message: 'WebSocket 传输需要路径' });
|
||||
}
|
||||
});
|
||||
|
||||
export type NodeInput = z.input<typeof nodeInputSchema>;
|
||||
export type NodeParsed = z.output<typeof nodeInputSchema>;
|
||||
|
||||
export const groupInputSchema = z.object({
|
||||
name: z.string().min(1).max(64),
|
||||
type: z.enum(['select', 'url-test', 'fallback']),
|
||||
testUrl: z.string().url().optional().nullable(),
|
||||
interval: z.number().int().positive().optional().nullable(),
|
||||
tolerance: z.number().int().positive().optional().nullable(),
|
||||
filterRegex: z.string().optional().nullable(),
|
||||
includeAllNodes: z.boolean().default(false),
|
||||
sortOrder: z.number().int().default(0),
|
||||
members: z
|
||||
.array(
|
||||
z.object({
|
||||
kind: z.enum(['node', 'group', 'builtin']),
|
||||
nodeId: z.string().optional().nullable(),
|
||||
refName: z.string().optional().nullable(),
|
||||
})
|
||||
)
|
||||
.default([]),
|
||||
});
|
||||
export type GroupInput = z.input<typeof groupInputSchema>;
|
||||
|
||||
export const RULE_TYPES = [
|
||||
'RULE-SET',
|
||||
'DOMAIN-SET',
|
||||
'DOMAIN',
|
||||
'DOMAIN-SUFFIX',
|
||||
'DOMAIN-KEYWORD',
|
||||
'IP-CIDR',
|
||||
'IP-CIDR6',
|
||||
'IP-ASN',
|
||||
'GEOIP',
|
||||
'PROCESS-NAME',
|
||||
'USER-AGENT',
|
||||
'URL-REGEX',
|
||||
'AND',
|
||||
'OR',
|
||||
'NOT',
|
||||
'FINAL',
|
||||
] as const;
|
||||
|
||||
export const ruleInputSchema = z.object({
|
||||
type: z.enum(RULE_TYPES),
|
||||
value: z.string().optional().nullable(),
|
||||
policy: z.string().min(1),
|
||||
params: z.array(z.string()).default([]),
|
||||
enabled: z.boolean().default(true),
|
||||
});
|
||||
export type RuleInput = z.input<typeof ruleInputSchema>;
|
||||
|
||||
export const tokenInputSchema = z.object({
|
||||
name: z.string().min(1).max(64),
|
||||
});
|
||||
11
shared/tsconfig.json
Normal file
11
shared/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user