2026-09-01 13:13:40 +08:00
|
|
|
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<string,string> }
|
|
|
|
|
|
|
|
|
|
async function request(url: string, opts: ReqOpts = {}) {
|
|
|
|
|
const headers: Record<string,string> = { '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<string, any>, 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 } })
|
2026-09-03 00:44:38 +08:00
|
|
|
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<string, any>) =>
|
|
|
|
|
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() }
|
|
|
|
|
}
|
2026-09-01 13:13:40 +08:00
|
|
|
|
|
|
|
|
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' }),
|
|
|
|
|
})
|