fix: 修正AiModel接口字段名model_id改为id
parent
4fbb15f3d0
commit
d00ef10e9f
622
src/api.ts
622
src/api.ts
|
|
@ -1,13 +1,13 @@
|
|||
import axios from 'axios';
|
||||
|
||||
const API_BASE_URL = 'https://api.hoyidata.com/aura/v1';
|
||||
const APP_BASE = (import.meta.env.BASE_URL || '/').replace(/\/$/, '');
|
||||
const withAppBase = (path: string) => `${APP_BASE}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
const withApiBase = (path: string) => `${API_BASE_URL}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
import axios from 'axios';
|
||||
|
||||
const API_BASE_URL = 'https://api.hoyidata.com/aura/v1';
|
||||
const APP_BASE = (import.meta.env.BASE_URL || '/').replace(/\/$/, '');
|
||||
const withAppBase = (path: string) => `${APP_BASE}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
const withApiBase = (path: string) => `${API_BASE_URL}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
const isMockAuth = () => typeof localStorage !== 'undefined' && localStorage.getItem('mock-auth') === '1';
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
baseURL: API_BASE_URL,
|
||||
timeout: 90000,
|
||||
withCredentials: true // 关键:跨域请求带 cookie
|
||||
});
|
||||
|
|
@ -16,10 +16,10 @@ export const api = axios.create({
|
|||
api.interceptors.response.use(
|
||||
(r) => r,
|
||||
(err) => {
|
||||
const isLoginPage = location.pathname === '/login' || location.pathname === withAppBase('/login');
|
||||
if (err?.response?.status === 401 && !isLoginPage && !isMockAuth()) {
|
||||
const isLoginPage = location.pathname === '/login' || location.pathname === withAppBase('/login');
|
||||
if (err?.response?.status === 401 && !isLoginPage && !isMockAuth()) {
|
||||
const next = encodeURIComponent(location.pathname + location.search);
|
||||
location.href = `${withAppBase('/login')}?next=${next}`;
|
||||
location.href = `${withAppBase('/login')}?next=${next}`;
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
|
@ -91,20 +91,20 @@ export interface ToolCallTrace {
|
|||
result: any;
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
reasoning?: string | null;
|
||||
parentId?: string | null;
|
||||
createdAt: number;
|
||||
meta?: {
|
||||
retrieved?: RetrievedSnippet[];
|
||||
toolCalls?: ToolCallTrace[];
|
||||
aborted?: boolean;
|
||||
reasoning?: string;
|
||||
error?: string;
|
||||
} | null;
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
reasoning?: string | null;
|
||||
parentId?: string | null;
|
||||
createdAt: number;
|
||||
meta?: {
|
||||
retrieved?: RetrievedSnippet[];
|
||||
toolCalls?: ToolCallTrace[];
|
||||
aborted?: boolean;
|
||||
reasoning?: string;
|
||||
error?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface BranchInfo {
|
||||
|
|
@ -118,18 +118,18 @@ export interface ChatHistoryResp {
|
|||
branches: Record<string, BranchInfo>;
|
||||
}
|
||||
|
||||
export interface AiModel {
|
||||
model_id: string;
|
||||
model_name: string;
|
||||
icon: string;
|
||||
model_ratio: number;
|
||||
completion_ratio: number;
|
||||
}
|
||||
|
||||
export const ModelAPI = {
|
||||
list: () => api.get<{ data: AiModel[] }>('/models').then((r) => r.data.data),
|
||||
};
|
||||
|
||||
export interface AiModel {
|
||||
id: string;
|
||||
model_name: string;
|
||||
icon: string;
|
||||
model_ratio: number;
|
||||
completion_ratio: number;
|
||||
}
|
||||
|
||||
export const ModelAPI = {
|
||||
list: () => api.get<{ data: AiModel[] }>('/models').then((r) => r.data.data),
|
||||
};
|
||||
|
||||
export const AgentAPI = {
|
||||
list: () => api.get<Agent[]>('/agents').then((r) => r.data),
|
||||
detail: (id: string) => api.get<Agent>(`/agents/${id}`).then((r) => r.data),
|
||||
|
|
@ -176,14 +176,14 @@ export const ChatAPI = {
|
|||
agentId: string,
|
||||
content: string,
|
||||
sessionId?: string,
|
||||
model?: string,
|
||||
model?: string,
|
||||
imageUrls?: string[]
|
||||
) =>
|
||||
api
|
||||
.post<{ user: ChatMessage; assistant: ChatMessage }>(`/chat/${agentId}/messages`, {
|
||||
content,
|
||||
sessionId,
|
||||
model,
|
||||
model,
|
||||
imageUrls
|
||||
})
|
||||
.then((r) => r.data),
|
||||
|
|
@ -320,7 +320,7 @@ export const SessionAPI = {
|
|||
.then((r) => r.data),
|
||||
/** 导出会话为文件并触发浏览器下载 */
|
||||
exportSession: (agentId: string, sessionId: string, format: 'md' | 'json' = 'md') => {
|
||||
const url = withApiBase(`/agents/${agentId}/sessions/${sessionId}/export?format=${format}`);
|
||||
const url = withApiBase(`/agents/${agentId}/sessions/${sessionId}/export?format=${format}`);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.rel = 'noopener';
|
||||
|
|
@ -473,114 +473,114 @@ export const PromptTemplateAPI = {
|
|||
use: (id: string) => api.post(`/prompt-templates/${id}/use`).then((r) => r.data)
|
||||
};
|
||||
|
||||
// ============== 积分商城 (v1) ==============
|
||||
|
||||
export interface PointsMallMe {
|
||||
points: number;
|
||||
level?: string;
|
||||
totalSpentUSD?: number;
|
||||
}
|
||||
|
||||
export interface PointsExchangeRequest {
|
||||
productId: string;
|
||||
recipientName: string;
|
||||
phone: string;
|
||||
province: string;
|
||||
city: string;
|
||||
district: string;
|
||||
address: string;
|
||||
zipCode?: string;
|
||||
}
|
||||
|
||||
export interface PointsExchangeResponse {
|
||||
orderId: string;
|
||||
pointsDeducted: number;
|
||||
remainingPoints: number;
|
||||
}
|
||||
|
||||
export interface PointsMallCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
export interface PointsMallAnnouncement {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
linkUrl?: string;
|
||||
}
|
||||
|
||||
export interface PointsMallBanner {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
imageUrl: string;
|
||||
linkUrl?: string;
|
||||
}
|
||||
|
||||
export interface PointsMallPromoEntry {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
iconUrl?: string;
|
||||
linkUrl?: string;
|
||||
}
|
||||
|
||||
export interface PointsMallProduct {
|
||||
id: string;
|
||||
categoryId: string;
|
||||
name: string;
|
||||
subtitle?: string;
|
||||
coverUrl: string;
|
||||
pointsPrice: number;
|
||||
stock: number;
|
||||
sold: number;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface PointsMallOverview {
|
||||
me: PointsMallMe;
|
||||
categories: PointsMallCategory[];
|
||||
announcements: PointsMallAnnouncement[];
|
||||
banners: PointsMallBanner[];
|
||||
promoEntries: PointsMallPromoEntry[];
|
||||
}
|
||||
|
||||
export interface PointsMallProductsResponse {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
items: PointsMallProduct[];
|
||||
}
|
||||
|
||||
export const PointsMallAPI = {
|
||||
overview: () => api.get<PointsMallOverview>('/points-mall/overview').then((r) => r.data),
|
||||
products: (opts: {
|
||||
categoryId?: string;
|
||||
q?: string;
|
||||
sort?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
} = {}) =>
|
||||
api
|
||||
.get<PointsMallProductsResponse>('/points-mall/products', {
|
||||
params: {
|
||||
categoryId: opts.categoryId,
|
||||
q: opts.q ?? '',
|
||||
sort: opts.sort ?? 'popular',
|
||||
page: opts.page ?? 1,
|
||||
pageSize: opts.pageSize ?? 24
|
||||
}
|
||||
})
|
||||
.then((r) => r.data),
|
||||
exchange: (productId: string, shippingInfo: Omit<PointsExchangeRequest, 'productId'>) =>
|
||||
api.post<PointsExchangeResponse>('/points-mall/exchange', {
|
||||
productId,
|
||||
...shippingInfo
|
||||
}).then((r) => r.data)
|
||||
};
|
||||
|
||||
// ============== 积分商城 (v1) ==============
|
||||
|
||||
export interface PointsMallMe {
|
||||
points: number;
|
||||
level?: string;
|
||||
totalSpentUSD?: number;
|
||||
}
|
||||
|
||||
export interface PointsExchangeRequest {
|
||||
productId: string;
|
||||
recipientName: string;
|
||||
phone: string;
|
||||
province: string;
|
||||
city: string;
|
||||
district: string;
|
||||
address: string;
|
||||
zipCode?: string;
|
||||
}
|
||||
|
||||
export interface PointsExchangeResponse {
|
||||
orderId: string;
|
||||
pointsDeducted: number;
|
||||
remainingPoints: number;
|
||||
}
|
||||
|
||||
export interface PointsMallCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
export interface PointsMallAnnouncement {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
linkUrl?: string;
|
||||
}
|
||||
|
||||
export interface PointsMallBanner {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
imageUrl: string;
|
||||
linkUrl?: string;
|
||||
}
|
||||
|
||||
export interface PointsMallPromoEntry {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
iconUrl?: string;
|
||||
linkUrl?: string;
|
||||
}
|
||||
|
||||
export interface PointsMallProduct {
|
||||
id: string;
|
||||
categoryId: string;
|
||||
name: string;
|
||||
subtitle?: string;
|
||||
coverUrl: string;
|
||||
pointsPrice: number;
|
||||
stock: number;
|
||||
sold: number;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface PointsMallOverview {
|
||||
me: PointsMallMe;
|
||||
categories: PointsMallCategory[];
|
||||
announcements: PointsMallAnnouncement[];
|
||||
banners: PointsMallBanner[];
|
||||
promoEntries: PointsMallPromoEntry[];
|
||||
}
|
||||
|
||||
export interface PointsMallProductsResponse {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
items: PointsMallProduct[];
|
||||
}
|
||||
|
||||
export const PointsMallAPI = {
|
||||
overview: () => api.get<PointsMallOverview>('/points-mall/overview').then((r) => r.data),
|
||||
products: (opts: {
|
||||
categoryId?: string;
|
||||
q?: string;
|
||||
sort?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
} = {}) =>
|
||||
api
|
||||
.get<PointsMallProductsResponse>('/points-mall/products', {
|
||||
params: {
|
||||
categoryId: opts.categoryId,
|
||||
q: opts.q ?? '',
|
||||
sort: opts.sort ?? 'popular',
|
||||
page: opts.page ?? 1,
|
||||
pageSize: opts.pageSize ?? 24
|
||||
}
|
||||
})
|
||||
.then((r) => r.data),
|
||||
exchange: (productId: string, shippingInfo: Omit<PointsExchangeRequest, 'productId'>) =>
|
||||
api.post<PointsExchangeResponse>('/points-mall/exchange', {
|
||||
productId,
|
||||
...shippingInfo
|
||||
}).then((r) => r.data)
|
||||
};
|
||||
|
||||
// ============== 调用统计 (v0.8 P1) ==============
|
||||
|
||||
export interface StatsOverview {
|
||||
|
|
@ -601,45 +601,45 @@ export interface AgentStats {
|
|||
daily: { day: string; count: number }[];
|
||||
}
|
||||
|
||||
export interface AgentTokenStatsByModel {
|
||||
providerKind: string;
|
||||
model: string;
|
||||
calls: number;
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
costUSD: number;
|
||||
}
|
||||
|
||||
export interface AgentTokenStatsDaily {
|
||||
day: string;
|
||||
calls: number;
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
costUSD: number;
|
||||
}
|
||||
|
||||
export interface AgentTokenStats {
|
||||
agentId: string;
|
||||
days: number;
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
costUSD: number;
|
||||
byModel: AgentTokenStatsByModel[];
|
||||
daily: AgentTokenStatsDaily[];
|
||||
}
|
||||
|
||||
export interface AgentTokenStatsByModel {
|
||||
providerKind: string;
|
||||
model: string;
|
||||
calls: number;
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
costUSD: number;
|
||||
}
|
||||
|
||||
export interface AgentTokenStatsDaily {
|
||||
day: string;
|
||||
calls: number;
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
costUSD: number;
|
||||
}
|
||||
|
||||
export interface AgentTokenStats {
|
||||
agentId: string;
|
||||
days: number;
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
costUSD: number;
|
||||
byModel: AgentTokenStatsByModel[];
|
||||
daily: AgentTokenStatsDaily[];
|
||||
}
|
||||
|
||||
export const StatsAPI = {
|
||||
overview: () => api.get<StatsOverview>('/stats/overview').then((r) => r.data),
|
||||
agent: (id: string) => api.get<AgentStats>(`/stats/agents/${id}`).then((r) => r.data),
|
||||
agentTokens: (id: string, opts: { days?: number; limit?: number } = {}) =>
|
||||
api
|
||||
.get<AgentTokenStats>(`/stats/agents/${id}/tokens`, {
|
||||
params: { days: opts.days ?? 30, limit: opts.limit ?? 10 }
|
||||
})
|
||||
.then((r) => r.data)
|
||||
agent: (id: string) => api.get<AgentStats>(`/stats/agents/${id}`).then((r) => r.data),
|
||||
agentTokens: (id: string, opts: { days?: number; limit?: number } = {}) =>
|
||||
api
|
||||
.get<AgentTokenStats>(`/stats/agents/${id}/tokens`, {
|
||||
params: { days: opts.days ?? 30, limit: opts.limit ?? 10 }
|
||||
})
|
||||
.then((r) => r.data)
|
||||
};
|
||||
|
||||
// ============== 流式(手写 SSE,不用 EventSource,因为它不能 POST) ==============
|
||||
|
|
@ -663,17 +663,17 @@ export interface LLMProvider {
|
|||
updatedAt: number;
|
||||
}
|
||||
|
||||
export interface AgentAllowedLLMResponse {
|
||||
agentId: string;
|
||||
model: string;
|
||||
}
|
||||
|
||||
export interface AgentAllowedLLMResponse {
|
||||
agentId: string;
|
||||
model: string;
|
||||
}
|
||||
|
||||
export const LLMProviderAPI = {
|
||||
list: () => api.get<LLMProvider[]>('/llm-providers').then((r) => r.data),
|
||||
allowedModels: (agentId: string) =>
|
||||
api
|
||||
.get<AgentAllowedLLMResponse>('/llm-providers', { params: { agent_id: agentId } })
|
||||
.then((r) => r.data),
|
||||
allowedModels: (agentId: string) =>
|
||||
api
|
||||
.get<AgentAllowedLLMResponse>('/llm-providers', { params: { agent_id: agentId } })
|
||||
.then((r) => r.data),
|
||||
create: (payload: Partial<LLMProvider> & { apiKey?: string }) =>
|
||||
api.post<{ id: string }>('/llm-providers', payload).then((r) => r.data),
|
||||
update: (id: string, payload: Partial<LLMProvider> & { apiKey?: string }) =>
|
||||
|
|
@ -689,10 +689,10 @@ export const LLMProviderAPI = {
|
|||
|
||||
|
||||
export interface StreamEvents {
|
||||
onMeta?: (data: any) => void;
|
||||
onReasoningDelta?: (text: string) => void;
|
||||
onDelta?: (text: string) => void;
|
||||
onRetry?: (data: any) => void;
|
||||
onMeta?: (data: any) => void;
|
||||
onReasoningDelta?: (text: string) => void;
|
||||
onDelta?: (text: string) => void;
|
||||
onRetry?: (data: any) => void;
|
||||
onToolCall?: (data: { id: string; name: string; args: any }) => void;
|
||||
onToolResult?: (data: { id: string; name: string; result: any }) => void;
|
||||
onDone?: (data: { user: ChatMessage; assistant: ChatMessage }) => void;
|
||||
|
|
@ -708,30 +708,30 @@ export interface ModelOverrides {
|
|||
maxTokens?: number;
|
||||
}
|
||||
|
||||
export async function streamChat(
|
||||
agentId: string,
|
||||
content: string,
|
||||
handlers: StreamEvents,
|
||||
signal?: AbortSignal,
|
||||
sessionId?: string,
|
||||
model?: string,
|
||||
modelId?: string,
|
||||
imageUrls?: string[]
|
||||
) {
|
||||
const resp = await fetch(`https://api.hoyidata.com/aura/v1/chat/${agentId}/messages/stream`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
|
||||
body: JSON.stringify({
|
||||
content,
|
||||
sessionId,
|
||||
model,
|
||||
model_id: modelId,
|
||||
imageUrls: imageUrls ?? []
|
||||
}),
|
||||
signal,
|
||||
credentials: 'include'
|
||||
});
|
||||
return await consumeSSE(resp, handlers, signal);
|
||||
export async function streamChat(
|
||||
agentId: string,
|
||||
content: string,
|
||||
handlers: StreamEvents,
|
||||
signal?: AbortSignal,
|
||||
sessionId?: string,
|
||||
model?: string,
|
||||
modelId?: string,
|
||||
imageUrls?: string[]
|
||||
) {
|
||||
const resp = await fetch(`https://api.hoyidata.com/aura/v1/chat/${agentId}/messages/stream`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
|
||||
body: JSON.stringify({
|
||||
content,
|
||||
sessionId,
|
||||
model,
|
||||
model_id: modelId,
|
||||
imageUrls: imageUrls ?? []
|
||||
}),
|
||||
signal,
|
||||
credentials: 'include'
|
||||
});
|
||||
return await consumeSSE(resp, handlers, signal);
|
||||
}
|
||||
|
||||
/** 重新生成(开新分支);行为同 streamChat */
|
||||
|
|
@ -745,7 +745,7 @@ export async function regenerateMessage(
|
|||
) {
|
||||
const resp = await fetch(`https://api.hoyidata.com/aura/v1/chat/${agentId}/messages/${messageId}/regenerate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
|
||||
body: JSON.stringify({ overrides, attachmentsText }),
|
||||
signal,
|
||||
credentials: 'include'
|
||||
|
|
@ -753,94 +753,94 @@ export async function regenerateMessage(
|
|||
return await consumeSSE(resp, handlers, signal);
|
||||
}
|
||||
|
||||
async function consumeSSE(resp: Response, h: StreamEvents, signal?: AbortSignal) {
|
||||
if (!resp.ok || !resp.body) {
|
||||
const txt = await resp.text().catch(() => '');
|
||||
h.onError?.(`HTTP ${resp.status}: ${txt}`);
|
||||
return;
|
||||
}
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let buf = '';
|
||||
let hasError = false; // 标记是否已发生错误,避免后续事件继续处理
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done || hasError) break;
|
||||
|
||||
buf += decoder.decode(value, { stream: true });
|
||||
buf = buf.replace(/\r\n/g, '\n');
|
||||
|
||||
let idx;
|
||||
while ((idx = buf.indexOf('\n\n')) !== -1 && !hasError) {
|
||||
const raw = buf.slice(0, idx);
|
||||
buf = buf.slice(idx + 2);
|
||||
if (!raw.trim() || raw.startsWith(':')) continue;
|
||||
|
||||
let event = 'message';
|
||||
let dataStr = '';
|
||||
for (const line of raw.split('\n')) {
|
||||
if (line.startsWith('event:')) event = line.slice(6).trim();
|
||||
else if (line.startsWith('data:')) {
|
||||
let part = line.slice(5);
|
||||
if (part.startsWith(' ')) part = part.slice(1);
|
||||
dataStr += (dataStr ? '\n' : '') + part;
|
||||
}
|
||||
}
|
||||
if (!dataStr) continue;
|
||||
let data: any;
|
||||
try {
|
||||
data = JSON.parse(dataStr);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
switch (event) {
|
||||
case 'meta':
|
||||
h.onMeta?.(data);
|
||||
break;
|
||||
case 'retry':
|
||||
h.onRetry?.(data);
|
||||
break;
|
||||
case 'reasoning_delta':
|
||||
h.onReasoningDelta?.(data.content || '');
|
||||
break;
|
||||
case 'delta':
|
||||
h.onDelta?.(data.content || '');
|
||||
break;
|
||||
case 'tool_call':
|
||||
h.onToolCall?.(data);
|
||||
break;
|
||||
case 'tool_result':
|
||||
h.onToolResult?.(data);
|
||||
break;
|
||||
case 'done':
|
||||
// 只有在没有错误的情况下才处理 done 事件
|
||||
if (!hasError) {
|
||||
h.onDone?.(data);
|
||||
}
|
||||
break;
|
||||
case 'aborted':
|
||||
h.onAborted?.(data);
|
||||
break;
|
||||
case 'error':
|
||||
hasError = true;
|
||||
h.onError?.(data.message || 'stream error');
|
||||
// 发生错误后立即停止读取
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (signal?.aborted || e?.name === 'AbortError') {
|
||||
// 静默:本地已 abort
|
||||
return;
|
||||
}
|
||||
h.onError?.(e?.message ?? String(e));
|
||||
} finally {
|
||||
// 确保 reader 被释放
|
||||
reader.cancel().catch(() => {});
|
||||
}
|
||||
async function consumeSSE(resp: Response, h: StreamEvents, signal?: AbortSignal) {
|
||||
if (!resp.ok || !resp.body) {
|
||||
const txt = await resp.text().catch(() => '');
|
||||
h.onError?.(`HTTP ${resp.status}: ${txt}`);
|
||||
return;
|
||||
}
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let buf = '';
|
||||
let hasError = false; // 标记是否已发生错误,避免后续事件继续处理
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done || hasError) break;
|
||||
|
||||
buf += decoder.decode(value, { stream: true });
|
||||
buf = buf.replace(/\r\n/g, '\n');
|
||||
|
||||
let idx;
|
||||
while ((idx = buf.indexOf('\n\n')) !== -1 && !hasError) {
|
||||
const raw = buf.slice(0, idx);
|
||||
buf = buf.slice(idx + 2);
|
||||
if (!raw.trim() || raw.startsWith(':')) continue;
|
||||
|
||||
let event = 'message';
|
||||
let dataStr = '';
|
||||
for (const line of raw.split('\n')) {
|
||||
if (line.startsWith('event:')) event = line.slice(6).trim();
|
||||
else if (line.startsWith('data:')) {
|
||||
let part = line.slice(5);
|
||||
if (part.startsWith(' ')) part = part.slice(1);
|
||||
dataStr += (dataStr ? '\n' : '') + part;
|
||||
}
|
||||
}
|
||||
if (!dataStr) continue;
|
||||
let data: any;
|
||||
try {
|
||||
data = JSON.parse(dataStr);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
switch (event) {
|
||||
case 'meta':
|
||||
h.onMeta?.(data);
|
||||
break;
|
||||
case 'retry':
|
||||
h.onRetry?.(data);
|
||||
break;
|
||||
case 'reasoning_delta':
|
||||
h.onReasoningDelta?.(data.content || '');
|
||||
break;
|
||||
case 'delta':
|
||||
h.onDelta?.(data.content || '');
|
||||
break;
|
||||
case 'tool_call':
|
||||
h.onToolCall?.(data);
|
||||
break;
|
||||
case 'tool_result':
|
||||
h.onToolResult?.(data);
|
||||
break;
|
||||
case 'done':
|
||||
// 只有在没有错误的情况下才处理 done 事件
|
||||
if (!hasError) {
|
||||
h.onDone?.(data);
|
||||
}
|
||||
break;
|
||||
case 'aborted':
|
||||
h.onAborted?.(data);
|
||||
break;
|
||||
case 'error':
|
||||
hasError = true;
|
||||
h.onError?.(data.message || 'stream error');
|
||||
// 发生错误后立即停止读取
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (signal?.aborted || e?.name === 'AbortError') {
|
||||
// 静默:本地已 abort
|
||||
return;
|
||||
}
|
||||
h.onError?.(e?.message ?? String(e));
|
||||
} finally {
|
||||
// 确保 reader 被释放
|
||||
reader.cancel().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Workflow (v1.1) ==============
|
||||
|
|
@ -936,7 +936,7 @@ export function streamWorkflowRun(
|
|||
}
|
||||
): () => void {
|
||||
const qs = input ? `?input=${encodeURIComponent(JSON.stringify(input))}` : '';
|
||||
const url = withApiBase(`/workflows/${workflowId}/run-stream${qs}`);
|
||||
const url = withApiBase(`/workflows/${workflowId}/run-stream${qs}`);
|
||||
const es = new EventSource(url, { withCredentials: true } as any);
|
||||
es.addEventListener('ready', (e: any) => handlers.onReady?.(JSON.parse(e.data)));
|
||||
es.addEventListener('step_start', (e: any) => handlers.onStepStart?.(JSON.parse(e.data)));
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export default function ModelCheckboxDropdown({ value = [], onChange, models }:
|
|||
const inputPrice = 2 * m.model_ratio;
|
||||
const outputPrice = inputPrice * m.completion_ratio;
|
||||
return (
|
||||
<Checkbox key={m.model_id} value={m.model_id} className="agent-model-checkbox-item">
|
||||
<Checkbox key={m.id} value={m.id} className="agent-model-checkbox-item">
|
||||
<div className="agent-model-checkbox-content">
|
||||
<div className="agent-model-checkbox-meta">
|
||||
<img
|
||||
|
|
|
|||
Loading…
Reference in New Issue