feat(web-ai): add channel-aware assistant panel

This commit is contained in:
2026-04-06 13:51:44 +08:00
parent d0ba581184
commit ea23f6264c
4 changed files with 977 additions and 4 deletions
+155
View File
@@ -0,0 +1,155 @@
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;
endpoint: 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;
}