mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-05-06 06:00:44 +08:00
Problem:
Upstream channels can reject monitor probes based on client fingerprint
(e.g. "only Claude Code clients allowed"). The monitor had no way to
customize the outgoing request to bypass such restrictions.
Solution:
Introduce reusable request templates that carry extra_headers plus an
optional body override; monitors reference a template and receive a
snapshot copy on apply. Template edits do NOT auto-propagate — users
must click "apply to associated monitors" to refresh snapshots, so a
bad template edit cannot instantly break all production monitors.
Data model (migration 112):
- channel_monitor_request_templates: id, name, provider, description,
extra_headers jsonb, body_override_mode ('off'|'merge'|'replace'),
body_override jsonb. Unique (provider, name).
- channel_monitors: +template_id (FK, ON DELETE SET NULL), +extra_headers,
+body_override_mode, +body_override (the three runtime snapshot fields).
Checker (channel_monitor_checker.go):
- callProvider + runCheckForModel accept a CheckOptions carrying the
snapshot fields. mergeHeaders applies user headers on top of adapter
defaults (forbidden list: Host / Content-Length / Transfer-Encoding /
Connection / Content-Encoding).
- buildRequestBody:
off -> adapter default body
merge -> shallow-merge over default; per-provider deny list
(model/messages/contents) protects the challenge contract
replace -> user body verbatim
- Replace mode skips challenge validation; instead HTTP 2xx + non-empty
extracted response text = operational, empty = failed.
- 4 new unit tests cover all three modes + replace/empty-response case.
Admin API:
- /admin/channel-monitor-templates CRUD + /:id/apply (overwrite snapshot
on all template_id=id monitors, returns affected count).
- channel_monitor request/response DTOs gain the 4 new fields.
Frontend:
- channelMonitorTemplate.ts API client.
- MonitorAdvancedRequestConfig.vue shared component for headers textarea
+ body mode radio + body JSON editor; used by both template and monitor
forms.
- MonitorTemplateManagerDialog.vue: provider tabs, list/create/edit/
delete/apply, live "associated monitors" count per row.
- MonitorFiltersBar: new 模板管理 button next to 新增监控.
- MonitorFormDialog: collapsible 高级 section with template dropdown
(filtered by form.provider, clears on provider change) + embedded
AdvancedRequestConfig. Picking a template copies its fields into the
form (snapshot semantics mirrored on the client).
- i18n zh/en entries for all new copy.
chore: bump version to 0.1.114.32
203 lines
4.9 KiB
TypeScript
203 lines
4.9 KiB
TypeScript
/**
|
||
* Admin Channel Monitor API endpoints
|
||
* Handles channel monitor (uptime/health) management for administrators
|
||
*/
|
||
|
||
import { apiClient } from '../client'
|
||
|
||
export type Provider = 'openai' | 'anthropic' | 'gemini'
|
||
export type MonitorStatus = 'operational' | 'degraded' | 'failed' | 'error'
|
||
export type BodyOverrideMode = 'off' | 'merge' | 'replace'
|
||
|
||
export interface ChannelMonitor {
|
||
id: number
|
||
name: string
|
||
provider: Provider
|
||
endpoint: string
|
||
api_key_masked: string
|
||
/**
|
||
* True when the stored encrypted API key cannot be decrypted (e.g. the
|
||
* encryption key has changed). Admin must re-edit the monitor to provide
|
||
* a fresh key. Backend skips checks for these monitors.
|
||
*/
|
||
api_key_decrypt_failed?: boolean
|
||
primary_model: string
|
||
extra_models: string[]
|
||
group_name: string
|
||
enabled: boolean
|
||
interval_seconds: number
|
||
last_checked_at: string | null
|
||
created_by: number
|
||
created_at: string
|
||
updated_at: string
|
||
/** Latest status of the primary model (empty when no history yet) */
|
||
primary_status: MonitorStatus | ''
|
||
/** Latest latency of the primary model in ms (null when no history yet) */
|
||
primary_latency_ms: number | null
|
||
/** Primary model 7-day availability percentage (0-100) */
|
||
availability_7d: number
|
||
/** Latest status per extra model (used for hover tooltip) */
|
||
extra_models_status: ExtraModelStatus[]
|
||
/** 请求自定义快照字段(高级设置) */
|
||
template_id: number | null
|
||
extra_headers: Record<string, string>
|
||
body_override_mode: BodyOverrideMode
|
||
body_override: Record<string, unknown> | null
|
||
}
|
||
|
||
export interface ExtraModelStatus {
|
||
model: string
|
||
status: MonitorStatus | ''
|
||
latency_ms: number | null
|
||
}
|
||
|
||
export interface ListParams {
|
||
page?: number
|
||
page_size?: number
|
||
provider?: Provider
|
||
enabled?: boolean
|
||
search?: string
|
||
}
|
||
|
||
export interface ListResponse {
|
||
items: ChannelMonitor[]
|
||
total: number
|
||
page: number
|
||
page_size: number
|
||
pages: number
|
||
}
|
||
|
||
export interface CreateParams {
|
||
name: string
|
||
provider: Provider
|
||
endpoint: string
|
||
api_key: string
|
||
primary_model: string
|
||
extra_models?: string[]
|
||
group_name?: string
|
||
enabled?: boolean
|
||
interval_seconds: number
|
||
template_id?: number | null
|
||
extra_headers?: Record<string, string>
|
||
body_override_mode?: BodyOverrideMode
|
||
body_override?: Record<string, unknown> | null
|
||
}
|
||
|
||
// Update request: api_key 空串 = 不修改;clear_template=true 时把 template_id 置空
|
||
export type UpdateParams = Partial<CreateParams> & {
|
||
clear_template?: boolean
|
||
}
|
||
|
||
export interface CheckResult {
|
||
model: string
|
||
status: MonitorStatus
|
||
latency_ms: number | null
|
||
ping_latency_ms: number | null
|
||
message: string
|
||
checked_at: string
|
||
}
|
||
|
||
export interface RunNowResponse {
|
||
results: CheckResult[]
|
||
}
|
||
|
||
export interface HistoryItem {
|
||
id: number
|
||
model: string
|
||
status: MonitorStatus
|
||
latency_ms: number | null
|
||
ping_latency_ms: number | null
|
||
message: string
|
||
checked_at: string
|
||
}
|
||
|
||
export interface HistoryParams {
|
||
model?: string
|
||
limit?: number
|
||
}
|
||
|
||
export interface HistoryResponse {
|
||
items: HistoryItem[]
|
||
}
|
||
|
||
/**
|
||
* List channel monitors with pagination and filters
|
||
*/
|
||
export async function list(
|
||
params: ListParams = {},
|
||
options?: { signal?: AbortSignal }
|
||
): Promise<ListResponse> {
|
||
const { data } = await apiClient.get<ListResponse>('/admin/channel-monitors', {
|
||
params,
|
||
signal: options?.signal,
|
||
})
|
||
return data
|
||
}
|
||
|
||
/**
|
||
* Get a channel monitor by ID
|
||
*/
|
||
export async function get(id: number): Promise<ChannelMonitor> {
|
||
const { data } = await apiClient.get<ChannelMonitor>(`/admin/channel-monitors/${id}`)
|
||
return data
|
||
}
|
||
|
||
/**
|
||
* Create a new channel monitor
|
||
*/
|
||
export async function create(params: CreateParams): Promise<ChannelMonitor> {
|
||
const { data } = await apiClient.post<ChannelMonitor>('/admin/channel-monitors', params)
|
||
return data
|
||
}
|
||
|
||
/**
|
||
* Update an existing channel monitor.
|
||
* api_key field: empty string means "do not modify".
|
||
*/
|
||
export async function update(id: number, params: UpdateParams): Promise<ChannelMonitor> {
|
||
const { data } = await apiClient.put<ChannelMonitor>(`/admin/channel-monitors/${id}`, params)
|
||
return data
|
||
}
|
||
|
||
/**
|
||
* Delete a channel monitor
|
||
*/
|
||
export async function del(id: number): Promise<void> {
|
||
await apiClient.delete(`/admin/channel-monitors/${id}`)
|
||
}
|
||
|
||
/**
|
||
* Trigger an immediate manual check for a channel monitor.
|
||
* Returns the latest check results for primary + extra models.
|
||
*/
|
||
export async function runNow(id: number): Promise<RunNowResponse> {
|
||
const { data } = await apiClient.post<RunNowResponse>(`/admin/channel-monitors/${id}/run`)
|
||
return data
|
||
}
|
||
|
||
/**
|
||
* List historical check results for a monitor.
|
||
*/
|
||
export async function listHistory(
|
||
id: number,
|
||
params: HistoryParams = {}
|
||
): Promise<HistoryResponse> {
|
||
const { data } = await apiClient.get<HistoryResponse>(
|
||
`/admin/channel-monitors/${id}/history`,
|
||
{ params }
|
||
)
|
||
return data
|
||
}
|
||
|
||
export const channelMonitorAPI = {
|
||
list,
|
||
get,
|
||
create,
|
||
update,
|
||
del,
|
||
runNow,
|
||
listHistory,
|
||
}
|
||
|
||
export default channelMonitorAPI
|