feat: 工作空间切换校验 + 初始化检测 + 继承基础设置
新增功能: - workspace:check IPC — 校验路径有效性 + 检测 4 个必需文件状态 - workspace:inheritFiles IPC — 从旧工作空间继承 SOUL/AGENTS/USERS 到新工作空间 - app:restart IPC — 渲染进程触发应用重启(app.relaunch + app.exit) 前端 WorkspaceSettings 改造: - 选择文件夹后立即校验路径(是否存在、是否目录、4 个必需文件状态) - 显示工作空间状态: · 新工作空间 → 提示将自动创建 4 个文件 · 部分文件缺失 → 提示缺失文件将自动创建 · 已有工作空间 → 提示将直接加载现有配置 - 继承选项:当切换到新工作空间时,可勾选继承 SOUL/AGENTS/USERS.md · MEMORY.md 不继承(记忆与工作空间项目上下文绑定) · 继承白名单:仅允许 SOUL.md、AGENTS.md、USERS.md(防路径遍历) - 确认切换按钮 + 取消按钮 - 切换完成后弹出重启确认对话框(立即重启 / 稍后手动重启) 安全考虑: - workspace:inheritFiles 使用文件名白名单,防止路径遍历攻击 - workspace:check 返回 resolvedPath,前端只展示不直接用于文件操作 - app:restart 延迟 200ms 让 IPC 响应先返回
This commit is contained in:
@@ -72,11 +72,77 @@ const PROVIDER_URLS: Record<string, string> = { deepseek: 'https://api.deepseek.
|
||||
|
||||
function WorkspaceSettings() {
|
||||
const [workspacePath, setWorkspacePath] = useConfig('workspace.path', '');
|
||||
// 切换工作空间的中间状态
|
||||
const [pendingPath, setPendingPath] = useState<string | null>(null);
|
||||
const [checkResult, setCheckResult] = useState<MetonaWorkspaceCheckResult | null>(null);
|
||||
const [checking, setChecking] = useState(false);
|
||||
const [inheritSoul, setInheritSoul] = useState(true);
|
||||
const [inheritAgents, setInheritAgents] = useState(true);
|
||||
const [inheritUsers, setInheritUsers] = useState(true);
|
||||
const [applying, setApplying] = useState(false);
|
||||
const [showRestartDialog, setShowRestartDialog] = useState(false);
|
||||
|
||||
const currentPath = workspacePath;
|
||||
|
||||
const handleSelect = async () => {
|
||||
if (!window.metona?.app?.selectFolder) return;
|
||||
const r = await window.metona.app.selectFolder(workspacePath || undefined);
|
||||
if (!r.canceled && r.path) setWorkspacePath(r.path);
|
||||
const r = await window.metona.app.selectFolder(currentPath || undefined);
|
||||
if (!r.canceled && r.path) {
|
||||
// 立即校验新路径
|
||||
setPendingPath(r.path);
|
||||
setChecking(true);
|
||||
setCheckResult(null);
|
||||
try {
|
||||
const result = await window.metona.workspace.check(r.path);
|
||||
setCheckResult(result);
|
||||
} catch (err) {
|
||||
console.error('[SettingsModal]', err);
|
||||
setCheckResult({ valid: false, reason: (err as Error).message });
|
||||
} finally {
|
||||
setChecking(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleApply = async () => {
|
||||
if (!pendingPath || !checkResult?.valid) return;
|
||||
setApplying(true);
|
||||
try {
|
||||
// 如果勾选了继承文件,从旧工作空间复制到新工作空间
|
||||
const filesToInherit: string[] = [];
|
||||
if (inheritSoul) filesToInherit.push('SOUL.md');
|
||||
if (inheritAgents) filesToInherit.push('AGENTS.md');
|
||||
if (inheritUsers) filesToInherit.push('USERS.md');
|
||||
|
||||
if (filesToInherit.length > 0 && currentPath) {
|
||||
try {
|
||||
await window.metona.workspace.inheritFiles({
|
||||
targetPath: pendingPath,
|
||||
sourcePath: currentPath,
|
||||
files: filesToInherit,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[SettingsModal] Inherit failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存新路径到配置
|
||||
setWorkspacePath(pendingPath);
|
||||
// 弹出重启确认对话框
|
||||
setShowRestartDialog(true);
|
||||
// 清理中间状态
|
||||
setPendingPath(null);
|
||||
setCheckResult(null);
|
||||
} catch (err) {
|
||||
console.error('[SettingsModal]', err);
|
||||
} finally {
|
||||
setApplying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setPendingPath(null);
|
||||
setCheckResult(null);
|
||||
};
|
||||
|
||||
const handleOpen = () => { if (workspacePath) window.metona?.app?.showItemInFolder(workspacePath); };
|
||||
@@ -94,7 +160,108 @@ function WorkspaceSettings() {
|
||||
📂 在文件管理器中打开
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 校验中状态 */}
|
||||
{checking && (
|
||||
<Stack direction="row" spacing={1} sx={{ alignItems: 'center', py: 1 }}>
|
||||
<CircularProgress size={14} />
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>正在校验工作空间...</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* 校验失败 */}
|
||||
{pendingPath && checkResult && !checkResult.valid && (
|
||||
<Alert severity="error" sx={{ py: 0.5 }}>
|
||||
<Typography variant="caption">路径无效:{checkResult.reason}</Typography>
|
||||
<Stack direction="row" spacing={1} sx={{ mt: 1 }}>
|
||||
<Button size="small" onClick={handleCancel}>取消</Button>
|
||||
</Stack>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* 校验成功 — 显示工作空间状态 + 继承选项 */}
|
||||
{pendingPath && checkResult?.valid && (
|
||||
<Stack spacing={1.5} sx={{ p: 1.5, borderRadius: 1.5, bgcolor: 'background.default', border: '1px solid', borderColor: 'divider' }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: 'text.primary' }}>
|
||||
目标工作空间:{checkResult.path}
|
||||
</Typography>
|
||||
|
||||
{checkResult.isNewWorkspace ? (
|
||||
<Alert severity="info" sx={{ py: 0.5, '& .MuiAlert-message': { fontSize: 12 } }}>
|
||||
新工作空间 — 切换后将自动创建 4 个必需文件(SOUL.md、AGENTS.md、MEMORY.md、USERS.md)
|
||||
</Alert>
|
||||
) : checkResult.missingFiles && checkResult.missingFiles.length > 0 ? (
|
||||
<Alert severity="warning" sx={{ py: 0.5, '& .MuiAlert-message': { fontSize: 12 } }}>
|
||||
已有目录但缺少 {checkResult.missingFiles.length} 个文件:{checkResult.missingFiles.join(', ')}。缺失文件将自动创建。
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert severity="success" sx={{ py: 0.5, '& .MuiAlert-message': { fontSize: 12 } }}>
|
||||
已有工作空间 — 4 个必需文件均已就绪,将直接加载现有配置。
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* 继承选项(仅当当前有工作空间且新工作空间需要创建文件时显示) */}
|
||||
{currentPath && currentPath !== pendingPath && (checkResult.isNewWorkspace || (checkResult.missingFiles && checkResult.missingFiles.length > 0)) && (
|
||||
<Stack spacing={0.5} sx={{ mt: 0.5 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: 'text.secondary' }}>
|
||||
从当前工作空间继承基础设置:
|
||||
</Typography>
|
||||
<FormControlLabel
|
||||
control={<Checkbox size="small" checked={inheritSoul} onChange={(e) => setInheritSoul(e.target.checked)} />}
|
||||
label={<Typography variant="caption">SOUL.md(身份与角色定义)</Typography>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox size="small" checked={inheritAgents} onChange={(e) => setInheritAgents(e.target.checked)} />}
|
||||
label={<Typography variant="caption">AGENTS.md(行为规则)</Typography>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox size="small" checked={inheritUsers} onChange={(e) => setInheritUsers(e.target.checked)} />}
|
||||
label={<Typography variant="caption">USERS.md(用户画像)</Typography>}
|
||||
/>
|
||||
<Typography variant="caption" sx={{ color: 'text.disabled', fontSize: 10, pl: 3 }}>
|
||||
MEMORY.md 不继承(记忆与工作空间项目上下文绑定)
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Stack direction="row" spacing={1} sx={{ mt: 1 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleApply}
|
||||
disabled={applying}
|
||||
startIcon={applying ? <CircularProgress size={12} /> : undefined}
|
||||
>
|
||||
{applying ? '应用中...' : '确认切换'}
|
||||
</Button>
|
||||
<Button variant="outlined" size="small" onClick={handleCancel} disabled={applying}>
|
||||
取消
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Typography variant="caption" sx={{ color: 'text.disabled' }}>修改工作空间路径后需重启应用生效。</Typography>
|
||||
|
||||
{/* 重启确认对话框 */}
|
||||
<Dialog open={showRestartDialog} onClose={() => setShowRestartDialog(false)} maxWidth="xs" fullWidth>
|
||||
<DialogTitle sx={{ fontSize: 14 }}>工作空间已切换</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
工作空间已更新为:
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontFamily: 'monospace', fontSize: 12, mt: 0.5, p: 1, borderRadius: 1, bgcolor: 'background.default' }}>
|
||||
{workspacePath}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mt: 1.5, color: 'text.secondary' }}>
|
||||
需要重启应用以加载新工作空间的配置和文件。是否立即重启?
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button size="small" onClick={() => setShowRestartDialog(false)}>稍后手动重启</Button>
|
||||
<Button size="small" variant="contained" color="primary" onClick={() => window.metona?.app?.restart()}>立即重启</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user