mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
fix: 构建 workspace 目录 4 层健康防护体系,彻底解决路径缺失问题
## 根本原因 workspace_create 只写 DB 不建目录,导致任何主题(小说/文章/海报等) 切换到目录已被删除/外置磁盘未挂载的 workspace 后,发送消息必然报错, 且旧提示语引导用户去不存在的"工作区设置"页面。 ## 4 层防护 **L1 创建时(workspace_create)** - 建 workspace 前调 ensure_workspace_root_ready,目录不存在则自动创建 **L2 启动/切换时(useWorkspace + useProjects)** - refresh/setDefault/projects_refresh 时调 workspace_ensure_ready - 静默自动修复已知 workspace 的缺失目录 **L3 发消息前(aster_agent_cmd)** - aster_session_create + aster_agent_chat_stream 各自调 ensure_workspace_root_ready - 能自动创建则创建并继续,无法创建则返回语义化错误 **L4 主动检查 + 兜底 UI(AgentChatPage + CrashRecoveryPanel)** - 打开任意项目时主动调 workspace_ensure_ready,失败时显示 amber banner - 发送失败也显示 amber banner,"重新选择目录"按钮处理两种场景 - CrashRecoveryPanel 新增 workspace-path-missing 场景识别与蓝色修复按钮 ## 新增功能 - WorkspaceUpdate.root_path:支持通过 API 修改 workspace 根目录 - workspace_ensure_ready / workspace_ensure_default_ready:Tauri 健康检查命令 - workspace_health_service.rs:独立服务模块,含详细诊断信息 - AppCrashBoundary / CrashRecoveryPanel:通用 React 错误边界与恢复面板 - workspaceHealthTelemetry.ts:localStorage 修复历史记录(最多 50 条) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
16b48bad36
commit
2d00911c28
@@ -0,0 +1,82 @@
|
||||
export type WorkspaceRepairSource =
|
||||
| "app_startup"
|
||||
| "workspace_refresh"
|
||||
| "workspace_set_default"
|
||||
| "projects_refresh"
|
||||
| "agent_chat_page";
|
||||
|
||||
export interface WorkspaceRepairRecord {
|
||||
timestamp: string;
|
||||
workspace_id: string;
|
||||
root_path: string;
|
||||
source: WorkspaceRepairSource;
|
||||
}
|
||||
|
||||
export interface RecordWorkspaceRepairInput {
|
||||
workspaceId: string;
|
||||
rootPath: string;
|
||||
source: WorkspaceRepairSource;
|
||||
}
|
||||
|
||||
const WORKSPACE_REPAIR_HISTORY_KEY = "proxycast.workspace_repair_history.v1";
|
||||
const MAX_WORKSPACE_REPAIR_HISTORY = 50;
|
||||
|
||||
export function recordWorkspaceRepair(
|
||||
input: RecordWorkspaceRepairInput,
|
||||
): void {
|
||||
if (!input.workspaceId || !input.rootPath) return;
|
||||
|
||||
const nextRecord: WorkspaceRepairRecord = {
|
||||
timestamp: new Date().toISOString(),
|
||||
workspace_id: input.workspaceId,
|
||||
root_path: input.rootPath,
|
||||
source: input.source,
|
||||
};
|
||||
|
||||
const history = readWorkspaceRepairHistory();
|
||||
history.push(nextRecord);
|
||||
const trimmed = history.slice(-MAX_WORKSPACE_REPAIR_HISTORY);
|
||||
writeWorkspaceRepairHistory(trimmed);
|
||||
}
|
||||
|
||||
export function getWorkspaceRepairHistory(limit = 50): WorkspaceRepairRecord[] {
|
||||
if (limit <= 0) return [];
|
||||
const history = readWorkspaceRepairHistory();
|
||||
return history.slice(-limit).reverse();
|
||||
}
|
||||
|
||||
export function clearWorkspaceRepairHistory(): void {
|
||||
if (typeof localStorage === "undefined") return;
|
||||
localStorage.removeItem(WORKSPACE_REPAIR_HISTORY_KEY);
|
||||
}
|
||||
|
||||
function readWorkspaceRepairHistory(): WorkspaceRepairRecord[] {
|
||||
if (typeof localStorage === "undefined") return [];
|
||||
|
||||
const raw = localStorage.getItem(WORKSPACE_REPAIR_HISTORY_KEY);
|
||||
if (!raw) return [];
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.filter(isWorkspaceRepairRecord);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function writeWorkspaceRepairHistory(records: WorkspaceRepairRecord[]): void {
|
||||
if (typeof localStorage === "undefined") return;
|
||||
localStorage.setItem(WORKSPACE_REPAIR_HISTORY_KEY, JSON.stringify(records));
|
||||
}
|
||||
|
||||
function isWorkspaceRepairRecord(value: unknown): value is WorkspaceRepairRecord {
|
||||
if (typeof value !== "object" || value === null) return false;
|
||||
const record = value as Record<string, unknown>;
|
||||
return (
|
||||
typeof record.timestamp === "string" &&
|
||||
typeof record.workspace_id === "string" &&
|
||||
typeof record.root_path === "string" &&
|
||||
typeof record.source === "string"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user