import { randomBytes } from 'node:crypto'; const DEFAULT_BASE_URL = 'https://superagent.nianxx.cn'; const DEFAULT_TIMEOUT_MS = 120_000; const DEFAULT_TOTAL_TIMEOUT_MS = 180_000; export class OpenAgentAPIError extends Error { constructor(message, { statusCode = 0, detail = null, responseText = '' } = {}) { super(message); this.name = 'OpenAgentAPIError'; this.statusCode = statusCode; this.detail = detail; this.responseText = responseText; } } export class ExternalAgentParser { constructor({ baseUrl = DEFAULT_BASE_URL, apiKey, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS, totalTimeoutMs = DEFAULT_TOTAL_TIMEOUT_MS, now = () => new Date(), csrfToken = randomBytes(32).toString('base64url') } = {}) { this.baseUrl = normalizeBaseUrl(baseUrl); this.apiKey = normalizeApiKey(apiKey); this.fetchImpl = fetchImpl; this.timeoutMs = timeoutMs; this.totalTimeoutMs = totalTimeoutMs; this.now = now; this.csrfToken = csrfToken; if (typeof this.fetchImpl !== 'function') { throw new Error('当前 Node.js 运行时没有可用的 fetch。'); } } isConfigured() { return Boolean(this.baseUrl && this.apiKey); } async checkConnection({ timeoutMs = Math.min(this.timeoutMs, 8_000) } = {}) { if (!this.isConfigured()) { return { ok: false, configured: false, reachable: false, error_code: 'external_service_not_configured' }; } try { // The open session endpoint is state-changing on some deployments even // when the payload is empty. Use the provider health endpoint here so a // browser status refresh cannot create orphaned Agent sessions. const response = await this.requestRaw('GET', '/health', { accept: 'application/json', timeoutMs }); const httpStatus = response.status; await response.text().catch(() => ''); if (response.ok) { return { ok: true, configured: true, reachable: true, authenticated: null, authentication_checked: false, http_status: httpStatus, probe: 'health_endpoint' }; } return { ok: false, configured: true, reachable: true, authenticated: null, authentication_checked: false, http_status: httpStatus, error_code: 'external_health_unhealthy' }; } catch (error) { return { ok: false, configured: true, reachable: false, error_code: errorCodeFor(error) }; } } async parse({ rawText, taskId, receivedAt = this.now().toISOString(), signal: parentSignal } = {}) { let sessionId = ''; let requestTrace = makeRequestTrace(this.now()); const normalizedRawText = String(rawText || '').trim(); const normalizedTaskId = String(taskId || '').trim(); if (!normalizedRawText) return blockedResult(['原始指令不能为空。']); if (!normalizedTaskId) return blockedResult(['缺少任务 ID,无法建立独立解析会话。']); if (!this.isConfigured()) { return blockedResult([ '解析服务未配置:请在服务端设置 DEERFLOW_BASE_URL 和 DEERFLOW_OPEN_API_KEY。' ], { error_code: 'external_service_not_configured', external_request: updateRequestTrace(requestTrace, 'not_configured', this.now()) }); } const controller = new AbortController(); const unlinkParentSignal = linkAbortSignal(parentSignal, controller); const deadlineTimer = setTimeout(() => controller.abort(), this.totalTimeoutMs); const deadline = Date.now() + this.totalTimeoutMs; const remainingTimeout = () => { if (controller.signal.aborted) { throw new OpenAgentAPIError('外部解析服务已取消。', { detail: 'parse_cancelled' }); } const remaining = deadline - Date.now(); if (remaining <= 0) { throw new OpenAgentAPIError(`外部解析服务总超时(>${this.totalTimeoutMs}ms)。`, { detail: 'parse_timeout' }); } return Math.min(this.timeoutMs, remaining); }; const sessionKey = `business-parser-session:${normalizedTaskId}`; const messageKey = `business-parser-message:${normalizedTaskId}`; const metadata = { source: 'business_system', module: 'input_parser', task_id: normalizedTaskId }; try { requestTrace = updateRequestTrace(requestTrace, 'creating_session', this.now()); const session = await this.createSession({ externalSubjectId: `business-task:${normalizedTaskId}`, idempotencyKey: sessionKey, metadata, timeoutMs: remainingTimeout(), signal: controller.signal }); sessionId = String(session?.session_id || '').trim(); if (!sessionId) { throw new OpenAgentAPIError('外部服务创建会话成功但未返回 session_id。', { detail: session }); } requestTrace = updateRequestTrace(requestTrace, 'session_created', this.now(), { session_id_present: true }); const message = buildParserMessage({ rawText: normalizedRawText, taskId: normalizedTaskId, receivedAt }); const stream = await this.streamMessage(sessionId, message, { idempotencyKey: messageKey, metadata: { ...metadata, session_id: sessionId }, timeoutMs: remainingTimeout(), signal: controller.signal }); requestTrace = updateRequestTrace(requestTrace, 'stream_connecting', this.now()); const streamed = await collectSseOutputWithTimeout( stream.body, remainingTimeout(), () => controller.abort(), controller.signal ); requestTrace = updateRequestTrace(requestTrace, 'stream_completed', this.now(), { event_count: streamed.eventCount }); const parsed = streamed.result || parseJsonObject(streamed.text); if (!parsed) { return blockedResult(['外部解析服务未返回可识别的 JSON 结果。'], { error_code: 'external_result_not_json', session_id: sessionId, event_count: streamed.eventCount, external_request: requestTrace }); } return normalizeParseResult(parsed, { taskId: normalizedTaskId, receivedAt, sessionId, requestTrace }); } catch (error) { requestTrace = updateRequestTrace(requestTrace, 'failed', this.now(), { session_id_present: Boolean(sessionId), error_code: errorCodeFor(error) }); return blockedResult([formatExternalError(error)], { error_code: errorCodeFor(error), external_status: error?.statusCode || undefined, external_request: requestTrace }); } finally { clearTimeout(deadlineTimer); unlinkParentSignal(); } } async createSession({ externalSubjectId, idempotencyKey, metadata, timeoutMs = this.timeoutMs, signal } = {}) { return this.requestJson('POST', '/api/open/agent-sessions', { body: compactPayload({ external_subject_id: externalSubjectId, idempotency_key: idempotencyKey, metadata }), timeoutMs, signal }); } async streamMessage(sessionId, message, { idempotencyKey, metadata, timeoutMs = this.timeoutMs, signal } = {}) { return this.request('POST', `/api/open/agent-sessions/${encodeURIComponent(sessionId)}/messages/stream`, { accept: 'text/event-stream', body: compactPayload({ message, idempotency_key: idempotencyKey, metadata }), timeoutMs, signal }); } async requestJson(method, path, { timeoutMs = this.timeoutMs, ...options } = {}) { const response = await this.request(method, path, { ...options, accept: 'application/json', timeoutMs }); const text = await response.text(); if (!text.trim()) return {}; try { return JSON.parse(text); } catch (error) { throw new OpenAgentAPIError('外部服务返回了无效 JSON。', { statusCode: response.status, responseText: text }); } } async request(method, path, { body, accept = 'application/json', timeoutMs = this.timeoutMs, signal } = {}) { const response = await this.requestRaw(method, path, { body, accept, timeoutMs, signal }); if (response.status >= 400) { const responseText = await response.text(); throw new OpenAgentAPIError(formatHttpError(response.status), { statusCode: response.status, detail: parseErrorDetail(responseText), responseText }); } return response; } async requestRaw(method, path, { body, accept = 'application/json', timeoutMs = this.timeoutMs, signal } = {}) { const requestController = new AbortController(); const requestSignal = signal ? AbortSignal.any([signal, requestController.signal]) : requestController.signal; const timeout = setTimeout(() => requestController.abort(), timeoutMs); try { const response = await this.fetchImpl(`${this.baseUrl}${path}`, { method, headers: { Accept: accept, Authorization: `Bearer ${this.apiKey}`, ...(isStateChangingMethod(method) ? { 'X-CSRF-Token': this.csrfToken, Cookie: `csrf_token=${this.csrfToken}` } : {}), ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) }, ...(body === undefined ? {} : { body: JSON.stringify(body) }), signal: requestSignal }); return response; } catch (error) { if (error?.name === 'AbortError') { throw new OpenAgentAPIError( signal?.aborted ? '外部解析服务请求已取消。' : `外部解析服务超时(>${timeoutMs}ms)。`, { detail: signal?.aborted ? 'parse_cancelled' : 'request_timeout' } ); } throw error; } finally { clearTimeout(timeout); } } } export function normalizeParseResult(result, { taskId, receivedAt, sessionId, requestTrace } = {}) { const normalized = normalizeResultShape(result); if (!normalized || typeof normalized !== 'object') { return blockedResult(['外部解析服务返回的内容不是 JSON 对象。'], { error_code: 'external_result_not_object', session_id: sessionId, external_request: requestTrace }); } const status = normalized.status; if (!status && !normalized.operation && !normalized.blockers) { return blockedResult([ '外部 Profile 返回的 JSON 不符合标准 operation 契约,请检查已发布 Profile 的输出配置。' ], { error_code: 'external_result_contract_mismatch', response_keys: Object.keys(normalized).slice(0, 30), session_id: sessionId, external_request: requestTrace }); } if (status !== 'agent_parse_passed') { return { ...normalized, status: status || 'agent_parse_blocked', blockers: Array.isArray(normalized.blockers) ? normalized.blockers : ['外部解析服务未通过输入校验。'], operation: normalized.operation && typeof normalized.operation === 'object' ? normalized.operation : null, session_id: sessionId, external_request: requestTrace }; } const operation = normalized.operation; if (!operation || typeof operation !== 'object') { return blockedResult(['外部解析服务返回通过状态,但缺少 operation。'], { error_code: 'external_operation_missing', session_id: sessionId, external_request: requestTrace }); } operation.action = operation.action || 'team_order_create'; operation.submit_mode = 'dry_run'; operation.source = { ...(operation.source || {}), task_id: taskId, instruction_id: operation.source?.instruction_id || taskId, received_at: operation.source?.received_at || receivedAt, operator: operation.source?.operator || 'business-system-parser', parser_prompt_version: operation.source?.parser_prompt_version || 'external-profile' }; operation.data = operation.data || {}; operation.data.passenger_list = operation.data.passenger_list || { operation: 'none', passenger_count: 0, file_count: 0 }; operation.data.attachments = Array.isArray(operation.data.attachments) ? operation.data.attachments : []; operation.data.system_defaults = { fabudanwei: '老挝联泰', business_status: '预订', filing_required: true, ...(operation.data.system_defaults || {}) }; return { ...normalized, status: 'agent_parse_passed', blockers: Array.isArray(normalized.blockers) ? normalized.blockers : [], operation, session_id: sessionId, external_request: requestTrace }; } export async function collectSseOutput(body) { if (!body) return { text: '', result: null, eventCount: 0 }; let text = ''; let finalText = ''; let result = null; let eventCount = 0; for await (const event of iterateSseEvents(body)) { eventCount += 1; const structured = findStructuredResult(event.data); if (structured) result = structured; const fragment = event.event === 'messages' ? extractAssistantMessageText(event.data) : extractEventText(event.data); if (!fragment) continue; if (isFinalEvent(event.event)) finalText = fragment; else text += fragment; } return { text: finalText || text, result, eventCount }; } async function collectSseOutputWithTimeout(body, timeoutMs, onTimeout, signal) { let timer; let abortHandler; const cancellation = signal ? new Promise((resolve, reject) => { abortHandler = () => reject(new OpenAgentAPIError('外部解析服务请求已取消。', { detail: 'parse_cancelled' })); if (signal.aborted) abortHandler(); else signal.addEventListener('abort', abortHandler, { once: true }); }) : null; try { const races = [ collectSseOutput(body), new Promise((resolve, reject) => { timer = setTimeout(() => { onTimeout?.(); reject(new OpenAgentAPIError(`外部解析服务超时(>${timeoutMs}ms)。`, { detail: 'request_timeout' })); }, timeoutMs); }) ]; if (cancellation) races.push(cancellation); return await Promise.race(races); } catch (error) { await cancelBodyWithBoundedWait(body); throw error; } finally { clearTimeout(timer); if (abortHandler) signal.removeEventListener('abort', abortHandler); } } export async function* iterateSseEvents(body) { const decoder = new TextDecoder(); let buffer = ''; for await (const chunk of body) { buffer += typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true }); while (true) { const separator = findEventSeparator(buffer); if (separator < 0) break; const raw = buffer.slice(0, separator); buffer = buffer.slice(separator + eventSeparatorLength(buffer, separator)); const event = parseSseBlock(raw); if (!event || event.done) return; yield event; if (isFinalEvent(event.event)) return; } } buffer += decoder.decode(); if (buffer.trim()) { const event = parseSseBlock(buffer); if (event && !event.done) yield event; } } export function parseSseBlock(raw) { let event; const dataLines = []; for (const line of String(raw).split(/\r?\n/)) { if (!line || line.startsWith(':')) continue; if (line.startsWith('event:')) { event = line.slice('event:'.length).trim(); continue; } if (line.startsWith('data:')) dataLines.push(line.slice('data:'.length).replace(/^ /, '')); } if (event === undefined && dataLines.length === 0) return null; const dataText = dataLines.join('\n'); if (!dataText) return { event: event || null, data: null }; if (dataText.trim() === '[DONE]') return { event: event || null, data: null, done: true }; try { return { event: event || null, data: JSON.parse(dataText) }; } catch (error) { return { event: event || null, data: dataText }; } } export function parseJsonObject(text) { const raw = stripThinkBlocks(text); if (!raw) return null; const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i); const candidate = fenced ? fenced[1].trim() : raw; try { return JSON.parse(candidate); } catch (error) { const first = candidate.indexOf('{'); const last = candidate.lastIndexOf('}'); if (first < 0 || last <= first) return null; try { return JSON.parse(candidate.slice(first, last + 1)); } catch (nestedError) { return null; } } } export function buildParserMessage({ rawText, taskId, receivedAt }) { return [ `任务 ID:${taskId}`, `当前时间:${receivedAt}`, '', '请将以下原始业务指令解析为既定标准 operation JSON,只输出 JSON:', rawText ].join('\n'); } export function blockedResult(blockers, extra = {}) { return { status: 'agent_parse_blocked', blockers: Array.isArray(blockers) ? blockers : [String(blockers)], operation: null, ...extra }; } export function normalizeBaseUrl(value) { const raw = String(value || DEFAULT_BASE_URL).trim().replace(/\/+$/, ''); try { const url = new URL(raw); if (!['http:', 'https:'].includes(url.protocol)) return ''; return url.toString().replace(/\/+$/, ''); } catch (error) { return ''; } } export function normalizeApiKey(value) { return String(value || '') .trim() .replace(/^['"]|['"]$/g, '') .replace(/^Bearer\s+/i, '') .trim(); } function normalizeResultShape(result) { if (!result || typeof result !== 'object') return null; if (result.result && typeof result.result === 'object') return normalizeResultShape(result.result); if (result.output && typeof result.output === 'object') return normalizeResultShape(result.output); if (result.data && typeof result.data === 'object') return normalizeResultShape(result.data); if (result.status || result.operation || result.blockers) return result; if (result.action && result.data) return { status: 'agent_parse_passed', blockers: [], operation: result }; if (looksLikeFlatParserResult(result)) { return { status: 'agent_parse_passed', blockers: [], operation: flatParserResultToOperation(result) }; } return result; } function findStructuredResult(value) { if (!value || typeof value !== 'object') return null; if (value.status === 'agent_parse_passed' || value.status === 'agent_parse_blocked') return value; if (value.operation && typeof value.operation === 'object') { return { status: value.status || 'agent_parse_passed', blockers: value.blockers || [], operation: value.operation }; } if (value.result && typeof value.result === 'object') return findStructuredResult(value.result); if (value.output && typeof value.output === 'object') return findStructuredResult(value.output); if (value.data && typeof value.data === 'object') return findStructuredResult(value.data); if (value.payload && typeof value.payload === 'object') return findStructuredResult(value.payload); if (value.action && value.data) return { status: 'agent_parse_passed', blockers: [], operation: value }; return null; } function extractEventText(value) { if (typeof value === 'string') return value; if (!value || typeof value !== 'object') return ''; if (findStructuredResult(value)) return ''; for (const key of ['content', 'text', 'delta', 'output_text', 'final_output', 'answer', 'data', 'payload']) { if (value[key] === undefined || value[key] === null) continue; const nested = extractEventText(value[key]); if (nested) return nested; } if (value.message !== undefined) return extractEventText(value.message); return ''; } function extractAssistantMessageText(value) { if (!Array.isArray(value)) return ''; return value .filter((item) => isAssistantMessage(item)) .map((item) => extractEventText(item)) .join(''); } function isAssistantMessage(value) { const type = String(value?.type || '').toLowerCase(); return type.includes('ai') || type.includes('assistant'); } function isFinalEvent(eventName) { return /(^|[._-])(completed|complete|final|done|finished)([._-]|$)/i.test(String(eventName || '')); } function compactPayload(payload) { return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== undefined && value !== null)); } function makeRequestTrace(now) { return { service: 'external_parse_api', transport: 'sse', stage: 'not_started', session_id_present: false, event_count: 0, updated_at: now.toISOString() }; } function updateRequestTrace(trace, stage, now, patch = {}) { return { ...trace, ...patch, stage, updated_at: now.toISOString() }; } function linkAbortSignal(parentSignal, controller) { if (!parentSignal) return () => {}; const abort = () => controller.abort(); if (parentSignal.aborted) abort(); else parentSignal.addEventListener('abort', abort, { once: true }); return () => parentSignal.removeEventListener('abort', abort); } async function cancelBodyWithBoundedWait(body) { if (typeof body?.cancel !== 'function') return; try { const cancellation = Promise.resolve(body.cancel()).catch(() => undefined); await Promise.race([ cancellation, new Promise((resolve) => setTimeout(resolve, 1_000)) ]); } catch { // The request has already been aborted. A provider that does not resolve // cancel() must not hold the business task open indefinitely. } } function isStateChangingMethod(method) { return ['POST', 'PUT', 'PATCH', 'DELETE'].includes(String(method || '').toUpperCase()); } function stripThinkBlocks(text) { return String(text || '').replace(/[\s\S]*?<\/think>/gi, '').trim(); } function findEventSeparator(buffer) { const lf = buffer.indexOf('\n\n'); const crlf = buffer.indexOf('\r\n\r\n'); if (lf < 0) return crlf; if (crlf < 0) return lf; return Math.min(lf, crlf); } function eventSeparatorLength(buffer, index) { return buffer.startsWith('\r\n\r\n', index) ? 4 : 2; } function parseErrorDetail(responseText) { try { const body = JSON.parse(responseText); if (typeof body === 'string') return body; if (body && typeof body === 'object') return body.detail || body.message || body.error || body; } catch (error) { // Keep the text path below intentionally short and redacted. } return redactSensitiveText(String(responseText || '').slice(0, 500)); } function formatHttpError(statusCode) { if (statusCode === 401) return '外部解析服务鉴权失败(401)。'; if (statusCode === 403) return '外部解析服务权限不足(403),请检查外部应用 scope。'; if (statusCode === 404) return '外部解析服务未找到可用的已发布 Profile(404)。'; if (statusCode === 409) return '外部解析服务报告当前 session 有运行中的任务(409)。'; return `外部解析服务请求失败(HTTP ${statusCode})。`; } function formatExternalError(error) { if (error instanceof OpenAgentAPIError) { const detail = error.detail && typeof error.detail === 'string' ? redactSensitiveText(error.detail).slice(0, 300) : ''; return detail ? `${error.message} ${detail}` : error.message; } if (error?.code === 'ENOTFOUND' || error?.code === 'ECONNREFUSED') { return '无法连接外部解析服务,请检查服务地址和网络。'; } return `外部解析服务调用异常:${redactSensitiveText(error?.message || String(error)).slice(0, 300)}`; } function errorCodeFor(error) { if (error?.statusCode === 401) return 'external_unauthorized'; if (error?.statusCode === 403) return 'external_forbidden'; if (error?.statusCode === 404) return 'external_profile_not_published'; if (error?.statusCode === 409) return 'external_active_run'; if (error?.detail === 'parse_cancelled') return 'external_cancelled'; if (error?.detail === 'request_timeout') return 'external_timeout'; return 'external_request_failed'; } function redactSensitiveText(value) { return String(value || '').replace(/df_open_[A-Za-z0-9_-]+/g, '[redacted-open-api-key]'); } function looksLikeFlatParserResult(result) { return Boolean( result.order_mode || result.orderMode || result.product_name || result.productName || result.departure_date || result.departureDate || result.op || result.operator || result.sales || result.salesperson ); } function flatParserResultToOperation(result) { const counts = result.passenger_counts || result.passengers || {}; const rooms = result.room_counts || result.rooms || {}; const prices = result.prices || result.unit_prices || {}; const passengerCounts = { adult: numberValue(pick(result, ['adult', 'adults'], counts.adult)), child_bed: numberValue(pick(result, ['child_with_bed', 'child_bed', 'childrenWithBed'], pick(counts, ['child_with_bed', 'child_bed', 'childrenWithBed']))), child_no_bed: numberValue(pick(result, ['child_without_bed', 'child_no_bed', 'childrenWithoutBed'], pick(counts, ['child_without_bed', 'child_no_bed', 'childrenWithoutBed']))), infant: numberValue(pick(result, ['infant', 'infants'], counts.infant)), leader: numberValue(pick(result, ['tour_leader', 'leader', 'tourLeader'], pick(counts, ['tour_leader', 'leader', 'tourLeader']))) }; passengerCounts.expected_total = numberValue( pick(result, ['expected_total', 'passenger_total'], pick(counts, ['expected_total', 'passenger_total'])) ) || Object.values(passengerCounts).reduce((sum, value) => sum + value, 0); const productName = pick(result, ['product_name', 'productName'], result.product?.name || ''); const departureDate = pick(result, ['departure_date', 'departureDate'], result.departure_dates?.[0] || ''); const source = result.source && typeof result.source === 'object' ? { ...result.source } : {}; if (result.notes && !source.notes) source.notes = result.notes; return { action: result.action || 'team_order_create', order_nature: normalizeOrderNature(pick(result, ['order_nature', 'orderNature'])), submit_mode: 'dry_run', source, data: { order_mode: pick(result, ['order_mode', 'orderMode'], '团队-单个下单'), test_marker: pick(result, ['test_marker', 'testMarker', 'marker'], ''), product: { name: productName }, departure_dates: departureDate ? [String(departureDate)] : [], passenger_counts: passengerCounts, room_counts: { SGL: numberValue(pick(result, ['SGL', 'sgl', 'single_room'], pick(rooms, ['SGL', 'sgl', 'single_room']))), TWN: numberValue(pick(result, ['TWN', 'twn', 'twin_room'], pick(rooms, ['TWN', 'twn', 'twin_room']))), TRP: numberValue(pick(result, ['TRP', 'trp', 'triple_room'], pick(rooms, ['TRP', 'trp', 'triple_room']))), DBL: numberValue(pick(result, ['DBL', 'dbl', 'double_room'], pick(rooms, ['DBL', 'dbl', 'double_room']))), HNM: numberValue(pick(result, ['HNM', 'hnm', 'honeymoon_room'], pick(rooms, ['HNM', 'hnm', 'honeymoon_room']))), TL: numberValue(pick(result, ['TL', 'tl', 'leader_room'], pick(rooms, ['TL', 'tl', 'leader_room']))) }, prices: { adult: numberValue(pick(result, ['adult_price', 'price_adult'], pick(prices, ['adult', 'adult_price']))), child_bed: numberValue(pick(result, ['child_with_bed_price', 'child_bed_price'], pick(prices, ['child_with_bed', 'child_bed']))), child_no_bed: numberValue(pick(result, ['child_without_bed_price', 'child_no_bed_price'], pick(prices, ['child_without_bed', 'child_no_bed']))), infant: numberValue(pick(result, ['infant_price'], prices.infant)), leader: numberValue(pick(result, ['tour_leader_price', 'leader_price'], pick(prices, ['tour_leader', 'leader']))) }, op_user: { name: pick(result, ['op', 'operator'], result.op_user?.name || '') }, sales_user: { name: pick(result, ['sales', 'salesperson'], result.sales_user?.name || '') }, special_requests: pick(result, ['remarks', 'special_requests', 'specialRequests'], ''), passenger_list: result.passenger_list || result.passengerList || { operation: 'none', passenger_count: 0, file_count: 0 }, attachments: Array.isArray(result.attachments) ? result.attachments : [], system_defaults: result.system_defaults || result.systemDefaults || {} } }; } function pick(object, keys, fallback = '') { if (!object || typeof object !== 'object') return fallback; for (const key of keys) { const value = object[key]; if (value !== undefined && value !== null && String(value).trim() !== '') return value; } return fallback; } function numberValue(value) { const number = Number(String(value ?? '').replace(/[,,\s]/g, '')); return Number.isFinite(number) ? number : 0; } function normalizeOrderNature(value) { const text = String(value || '').trim().toLowerCase(); if (/test|测试/.test(text)) return 'test'; if (/formal|正式/.test(text)) return 'formal'; return value || 'test'; } export { DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, DEFAULT_TOTAL_TIMEOUT_MS };