Files
HeXi/hexi/web/src/api/client.ts
T
sansenhoshiandClaude 9371a28e35 refactor: restructure plugins per MTSS standard, consolidate assets to res/
Per docs/plugin-audit-report.md (plugins normalized to
Trigger(handlers) → Service(services) → Model(repository/models) + utils):

- Split monolithic __init__.py into handlers/services/utils across
  dailywife, deer_pipe, dice, galgame_card, helldivers_tools,
  huoziyinshua, learning_chat, makeaquote, mc_server_status,
  ncm_saying, picfinder_take, picstatus, random_jm_code, regif,
  steam_info, video_analysis, group_tools
- Move static assets under res/: deer_pipe font/img, makeaquote font,
  helldivers img/templates, huoziyinshua HuoZiYinShua
- Add config.py + register_config_items to ncm_saying, random_jm_code,
  group_tools; learning_chat unified config bridge
- Remove deprecated: voice_trans plugin, bf_bot/test.py, dead code in
  dailywife/deer_pipe, empty dirs, debug scripts under helldivers temp
- Disable brash_general_supercredits_tools (stub comment only)
- bot.py: optional stdout/stderr redirect to log file for Web log viewer,
  force ANSI colorize on non-TTY sinks
- Move runtime data (jm_code.json) out of plugin dir into hexi/data
- Docs: plugin-audit-report.md; README reflects removed plugins

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-03 00:44:38 +08:00

175 lines
7.4 KiB
TypeScript

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 } })
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() }
}
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' }),
})