Files
kefu/admin-web/apps/web-antd/src/api/console.ts
T
2026-08-27 14:04:28 +08:00

331 lines
9.5 KiB
TypeScript

/**
* 业务接口:模型清单、角色权限、用户、调用统计。
*
* 全部对 `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<string, number>;
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<ModelProvider>) {
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<ModelPlan>) {
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<CallStats>(`/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<ConfigEnvelope>('/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<ReleasePolicy>('/release');
}
export function saveRelease(data: {
download_url: string;
force_upgrade: boolean;
latest_version: string;
release_notes: string;
}) {
return requestClient.post<ReleasePolicy>('/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,
);
}