144 lines
6.8 KiB
TypeScript
144 lines
6.8 KiB
TypeScript
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 type FollowupAudioChannel = 0 | 1
|
|
export type FollowupChannelRoles = 'unconfirmed' | 'left_service' | 'right_service'
|
|
export interface FollowupTranscriptSegment {
|
|
id: string
|
|
start_ms: number
|
|
end_ms: number
|
|
text: string
|
|
channel?: FollowupAudioChannel
|
|
}
|
|
export interface FollowupCapabilities {
|
|
enabled: boolean
|
|
audio_verified: boolean
|
|
preview_only?: boolean
|
|
preview_ready?: boolean
|
|
manual_review_enabled?: boolean
|
|
manual_review_ready?: boolean
|
|
can_review?: boolean
|
|
test_scope_allowed?: 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; segment_id?: string; position_type?: 'segment'; channel?: FollowupAudioChannel }>
|
|
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
|
|
preview_only?: boolean
|
|
transcript_available?: boolean
|
|
pipeline_progress?: { transcribed: number; total: number }
|
|
}
|
|
export interface FollowupTaskDetail extends FollowupTaskSummary {
|
|
summary: string
|
|
transcript: string
|
|
transcript_segments?: FollowupTranscriptSegment[]
|
|
channel_roles?: FollowupChannelRoles
|
|
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; channel_roles?: FollowupChannelRoles }, 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[]; channel_roles?: FollowupChannelRoles }, 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
|
|
}
|