|
|
|
@@ -0,0 +1,383 @@
|
|
|
|
|
/** BossAi 2.0 API 客户端
|
|
|
|
|
* 在 v1.0 基础上新增:案卡 CRUD、老板台聚合、我的团队、快捷指令、风险推送
|
|
|
|
|
* 共享同一后端,新增端点向后兼容
|
|
|
|
|
*/
|
|
|
|
|
import type {
|
|
|
|
|
AskResponse, EmotionMode, Expert, ExpertService, Consultation,
|
|
|
|
|
FileInspectResponse, HistoryMessage, HistorySession, KnowledgeDoc,
|
|
|
|
|
Scene, SessionMode, SessionResponse, Settings,
|
|
|
|
|
Matter, MatterBoard, DashboardToday, RiskAlert,
|
|
|
|
|
ExpertRelation, QuickCommands,
|
|
|
|
|
} from '@/types'
|
|
|
|
|
|
|
|
|
|
const BASE = '/api'
|
|
|
|
|
const DEFAULT_TIMEOUT = 30000
|
|
|
|
|
const LONG_TIMEOUT = 60000
|
|
|
|
|
|
|
|
|
|
export const network = {
|
|
|
|
|
isOnline: () => typeof navigator !== 'undefined' ? navigator.onLine : true,
|
|
|
|
|
onChange: (cb: (online: boolean) => void) => {
|
|
|
|
|
if (typeof window === 'undefined') return () => {}
|
|
|
|
|
const handler = () => cb(navigator.onLine)
|
|
|
|
|
window.addEventListener('online', handler)
|
|
|
|
|
window.addEventListener('offline', handler)
|
|
|
|
|
return () => {
|
|
|
|
|
window.removeEventListener('online', handler)
|
|
|
|
|
window.removeEventListener('offline', handler)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function authHeaders(): Record<string, string> {
|
|
|
|
|
const token = typeof window !== 'undefined' ? localStorage.getItem('bossai_token') : null
|
|
|
|
|
return token ? { Authorization: `Bearer ${token}` } : {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function fetchWithTimeout(url: string, init: RequestInit, timeout: number): Promise<Response> {
|
|
|
|
|
const controller = new AbortController()
|
|
|
|
|
const timer = setTimeout(() => controller.abort(), timeout)
|
|
|
|
|
try {
|
|
|
|
|
return await fetch(url, { ...init, signal: controller.signal })
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err instanceof DOMException && err.name === 'AbortError') {
|
|
|
|
|
throw new Error('请求超时,请检查网络后重试')
|
|
|
|
|
}
|
|
|
|
|
if (!navigator.onLine) throw new Error('网络已断开,请检查网络连接')
|
|
|
|
|
throw err
|
|
|
|
|
} finally {
|
|
|
|
|
clearTimeout(timer)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function requestWithRetry<T>(
|
|
|
|
|
path: string, init?: RequestInit, options?: { timeout?: number; retries?: number },
|
|
|
|
|
): Promise<T> {
|
|
|
|
|
const timeout = options?.timeout ?? DEFAULT_TIMEOUT
|
|
|
|
|
const maxRetries = options?.retries ?? 1
|
|
|
|
|
let lastError: Error
|
|
|
|
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
|
|
|
if (!navigator.onLine) throw new Error('网络已断开,请检查网络连接')
|
|
|
|
|
try {
|
|
|
|
|
const isFormData = init?.body instanceof FormData
|
|
|
|
|
const res = await fetchWithTimeout(BASE + path, {
|
|
|
|
|
...init,
|
|
|
|
|
headers: {
|
|
|
|
|
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
|
|
|
|
|
...authHeaders(),
|
|
|
|
|
...(init?.headers || {}),
|
|
|
|
|
},
|
|
|
|
|
}, timeout)
|
|
|
|
|
if (res.status === 429) {
|
|
|
|
|
const retryAfter = res.headers.get('Retry-After')
|
|
|
|
|
const wait = retryAfter ? parseInt(retryAfter, 10) : 2
|
|
|
|
|
throw new Error(`请求过于频繁,请 ${wait} 秒后重试`)
|
|
|
|
|
}
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const body = await res.json().catch(() => ({ detail: res.statusText }))
|
|
|
|
|
let msg: string
|
|
|
|
|
if (typeof body.detail === 'string') msg = body.detail
|
|
|
|
|
else if (Array.isArray(body.detail) && body.detail.length > 0)
|
|
|
|
|
msg = body.detail.map((e: { msg?: string }) => e.msg || JSON.stringify(e)).join('; ')
|
|
|
|
|
else msg = `请求失败 ${res.status}`
|
|
|
|
|
throw new Error(msg)
|
|
|
|
|
}
|
|
|
|
|
return res.json() as Promise<T>
|
|
|
|
|
} catch (err) {
|
|
|
|
|
lastError = err as Error
|
|
|
|
|
const isRetryable = lastError.message.includes('超时') ||
|
|
|
|
|
lastError.message.includes('网络') || lastError.message.includes('Failed to fetch')
|
|
|
|
|
if (!isRetryable || attempt === maxRetries) throw lastError
|
|
|
|
|
await new Promise(r => setTimeout(r, 1000 * (attempt + 1)))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
throw lastError!
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
|
|
|
return requestWithRetry<T>(path, init)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const api = {
|
|
|
|
|
// ===== 基础 =====
|
|
|
|
|
health: () => request<{ status: string; service: string }>('/health'),
|
|
|
|
|
|
|
|
|
|
// ===== 认证 =====
|
|
|
|
|
register: (phone: string, password: string, name: string = '') =>
|
|
|
|
|
request<{ phone: string; name: string; token: string }>('/auth/register', {
|
|
|
|
|
method: 'POST', body: JSON.stringify({ phone, password, name }),
|
|
|
|
|
}),
|
|
|
|
|
login: (phone: string, password: string) =>
|
|
|
|
|
request<{ phone: string; name: string; token: string; role?: string; expert_id?: number; admin_id?: number }>('/auth/login', {
|
|
|
|
|
method: 'POST', body: JSON.stringify({ phone, password }),
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
// ===== 会话与提问(与 v1.0 兼容) =====
|
|
|
|
|
createSession: (mode: SessionMode) =>
|
|
|
|
|
request<SessionResponse>('/sessions', { method: 'POST', body: JSON.stringify({ mode }) }),
|
|
|
|
|
|
|
|
|
|
/** 检查内存 session 是否存活(后端重启后内存 session 会丢失) */
|
|
|
|
|
sessionAlive: (sid: string) =>
|
|
|
|
|
request<{ alive: boolean }>(`/sessions/${sid}/alive`),
|
|
|
|
|
|
|
|
|
|
ask: (sid: string, question: string, scene: Scene = 'legal', emotionMode?: EmotionMode) =>
|
|
|
|
|
requestWithRetry<AskResponse>(`/sessions/${sid}/ask`, {
|
|
|
|
|
method: 'POST', body: JSON.stringify({ question, scene, emotion_mode: emotionMode }),
|
|
|
|
|
}, { timeout: LONG_TIMEOUT, retries: 0 }),
|
|
|
|
|
|
|
|
|
|
askAnalyze: (
|
|
|
|
|
sid: string, question: string, scene: Scene = 'legal', emotionMode?: EmotionMode,
|
|
|
|
|
factAnswers?: Array<{ dim_key: string; dim_desc: string; answer: string }>, topic?: string,
|
|
|
|
|
) =>
|
|
|
|
|
request<{
|
|
|
|
|
need_ask: boolean; need_analysis: boolean; round: number;
|
|
|
|
|
questions?: Array<{ dim_key: string; dim_desc: string; question: string }>;
|
|
|
|
|
fact_summary?: string; question_type: string; risk_level: string;
|
|
|
|
|
profile_facts?: Array<{ dim_key: string; dim_desc: string; answer: string }>;
|
|
|
|
|
element_template?: Record<string, unknown>;
|
|
|
|
|
}>(`/sessions/${sid}/ask-analyze`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({ question, scene, emotion_mode: emotionMode, fact_answers: factAnswers, topic }),
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
askStream: async (
|
|
|
|
|
sid: string, question: string, scene: Scene = 'legal', emotionMode?: EmotionMode,
|
|
|
|
|
callbacks?: {
|
|
|
|
|
onRisk?: (risk: AskResponse['risk']) => void
|
|
|
|
|
onDelta?: (content: string) => void
|
|
|
|
|
onDone?: (data: Partial<AskResponse>) => void
|
|
|
|
|
onError?: (error: string) => void
|
|
|
|
|
},
|
|
|
|
|
factAnswers?: Array<{ dim_key: string; dim_desc: string; answer: string }>,
|
|
|
|
|
) => {
|
|
|
|
|
if (!navigator.onLine) { callbacks?.onError?.('网络已断开,请检查网络连接'); return }
|
|
|
|
|
const controller = new AbortController()
|
|
|
|
|
const timer = setTimeout(() => controller.abort(), LONG_TIMEOUT)
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${BASE}/sessions/${sid}/ask-stream`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
|
|
|
|
body: JSON.stringify({ question, scene, emotion_mode: emotionMode, fact_answers: factAnswers }),
|
|
|
|
|
signal: controller.signal,
|
|
|
|
|
})
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const detail = await res.json().catch(() => ({ detail: res.statusText }))
|
|
|
|
|
callbacks?.onError?.(detail.detail || `请求失败 ${res.status}`); return
|
|
|
|
|
}
|
|
|
|
|
const reader = res.body?.getReader()
|
|
|
|
|
if (!reader) { callbacks?.onError?.('浏览器不支持流式读取'); return }
|
|
|
|
|
const decoder = new TextDecoder()
|
|
|
|
|
let buffer = ''
|
|
|
|
|
while (true) {
|
|
|
|
|
const { done, value } = await reader.read()
|
|
|
|
|
if (done) break
|
|
|
|
|
buffer += decoder.decode(value, { stream: true })
|
|
|
|
|
const parts = buffer.split('\n\n')
|
|
|
|
|
buffer = parts.pop() || ''
|
|
|
|
|
for (const part of parts) {
|
|
|
|
|
if (!part.trim()) continue
|
|
|
|
|
let eventType = 'message'; let dataLine = ''
|
|
|
|
|
for (const line of part.split('\n')) {
|
|
|
|
|
if (line.startsWith('event: ')) eventType = line.slice(7)
|
|
|
|
|
else if (line.startsWith('data: ')) dataLine = line.slice(6)
|
|
|
|
|
}
|
|
|
|
|
if (!dataLine) continue
|
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(dataLine)
|
|
|
|
|
if (eventType === 'risk') callbacks?.onRisk?.(data)
|
|
|
|
|
else if (eventType === 'delta') callbacks?.onDelta?.(data.content || '')
|
|
|
|
|
else if (eventType === 'done') callbacks?.onDone?.(data)
|
|
|
|
|
else if (eventType === 'error') callbacks?.onError?.(data.detail || '未知错误')
|
|
|
|
|
} catch { /* 忽略解析失败 */ }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err instanceof DOMException && err.name === 'AbortError') callbacks?.onError?.('请求超时,请检查网络后重试')
|
|
|
|
|
else callbacks?.onError?.(err instanceof Error ? err.message : '流式请求失败')
|
|
|
|
|
} finally { clearTimeout(timer) }
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
destroy: (sid: string) =>
|
|
|
|
|
request<{ destroyed: boolean; session_id: string }>(`/sessions/${sid}/destroy`, { method: 'POST' }),
|
|
|
|
|
|
|
|
|
|
uploadFile: async (sid: string, file: File) => {
|
|
|
|
|
const form = new FormData(); form.append('file', file)
|
|
|
|
|
const res = await fetch(`${BASE}/sessions/${sid}/file`, { method: 'POST', body: form })
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const detail = await res.json().catch(() => ({ detail: res.statusText }))
|
|
|
|
|
throw new Error(detail.detail || `上传失败 ${res.status}`)
|
|
|
|
|
}
|
|
|
|
|
return res.json() as Promise<FileInspectResponse>
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// ===== 画像 =====
|
|
|
|
|
getProfile: () => requestWithRetry<Record<string, unknown>>('/profile/me', {}),
|
|
|
|
|
updateProfile: (data: {
|
|
|
|
|
industry?: string; company_size?: string; role?: string;
|
|
|
|
|
risk_tolerance?: string; decision_style?: string; key_facts?: Record<string, string>;
|
|
|
|
|
}) => request<{ updated: boolean }>('/profile/me', { method: 'PUT', body: JSON.stringify(data) }),
|
|
|
|
|
getProfileHistory: () =>
|
|
|
|
|
request<{ results: Array<{ id: number; version: number; profile: Record<string, unknown>; source: string; created_at: string }> }>('/profile/me/history'),
|
|
|
|
|
|
|
|
|
|
// ===== 要素化立案 =====
|
|
|
|
|
elementFiling: (sid: string, question: string, causeOfAction: string,
|
|
|
|
|
factAnswers: Array<{ dim_key: string; dim_desc: string; answer: string }>, template?: Record<string, unknown>,
|
|
|
|
|
) => request<{
|
|
|
|
|
cause_of_action: string;
|
|
|
|
|
elements: Array<{ key: string; label: string; value: string }>;
|
|
|
|
|
claims: string[]; filing_text: string;
|
|
|
|
|
}>(`/sessions/${sid}/element-filing`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({ question, cause_of_action: causeOfAction, fact_answers: factAnswers, template }),
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
// ===== 话题与示例 =====
|
|
|
|
|
getTopics: (scene: Scene = 'legal', existing?: string[]) =>
|
|
|
|
|
request<{ topics: Array<{ title: string; desc: string; samples: string[] }> }>('/topics', {
|
|
|
|
|
method: 'POST', body: JSON.stringify({ scene, existing }),
|
|
|
|
|
}),
|
|
|
|
|
generateSamples: (scene: Scene = 'legal', topic?: string, existing?: string[]) =>
|
|
|
|
|
request<{ samples: string[] }>('/samples', {
|
|
|
|
|
method: 'POST', body: JSON.stringify({ scene, topic, existing }),
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
// ===== 知识库 =====
|
|
|
|
|
addKnowledge: (data: { title: string; content: string; source?: string; doc_type?: string; region?: string | null; effective_date?: string | null; keywords?: string | null }) =>
|
|
|
|
|
request<KnowledgeDoc>('/knowledge', { method: 'POST', body: JSON.stringify(data) }),
|
|
|
|
|
listKnowledge: (docType?: string) =>
|
|
|
|
|
request<{ results: KnowledgeDoc[]; counts: Record<string, number>; total: number }>(`/knowledge${docType ? `?doc_type=${docType}` : ''}`),
|
|
|
|
|
searchKnowledge: (q: string, docType?: string) =>
|
|
|
|
|
request<{ results: KnowledgeDoc[] }>(`/knowledge/search?q=${encodeURIComponent(q)}${docType ? `&doc_type=${docType}` : ''}`),
|
|
|
|
|
deleteKnowledge: (id: number) => request<{ deleted: boolean; id: number }>(`/knowledge/${id}`, { method: 'DELETE' }),
|
|
|
|
|
vectorSearchKnowledge: (q: string, limit?: number) =>
|
|
|
|
|
request<{ results: KnowledgeDoc[] }>(`/knowledge/vector-search?q=${encodeURIComponent(q)}${limit ? `&limit=${limit}` : ''}`),
|
|
|
|
|
|
|
|
|
|
// ===== 设置 =====
|
|
|
|
|
getSettings: () => request<{ settings: Settings }>('/settings'),
|
|
|
|
|
updateSettings: (settings: Settings) =>
|
|
|
|
|
request<{ updated: boolean; settings: Settings }>('/settings', { method: 'PUT', body: JSON.stringify({ settings }) }),
|
|
|
|
|
|
|
|
|
|
// ===== 专家库(v1.0 兼容) =====
|
|
|
|
|
listExperts: (category?: string) =>
|
|
|
|
|
request<{ results: Expert[] }>(`/experts${category ? `?category=${category}` : ''}`),
|
|
|
|
|
getExpert: (id: number) => request<Expert>(`/experts/${id}`),
|
|
|
|
|
selectExpert: (expertId: number) =>
|
|
|
|
|
request<{ service_id: number; status: string }>(`/experts/${expertId}/select`, { method: 'POST' }),
|
|
|
|
|
myServices: () => request<{ results: ExpertService[] }>('/my/services'),
|
|
|
|
|
myConsultations: (status?: string) =>
|
|
|
|
|
request<{ results: Consultation[] }>(`/my/consultations${status ? `?status=${status}` : ''}`),
|
|
|
|
|
createConsultation: (data: { expert_id: number; topic: string; context?: string; question?: string }) =>
|
|
|
|
|
request<{ consultation_id: number; created_at: string; status: string }>('/consultations', {
|
|
|
|
|
method: 'POST', body: JSON.stringify(data),
|
|
|
|
|
}),
|
|
|
|
|
rateConsultation: (consultationId: number, rating: number, comment: string = '') =>
|
|
|
|
|
request<{ rating_id: number; created_at: string }>(`/consultations/${consultationId}/rate`, {
|
|
|
|
|
method: 'POST', body: JSON.stringify({ consultation_id: consultationId, rating, comment }),
|
|
|
|
|
}),
|
|
|
|
|
cancelMyService: (serviceId: number) =>
|
|
|
|
|
request<{ cancelled: boolean }>(`/my/services/${serviceId}/cancel`, { method: 'POST' }),
|
|
|
|
|
|
|
|
|
|
// ===== 专家工作台 =====
|
|
|
|
|
expertWorkbench: () =>
|
|
|
|
|
request<{ expert: Expert; services: ExpertService[]; consultations: Consultation[] }>('/expert/workbench'),
|
|
|
|
|
acceptService: (serviceId: number) =>
|
|
|
|
|
request<{ accepted: boolean }>(`/expert/services/${serviceId}/accept`, { method: 'POST' }),
|
|
|
|
|
cancelExpertService: (serviceId: number) =>
|
|
|
|
|
request<{ cancelled: boolean }>(`/expert/services/${serviceId}/cancel`, { method: 'POST' }),
|
|
|
|
|
replyConsultation: (consultationId: number, reply: string) =>
|
|
|
|
|
request<{ replied: boolean }>(`/expert/consultations/${consultationId}/reply`, {
|
|
|
|
|
method: 'POST', body: JSON.stringify({ reply }),
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
// ===== 管理后台 =====
|
|
|
|
|
adminListExperts: (status?: string) =>
|
|
|
|
|
request<{ results: Expert[] }>(`/admin/experts${status ? `?status=${status}` : ''}`),
|
|
|
|
|
adminUpdateExpertStatus: (expertId: number, status: 'active' | 'inactive') =>
|
|
|
|
|
request<{ updated: boolean; expert_id: number; status: string }>(`/admin/experts/${expertId}/status`, {
|
|
|
|
|
method: 'PATCH', body: JSON.stringify({ status }),
|
|
|
|
|
}),
|
|
|
|
|
adminUpdateExpert: (expertId: number, data: Partial<Pick<Expert, 'name' | 'category' | 'title' | 'bio' | 'expertise' | 'avatar_url' | 'status'>>) =>
|
|
|
|
|
request<{ updated: boolean; expert: Expert }>(`/admin/experts/${expertId}`, { method: 'PATCH', body: JSON.stringify(data) }),
|
|
|
|
|
adminUploadExpertAvatar: (expertId: number, file: File) => {
|
|
|
|
|
const formData = new FormData(); formData.append('file', file)
|
|
|
|
|
return requestWithRetry<{ avatar_url: string }>(`/admin/experts/${expertId}/avatar`, {
|
|
|
|
|
method: 'POST', body: formData, headers: {},
|
|
|
|
|
}, { timeout: 30000, retries: 0 })
|
|
|
|
|
},
|
|
|
|
|
adminListUsers: (q?: string) =>
|
|
|
|
|
request<{ results: Array<{ id: number; phone: string; name: string; status: string; created_at: string; message_count: number; last_active: string | null; last_scene: string | null }> }>(`/admin/users${q ? `?q=${encodeURIComponent(q)}` : ''}`),
|
|
|
|
|
adminResetPassword: (userId: number, password: string) =>
|
|
|
|
|
request<{ reset: boolean }>(`/admin/users/${userId}/reset-password`, { method: 'POST', body: JSON.stringify({ password }) }),
|
|
|
|
|
adminUpdateUserStatus: (userId: number, status: 'active' | 'inactive') =>
|
|
|
|
|
request<{ status: string }>(`/admin/users/${userId}/status`, { method: 'POST', body: JSON.stringify({ status }) }),
|
|
|
|
|
adminStats: () => request<{
|
|
|
|
|
users: number; experts_active: number; experts_total: number;
|
|
|
|
|
services_active: number; consultations_total: number; consultations_pending: number;
|
|
|
|
|
}>('/admin/stats'),
|
|
|
|
|
adminAiUsage: () => request<Record<string, unknown>>('/admin/ai-usage'),
|
|
|
|
|
adminExpertActivity: () => request<Record<string, unknown>>('/admin/expert-activity'),
|
|
|
|
|
|
|
|
|
|
// ===== 对话历史 =====
|
|
|
|
|
listHistorySessions: (mode?: 'normal' | 'confidential') =>
|
|
|
|
|
request<{ results: HistorySession[] }>(`/history/sessions${mode ? `?mode=${mode}` : ''}`),
|
|
|
|
|
getHistoryMessages: (sessionId: string) =>
|
|
|
|
|
request<{ session_id: string; mode: 'normal' | 'confidential'; need_verify: boolean; message_count?: number; messages?: HistoryMessage[] }>(`/history/sessions/${sessionId}`),
|
|
|
|
|
verifyHistory: (sessionId: string, password: string) =>
|
|
|
|
|
request<{ session_id: string; mode: 'confidential'; verified: boolean; messages: HistoryMessage[] }>(`/history/sessions/${sessionId}/verify`, {
|
|
|
|
|
method: 'POST', body: JSON.stringify({ password }),
|
|
|
|
|
}),
|
|
|
|
|
deleteHistory: (sessionId: string) =>
|
|
|
|
|
request<{ deleted: boolean; session_id: string; count: number }>(`/history/sessions/${sessionId}`, { method: 'DELETE' }),
|
|
|
|
|
|
|
|
|
|
// ===== 2.0 新增:案卡 =====
|
|
|
|
|
/** 案卡看板(按状态分组) */
|
|
|
|
|
getMatters: (status?: string) =>
|
|
|
|
|
request<{ board: MatterBoard; total: number }>(`/matters${status ? `?status=${status}` : ''}`),
|
|
|
|
|
/** 创建案卡(绑定会话) */
|
|
|
|
|
createMatter: (data: { scene: Scene; title: string; session_id?: string }) =>
|
|
|
|
|
request<Matter>('/matters', { method: 'POST', body: JSON.stringify(data) }),
|
|
|
|
|
/** 案卡详情 */
|
|
|
|
|
getMatter: (id: number) => request<Matter>(`/matters/${id}`),
|
|
|
|
|
/** 通过 session_id 查询案卡(断点续接时恢复 matterId) */
|
|
|
|
|
getMatterBySession: (sid: string) => request<Matter>(`/matters/by-session/${sid}`),
|
|
|
|
|
/** 更新案卡状态 */
|
|
|
|
|
updateMatterStatus: (id: number, status: Matter['status']) =>
|
|
|
|
|
request<{ updated: boolean; status: string }>(`/matters/${id}/status`, { method: 'PATCH', body: JSON.stringify({ status }) }),
|
|
|
|
|
/** 闭环案卡 */
|
|
|
|
|
closeMatter: (id: number, executionResult?: string, executionNote?: string) =>
|
|
|
|
|
request<{ closed: boolean; closed_at: string }>(`/matters/${id}/close`, {
|
|
|
|
|
method: 'POST', body: JSON.stringify({ execution_result: executionResult, execution_note: executionNote }),
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
// ===== 2.0 新增:老板台 =====
|
|
|
|
|
getDashboardToday: () => request<DashboardToday>('/dashboard/today'),
|
|
|
|
|
|
|
|
|
|
// ===== 2.0 新增:我的团队 =====
|
|
|
|
|
getTeam: () => request<{ results: ExpertRelation[] }>('/team'),
|
|
|
|
|
|
|
|
|
|
// ===== 2.0 新增:快捷指令 =====
|
|
|
|
|
getQuickCommands: () => request<QuickCommands>('/quick-commands'),
|
|
|
|
|
|
|
|
|
|
// ===== 2.0 新增:风险推送 =====
|
|
|
|
|
getRiskAlerts: (unread?: boolean) =>
|
|
|
|
|
request<{ results: RiskAlert[] }>(`/risk-alerts${unread ? '?unread=true' : ''}`),
|
|
|
|
|
readRiskAlert: (id: number) =>
|
|
|
|
|
request<{ read: boolean }>(`/risk-alerts/${id}/read`, { method: 'POST' }),
|
|
|
|
|
scanRiskAlerts: () =>
|
|
|
|
|
request<{ scanned: number; created: number }>('/risk-alerts/scan', { method: 'POST' }),
|
|
|
|
|
|
|
|
|
|
// ===== 2.0 新增:决策日志 =====
|
|
|
|
|
getDecisionsLog: (month?: string) =>
|
|
|
|
|
request<{
|
|
|
|
|
results: Matter[]
|
|
|
|
|
by_month: Record<string, Matter[]>
|
|
|
|
|
stats: { total: number; executed: number; execution_rate: number }
|
|
|
|
|
}>(`/decisions-log${month ? `?month=${month}` : ''}`),
|
|
|
|
|
updateExecutionResult: (matterId: number, executionResult: string, executionNote?: string) =>
|
|
|
|
|
request<{ updated: boolean }>(`/matters/${matterId}/execution`, {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
body: JSON.stringify({ execution_result: executionResult, execution_note: executionNote }),
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
// ===== 可观测性 =====
|
|
|
|
|
getMetrics: () => request<Record<string, unknown>>('/metrics'),
|
|
|
|
|
}
|