添加聊天框消息解析功能;优化请求头处理;更新部分 api base url

This commit is contained in:
2026-03-27 18:00:30 +08:00
parent 8713e5a468
commit a101e79750
11 changed files with 464 additions and 193 deletions
+19 -19
View File
@@ -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
View File
@@ -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 });
+9
View File
@@ -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
View File
@@ -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;
}
+46
View File
@@ -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;
};