合并 agent-mvp 到 master #1

Merged
jiang merged 282 commits from agent-mvp into master 2026-08-18 17:56:46 +08:00
2 changed files with 67 additions and 10 deletions
Showing only changes of commit 0dd521d8c9 - Show all commits
+46 -1
View File
@@ -6,6 +6,7 @@ import { GlobalChatbox } from "./GlobalChatbox";
const createSession = jest.fn();
const mockFetchAgentRuntimeHealth = jest.fn();
const mockFetchAgentModels = jest.fn();
let mockCurrentProjectId = "project-1";
jest.mock("@refinedev/core", () => ({
@@ -13,7 +14,7 @@ jest.mock("@refinedev/core", () => ({
}));
jest.mock("@/lib/chatModels", () => ({
fetchAgentModels: jest.fn(() => new Promise(() => {})),
fetchAgentModels: (...args: unknown[]) => mockFetchAgentModels(...args),
}));
jest.mock("@/lib/agentRuntime", () => ({
@@ -100,12 +101,15 @@ describe("GlobalChatbox lifecycle", () => {
createSession.mockClear();
mockFetchAgentRuntimeHealth.mockReset();
mockFetchAgentRuntimeHealth.mockImplementation(() => new Promise(() => {}));
mockFetchAgentModels.mockReset();
mockFetchAgentModels.mockImplementation(() => new Promise(() => {}));
mockCurrentProjectId = "project-1";
});
afterEach(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
jest.restoreAllMocks();
});
it("keeps content mounted and preserves the session across close and reopen", async () => {
@@ -146,4 +150,45 @@ describe("GlobalChatbox lifecycle", () => {
expect(screen.getByText("Workspace state: unavailable")).toBeInTheDocument();
});
it("reserves models unavailable for an empty model configuration", async () => {
mockFetchAgentRuntimeHealth.mockResolvedValueOnce(true);
mockFetchAgentModels.mockResolvedValueOnce({ models: [] });
render(<GlobalChatbox open onClose={jest.fn()} />);
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
expect(screen.getByText("Workspace state: models_unavailable")).toBeInTheDocument();
});
it("reports an auth dependency failure as unavailable and retries quickly", async () => {
jest.spyOn(console, "error").mockImplementation(() => undefined);
mockFetchAgentRuntimeHealth.mockResolvedValue(true);
mockFetchAgentModels
.mockRejectedValueOnce(new Error("authentication service unavailable"))
.mockResolvedValueOnce({
defaultModel: "provider/model",
models: [{ id: "provider/model", label: "Model" }],
});
render(<GlobalChatbox open onClose={jest.fn()} />);
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
expect(screen.getByText("Workspace state: unavailable")).toBeInTheDocument();
expect(mockFetchAgentModels).toHaveBeenCalledTimes(1);
await act(async () => {
jest.advanceTimersByTime(5_000);
await Promise.resolve();
await Promise.resolve();
});
expect(screen.getByText("Workspace state: ready")).toBeInTheDocument();
expect(mockFetchAgentModels).toHaveBeenCalledTimes(2);
});
});
+21 -9
View File
@@ -31,6 +31,7 @@ import { useAgentToolActions } from "./hooks/useAgentToolActions";
const STREAMING_BOTTOM_RESERVE_PX = 180;
const STREAMING_SCROLL_RESTORE_AT_PX = STREAMING_BOTTOM_RESERVE_PX - 36;
const AGENT_RUNTIME_POLL_MS = 30_000;
const AGENT_RUNTIME_RETRY_MS = 5_000;
const AGENT_RUNTIME_TIMEOUT_MS = 8_000;
export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
@@ -97,7 +98,7 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
setRuntimeState("unavailable");
setModelOptions([]);
setSelectedModel(undefined);
return;
return "unavailable" as const;
}
const modelConfig = await fetchAgentModels(controller.signal);
@@ -106,7 +107,7 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
setRuntimeState("models_unavailable");
setModelOptions([]);
setSelectedModel(undefined);
return;
return "models_unavailable" as const;
}
setModelOptions(modelConfig.models);
@@ -117,12 +118,14 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
return modelConfig.defaultModel;
});
setRuntimeState("ready");
return "ready" as const;
} catch (error) {
if (requestId !== runtimeRequestIdRef.current) return;
console.error("[GlobalChatbox] Failed to check agent runtime:", error);
setRuntimeState(runtimeHealthy ? "models_unavailable" : "unavailable");
setRuntimeState("unavailable");
setModelOptions([]);
setSelectedModel(undefined);
return "unavailable" as const;
} finally {
window.clearTimeout(timeoutId);
if (runtimeAbortRef.current === controller) {
@@ -134,14 +137,23 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
useEffect(() => {
if (!open) return;
void refreshAgentRuntime(true);
const intervalId = window.setInterval(
() => void refreshAgentRuntime(false),
AGENT_RUNTIME_POLL_MS,
);
let cancelled = false;
let pollTimerId: number | undefined;
const pollRuntime = async (showChecking: boolean) => {
const nextState = await refreshAgentRuntime(showChecking);
if (cancelled || !nextState) return;
pollTimerId = window.setTimeout(
() => void pollRuntime(false),
nextState === "ready" ? AGENT_RUNTIME_POLL_MS : AGENT_RUNTIME_RETRY_MS,
);
};
void pollRuntime(true);
return () => {
window.clearInterval(intervalId);
cancelled = true;
if (pollTimerId !== undefined) window.clearTimeout(pollTimerId);
runtimeRequestIdRef.current += 1;
runtimeAbortRef.current?.abort();
runtimeAbortRef.current = null;