/** * 业务接口:模型清单、角色权限、用户、调用统计。 * * 全部对 `admin_api.py` 的 /api/v2/*。这一层只负责搬数据,权限判定在后端—— * 前端隐藏按钮只是体验,后端不拦就等于没有权限系统。 */ import { requestClient } from '#/api/request'; export interface ModelProvider { api_key?: string; api_key_masked?: string; base_url: string; capabilities: string; enabled: boolean; /** 后端算好的实际请求地址(只读) */ endpoint?: string; /** auto = 按接口类型补全路径;exact = 地址原样使用 */ endpoint_mode: string; health: string; id: string; kind: string; max_inflight: number; max_tokens: number; model: string; name: string; rpm_limit: number; temperature: number; timeout_ms: number; } export interface ModelPlan { answer_ids: string; fallback_ids: string; judge_id: string; judge_mode: string; version: number; vision_id: string; } export interface RoleItem { builtin: boolean; code: string; name: string; permissions: string[]; user_count: number; } export interface PermissionItem { code: string; group_name: string; name: string; } export interface UserItem { active: boolean; created_at: string; id: number; must_change_password: boolean; role: string; username: string; } export interface CallStats { avg_ms: number; avg_score: number; chosen: { count: number; provider: string }[]; judged: number; max_ms: number; risk: Record; score_buckets: { count: number; range: string }[]; since: string; total: number; } /** 单次调用的候选出口——`model_calls.candidates_json` 解出来的一项。 */ export interface ModelCallCandidate { error?: string; latency_ms?: number; provider: string; text: string; } /** 单条调用留痕:客户说了什么、模型答了什么、有没有被拦下来。 */ export interface ModelCallLogItem { candidates: ModelCallCandidate[]; chosen: string; created_at: string; customer_text: string; device_id: string; id: number; judge_mode: string; judge_risk: string; judge_score: number; judge_winner: string; /** chat = 回客户的话;guard = 界面识别之类的内部判断 */ purpose: string; reply_text: string; review_reason: string; roles_version: number; task_id: string; total_ms: number; } // ── 模型 ───────────────────────────────────────────────────────────────────── export function fetchModels() { return requestClient.get<{ judge_modes: string[]; kinds: string[]; models: ModelProvider[]; roles: ModelPlan; }>('/models'); } export function saveModel(data: Partial) { return requestClient.post<{ models: ModelProvider[] }>('/models', data); } export function deleteModel(id: string) { return requestClient.delete<{ models: ModelProvider[] }>( `/models/${encodeURIComponent(id)}`, ); } export function saveModelPlan(data: Partial) { return requestClient.post<{ roles: ModelPlan; version: number }>( '/models/plan', data, ); } // ── 角色与权限 ─────────────────────────────────────────────────────────────── export function fetchRoles() { return requestClient.get<{ roles: RoleItem[] }>('/roles'); } export function fetchPermissions() { return requestClient.get<{ permissions: PermissionItem[] }>('/permissions'); } export function saveRole(data: { code: string; name: string; permissions: string[]; }) { return requestClient.post<{ roles: RoleItem[] }>('/roles', data); } export function deleteRole(code: string) { return requestClient.delete<{ roles: RoleItem[] }>( `/roles/${encodeURIComponent(code)}`, ); } // ── 用户 ───────────────────────────────────────────────────────────────────── export function fetchUsers() { return requestClient.get<{ users: UserItem[] }>('/users'); } export function createUser(data: { password: string; role: string; username: string; }) { return requestClient.post<{ users: UserItem[] }>('/users', data); } export function updateUser( id: number, data: { active: boolean; role: string }, ) { return requestClient.put<{ users: UserItem[] }>(`/users/${id}`, data); } // ── 运营 ───────────────────────────────────────────────────────────────────── export function fetchCallStats(days = 7) { return requestClient.get(`/stats/model-calls?days=${days}`); } export function fetchCallLog(params: { days?: number; limit?: number; offset?: number; /** chat = 回客户的话(默认);guard = 界面识别;'' = 全都要 */ purpose?: string; q?: string; }) { const query = new URLSearchParams(); query.set('days', String(params.days ?? 7)); query.set('limit', String(params.limit ?? 50)); query.set('offset', String(params.offset ?? 0)); query.set('purpose', params.purpose ?? 'chat'); if (params.q) query.set('q', params.q); return requestClient.get<{ items: ModelCallLogItem[]; total: number }>( `/stats/model-calls/log?${query.toString()}`, ); } export function fetchAudit(limit = 200) { return requestClient.get<{ entries: { action: string; created_at: string; detail: string; id: number; ip_address: string; username: null | string; }[]; }>(`/audit?limit=${limit}`); } export function changeMyPassword(data: { current_password: string; new_password: string; }) { return requestClient.post('/me/password', data); } // ── 桌面端配置 ─────────────────────────────────────────────────────────────── /** * 下发给桌面客户端的配置。 * * 字段名是大写下划线,和老网页后台的表单完全一致——两边共用后端同一个校验函数, * 不会出现"这边填得进、那边填不进"。 * * 这里**没有模型连接参数**:服务类型、API 地址、密钥、模型名、温度、tokens、 * 超时全部搬到了「AI 模型 → 模型清单 / 角色编排」。桌面端的模型调用走网关, * 密钥不出后端。 */ export interface DesktopConfig { AI_AGENT_NAME: string; AI_CONTEXT_ENABLED: boolean; AI_CONTEXT_MAX_ROUNDS: number; AI_COUNTER_INSULT_ENABLED: boolean; AI_DEVELOPMENT_MODE: boolean; AI_ENABLED: boolean; AI_HOSPITAL_NAME: string; AI_MCP_ENABLED: boolean; AI_MCP_MAX_ROUNDS: number; AI_MCP_SERVERS: unknown[]; AI_UI_GUARD_ENABLED: boolean; AI_USE_VISION: boolean; /** 留空 = 按后台自己的地址自动推算 */ AI_GATEWAY_URL: string; AI_REVIEW_RULES: ReviewRule[]; } /** * 选择性审核规则:命中任意一条,这一条回复才会停下来等人工确认,其余照常 * 自动发送。`id` 可以留空——留空由后端根据 label 自动生成并去重。 */ export interface ReviewRule { id?: string; label: string; keywords: string[]; enabled: boolean; } export interface ConfigEnvelope { bool_keys: string[]; config: DesktopConfig; /** 留空时实际会下发给客户端的网关地址 */ gateway_url_effective: string; /** 模型设置搬去哪儿了,用来在页面上给人指路 */ model_settings_moved_to: string; retired_keys: string[]; updated_at: string; updated_by: string; version: number; } export function fetchConfig() { return requestClient.get('/config'); } export function saveConfig(data: DesktopConfig) { return requestClient.post<{ version: number }>('/config', data); } // ── 桌面端版本升级 ─────────────────────────────────────────────────────────── export interface ReleasePolicy { download_url: string; force_upgrade: boolean; latest_version: string; release_notes: string; updated_at: string; updated_by: string; } export function fetchRelease() { return requestClient.get('/release'); } export function saveRelease(data: { download_url: string; force_upgrade: boolean; latest_version: string; release_notes: string; }) { return requestClient.post('/release', data); } // ── 模型连通性测试 ─────────────────────────────────────────────────────────── export interface ModelTestResult { endpoint: string; http_status: null | number; latency_ms: number; message: string; model: string; ok: boolean; provider: string; } /** * 给了 provider_id 就测清单里那一条,密钥由后端从库里解密取用—— * 前端从头到尾拿不到明文,也就不可能在网络上多走一趟。 */ export function testModel(data: { api_key?: string; base_url?: string; endpoint_mode?: string; kind?: string; model?: string; provider_id?: string; timeout_seconds?: number; }) { return requestClient.post<{ label: string; result: ModelTestResult }>( '/models/test', data, ); }