feat(admin): 添加网络代理配置和钱包交易功能

- 在管理面板中集成网络代理配置显示和平台代理设置
- 添加钱包摘要和交易列表API接口及数据管理
- 实现SSE流式响应中的错误处理机制
- 添加全局HTTP代理环境变量配置支持
- 更新平台表单以支持代理模式选择和自定义代理地址
- 集成钱包交易查询过滤和分页功能
- 优化API错误详情解析和显示格式
This commit is contained in:
2026-05-11 23:02:10 +08:00
parent c992f1de60
commit f550c0acd5
30 changed files with 1455 additions and 76 deletions
+131 -16
View File
@@ -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;
}