Files
zyt/admin/src/api/followupAudio.ts
T

142 lines
6.7 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
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 }, 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
}