331 lines
9.5 KiB
TypeScript
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,
|
|
);
|
|
}
|