feat(admin): 添加网络代理配置和钱包交易功能
- 在管理面板中集成网络代理配置显示和平台代理设置 - 添加钱包摘要和交易列表API接口及数据管理 - 实现SSE流式响应中的错误处理机制 - 添加全局HTTP代理环境变量配置支持 - 更新平台表单以支持代理模式选择和自定义代理地址 - 集成钱包交易查询过滤和分页功能 - 优化API错误详情解析和显示格式
This commit is contained in:
+131
-16
@@ -12,9 +12,11 @@ import type {
|
||||
GatewayAuditLog,
|
||||
GatewayTenant,
|
||||
GatewayTenantUpsertRequest,
|
||||
GatewayNetworkProxyConfig,
|
||||
GatewayTask,
|
||||
GatewayUser,
|
||||
GatewayUserUpsertRequest,
|
||||
GatewayWalletTransaction,
|
||||
IntegrationPlatform,
|
||||
ListResponse,
|
||||
ModelCatalogResponse,
|
||||
@@ -30,11 +32,35 @@ import type {
|
||||
UserGroupUpsertRequest,
|
||||
WalletAdjustmentResponse,
|
||||
WalletBalanceAdjustmentRequest,
|
||||
WalletSummaryResponse,
|
||||
} from '@easyai-ai-gateway/contracts';
|
||||
import type { PlatformCreateInput, PlatformModelBindingInput, WorkspaceTaskQuery } from './types';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_GATEWAY_API_BASE_URL ?? 'http://localhost:8088';
|
||||
|
||||
interface GatewayErrorDetails {
|
||||
code?: string;
|
||||
message: string;
|
||||
requestId?: string;
|
||||
status?: number;
|
||||
taskId?: string;
|
||||
}
|
||||
|
||||
export class GatewayApiError extends Error {
|
||||
readonly details: GatewayErrorDetails;
|
||||
|
||||
constructor(details: GatewayErrorDetails | string) {
|
||||
const normalized = typeof details === 'string' ? { message: details } : details;
|
||||
super(formatGatewayErrorDetails(normalized));
|
||||
this.name = 'GatewayApiError';
|
||||
this.details = normalized;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.message;
|
||||
}
|
||||
}
|
||||
|
||||
export interface HealthResponse {
|
||||
ok: boolean;
|
||||
service: string;
|
||||
@@ -508,7 +534,7 @@ export async function* streamChatCompletionText(
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
throw new Error(parseErrorMessage(body) || `Request failed: ${response.status}`);
|
||||
throw new GatewayApiError(parseErrorDetails(body, response.status, `Request failed: ${response.status}`));
|
||||
}
|
||||
if (!response.body) {
|
||||
return;
|
||||
@@ -523,13 +549,15 @@ export async function* streamChatCompletionText(
|
||||
const events = buffer.split(/\n\n/);
|
||||
buffer = events.pop() ?? '';
|
||||
for (const eventBlock of events) {
|
||||
const delta = parseSSEBlockDelta(eventBlock);
|
||||
if (delta) yield delta;
|
||||
const event = parseSSEBlock(eventBlock);
|
||||
if (event.error) throw new GatewayApiError(event.error);
|
||||
if (event.delta) yield event.delta;
|
||||
}
|
||||
}
|
||||
if (buffer.trim()) {
|
||||
const delta = parseSSEBlockDelta(buffer);
|
||||
if (delta) yield delta;
|
||||
const event = parseSSEBlock(buffer);
|
||||
if (event.error) throw new GatewayApiError(event.error);
|
||||
if (event.delta) yield event.delta;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,7 +635,7 @@ export async function estimatePricing(
|
||||
}
|
||||
|
||||
export async function getTask(token: string, taskId: string): Promise<GatewayTask> {
|
||||
return request<GatewayTask>(`/api/v1/tasks/${taskId}`, { token });
|
||||
return request<GatewayTask>(`/api/workspace/tasks/${taskId}`, { token });
|
||||
}
|
||||
|
||||
export async function listTasks(token: string, query: WorkspaceTaskQuery): Promise<ListResponse<GatewayTask>> {
|
||||
@@ -619,7 +647,27 @@ export async function listTasks(token: string, query: WorkspaceTaskQuery): Promi
|
||||
if (query.modelType) search.set('modelType', query.modelType);
|
||||
if (query.createdFrom) search.set('createdFrom', query.createdFrom);
|
||||
if (query.createdTo) search.set('createdTo', query.createdTo);
|
||||
return request<ListResponse<GatewayTask>>(`/api/v1/tasks?${search.toString()}`, { token });
|
||||
return request<ListResponse<GatewayTask>>(`/api/workspace/tasks?${search.toString()}`, { token });
|
||||
}
|
||||
|
||||
export async function getWalletSummary(token: string): Promise<WalletSummaryResponse> {
|
||||
return request<WalletSummaryResponse>('/api/workspace/wallet', { token });
|
||||
}
|
||||
|
||||
export async function listWalletTransactions(
|
||||
token: string,
|
||||
input: { query?: string; direction?: string; transactionType?: string; createdFrom?: string; createdTo?: string; page?: number; pageSize?: number } = {},
|
||||
): Promise<ListResponse<GatewayWalletTransaction>> {
|
||||
const search = new URLSearchParams({
|
||||
page: String(input.page ?? 1),
|
||||
pageSize: String(input.pageSize ?? 50),
|
||||
});
|
||||
if (input.query) search.set('q', input.query);
|
||||
if (input.direction) search.set('direction', input.direction);
|
||||
if (input.transactionType) search.set('transactionType', input.transactionType);
|
||||
if (input.createdFrom) search.set('createdFrom', input.createdFrom);
|
||||
if (input.createdTo) search.set('createdTo', input.createdTo);
|
||||
return request<ListResponse<GatewayWalletTransaction>>(`/api/workspace/wallet/transactions?${search.toString()}`, { token });
|
||||
}
|
||||
|
||||
export function resolveApiAssetUrl(src: string) {
|
||||
@@ -631,6 +679,10 @@ export async function listRateLimitWindows(token: string): Promise<ListResponse<
|
||||
return request<ListResponse<RateLimitWindow>>('/api/admin/runtime/rate-limit-windows', { token });
|
||||
}
|
||||
|
||||
export async function getNetworkProxyConfig(token: string): Promise<GatewayNetworkProxyConfig> {
|
||||
return request<GatewayNetworkProxyConfig>('/api/admin/config/network-proxy', { token });
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
path: string,
|
||||
options: { token?: string; auth?: boolean; method?: string; body?: unknown } = {},
|
||||
@@ -649,7 +701,7 @@ async function request<T>(
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
throw new Error(parseErrorMessage(body) || `Request failed: ${response.status}`);
|
||||
throw new GatewayApiError(parseErrorDetails(body, response.status, `Request failed: ${response.status}`));
|
||||
}
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
@@ -658,33 +710,96 @@ async function request<T>(
|
||||
}
|
||||
|
||||
function parseErrorMessage(body: string) {
|
||||
return formatGatewayErrorDetails(parseErrorDetails(body));
|
||||
}
|
||||
|
||||
function parseErrorDetails(body: string, status?: number, fallback = ''): GatewayErrorDetails {
|
||||
if (!body) {
|
||||
return '';
|
||||
return { message: fallback, status };
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(body) as { error?: { message?: string } };
|
||||
return parsed.error?.message ?? body;
|
||||
const parsed = JSON.parse(body) as unknown;
|
||||
return errorDetailsFromParsed(parsed, status, fallback || body);
|
||||
} catch {
|
||||
return body;
|
||||
return { message: body || fallback, status };
|
||||
}
|
||||
}
|
||||
|
||||
function parseSSEBlockDelta(block: string) {
|
||||
function parseSSEBlock(block: string): { delta: string; error?: GatewayErrorDetails } {
|
||||
const eventName = block
|
||||
.split(/\n/)
|
||||
.find((line) => line.startsWith('event:'))
|
||||
?.replace(/^event:\s?/, '')
|
||||
.trim();
|
||||
const data = block
|
||||
.split(/\n/)
|
||||
.filter((line) => line.startsWith('data:'))
|
||||
.map((line) => line.replace(/^data:\s?/, ''))
|
||||
.join('\n')
|
||||
.trim();
|
||||
if (!data || data === '[DONE]') return '';
|
||||
if (!data || data === '[DONE]') return { delta: '' };
|
||||
try {
|
||||
const parsed = JSON.parse(data) as {
|
||||
choices?: Array<{ delta?: { content?: string }; message?: { content?: string } }>;
|
||||
delta?: string;
|
||||
error?: unknown;
|
||||
output_text?: string;
|
||||
};
|
||||
return parsed.choices?.[0]?.delta?.content ?? parsed.delta ?? parsed.output_text ?? '';
|
||||
if (eventName === 'error' || parsed.error) {
|
||||
return { delta: '', error: errorDetailsFromParsed(parsed, undefined, data) };
|
||||
}
|
||||
return { delta: parsed.choices?.[0]?.delta?.content ?? parsed.delta ?? parsed.output_text ?? '' };
|
||||
} catch {
|
||||
return data;
|
||||
if (eventName === 'error') return { delta: '', error: { message: data } };
|
||||
return { delta: data };
|
||||
}
|
||||
}
|
||||
|
||||
function errorDetailsFromParsed(parsed: unknown, status?: number, fallback = ''): GatewayErrorDetails {
|
||||
const root = recordFromUnknown(parsed);
|
||||
if (!root) return { message: fallback, status };
|
||||
const error = recordFromUnknown(root.error);
|
||||
const message = firstString(
|
||||
error?.message,
|
||||
error?.error,
|
||||
root.message,
|
||||
root.errorMessage,
|
||||
fallback,
|
||||
);
|
||||
return {
|
||||
code: firstString(error?.code, error?.type, root.code, root.errorCode),
|
||||
message: message || fallback,
|
||||
requestId: firstString(error?.requestId, error?.request_id, root.requestId, root.request_id),
|
||||
status: numberFromUnknown(error?.status) ?? numberFromUnknown(root.status) ?? status,
|
||||
taskId: firstString(error?.taskId, error?.task_id, root.taskId, root.task_id),
|
||||
};
|
||||
}
|
||||
|
||||
function formatGatewayErrorDetails(details: GatewayErrorDetails) {
|
||||
const message = details.message || '请求失败';
|
||||
const meta = [
|
||||
details.code ? `错误码: ${details.code}` : '',
|
||||
details.status ? `状态: ${details.status}` : '',
|
||||
details.requestId ? `RequestID: ${details.requestId}` : '',
|
||||
details.taskId ? `TaskID: ${details.taskId}` : '',
|
||||
].filter(Boolean);
|
||||
return meta.length ? `${message}(${meta.join(',')})` : message;
|
||||
}
|
||||
|
||||
function recordFromUnknown(value: unknown): Record<string, unknown> | undefined {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function firstString(...values: unknown[]) {
|
||||
return values.map((value) => typeof value === 'string' ? value.trim() : '').find(Boolean) ?? '';
|
||||
}
|
||||
|
||||
function numberFromUnknown(value: unknown) {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) return parsed;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user