fix(runtime): 本地任务恢复 - 恢复本地画布和延迟视频任务

This commit is contained in:
ddCat committed 2026-08-18 15:16:05 +08:00
1 parent ea657f179d
commit bda114b29e
13 files changed
+739 -67

No files matched your search

@@ -35,10 +35,50 @@ type CanvasAgentStore = {
clearEventLogs: () => void;
};
const CANVAS_AGENT_ENABLED_STORAGE_KEY = "canvas-agent-enabled";
type CanvasAgentPreferenceStorage = {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
};
export function readCanvasAgentEnabledPreference(storage: Pick<CanvasAgentPreferenceStorage, "getItem"> | undefined = browserPreferenceStorage()) {
try {
return storage?.getItem(CANVAS_AGENT_ENABLED_STORAGE_KEY) === "true";
} catch {
return false;
}
}
export function writeCanvasAgentEnabledPreference(enabled: boolean, storage: Pick<CanvasAgentPreferenceStorage, "setItem"> | undefined = browserPreferenceStorage()) {
try {
storage?.setItem(CANVAS_AGENT_ENABLED_STORAGE_KEY, String(enabled));
} catch {
// 浏览器隐私模式可能拒绝本地偏好写入;连接本身仍可继续。
}
}
export function canvasAgentConnectionStartingPatch() {
return { enabled: true, connected: false, activity: "连接中", connectError: "", activeTab: "setup" as const };
}
export function canvasAgentTransientDisconnectPatch(activity: string, connectError: string) {
return { enabled: true, connected: false, activity, connectError, waiting: false, sending: false };
}
export function canvasAgentConnectionStatusText({ enabled, connected, activity, connectError }: Pick<CanvasAgentStore, "enabled" | "connected" | "activity" | "connectError">) {
if (enabled && !connected && activity === "正在重连") return activity;
return connectError ? "连接失败" : connected ? activity : enabled ? "连接中" : "未连接";
}
function browserPreferenceStorage(): CanvasAgentPreferenceStorage | undefined {
return typeof window === "undefined" ? undefined : window.localStorage;
}
export const useCanvasAgentStore = create<CanvasAgentStore>((set) => ({
width: typeof window === "undefined" ? 440 : Number(localStorage.getItem("canvas-agent-panel-width")) || 440,
connected: false,
enabled: false,
enabled: readCanvasAgentEnabledPreference(),
prompt: "",
attachments: [],
sending: false,
@@ -54,7 +94,10 @@ export const useCanvasAgentStore = create<CanvasAgentStore>((set) => ({
activity: "就绪",
connectError: "",
pendingTool: null,
setAgentState: (patch) => set(patch),
setAgentState: (patch) => {
if (typeof patch.enabled === "boolean") writeCanvasAgentEnabledPreference(patch.enabled);
set(patch);
},
addMessage: (item) => set((state) => ({ messages: [...state.messages.slice(-120), item] })),
addEventLog: (item) => set((state) => ({ eventLogs: [...state.eventLogs.slice(-160), item] })),
clearEventLogs: () => set({ eventLogs: [] }),
@@ -4,31 +4,103 @@ import { create } from "zustand";
import { getDreaminaModelCatalogSnapshotWithSessionRecovery, type DreaminaLocalModel } from "@/services/local-dreamina-model-catalog";
import { getLocalRuntimeSessionClient, useLocalRuntimeStore } from "@/stores/use-local-runtime-store";
type State = { state: "idle" | "loading" | "ready" | "error"; models: DreaminaLocalModel[]; cacheScope?: string; sync(signal?: AbortSignal): Promise<void> };
type CatalogSnapshot = Awaited<ReturnType<typeof getDreaminaModelCatalogSnapshotWithSessionRecovery>>;
type RuntimeCatalogState = { connection: string; modules: Array<{ id: string; scopes: string[] }> };
type Dependencies = {
getRuntimeState(): RuntimeCatalogState;
getClient(): Parameters<typeof getDreaminaModelCatalogSnapshotWithSessionRecovery>[0];
loadSnapshot(client: Parameters<typeof getDreaminaModelCatalogSnapshotWithSessionRecovery>[0], signal: AbortSignal): Promise<CatalogSnapshot>;
requestTimeoutMs?: number;
};
type State = {
state: "idle" | "loading" | "ready" | "error";
models: DreaminaLocalModel[];
cacheScope?: string;
sync(signal?: AbortSignal): Promise<void>;
ensureReady(signal?: AbortSignal): Promise<DreaminaLocalModel[]>;
};
export const dreaminaModelCacheScopeKey = ({ accountBinding, sessionEpoch }: { accountBinding: string; sessionEpoch: string | number }) => `${accountBinding}:${sessionEpoch}`;
export const useLocalDreaminaModelStore = create<State>((set, get) => ({
state: "idle",
models: [],
cacheScope: undefined,
async sync(signal) {
const runtime = useLocalRuntimeStore.getState();
const available = runtime.connection === "connected" && runtime.modules.some((module) => module.id === "dreamina" && module.scopes.includes("dreamina:models"));
if (!available) {
set({ state: "idle", models: [], cacheScope: undefined });
return;
}
const client = getLocalRuntimeSessionClient();
try {
const snapshot = await getDreaminaModelCatalogSnapshotWithSessionRecovery(client, signal);
const cacheScope = dreaminaModelCacheScopeKey(snapshot);
set({ state: "ready", models: snapshot.models, cacheScope });
} catch {
if (!signal?.aborted) set({ state: "error", models: [], cacheScope: undefined });
}
},
}));
const DEFAULT_CATALOG_TIMEOUT_MS = 8_000;
export function createLocalDreaminaModelStore(dependencies: Dependencies) {
let requestRevision = 0;
let activeRequest: { revision: number; controller: AbortController; promise: Promise<DreaminaLocalModel[]> } | null = null;
return create<State>((set, get) => {
const load = (force: boolean, signal?: AbortSignal) => {
const runtime = dependencies.getRuntimeState();
const available = runtime.connection === "connected" && runtime.modules.some((module) => module.id === "dreamina" && module.scopes.includes("dreamina:models"));
if (!available) {
requestRevision++;
activeRequest?.controller.abort();
activeRequest = null;
set({ state: "idle", models: [], cacheScope: undefined });
return Promise.reject(new Error("即梦本机模型目录尚未就绪"));
}
const current = get();
if (!force && current.state === "ready" && current.models.length) return Promise.resolve(current.models);
if (force && activeRequest) {
requestRevision++;
activeRequest.controller.abort();
activeRequest = null;
}
if (!activeRequest) {
set({ state: "loading" });
const revision = ++requestRevision;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), Math.max(1, dependencies.requestTimeoutMs ?? DEFAULT_CATALOG_TIMEOUT_MS));
const promise = dependencies
.loadSnapshot(dependencies.getClient(), controller.signal)
.then((snapshot) => {
if (revision === requestRevision) set({ state: "ready", models: snapshot.models, cacheScope: dreaminaModelCacheScopeKey(snapshot) });
return snapshot.models;
})
.catch((error) => {
if (revision === requestRevision) set({ state: "error", models: [], cacheScope: undefined });
throw error;
})
.finally(() => {
clearTimeout(timer);
if (activeRequest?.revision === revision) activeRequest = null;
});
activeRequest = { revision, controller, promise };
}
return waitForCatalog(activeRequest.promise, signal);
};
return {
state: "idle",
models: [],
cacheScope: undefined,
async sync(signal) {
try {
await load(true, signal);
} catch {
// 页面引导读取失败只投影 store 状态;生成入口的 ensureReady 会明确失败。
}
},
ensureReady(signal) {
return load(false, signal);
},
};
});
}
function waitForCatalog<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
if (!signal) return promise;
if (signal.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
return new Promise<T>((resolve, reject) => {
const abort = () => reject(new DOMException("Aborted", "AbortError"));
signal.addEventListener("abort", abort, { once: true });
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
});
}
export const useLocalDreaminaModelStore = createLocalDreaminaModelStore({
getRuntimeState: () => useLocalRuntimeStore.getState(),
getClient: getLocalRuntimeSessionClient,
loadSnapshot: (client, signal) => getDreaminaModelCatalogSnapshotWithSessionRecovery(client, signal),
});
export function useLocalDreaminaModelBootstrap() {
const connection = useLocalRuntimeStore((state) => state.connection);