feat: 报销预审会话状态管理与工作台交互增强
- 新增差旅报销会话状态管理与对话模型重构 - 增强风险观测服务与运行时聊天上下文作用域 - 优化工作台图标资源、助理意图识别与摘要工具 - 完善报销创建视图样式与差旅详情页标准调整交互 - 补充风险观测、运行时聊天与报销端点测试覆盖
This commit is contained in:
138
web/src/services/steward.js
Normal file
138
web/src/services/steward.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import { apiRequest, getRuntimeApiBaseUrl } from './api.js'
|
||||
|
||||
export function fetchStewardPlan(payload, options = {}) {
|
||||
return apiRequest('/steward/plans', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
export async function fetchStewardPlanStream(payload, handlers = {}, options = {}) {
|
||||
const {
|
||||
timeoutMs = 0,
|
||||
timeoutMessage = '小财管家任务规划超时,请稍后重试。'
|
||||
} = options
|
||||
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null
|
||||
const timeoutId = controller && Number(timeoutMs) > 0
|
||||
? globalThis.setTimeout(() => controller.abort(), Number(timeoutMs))
|
||||
: 0
|
||||
|
||||
let response
|
||||
try {
|
||||
response = await fetch(`${getRuntimeApiBaseUrl()}/steward/plans/stream`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
signal: controller?.signal
|
||||
})
|
||||
} catch (error) {
|
||||
if (timeoutId) {
|
||||
globalThis.clearTimeout(timeoutId)
|
||||
}
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error(timeoutMessage)
|
||||
}
|
||||
throw new Error('无法连接小财管家流式服务,请确认后端已启动。')
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (timeoutId) {
|
||||
globalThis.clearTimeout(timeoutId)
|
||||
}
|
||||
throw new Error(await resolveStreamError(response))
|
||||
}
|
||||
|
||||
if (!response.body?.getReader) {
|
||||
const text = await response.text()
|
||||
if (timeoutId) {
|
||||
globalThis.clearTimeout(timeoutId)
|
||||
}
|
||||
return consumeNdjsonText(text, handlers)
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
const reader = response.body.getReader()
|
||||
let buffer = ''
|
||||
let finalPlan = null
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { value, done } = await reader.read()
|
||||
if (done) {
|
||||
break
|
||||
}
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() || ''
|
||||
for (const line of lines) {
|
||||
const event = parseStreamLine(line)
|
||||
if (!event) continue
|
||||
finalPlan = handleStreamEvent(event, handlers) || finalPlan
|
||||
}
|
||||
}
|
||||
|
||||
buffer += decoder.decode()
|
||||
if (buffer.trim()) {
|
||||
const event = parseStreamLine(buffer)
|
||||
if (event) {
|
||||
finalPlan = handleStreamEvent(event, handlers) || finalPlan
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error(timeoutMessage)
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
if (timeoutId) {
|
||||
globalThis.clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
if (!finalPlan) {
|
||||
throw new Error('小财管家流式结果缺少最终任务计划。')
|
||||
}
|
||||
|
||||
return finalPlan
|
||||
}
|
||||
|
||||
async function resolveStreamError(response) {
|
||||
try {
|
||||
const payload = await response.json()
|
||||
return String(payload?.detail || payload?.message || '').trim() || '小财管家流式接口请求失败。'
|
||||
} catch {
|
||||
return '小财管家流式接口请求失败。'
|
||||
}
|
||||
}
|
||||
|
||||
function consumeNdjsonText(text, handlers) {
|
||||
let finalPlan = null
|
||||
String(text || '').split('\n').forEach((line) => {
|
||||
const event = parseStreamLine(line)
|
||||
if (!event) return
|
||||
finalPlan = handleStreamEvent(event, handlers) || finalPlan
|
||||
})
|
||||
return finalPlan
|
||||
}
|
||||
|
||||
function parseStreamLine(line) {
|
||||
const normalized = String(line || '').trim()
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
return JSON.parse(normalized)
|
||||
}
|
||||
|
||||
function handleStreamEvent(event, handlers) {
|
||||
if (event.event === 'error') {
|
||||
throw new Error(String(event.data?.message || '').trim() || '小财管家规划失败,请稍后重试。')
|
||||
}
|
||||
handlers.onEvent?.(event)
|
||||
if (event.event === 'plan') {
|
||||
return event.data
|
||||
}
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user