feat: enhance Codex agent connection handling with automatic thread recovery and improved error logging
This commit is contained in:
@@ -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: [] }),
|
||||
|
||||
@@ -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 }),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user