diff --git a/docs/docs/api/v1.md b/docs/docs/api/v1.md index 1b66b7c..b6a35d3 100644 --- a/docs/docs/api/v1.md +++ b/docs/docs/api/v1.md @@ -25,7 +25,7 @@ 响应: - Content-Type: application/json -- Body: 当前配置的 JSON 字符串 +- Body: 当前配置的 JSON 字符串,包含完整的运行时配置 ### 更新配置 @@ -52,12 +52,35 @@ 获取所有已注册的网络类型和代理类型。 +响应: + +```json +{ + "net": { + "<网络类型名称>": { + "schema": "", + "description": "网络类型描述" + } + // ... 其他网络类型 + }, + "proxy": { + "<代理类型名称>": { + "schema": "", + "description": "代理类型描述" + } + // ... 其他代理类型 + } +} +``` + ### 获取系统状态 **GET** `/state` 获取系统当前运行状态。 +响应:系统状态字符串 + ## 连接管理 ### 获取所有连接 @@ -66,12 +89,35 @@ 获取当前所有活动连接的信息。 +响应: + +```json +{ + "connections": [ + { + "id": "<连接UUID>", + "local_addr": "<本地地址>", + "remote_addr": "<远程地址>", + "net": "<使用的网络名称>", + "upload": "<上传字节数>", + "download": "<下载字节数>", + "start_time": "<开始时间>", + "chains": ["<经过的代理链>"] + } + ], + "upload": "<总上传字节数>", + "download": "<总下载字节数>" +} +``` + ### 关闭所有连接 **DELETE** `/connections` 关闭所有活动的连接。 +响应:返回已停止的连接信息,格式与 GET `/connections` 相同。 + ### 关闭指定连接 **DELETE** `/connection/{uuid}` @@ -82,7 +128,12 @@ - uuid: 连接的唯一标识符 -响应: 布尔值,表示操作是否成功 +响应: + +```json +true // 操作成功 +false // 操作失败 +``` ### WebSocket 连接监控 @@ -95,6 +146,51 @@ - patch: 是否使用增量更新模式 - without_connections: 是否排除详细连接信息 +响应: 根据 patch 参数返回不同格式的 WebSocket 消息 + +当 patch=false 时: + +```json +{ + "type": "full", + "value": { + "connections": [ + { + "id": "<连接UUID>", + "local_addr": "<本地地址>", + "remote_addr": "<远程地址>", + "net": "<使用的网络名称>", + "upload": "<上传字节数>", + "download": "<下载字节数>", + "start_time": "<开始时间>", + "chains": ["<经过的代理链>"] + } + ], + "upload": "<总上传字节数>", + "download": "<总下载字节数>" + } +} +``` + +当 patch=true 时,首次返回完整数据,之后返回差异数据: + +```json +{ + "type": "patch", + "value": [ + { "op": "replace", "path": "/upload", "value": "<新的上传字节数>" }, + { + "op": "add", + "path": "/connections/-", + "value": { + /* 新增的连接信息 */ + } + } + // 其他 JSON Patch 操作 + ] +} +``` + ## 代理节点管理 ### 更新代理选择 @@ -136,11 +232,17 @@ ```json { - "connect": "连接耗时(毫秒)", - "response": "响应耗时(毫秒)" + "connect": 100, // 连接耗时(毫秒) + "response": 200 // 响应耗时(毫秒) } ``` +如果测试超时或失败,返回: + +```json +null +``` + ## 用户数据管理 ### 获取用户数据 @@ -149,18 +251,36 @@ 获取指定键的用户数据。 +响应:存储的用户数据对象,格式取决于存储时的数据类型 + ### 设置用户数据 **PUT** `/userdata/{key}` 设置指定键的用户数据。 +响应: + +```json +{ + "copied": 1024 // 成功写入的字节数 +} +``` + ### 删除用户数据 **DELETE** `/userdata/{key}` 删除指定键的用户数据。 +响应: + +```json +{ + "ok": true // 删除成功 +} +``` + ### 列出所有用户数据 **GET** `/userdata` @@ -171,7 +291,11 @@ ```json { - "keys": ["key1", "key2", ...] + "keys": [ + "key1", + "key2" + // ... 更多键 + ] } ``` @@ -182,3 +306,5 @@ **WebSocket** `/log` 通过 WebSocket 实时接收系统日志。 + +响应:每个 WebSocket 消息包含一条日志文本字符串。 diff --git a/ui/src/api/v1.ts b/ui/src/api/v1.ts new file mode 100644 index 0000000..3c39563 --- /dev/null +++ b/ui/src/api/v1.ts @@ -0,0 +1,184 @@ +export interface ImportSource { + type: string + data: Record +} + +export interface ConnectionQuery { + patch?: boolean + without_connections?: boolean +} + +export interface DelayRequest { + url: string + timeout?: number +} + +export interface DelayResponse { + connect: number + response: number +} + +export interface PostSelectPayload { + selected: string +} + +// API 响应类型 +export type ApiResponse = T + +// API 错误响应 +export interface ApiError { + error: string +} + +// 创建类型安全的 fetch 包装函数 +async function apiFetch(path: string, init?: RequestInit): Promise { + const response = await fetch(`/api${path}`, init) + if (!response.ok) { + const error: ApiError = await response.json() + throw new Error(error.error) + } + return response.json() +} + +export async function getConfig(): Promise { + return apiFetch('/config') +} + +export async function postConfig(source: ImportSource): Promise { + return apiFetch('/config', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(source), + }) +} + +export type RegistryData = Record> +export async function getRegistry(): Promise { + return apiFetch('/registry') +} + +export interface Connection { + connections: Record +} +export async function getConnections(): Promise { + return apiFetch('/connections') +} + +export async function deleteConnections(): Promise { + return apiFetch('/connections', { + method: 'DELETE', + }) +} + +export async function getState(): Promise { + return apiFetch('/state') +} + +export async function postSelect(netName: string, selected: string): Promise { + return apiFetch(`/select/${encodeURIComponent(netName)}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ selected }), + }) +} + +export async function deleteConn(uuid: string): Promise { + return apiFetch(`/conn/${encodeURIComponent(uuid)}`, { + method: 'DELETE', + }) +} + +export async function getDelay(netName: string, request: DelayRequest): Promise { + const params = new URLSearchParams() + params.set('url', request.url) + if (request.timeout !== undefined) { + params.set('timeout', request.timeout.toString()) + } + return apiFetch(`/delay/${encodeURIComponent(netName)}?${params.toString()}`) +} + +// Userdata 相关 API +export async function getUserData(path: string): Promise { + return apiFetch(`/userdata/${encodeURIComponent(path)}`) +} + +export async function putUserData(path: string, data: string): Promise<{ copied: number }> { + const response = await fetch(`/api/v1/userdata/${encodeURIComponent(path)}`, { + method: 'PUT', + body: data, + }) + if (!response.ok) { + const error: ApiError = await response.json() + throw new Error(error.error) + } + return response.json() +} + +export async function deleteUserData(path: string): Promise<{ ok: boolean }> { + return apiFetch(`/userdata/${encodeURIComponent(path)}`, { + method: 'DELETE', + }) +} + +export interface UserDataList { + keys: string[] +} +export async function listUserData(): Promise { + return apiFetch('/userdata') +} + +// WebSocket 相关函数 +export function connectWebSocket(query: ConnectionQuery = {}): WebSocket { + const params = new URLSearchParams() + if (query.patch) params.set('patch', 'true') + if (query.without_connections) params.set('without_connections', 'true') + + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const ws = new WebSocket( + `${protocol}//${window.location.host}/api/v1/ws/connection?${params.toString()}` + ) + return ws +} + +export function connectLogWebSocket(): WebSocket { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const ws = new WebSocket(`${protocol}//${window.location.host}/api/v1/ws/log`) + return ws +} + +interface UseWebSocketOptions { + onMessage?: (data: unknown) => void + onError?: (error: Event) => void + onClose?: (event: CloseEvent) => void +} + +export function useWebSocket(url: string, options: UseWebSocketOptions = {}) { + const ws = new WebSocket(url) + + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data) + options.onMessage?.(data) + } catch (e) { + console.error('Failed to parse WebSocket message:', e) + options.onError?.(e as Event) + } + } + + ws.onclose = (event) => { + options.onClose?.(event) + } + + return { + send: (data: unknown) => { + ws.send(JSON.stringify(data)) + }, + close: () => { + ws.close() + }, + } +} \ No newline at end of file