添加聊天框消息解析功能;优化请求头处理;更新部分 api base url
This commit is contained in:
+19
-19
@@ -1,9 +1,11 @@
|
||||
import axios from "axios";
|
||||
import axios, { AxiosHeaders, type InternalAxiosRequestConfig } from "axios";
|
||||
import { config } from "@config/config";
|
||||
import { useProjectStore } from "@/store/projectStore";
|
||||
import { getAccessToken } from "@/lib/authToken";
|
||||
import { signOut } from "next-auth/react";
|
||||
import { useAuthStore } from "@/store/authStore";
|
||||
import {
|
||||
applyAuthContextHeaders,
|
||||
type AuthContextHeaderOptions,
|
||||
} from "@/lib/requestHeaders";
|
||||
|
||||
export const API_URL = process.env.NEXT_PUBLIC_API_URL || config.BACKEND_URL;
|
||||
|
||||
@@ -13,26 +15,24 @@ export const api = axios.create({
|
||||
|
||||
let isSigningOut = false;
|
||||
|
||||
const isMetaProjectsRequest = (request: {
|
||||
const resolveRequestUrl = (request: {
|
||||
baseURL?: string;
|
||||
url?: string;
|
||||
}) => {
|
||||
const url = `${request.baseURL ?? ""}${request.url ?? ""}`;
|
||||
return url.includes("/api/v1/meta/projects");
|
||||
};
|
||||
}) => `${request.baseURL ?? ""}${request.url ?? ""}`;
|
||||
|
||||
api.interceptors.request.use(async (request) => {
|
||||
const accessToken = await getAccessToken();
|
||||
if (accessToken) {
|
||||
request.headers = request.headers ?? {};
|
||||
request.headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
export interface ApiRequestConfig
|
||||
extends InternalAxiosRequestConfig,
|
||||
AuthContextHeaderOptions {}
|
||||
|
||||
const projectId = useProjectStore.getState().currentProjectId;
|
||||
if (projectId && !isMetaProjectsRequest(request)) {
|
||||
request.headers = request.headers ?? {};
|
||||
request.headers["X-Project-Id"] = projectId;
|
||||
}
|
||||
api.interceptors.request.use(async (request: ApiRequestConfig) => {
|
||||
const headers = new Headers(
|
||||
request.headers
|
||||
? AxiosHeaders.from(request.headers).toJSON() as Record<string, string>
|
||||
: undefined,
|
||||
);
|
||||
await applyAuthContextHeaders(resolveRequestUrl(request), headers, request);
|
||||
|
||||
request.headers = AxiosHeaders.from(Object.fromEntries(headers.entries()));
|
||||
|
||||
return request;
|
||||
});
|
||||
|
||||
+6
-14
@@ -1,7 +1,9 @@
|
||||
import { useProjectStore } from "@/store/projectStore";
|
||||
import { getAccessToken } from "@/lib/authToken";
|
||||
import { signOut } from "next-auth/react";
|
||||
import { useAuthStore } from "@/store/authStore";
|
||||
import {
|
||||
applyAuthContextHeaders,
|
||||
type AuthContextHeaderOptions,
|
||||
} from "@/lib/requestHeaders";
|
||||
|
||||
let isSigningOut = false;
|
||||
|
||||
@@ -12,10 +14,7 @@ const resolveUrl = (input: RequestInfo | URL) => {
|
||||
return "";
|
||||
};
|
||||
|
||||
const isMetaProjectsRequest = (input: RequestInfo | URL) =>
|
||||
resolveUrl(input).includes("/api/v1/meta/projects");
|
||||
|
||||
export interface ApiFetchInit extends RequestInit {
|
||||
export interface ApiFetchInit extends RequestInit, AuthContextHeaderOptions {
|
||||
skipAuthRedirect?: boolean;
|
||||
}
|
||||
|
||||
@@ -23,15 +22,8 @@ export const apiFetch = async (
|
||||
input: RequestInfo | URL,
|
||||
init: ApiFetchInit = {},
|
||||
) => {
|
||||
const projectId = useProjectStore.getState().currentProjectId;
|
||||
const headers = new Headers(init.headers ?? {});
|
||||
const accessToken = await getAccessToken();
|
||||
if (accessToken) {
|
||||
headers.set("Authorization", `Bearer ${accessToken}`);
|
||||
}
|
||||
if (projectId && !isMetaProjectsRequest(input)) {
|
||||
headers.set("X-Project-Id", projectId);
|
||||
}
|
||||
await applyAuthContextHeaders(resolveUrl(input), headers, init);
|
||||
|
||||
const response = await fetch(input, { ...init, headers });
|
||||
|
||||
|
||||
@@ -54,6 +54,15 @@ describe("streamCopilotChat", () => {
|
||||
onEvent: (event) => events.push(event),
|
||||
});
|
||||
|
||||
expect(apiFetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining("/api/v1/copilot/chat/stream"),
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
projectHeaderMode: "include",
|
||||
skipAuthRedirect: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(events).toEqual([
|
||||
{ type: "token", conversationId: "c1", content: "he" },
|
||||
{ type: "token", conversationId: "c1", content: "llo" },
|
||||
|
||||
+26
-16
@@ -4,7 +4,12 @@ import { config } from "@config/config";
|
||||
export type StreamEvent =
|
||||
| { type: "token"; conversationId: string; content: string }
|
||||
| { type: "done"; conversationId: string }
|
||||
| { type: "error"; conversationId?: string; message: string; detail?: string };
|
||||
| {
|
||||
type: "error";
|
||||
conversationId?: string;
|
||||
message: string;
|
||||
detail?: string;
|
||||
};
|
||||
|
||||
type StreamOptions = {
|
||||
message: string;
|
||||
@@ -40,19 +45,23 @@ export const streamCopilotChat = async ({
|
||||
}: StreamOptions) => {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await apiFetch(`${config.BACKEND_URL}/api/v1/copilot/chat/stream`, {
|
||||
method: "POST",
|
||||
signal,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
response = await apiFetch(
|
||||
`${config.COPILOT_URL}/api/v1/copilot/chat/stream`,
|
||||
{
|
||||
method: "POST",
|
||||
signal,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
conversation_id: conversationId,
|
||||
}),
|
||||
projectHeaderMode: "include",
|
||||
skipAuthRedirect: true,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
conversation_id: conversationId,
|
||||
}),
|
||||
skipAuthRedirect: true,
|
||||
});
|
||||
);
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
onEvent({
|
||||
@@ -66,17 +75,18 @@ export const streamCopilotChat = async ({
|
||||
if (!response.ok || !response.body) {
|
||||
const detail = await response.text();
|
||||
let message = "stream request failed";
|
||||
|
||||
|
||||
if (response.status === 403) {
|
||||
message = "Permission denied. Please contact administrator.";
|
||||
} else if (response.status === 401) {
|
||||
message = "Login expired. Please sign in again.";
|
||||
}
|
||||
|
||||
|
||||
onEvent({
|
||||
type: "error",
|
||||
message,
|
||||
detail: (response.status === 403 || response.status === 401) ? undefined : detail,
|
||||
detail:
|
||||
response.status === 403 || response.status === 401 ? undefined : detail,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { getAccessToken } from "@/lib/authToken";
|
||||
import { useProjectStore } from "@/store/projectStore";
|
||||
|
||||
export type AuthHeaderMode = "include" | "omit";
|
||||
export type ProjectHeaderMode = "auto" | "include" | "omit";
|
||||
|
||||
export interface AuthContextHeaderOptions {
|
||||
authHeaderMode?: AuthHeaderMode;
|
||||
projectHeaderMode?: ProjectHeaderMode;
|
||||
}
|
||||
|
||||
const shouldIncludeProjectHeader = (
|
||||
url: string,
|
||||
projectHeaderMode: ProjectHeaderMode,
|
||||
) => {
|
||||
if (projectHeaderMode === "include") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (projectHeaderMode === "omit") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !url.includes("/api/v1/meta/projects");
|
||||
};
|
||||
|
||||
export const applyAuthContextHeaders = async (
|
||||
url: string,
|
||||
headers: Headers,
|
||||
options: AuthContextHeaderOptions = {},
|
||||
) => {
|
||||
const accessToken = await getAccessToken();
|
||||
if (accessToken && options.authHeaderMode !== "omit") {
|
||||
headers.set("Authorization", `Bearer ${accessToken}`);
|
||||
}
|
||||
|
||||
const projectId = useProjectStore.getState().currentProjectId;
|
||||
if (
|
||||
projectId &&
|
||||
shouldIncludeProjectHeader(url, options.projectHeaderMode ?? "auto")
|
||||
) {
|
||||
headers.set("X-Project-Id", projectId);
|
||||
}
|
||||
|
||||
return headers;
|
||||
};
|
||||
Reference in New Issue
Block a user