Files
TodoList/apps/web/src/services/ai-api.ts
T

155 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { WebSession } from "@/services/session-storage";
export type WebAiChannel = "USER_KEY" | "ASTRBOT" | "PUBLIC_POOL";
export type WebAiRouteAttempt = {
channel: WebAiChannel;
providerName: string | null;
model: string | null;
status: "skipped" | "failed" | "success";
reasonCode: string | null;
reasonMessage: string | null;
};
export type WebAiBindingSummary = {
id: string;
channel: WebAiChannel;
providerName: string;
model: string | null;
configId: string | null;
configName: string | null;
endpoint: string | null;
isEnabled: boolean;
hasApiKey: boolean;
maskedApiKey: string | null;
updatedAt: string;
};
export type WebAiBindingsResponse = {
routeOrder: WebAiChannel[];
bindings: WebAiBindingSummary[];
publicPool: {
enabled: boolean;
providerName: string | null;
model: string | null;
hasApiKey: boolean;
} | null;
};
export type UpsertWebAiBindingInput = {
channel: Exclude<WebAiChannel, "PUBLIC_POOL">;
providerName?: string;
model?: string;
configId?: string;
configName?: string;
endpoint?: string;
apiKey?: string;
isEnabled?: boolean;
};
export type WebAiChatResponse = {
channel: WebAiChannel;
providerName: string;
model: string | null;
content: string;
sessionId: string | null;
attempts: WebAiRouteAttempt[];
};
export class WebAiApiError extends Error {
attempts: WebAiRouteAttempt[] | null;
constructor(message: string, attempts?: WebAiRouteAttempt[] | null) {
super(message);
this.name = "WebAiApiError";
this.attempts = attempts ?? null;
}
}
const DEFAULT_API_BASE_URL = "http://localhost:3000";
function resolveApiBaseUrl(): string {
const envBaseUrl = import.meta.env.VITE_API_BASE_URL as string | undefined;
if (!envBaseUrl) {
return DEFAULT_API_BASE_URL;
}
return envBaseUrl.replace(/\/+$/, "");
}
function createHeaders(session: WebSession): HeadersInit {
return {
"Content-Type": "application/json",
Authorization: `Bearer ${session.accessToken}`,
"x-user-id": session.user.id
};
}
async function createApiError(response: Response): Promise<WebAiApiError> {
try {
const body = (await response.json()) as {
message?: string | string[];
attempts?: WebAiRouteAttempt[];
};
const message = Array.isArray(body.message)
? body.message.join("")
: typeof body.message === "string" && body.message.trim().length > 0
? body.message
: `请求失败(${response.status}`;
return new WebAiApiError(message, body.attempts ?? null);
} catch {
return new WebAiApiError(`请求失败(${response.status}`);
}
}
export async function listAiBindings(session: WebSession): Promise<WebAiBindingsResponse> {
const response = await fetch(`${resolveApiBaseUrl()}/ai/bindings`, {
method: "GET",
headers: createHeaders(session)
});
if (!response.ok) {
throw await createApiError(response);
}
return (await response.json()) as WebAiBindingsResponse;
}
export async function upsertAiBinding(
session: WebSession,
payload: UpsertWebAiBindingInput
): Promise<WebAiBindingSummary> {
const response = await fetch(`${resolveApiBaseUrl()}/ai/bindings`, {
method: "POST",
headers: createHeaders(session),
body: JSON.stringify(payload)
});
if (!response.ok) {
throw await createApiError(response);
}
return (await response.json()) as WebAiBindingSummary;
}
export async function chatWithAi(
session: WebSession,
payload: {
channel: WebAiChannel;
message: string;
sessionId?: string;
}
): Promise<WebAiChatResponse> {
const response = await fetch(`${resolveApiBaseUrl()}/ai/chat`, {
method: "POST",
headers: createHeaders(session),
body: JSON.stringify(payload)
});
if (!response.ok) {
throw await createApiError(response);
}
return (await response.json()) as WebAiChatResponse;
}