feat(chat): 完善运行状态与权限交互

This commit is contained in:
2026-08-06 18:41:53 +08:00
parent 8d7c947897
commit 9e75e2df8a
21 changed files with 1025 additions and 226 deletions
+44
View File
@@ -0,0 +1,44 @@
import { fetchAgentRuntimeHealth } from "./agentRuntime";
const apiFetch = jest.fn();
jest.mock("@/lib/apiFetch", () => ({
apiFetch: (...args: unknown[]) => apiFetch(...args),
}));
describe("fetchAgentRuntimeHealth", () => {
beforeEach(() => {
apiFetch.mockReset();
});
it("reports ready only after runtime warmup is complete", async () => {
apiFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
ok: true,
ready: true,
warmed_up: true,
runtime: { healthy: true },
}),
});
await expect(fetchAgentRuntimeHealth()).resolves.toBe(true);
expect(apiFetch).toHaveBeenCalledWith(
expect.stringContaining("/health"),
expect.objectContaining({
method: "GET",
projectHeaderMode: "omit",
skipAuthRedirect: true,
}),
);
});
it("reports unavailable when runtime health is not ready", async () => {
apiFetch.mockResolvedValueOnce({
ok: false,
json: async () => ({ ok: false, ready: false, warmed_up: true }),
});
await expect(fetchAgentRuntimeHealth()).resolves.toBe(false);
});
});
+37
View File
@@ -0,0 +1,37 @@
import { apiFetch } from "@/lib/apiFetch";
import { config } from "@config/config";
export type AgentRuntimeState =
| "checking"
| "ready"
| "unavailable"
| "models_unavailable";
type AgentHealthPayload = {
ok?: unknown;
ready?: unknown;
warmed_up?: unknown;
runtime?: {
healthy?: unknown;
};
};
export const fetchAgentRuntimeHealth = async (
signal?: AbortSignal,
): Promise<boolean> => {
const response = await apiFetch(`${config.AGENT_URL}/health`, {
method: "GET",
signal,
projectHeaderMode: "omit",
skipAuthRedirect: true,
});
const payload = (await response.json().catch(() => null)) as AgentHealthPayload | null;
return Boolean(
response.ok &&
payload?.ok === true &&
payload.ready === true &&
payload.warmed_up === true &&
payload.runtime?.healthy === true,
);
};
+2 -1
View File
@@ -46,9 +46,10 @@ const normalizeModelOption = (value: unknown): AgentModelOption | null => {
};
};
export const fetchAgentModels = async (): Promise<AgentModelConfig> => {
export const fetchAgentModels = async (signal?: AbortSignal): Promise<AgentModelConfig> => {
const response = await apiFetch(`${config.AGENT_URL}/api/v1/agent/models`, {
method: "GET",
signal,
projectHeaderMode: "include",
skipAuthRedirect: true,
});
+14
View File
@@ -390,6 +390,7 @@ describe("streamAgentChat", () => {
skipAuthRedirect: true,
}),
);
});
it("calls permission reply endpoint", async () => {
@@ -413,6 +414,19 @@ describe("streamAgentChat", () => {
}),
}),
);
await replyAgentPermission("s1", "perm-2", "always");
expect(apiFetch).toHaveBeenLastCalledWith(
expect.stringContaining("/api/v1/agent/sessions/s1/permission-responses"),
expect.objectContaining({
method: "POST",
body: JSON.stringify({
request_id: "perm-2",
reply: "always",
}),
}),
);
});
it("submits refreshed credentials through the authenticated context", async () => {
+4 -3
View File
@@ -8,8 +8,9 @@ const getAgentSessionUrl = (sessionId: string, suffix = "") =>
export type AgentModel = string;
export type PermissionReply = "once" | "always" | "reject";
export type AgentApprovalMode = "request" | "always";
export type PermissionDecision = "once" | "always" | "reject";
export type PermissionReply = PermissionDecision;
export type AgentApprovalMode = "request" | "auto" | "always";
export type AgentQuestionStatus =
| "pending"
@@ -664,7 +665,7 @@ export const abortAgentChat = async (sessionId?: string) => {
export const replyAgentPermission = async (
sessionId: string,
requestId: string,
reply: PermissionReply,
reply: PermissionDecision,
) => {
const response = await apiFetch(
getAgentSessionUrl(sessionId, "/permission-responses"),