Files
sub-router/web/src/api.ts
YANG JIANKUAN d708240acf feat: 新增自定义配置合并到 Surge 输出
管理面板新增「自定义」面板,可按 Surge 格式书写 [Proxy]/[Rule]/
[General] 等配置片段,生成时合并进最终输出:[Proxy] 行追加到节点
之后,[Rule] 行注入到规则最顶部(优先级最高),其他段注入对应段
顶部且同名 key 覆盖模板取值(如 doh-server)。仅管理员链接生效,
游客链接不包含自定义内容。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 15:45:24 +08:00

156 lines
4.8 KiB
TypeScript

const API_BASE = '/api';
function getToken(): string | null {
return sessionStorage.getItem('sub-router-token');
}
export function setToken(token: string) {
sessionStorage.setItem('sub-router-token', token);
}
export function clearToken() {
sessionStorage.removeItem('sub-router-token');
}
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
const token = getToken();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(options.headers as Record<string, string> || {}),
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const res = await fetch(`${API_BASE}${path}`, { ...options, headers });
if (res.status === 401) {
clearToken();
window.location.reload();
throw new Error('Unauthorized');
}
if (!res.ok) {
const body = await res.json().catch(() => ({ error: res.statusText }));
throw new Error(body.error || res.statusText);
}
return res.json();
}
// Auth
export const auth = {
status: () => request<{ hasPassword: boolean }>('/auth/status'),
login: (password: string) => request<{ ok: boolean }>('/auth/login', {
method: 'POST',
body: JSON.stringify({ password }),
}),
};
// Subscriptions
export const subscriptions = {
list: () => request<any[]>('/subscriptions'),
create: (name: string, url: string, urlClash?: string, urlSsr?: string) => request<{ id: number }>('/subscriptions', {
method: 'POST',
body: JSON.stringify({ name, url, url_clash: urlClash || null, url_ssr: urlSsr || null }),
}),
update: (id: number, data: any) => request<{ ok: boolean }>(`/subscriptions/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
delete: (id: number) => request<{ ok: boolean }>(`/subscriptions/${id}`, {
method: 'DELETE',
}),
fetch: (id: number) => request<{ nodeCount: number }>(`/subscriptions/${id}/fetch`, {
method: 'POST',
}),
nodes: (id: number) => request<any[]>(`/subscriptions/${id}/nodes`),
};
// Nodes
export const nodes = {
fetchedToggle: (id: number, enabled: boolean) => request<{ ok: boolean }>(`/nodes/fetched/${id}`, {
method: 'PUT',
body: JSON.stringify({ enabled }),
}),
fetchedBatch: (ids: number[], enabled: boolean) => request<{ ok: boolean }>('/nodes/fetched/batch', {
method: 'PUT',
body: JSON.stringify({ ids, enabled }),
}),
staticList: () => request<any[]>('/nodes/static'),
staticCreate: (uri: string, name?: string) => request<any>('/nodes/static', {
method: 'POST',
body: JSON.stringify({ uri, name }),
}),
staticUpdate: (id: number, data: any) => request<{ ok: boolean }>(`/nodes/static/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
staticDelete: (id: number) => request<{ ok: boolean }>(`/nodes/static/${id}`, {
method: 'DELETE',
}),
staticReorder: (ids: number[]) => request<{ ok: boolean }>('/nodes/static/reorder', {
method: 'PUT',
body: JSON.stringify({ ids }),
}),
fetchedReorder: (ids: number[]) => request<{ ok: boolean }>('/nodes/fetched/reorder', {
method: 'PUT',
body: JSON.stringify({ ids }),
}),
};
// Rules
export const rules = {
list: () => request<any[]>('/rules'),
create: (data: any) => request<{ id: number }>('/rules', {
method: 'POST',
body: JSON.stringify(data),
}),
update: (id: number, data: any) => request<{ ok: boolean }>(`/rules/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
delete: (id: number) => request<{ ok: boolean }>(`/rules/${id}`, {
method: 'DELETE',
}),
reorder: (ids: number[]) => request<{ ok: boolean }>('/rules/reorder', {
method: 'PUT',
body: JSON.stringify({ ids }),
}),
};
// Config
export const config = {
preview: () => request<{ config: string }>('/config/preview'),
getSurgeToken: () => request<{ token: string }>('/config/surge-token'),
regenerateSurgeToken: () => request<{ token: string }>('/config/surge-token', { method: 'POST' }),
getCustom: () => request<{ content: string }>('/config/custom'),
saveCustom: (content: string) => request<{ ok: boolean }>('/config/custom', {
method: 'PUT',
body: JSON.stringify({ content }),
}),
};
// Guest links
export const guests = {
list: () => request<any[]>('/guests'),
availableNodes: () => request<{ static: any[]; subscriptions: any[] }>('/guests/available-nodes'),
create: (name: string) => request<{ id: number; token: string }>('/guests', {
method: 'POST',
body: JSON.stringify({ name }),
}),
get: (id: number) => request<any>(`/guests/${id}`),
update: (id: number, data: any) => request<{ ok: boolean }>(`/guests/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
delete: (id: number) => request<{ ok: boolean }>(`/guests/${id}`, {
method: 'DELETE',
}),
};
// Stats
export const stats = {
get: () => request<any>('/stats'),
};