feat: add reviewed follow-up audio patient enrichment
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export type FollowupKind = 'diagnosis' | 'blood' | 'diet' | 'exercise' | 'tracking_note'
|
||||
export type FollowupValue = string | number | boolean | null | Array<string | number>
|
||||
export type FollowupValues = Record<string, FollowupValue>
|
||||
export interface FollowupField {
|
||||
key: string
|
||||
label: string
|
||||
type: 'text' | 'number' | 'select' | 'multiselect' | 'date'
|
||||
options?: Array<{ label: string; value: string | number }>
|
||||
readonly?: boolean
|
||||
}
|
||||
export type FollowupFields = Record<FollowupKind, FollowupField[]>
|
||||
export interface FollowupCapabilities {
|
||||
enabled: boolean
|
||||
audio_verified: boolean
|
||||
can_upload: boolean
|
||||
can_apply: boolean
|
||||
can_daily: boolean
|
||||
models: Array<{ value: string; label: string }>
|
||||
limits: { max_bytes: number; max_seconds: number; chunk_bytes: number }
|
||||
fields: FollowupFields
|
||||
}
|
||||
export interface FollowupReviewItem {
|
||||
id: string
|
||||
kind: FollowupKind
|
||||
values: FollowupValues
|
||||
record_date: string | null
|
||||
record_time: string | null
|
||||
time_period: string | null
|
||||
time_estimated: boolean
|
||||
time_text: string
|
||||
date_text: string
|
||||
evidence: Array<{ text: string; start_ms?: number; end_ms?: number }>
|
||||
needs_review: boolean
|
||||
selected: boolean
|
||||
target_id: number | null
|
||||
current_values: FollowupValues
|
||||
expected_hash: string
|
||||
conflict: boolean
|
||||
possible_duplicate: boolean
|
||||
candidates?: Array<{
|
||||
id: number
|
||||
values: FollowupValues
|
||||
record_time: string | null
|
||||
time_estimated: boolean
|
||||
time_period: string | null
|
||||
delete_time?: number | null
|
||||
}>
|
||||
}
|
||||
export interface FollowupTaskSummary {
|
||||
id: number
|
||||
diagnosis_id: number
|
||||
file_name: string
|
||||
recorded_at: string
|
||||
status: string
|
||||
stage: string
|
||||
error_code: string
|
||||
error_message: string
|
||||
version: number
|
||||
created_at: number | string
|
||||
expires_at: number | string
|
||||
can_retry: boolean
|
||||
audio_available: boolean
|
||||
}
|
||||
export interface FollowupTaskDetail extends FollowupTaskSummary {
|
||||
summary: string
|
||||
transcript: string
|
||||
uncertainties: unknown[]
|
||||
items: FollowupReviewItem[]
|
||||
fields?: FollowupFields
|
||||
applied_items: unknown[]
|
||||
review_refreshed?: boolean
|
||||
}
|
||||
interface Envelope<T> { code: number; msg?: string; data: T }
|
||||
export class FollowupAudioError extends Error {
|
||||
constructor(message: string, public readonly code = '') { super(message); this.name = 'FollowupAudioError' }
|
||||
}
|
||||
const options = { isTransformResponse: false, ignoreCancelToken: true, isOpenRetry: false, retryCount: 0 }
|
||||
const base = '/tcm.followupAudio/'
|
||||
function unwrap<T>(response: Envelope<T>): T {
|
||||
if (response.code !== 1) {
|
||||
const data = response.data
|
||||
const code = data && typeof data === 'object' && 'code' in data ? String(data.code) : ''
|
||||
throw new FollowupAudioError(response.msg || '回访录音请求失败,请刷新任务状态后确认', code)
|
||||
}
|
||||
return response.data
|
||||
}
|
||||
async function get<T>(name: string, params: Record<string, number>, signal?: AbortSignal): Promise<T> {
|
||||
return unwrap(await request.get<Envelope<T>>({ url: base + name, params, signal }, options))
|
||||
}
|
||||
async function post<T>(name: string, data: unknown, signal?: AbortSignal): Promise<T> {
|
||||
return unwrap(await request.post<Envelope<T>>({ url: base + name, data, signal, timeout: 120000 }, options))
|
||||
}
|
||||
export const followupAudioCapabilities = (diagnosisId: number, signal?: AbortSignal) => get<FollowupCapabilities>('capabilities', { diagnosis_id: diagnosisId }, signal)
|
||||
export const followupAudioLists = (diagnosisId: number, signal?: AbortSignal) => get<{ items: FollowupTaskSummary[] }>('lists', { diagnosis_id: diagnosisId }, signal)
|
||||
export const followupAudioDetail = (id: number, signal?: AbortSignal) => get<FollowupTaskDetail>('detail', { id }, signal)
|
||||
export const followupAudioUploadSession = (data: { diagnosis_id: number; file_name: string; total_bytes: number }, signal?: AbortSignal) => post<{ upload_id: string; chunk_bytes: number }>('uploadSession', data, signal)
|
||||
export async function followupAudioUploadChunk(uploadId: string, index: number, file: Blob, signal?: AbortSignal) {
|
||||
const data = new FormData()
|
||||
data.append('upload_id', uploadId)
|
||||
data.append('index', String(index))
|
||||
data.append('file', file, `chunk-${index}`)
|
||||
return unwrap(await request.post<Envelope<{ index: number; received: boolean }>>({
|
||||
url: base + 'uploadChunk', data, signal, timeout: 120000,
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
}, options))
|
||||
}
|
||||
export const followupAudioUploadComplete = (uploadId: string, signal?: AbortSignal) => post<{ upload_id: string; duration_seconds: number; sha256: string }>('uploadComplete', { upload_id: uploadId }, signal)
|
||||
export const followupAudioCreate = (data: { diagnosis_id: number; upload_id: string; recorded_at: string; model_key: string }, signal?: AbortSignal) => post<{ task_id: number; status: string; reused?: boolean; reuse_message?: string }>('create', data, signal)
|
||||
export const followupAudioSaveDraft = (data: { id: number; version: number; items: FollowupReviewItem[] }, signal?: AbortSignal) => post<FollowupTaskDetail>('saveDraft', data, signal)
|
||||
export const followupAudioApply = (data: { id: number; version: number; items: FollowupReviewItem[] }, signal?: AbortSignal) => post<{ id: number; status: 'applied'; applied_items: unknown[] }>('apply', data, signal)
|
||||
export const followupAudioRetry = (id: number, signal?: AbortSignal) => post<{ id: number; status: string }>('retry', { id }, signal)
|
||||
/** The existing request interceptor puts the token in a header, never in the audio URL. */
|
||||
export async function followupAudioBlob(id: number, signal?: AbortSignal): Promise<Blob> {
|
||||
const blob = await request.get<Blob>({ url: base + 'audio', params: { id }, responseType: 'blob', signal, timeout: 120000 }, options)
|
||||
if (blob.type.includes('json')) {
|
||||
const response: Envelope<unknown> = JSON.parse(await blob.text())
|
||||
unwrap(response)
|
||||
throw new FollowupAudioError('录音响应不是音频文件')
|
||||
}
|
||||
return blob
|
||||
}
|
||||
Reference in New Issue
Block a user