const TOKEN_KEY = 'hexi_web_token' const USER_KEY = 'hexi_web_username' export function getToken() { return localStorage.getItem(TOKEN_KEY) || '' } interface ReqOpts { method?: string; body?: any; headers?: Record } async function request(url: string, opts: ReqOpts = {}) { const headers: Record = { 'Content-Type': 'application/json', ...(opts.headers || {}) } const token = getToken() if (token) headers['Authorization'] = 'Bearer ' + token const res = await fetch(url, { method: opts.method || 'GET', headers, body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined }) let data: any = null try { data = await res.json() } catch { data = null } if (!res.ok) { const msg = (data && (data.detail || data.msg)) || ('HTTP ' + res.status) throw new Error(msg) } return data } export function setSession(token: string, username: string) { localStorage.setItem(TOKEN_KEY, token) localStorage.setItem(USER_KEY, username) } export function clearSession() { localStorage.removeItem(TOKEN_KEY) localStorage.removeItem(USER_KEY) } export function getUserName() { return localStorage.getItem(USER_KEY) || '' } export const authLogin = async (username: string, password: string) => { const res = await fetch('/hub/api/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ username, password }).toString(), }) const data = await res.json().catch(() => null) if (!res.ok) throw new Error((data && (data.detail || data.msg)) || ('HTTP ' + res.status)) if (data && data.access_token === undefined) throw new Error('登录失败') return { token: data.access_token, username: data.username || username } } export const authMe = async () => await request('/hub/api/auth/me') export const hubPlugins = async () => await request('/hub/api/plugins') export const hubDashboard = async () => await request('/hub/api/dashboard') export const hubPluginCatalog = async () => await request('/hub/api/plugins/catalog') export const hubGroups = async () => await request('/hub/api/groups') export const hubPluginSetGlobal = async (id: string, body: any) => await request('/hub/api/plugins/' + encodeURIComponent(id) + '/global', { method: 'POST', body }) export const hubPluginSetGroup = async (id: string, gid: string, body: any) => await request('/hub/api/plugins/' + encodeURIComponent(id) + '/groups/' + encodeURIComponent(gid), { method: 'POST', body }) export const hubPluginRemoveGroup = async (id: string, gid: string) => await request('/hub/api/plugins/' + encodeURIComponent(id) + '/groups/' + encodeURIComponent(gid), { method: 'DELETE' }) export const hubPluginGetConfig = async (id: string) => await request('/hub/api/plugins/' + encodeURIComponent(id) + '/config') export const hubPluginSaveConfig = async (id: string, values: Record, revision: number) => await request('/hub/api/plugins/' + encodeURIComponent(id) + '/config', { method: 'POST', body: { values, revision } }) export const hubSettings = async () => await request('/hub/api/settings') export const hubChangePassword = async (old_password: string, new_password: string) => await request('/hub/api/settings/password', { method: 'POST', body: { old_password, new_password } }) export const hubLogs = async (limit = 500) => await request('/hub/api/logs?limit=' + limit) export const hubBotAction = async (action: 'stop' | 'restart') => await request('/hub/api/system/bot', { method: 'POST', body: { action } }) export const hubBotConfig = async () => await request('/hub/api/system/bot_config') export const hubBotConfigSave = async (values: Record) => await request('/hub/api/system/bot_config', { method: 'POST', body: values }) export function hubLogStream(opts: { since: number onLine: (line: string) => void onStatus: (s: 'open' | 'reconnecting' | 'closed') => void }): () => void { const controller = new AbortController() let stopped = false let since = opts.since const read = async () => { while (!stopped) { try { const res = await fetch('/hub/api/logs/stream?since=' + since, { headers: { Authorization: 'Bearer ' + getToken() }, signal: controller.signal, }) if (!res.ok || !res.body) throw new Error('HTTP ' + res.status) opts.onStatus('open') const reader = res.body.getReader() const decoder = new TextDecoder() let buf = '' while (!stopped) { const { done, value } = await reader.read() if (done) break buf += decoder.decode(value, { stream: true }) let i while ((i = buf.indexOf('\n\n')) !== -1) { const block = buf.slice(0, i) buf = buf.slice(i + 2) for (const ln of block.split('\n')) { if (ln.startsWith('data: ')) { try { const obj = JSON.parse(ln.slice(6)) if (obj && obj.line !== undefined) { opts.onLine(obj.line) if (typeof obj.offset === 'number') since = obj.offset } } catch { /* 忽略坏帧 */ } } } } } if (!stopped) opts.onStatus('reconnecting') } catch (e: any) { if (stopped || (e && e.name === 'AbortError')) return opts.onStatus('reconnecting') } if (!stopped) await new Promise(res => setTimeout(res, 1500)) } } read() return () => { stopped = true; controller.abort() } } export function hubDashboardStream(opts: { onData: (data: any) => void onStatus?: (s: 'open' | 'reconnecting' | 'closed') => void }): () => void { const controller = new AbortController() let stopped = false const read = async () => { while (!stopped) { try { const res = await fetch('/hub/api/dashboard/stream', { headers: { Authorization: 'Bearer ' + getToken() }, signal: controller.signal, }) if (!res.ok || !res.body) throw new Error('HTTP ' + res.status) opts.onStatus?.('open') const reader = res.body.getReader() const decoder = new TextDecoder() let buf = '' while (!stopped) { const { done, value } = await reader.read() if (done) break buf += decoder.decode(value, { stream: true }) let i while ((i = buf.indexOf('\n\n')) !== -1) { const block = buf.slice(0, i) buf = buf.slice(i + 2) for (const ln of block.split('\n')) { if (ln.startsWith('data: ')) { try { opts.onData(JSON.parse(ln.slice(6))) } catch { /* 忽略坏帧 */ } } } } } if (!stopped) opts.onStatus?.('reconnecting') } catch (e: any) { if (stopped || (e && e.name === 'AbortError')) return opts.onStatus?.('reconnecting') } if (!stopped) await new Promise(res => setTimeout(res, 2000)) } } read() return () => { stopped = true; controller.abort() } } export const api = (plugin: string) => ({ get: (path: string) => request(`/api/${plugin}/${path}`), post: (path: string, body: any) => request(`/api/${plugin}/${path}`, { method: 'POST', body }), put: (path: string, body: any) => request(`/api/${plugin}/${path}`, { method: 'PUT', body }), del: (path: string) => request(`/api/${plugin}/${path}`, { method: 'DELETE' }), })