gengx
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* 业务接口:模型清单、角色权限、用户、调用统计。
|
||||
*
|
||||
* 全部对 `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,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user