- 新增 streamSelectiveUpdate 函数支持选择性服务更新 - 添加 UpdateCheckResult 接口定义版本更新检查结果 - 实现 checkForUpdates 函数获取版本更新信息 - 在安全中心视图添加版本信息显示和更新检查按钮 - 添加选择性更新对话框支持按服务选择更新 - 实现版本管理视图中的实时更新通知配置 - 添加从 IM 导入灰度成员功能 - 移除应用密钥管理相关的重复组件实现
172 行
5.5 KiB
TypeScript
172 行
5.5 KiB
TypeScript
const BASE = import.meta.env.VITE_API_BASE_URL ?? '/api'
|
|
|
|
export interface DeploymentStatus {
|
|
mode: 'PUBLIC' | 'PRIVATE'
|
|
tenantRegisterEnabled: boolean
|
|
services: Record<string, { enabled: boolean; baseUrl: string | null }>
|
|
}
|
|
|
|
export async function getDeploymentStatus(): Promise<DeploymentStatus> {
|
|
const res = await fetch(`${BASE}/private/deployment/status`)
|
|
const json = await res.json()
|
|
return json.data as DeploymentStatus
|
|
}
|
|
|
|
async function streamOperation(
|
|
path: string,
|
|
onLine: (line: string) => void,
|
|
signal?: AbortSignal,
|
|
): Promise<void> {
|
|
const token = localStorage.getItem('token') ?? ''
|
|
const res = await fetch(`${BASE}${path}`, {
|
|
method: 'POST',
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
signal,
|
|
})
|
|
if (!res.ok) {
|
|
const text = await res.text()
|
|
throw new Error(text || `HTTP ${res.status}`)
|
|
}
|
|
const reader = res.body!.getReader()
|
|
const decoder = new TextDecoder()
|
|
let buf = ''
|
|
try {
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
buf += decoder.decode(value, { stream: true })
|
|
const lines = buf.split('\n')
|
|
buf = lines.pop() ?? ''
|
|
for (const line of lines) {
|
|
onLine(line)
|
|
}
|
|
}
|
|
if (buf) onLine(buf)
|
|
} catch (e: any) {
|
|
// Connection drop during RESTART_SELF is expected - don't throw
|
|
if (e?.name === 'TypeError' || e?.message?.includes('network') || e?.message?.includes('fetch')) {
|
|
onLine('>>> 连接已中断(服务正在重启)')
|
|
return
|
|
}
|
|
throw e
|
|
}
|
|
}
|
|
|
|
export async function getRunningServices(): Promise<string[]> {
|
|
const token = localStorage.getItem('token') ?? ''
|
|
const res = await fetch(`${BASE}/system/services`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
})
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
const json = await res.json()
|
|
return json.data as string[]
|
|
}
|
|
|
|
export async function fetchServiceLogs(service: string, lines: number): Promise<string> {
|
|
const token = localStorage.getItem('token') ?? ''
|
|
const res = await fetch(
|
|
`${BASE}/system/logs/${encodeURIComponent(service)}?lines=${lines}`,
|
|
{ headers: { Authorization: `Bearer ${token}` } },
|
|
)
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
return res.text()
|
|
}
|
|
|
|
export async function getSystemVersion(): Promise<{ currentVersion: string }> {
|
|
const token = localStorage.getItem('token') ?? ''
|
|
const res = await fetch(`${BASE}/system/version`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
})
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
const json = await res.json()
|
|
return json.data as { currentVersion: string }
|
|
}
|
|
|
|
export function streamSystemUpdate(onLine: (line: string) => void, signal?: AbortSignal) {
|
|
return streamOperation('/system/update', onLine, signal)
|
|
}
|
|
|
|
export function streamSelectiveUpdate(services: string[], onLine: (line: string) => void, signal?: AbortSignal) {
|
|
const qs = services.map(s => `services=${encodeURIComponent(s)}`).join('&')
|
|
return streamOperation(`/system/update-selective${qs ? '?' + qs : ''}`, onLine, signal)
|
|
}
|
|
|
|
export function streamSystemReset(onLine: (line: string) => void, signal?: AbortSignal) {
|
|
return streamOperation('/system/reset', onLine, signal)
|
|
}
|
|
|
|
export interface UpdateCheckResult {
|
|
currentVersion: string
|
|
latestVersion: string
|
|
hasUpdate: boolean
|
|
releasedAt: string
|
|
changelog: string
|
|
services: Record<string, { current: string; latest: string; changed: boolean }>
|
|
}
|
|
|
|
export async function checkForUpdates(): Promise<UpdateCheckResult> {
|
|
const json = await authFetch('/system/check-update')
|
|
return json as UpdateCheckResult
|
|
}
|
|
|
|
// ── Database Management (PRIVATE mode only) ──────────────────────────────
|
|
|
|
export interface TableInfo {
|
|
name: string
|
|
comment: string
|
|
}
|
|
|
|
export interface ColumnInfo {
|
|
name: string
|
|
type: string
|
|
size: number
|
|
nullable: boolean
|
|
comment: string
|
|
}
|
|
|
|
export interface TableDataPage {
|
|
columns: string[]
|
|
rows: Record<string, any>[]
|
|
total: number
|
|
totalPages: number
|
|
page: number
|
|
size: number
|
|
}
|
|
|
|
async function authFetch(path: string): Promise<any> {
|
|
const token = localStorage.getItem('token') ?? ''
|
|
const res = await fetch(`${BASE}${path}`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
})
|
|
if (!res.ok) {
|
|
const json = await res.json().catch(() => null)
|
|
throw new Error(json?.message ?? `HTTP ${res.status}`)
|
|
}
|
|
return res.json()
|
|
}
|
|
|
|
export async function listDatabaseTables(): Promise<TableInfo[]> {
|
|
const json = await authFetch('/system/database/tables')
|
|
return json.data as TableInfo[]
|
|
}
|
|
|
|
export async function getTableColumns(tableName: string): Promise<ColumnInfo[]> {
|
|
const json = await authFetch(`/system/database/tables/${encodeURIComponent(tableName)}/columns`)
|
|
return json.data as ColumnInfo[]
|
|
}
|
|
|
|
export async function getTableData(
|
|
tableName: string,
|
|
params: { page?: number; size?: number; keyword?: string; sortColumn?: string; sortDirection?: string },
|
|
): Promise<TableDataPage> {
|
|
const query = new URLSearchParams()
|
|
if (params.page != null) query.set('page', String(params.page))
|
|
if (params.size != null) query.set('size', String(params.size))
|
|
if (params.keyword) query.set('keyword', params.keyword)
|
|
if (params.sortColumn) query.set('sortColumn', params.sortColumn)
|
|
if (params.sortDirection) query.set('sortDirection', params.sortDirection)
|
|
const qs = query.toString()
|
|
const json = await authFetch(`/system/database/tables/${encodeURIComponent(tableName)}/data${qs ? '?' + qs : ''}`)
|
|
return json.data as TableDataPage
|
|
}
|