feat: enhance Codex agent connection handling with automatic thread recovery and improved error logging

This commit is contained in:
HouYunFei
2026-07-05 23:40:54 +08:00
parent 6113986637
commit 3080d6da10
9 changed files with 154 additions and 37 deletions
@@ -10,6 +10,10 @@ export type AgentPendingToolCall = { requestId: string; name: string; input?: {
export type AgentThreadSummary = { id: string; preview: string; name?: string | null; cwd?: string; status?: string; source?: unknown; createdAt?: number; updatedAt?: number };
export type AgentPanelTab = "chat" | "setup" | "history" | "log";
const CONNECT_TIMEOUT_MS = 6000;
let agentSource: EventSource | null = null;
let connectTimer: ReturnType<typeof setTimeout> | null = null;
type CanvasAgentStore = {
width: number;
url: string;
@@ -31,13 +35,15 @@ type CanvasAgentStore = {
activity: string;
connectError: string;
pendingTool: AgentPendingToolCall | null;
setAgentState: (patch: Partial<Omit<CanvasAgentStore, "setAgentState" | "addMessage" | "addEventLog" | "clearEventLogs">>) => void;
setAgentState: (patch: Partial<Omit<CanvasAgentStore, "setAgentState" | "connectAgent" | "disconnectAgent" | "addMessage" | "addEventLog" | "clearEventLogs">>) => void;
connectAgent: () => void;
disconnectAgent: (patch?: Partial<Omit<CanvasAgentStore, "setAgentState" | "connectAgent" | "disconnectAgent" | "addMessage" | "addEventLog" | "clearEventLogs">>) => void;
addMessage: (item: AgentChatItem) => void;
addEventLog: (item: AgentEventLog) => void;
clearEventLogs: () => void;
};
export const useCanvasAgentStore = create<CanvasAgentStore>((set) => ({
export const useCanvasAgentStore = create<CanvasAgentStore>((set, get) => ({
width: typeof window === "undefined" ? 440 : Number(localStorage.getItem("canvas-agent-panel-width")) || 440,
url: typeof window === "undefined" ? "http://127.0.0.1:17371" : localStorage.getItem("canvas-agent-url") || "http://127.0.0.1:17371",
token: typeof window === "undefined" ? "" : localStorage.getItem("canvas-agent-token") || "",
@@ -59,6 +65,42 @@ export const useCanvasAgentStore = create<CanvasAgentStore>((set) => ({
connectError: "",
pendingTool: null,
setAgentState: (patch) => set(patch),
connectAgent: () => {
const endpoint = get().url.trim().replace(/\/$/, "");
const token = get().token.trim();
if (!endpoint || !token) return set({ connectError: "请填写 Local URL 和 Connect token" });
try {
const parsed = new URL(endpoint);
if (!["http:", "https:"].includes(parsed.protocol)) throw new Error();
} catch {
return set({ connectError: "Local URL 格式不正确" });
}
get().disconnectAgent({ url: endpoint, token, enabled: true, activity: "连接中", connectError: "" });
localStorage.setItem("canvas-agent-url", endpoint);
localStorage.setItem("canvas-agent-token", token);
const clientId = typeof crypto === "undefined" ? `${Date.now()}` : crypto.randomUUID();
const source = new EventSource(`${endpoint}/events?token=${encodeURIComponent(token)}&clientId=${encodeURIComponent(clientId)}&role=status`);
agentSource = source;
connectTimer = setTimeout(() => {
if (agentSource !== source) return;
get().disconnectAgent({ activity: "连接超时", connectError: "连接超时:请确认 Canvas Agent 正在运行,并使用当前启动输出的 Connect token" });
}, CONNECT_TIMEOUT_MS);
source.addEventListener("hello", () => {
if (connectTimer) clearTimeout(connectTimer);
connectTimer = null;
set({ enabled: true, connected: true, activity: "已连接", connectError: "" });
});
source.onerror = () => {
if (agentSource === source) get().disconnectAgent({ activity: "连接失败", connectError: "连接失败:请检查 Local URL、Connect token 或已绑定的网页 Origin" });
};
},
disconnectAgent: (patch = {}) => {
agentSource?.close();
agentSource = null;
if (connectTimer) clearTimeout(connectTimer);
connectTimer = null;
set({ enabled: false, connected: false, activity: "离线", ...patch });
},
addMessage: (item) => set((state) => ({ messages: [...state.messages.slice(-120), item] })),
addEventLog: (item) => set((state) => ({ eventLogs: [...state.eventLogs.slice(-160), item] })),
clearEventLogs: () => set({ eventLogs: [] }),
+5 -2
View File
@@ -52,6 +52,7 @@ export type WebdavSyncConfig = {
directory: string;
lastSyncedAt: string;
};
export type ConfigTabKey = "channels" | "models" | "preferences" | "webdav" | "codex";
export const CONFIG_STORE_KEY = "infinite-canvas:ai_config_store";
export type ModelCapability = "image" | "video" | "text" | "audio";
@@ -111,11 +112,12 @@ type ConfigStore = {
config: AiConfig;
webdav: WebdavSyncConfig;
isConfigOpen: boolean;
configTab: ConfigTabKey;
shouldPromptContinue: boolean;
updateConfig: <K extends keyof AiConfig>(key: K, value: AiConfig[K]) => void;
updateWebdavConfig: <K extends keyof WebdavSyncConfig>(key: K, value: WebdavSyncConfig[K]) => void;
isAiConfigReady: (config: AiConfig, model: string) => boolean;
openConfigDialog: (shouldPromptContinue?: boolean) => void;
openConfigDialog: (shouldPromptContinue?: boolean, tab?: ConfigTabKey) => void;
setConfigDialogOpen: (isOpen: boolean) => void;
clearPromptContinue: () => void;
};
@@ -171,6 +173,7 @@ export const useConfigStore = create<ConfigStore>()(
config: defaultConfig,
webdav: defaultWebdavSyncConfig,
isConfigOpen: false,
configTab: "channels",
shouldPromptContinue: false,
updateConfig: (key, value) =>
set((state) => ({
@@ -187,7 +190,7 @@ export const useConfigStore = create<ConfigStore>()(
},
})),
isAiConfigReady: (config, model) => isAiConfigReady(config, model),
openConfigDialog: (shouldPromptContinue = false) => set({ isConfigOpen: true, shouldPromptContinue }),
openConfigDialog: (shouldPromptContinue = false, configTab = "channels") => set({ isConfigOpen: true, shouldPromptContinue, configTab }),
setConfigDialogOpen: (isConfigOpen) => set({ isConfigOpen }),
clearPromptContinue: () => set({ shouldPromptContinue: false }),
}),