feat(web): 工作台 - 优化 Agent 工作区与技能库

This commit is contained in:
ddcat666
2026-09-27 08:21:08 +08:00
parent b4111a8721
commit 5df5e09c63
22 changed files with 1681 additions and 196 deletions

View File

@@ -26,6 +26,7 @@ let remoteUserDataPhase: RemoteUserDataPhase = "inactive";
let syncTimer: number | null = null;
let syncPromise: Promise<void> | null = null;
let syncQueued = false;
let syncRetryAttempt = 0;
let remoteOperationTail: Promise<void> = Promise.resolve();
let subscriptionsInstalled = false;
let acknowledgedAssets = new Map<string, Asset>();
@@ -35,6 +36,7 @@ let sessionEpoch = 0;
const verifiedProjects = new Set<string>();
const verifiedAssets = new Set<string>();
const remoteProjectLoadPromises = new Map<string, Promise<CanvasProject | undefined>>();
const REMOTE_SYNC_RETRY_DELAYS_MS = [2_000, 4_000, 8_000, 16_000, 30_000] as const;
export async function initializeRemoteUserDataSession(userId: string) {
await withRemoteUserDataSyncExclusive(async () => {
@@ -373,6 +375,7 @@ export function resetRemoteUserDataSync() {
window.clearTimeout(syncTimer);
syncTimer = null;
}
syncRetryAttempt = 0;
syncQueued = false;
useSyncProgressStore.getState().clearAll();
}
@@ -409,6 +412,66 @@ export function scheduleRemoteUserDataSync() {
}, 1200);
}
function isRetryableRemoteSyncError(error: unknown) {
if (error instanceof ApiError) return error.retryable;
if (error instanceof DOMException && error.name === "AbortError") return false;
if (typeof error === "object" && error !== null && "permanent" in error) {
return (error as { permanent?: unknown }).permanent !== true;
}
// Plain errors from validation/local storage should remain visible instead of
// being retried indefinitely. Transport failures are normalized as ApiError.
return false;
}
function updateRetryProgress(delayMs: number) {
const seconds = Math.max(1, Math.ceil(delayMs / 1_000));
const progress = useSyncProgressStore.getState();
for (const item of Object.values(progress.syncingProjects)) {
if (item.phase !== "error") continue;
progress.setProjectProgress(item.projectId, {
message: `${item.message || "云端同步失败"};${seconds} 秒后自动重试`,
});
}
}
function stopRetryProgress() {
const progress = useSyncProgressStore.getState();
for (const item of Object.values(progress.syncingProjects)) {
if (item.phase !== "error" || !item.message?.includes("自动重试")) continue;
progress.setProjectProgress(item.projectId, { message: "云端同步失败,请点击立即重试" });
}
}
function scheduleRemoteUserDataRetry(error: unknown) {
if (!isRetryableRemoteSyncError(error) || !activeRemoteUserId || remoteUserDataPhase !== "ready") return false;
if (syncRetryAttempt >= REMOTE_SYNC_RETRY_DELAYS_MS.length) {
stopRetryProgress();
return false;
}
const delayMs = Math.max(
REMOTE_SYNC_RETRY_DELAYS_MS[syncRetryAttempt],
error instanceof ApiError ? error.retryAfterMs || 0 : 0,
);
syncRetryAttempt += 1;
if (syncTimer) window.clearTimeout(syncTimer);
updateRetryProgress(delayMs);
syncTimer = window.setTimeout(() => {
syncTimer = null;
void saveRemoteUserDataNow().catch((retryError) => console.warn("云端自动重试失败", retryError));
}, delayMs);
return true;
}
/** 用户主动点击重试时重新开始退避窗口,不影响本地已保存的内容。 */
export function retryRemoteUserDataSync(projectId?: string) {
syncRetryAttempt = 0;
if (syncTimer) {
window.clearTimeout(syncTimer);
syncTimer = null;
}
return saveRemoteUserDataNow(projectId);
}
export function formatLocalSavedRemotePending(localAction: string, error: unknown): string {
const detail = error instanceof Error && error.message.trim() ? error.message.trim() : "未知错误";
if (error instanceof ApiError && error.reason === "canvas_history_resources_missing") {
@@ -616,13 +679,23 @@ export async function saveRemoteUserDataNow(input?: string | readonly string[] |
if (epoch !== sessionEpoch) throw new Error("账号已切换,已停止旧会话保存");
await drainRemoteUserDataChanges(options);
});
let syncFailed = false;
let syncError: unknown;
try {
await syncPromise;
assertNoConflict();
syncRetryAttempt = 0;
} catch (error) {
syncFailed = true;
syncError = error;
} finally {
syncPromise = null;
if (syncQueued) scheduleRemoteUserDataSync();
}
if (syncFailed) {
scheduleRemoteUserDataRetry(syncError);
throw syncError;
}
}
/**