import { useEffect, useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { open } from "@tauri-apps/plugin-dialog"; import { check as checkUpdate } from "@tauri-apps/plugin-updater"; import { relaunch } from "@tauri-apps/plugin-process"; import { getVersion } from "@tauri-apps/api/app"; import { detectEditors, getSettings, getSpaces, searchEditorPath, updateSettings, getWslDistros, getProjects, syncBlueprintRules, checkMcpHealth, type EditorCandidate, type SyncResult, type McpHealthResult, } from "../../lib/commands"; // GitHub OAuth 配置已移至侧边栏「应用设置」 import { useUIStore } from "../../store/ui"; import { Dialog } from "../ui/Dialog"; import { GiteaInstancesSection } from "../dashboard/GiteaPanel"; // 编辑器条目数据结构(存为 JSON 保存到 settings 表) interface EditorEntry { id: string; // 唯一 id name: string; // 显示名 path: string; // 完整可执行路径或命令名 } function loadEditors(raw: string | undefined): EditorEntry[] { try { return raw ? JSON.parse(raw) : []; } catch { return []; } } const inputCls = "w-full border border-gray-200 rounded-lg px-3 py-2 text-sm font-mono outline-none focus:ring-2 focus:ring-blue-200 focus:border-blue-400"; /* ── 编辑器表单弹窗 ─────────────────────────────────────────────────────────── */ function EditorFormModal({ entry, onClose, onSave }: { entry: EditorEntry | null; // null = 新增 onClose: () => void; onSave: (e: EditorEntry) => void; }) { const showToast = useUIStore((s) => s.showToast); const [name, setName] = useState(entry?.name ?? ""); const [path, setPath] = useState(entry?.path ?? ""); const [searching, setSearching] = useState(false); const { data: detected, isLoading: detecting } = useQuery({ queryKey: ["editors"], queryFn: detectEditors, staleTime: 60000, }); const pickExe = async () => { const selected = await open({ multiple: false, filters: [{ name: "可执行文件", extensions: ["exe", "cmd", "bat"] }], }); if (selected) setPath(selected as string); }; const autoSearch = async () => { if (!name.trim()) { showToast("❌ 请先填写名称或命令关键词"); return; } setSearching(true); try { // 把名称当命令名搜一下 const keyword = name.trim().toLowerCase().replace(/\s+/g, ""); const found = await searchEditorPath(keyword); if (found) { setPath(found); showToast(`已找到:${found}`); } else { showToast("未找到,请手动填写路径"); } } finally { setSearching(false); } }; const fillFromDetected = (c: EditorCandidate) => { setName(c.name); setPath(c.path ?? c.cmd); }; const handleSave = () => { if (!name.trim()) { showToast("❌ 名称不能为空"); return; } if (!path.trim()) { showToast("❌ 路径不能为空"); return; } onSave({ id: entry?.id ?? `editor-${Date.now()}`, name: name.trim(), path: path.trim(), }); }; return (
{/* 检测到的编辑器快速填充 */}

系统检测到的编辑器(点击快速填充)

{detecting ? (

检测中…

) : (
{(detected ?? []).map((c) => ( ))}
)}

{/* 名称 */}
setName(e.target.value)} placeholder="Cursor / VS Code / …" />
{/* 路径 */}
setPath(e.target.value)} placeholder="cursor 或 C:\...\Cursor.exe" />
{/* 搜索按钮 */}
); } /* ── 主设置页 ───────────────────────────────────────────────────────────────── */ export function SettingsPage() { const showToast = useUIStore((s) => s.showToast); const qc = useQueryClient(); const { data: spaces = [] } = useQuery({ queryKey: ["spaces"], queryFn: getSpaces }); const spaceId = spaces.find((s) => s.is_current)?.id; const { data: settings } = useQuery({ queryKey: ["settings", spaceId], queryFn: () => getSettings(spaceId), enabled: spaces.length > 0, }); const [editors, setEditors] = useState([]); const [activeIds, setActiveIds] = useState([]); // 多选 const [wslDistro, setWslDistro] = useState("Ubuntu"); const [wslOpenMode, setWslOpenMode] = useState("wsl_internal"); const [wslDistros, setWslDistros] = useState([]); const [detectingWsl, setDetectingWsl] = useState(false); const [wslError, setWslError] = useState(""); const [formTarget, setFormTarget] = useState(undefined); const [mcpPort, setMcpPort] = useState("27190"); const [mcpHealth, setMcpHealth] = useState(null); const [mcpChecking, setMcpChecking] = useState(false); useEffect(() => { if (settings) { setEditors(loadEditors(settings.editors)); try { setActiveIds(settings.active_editors ? JSON.parse(settings.active_editors) : []); } catch { // 兼容旧的单选字段 const old = settings.active_editor; setActiveIds(old ? [old] : []); } setWslDistro(settings.wsl_distro ?? "Ubuntu"); setWslOpenMode(settings.wsl_open_mode ?? "wsl_internal"); setMcpPort(settings.mcp_port ?? "27190"); // 设置加载后自动检查一次 MCP 健康状态 pingMcp(); } }, [settings]); const saveAll = async (nextEditors: EditorEntry[], nextActiveIds: string[]) => { await updateSettings({ editors: JSON.stringify(nextEditors), active_editors: JSON.stringify(nextActiveIds), wsl_distro: wslDistro, }, spaceId); qc.invalidateQueries({ queryKey: ["settings", spaceId] }); showToast("设置已保存 ✓"); }; const handleSaveEditor = (entry: EditorEntry) => { const next = formTarget ? editors.map((e) => (e.id === entry.id ? entry : e)) : [...editors, entry]; // 新增时自动勾选 const nextActive = formTarget ? activeIds : [...new Set([...activeIds, entry.id])]; setEditors(next); setActiveIds(nextActive); setFormTarget(undefined); saveAll(next, nextActive); }; const handleDelete = (id: string) => { const next = editors.filter((e) => e.id !== id); const nextActive = activeIds.filter((a) => a !== id); setEditors(next); setActiveIds(nextActive); saveAll(next, nextActive); showToast("已删除"); }; const toggleActive = (id: string) => { const next = activeIds.includes(id) ? activeIds.filter((a) => a !== id) : [...activeIds, id]; setActiveIds(next); saveAll(editors, next); }; const saveWSL = async () => { await updateSettings({ wsl_distro: wslDistro, wsl_open_mode: wslOpenMode }, spaceId); qc.invalidateQueries({ queryKey: ["settings", spaceId] }); showToast("已保存 ✓"); }; const pingMcp = async () => { setMcpChecking(true); try { const result = await checkMcpHealth(); setMcpHealth(result); } catch { setMcpHealth({ ok: false, port: parseInt(mcpPort, 10), error: "检测失败" }); } finally { setMcpChecking(false); } }; const saveMcp = async () => { const port = parseInt(mcpPort, 10); if (isNaN(port) || port < 1024 || port > 65535) { showToast("❌ 端口范围:1024 ~ 65535"); return; } await updateSettings({ mcp_port: mcpPort }); qc.invalidateQueries({ queryKey: ["settings", spaceId] }); showToast("已保存,所有产品组成员配置已更新 ✓"); pingMcp(); }; const detectWsl = async () => { setDetectingWsl(true); setWslError(""); try { const list = await getWslDistros(); setWslDistros(list); if (list.length > 0 && !list.includes(wslDistro)) { setWslDistro(list[0]); } } catch (e) { setWslError(String(e)); setWslDistros([]); } finally { setDetectingWsl(false); } }; return (

设置

{/* ── 编辑器管理 ── */}

编辑器

{editors.length === 0 ? (

🖥️

还没有配置编辑器

点击「新增编辑器」添加

) : (
{editors.map((e) => { const checked = activeIds.includes(e.id); return (
{/* 多选 checkbox */} toggleActive(e.id)} className="w-4 h-4 rounded accent-blue-600 shrink-0 cursor-pointer" /> {/* 信息 */}
toggleActive(e.id)}>
{e.name} {checked && ( 已启用 )}
{e.path}
{/* 操作 */}
); })}
)}

勾选的编辑器会在项目卡片上各显示一个按钮,可同时启用多个。

{/* ── WSL ── */}

WSL 配置

{/* 检测按钮 */}
{wslDistros.length > 0 && ( ✓ 检测到 {wslDistros.length} 个发行版 )} {wslError && ( {wslError} )}
{/* 发行版选择 */}
{wslDistros.length > 0 ? ( ) : ( setWslDistro(e.target.value)} placeholder="Ubuntu" /> )}

路径前缀: \\wsl.localhost\{wslDistro}\

{/* 编辑器启动方式 */}
{([ { value: "wsl_internal", label: "从 WSL 内部启动(推荐)", desc: `wsl -d ${wslDistro} -- `, }, { value: "remote_flag", label: "Remote WSL 参数", desc: ` --remote wsl+${wslDistro} `, }, { value: "unc_path", label: "Windows UNC 路径(不走 Remote)", desc: ` \\\\wsl.localhost\\${wslDistro}\\`, }, ] as const).map(({ value, label, desc }) => ( ))}
{/* ── MCP 服务 ── */}

MCP 服务

{mcpChecking ? ( 检测中… ) : mcpHealth ? ( mcpHealth.ok ? ( 运行中 {mcpHealth.latency_ms}ms ) : ( 端口未响应 ) ) : null}

产品组 MCP Server,为组内项目的 Claude 提供跨项目上下文能力。配置好产品组成员后,每个项目的 {" "}.claude/settings.json{" "} 会自动注入对应的 MCP 连接。

setMcpPort(e.target.value)} min={1024} max={65535} />

MCP 地址(单一全局服务器): http://localhost:{mcpPort}/mcp

{/* 编辑器表单弹窗 */} {formTarget !== undefined && ( setFormTarget(undefined)} onSave={handleSaveEditor} /> )}
); } /* ── 蓝图规则批量同步区块 ────────────────────────────────────────────────────── */ interface SyncRow { name: string; path: string; status: "pending" | "ok" | "error"; result?: SyncResult; error?: string; } function BatchSyncSection() { const [rows, setRows] = useState([]); const [running, setRunning] = useState(false); const [done, setDone] = useState(false); const handleSync = async () => { setRunning(true); setDone(false); let projects: Awaited> = []; try { projects = await getProjects(undefined, undefined); } catch { setRunning(false); return; } // 只处理有 win_path 的项目 const targets = projects .filter((p) => p.win_path) .map((p) => ({ name: p.name, path: p.win_path! })); if (targets.length === 0) { setRows([]); setRunning(false); setDone(true); return; } const initial: SyncRow[] = targets.map((t) => ({ ...t, status: "pending" })); setRows(initial); const results = [...initial]; for (let i = 0; i < results.length; i++) { try { const result = await syncBlueprintRules(results[i].path); results[i] = { ...results[i], status: "ok", result }; } catch (e) { results[i] = { ...results[i], status: "error", error: String(e) }; } setRows([...results]); } setRunning(false); setDone(true); }; return (

蓝图规则同步

将最新 CONVENTIONS.md 同步至所有已注册项目

{rows.length > 0 && (
{rows.map((row) => (
{row.status === "ok" ? "✓" : row.status === "error" ? "✗" : "…"}
{row.name} {row.status === "ok" && row.result && ( {row.result.message} )} {row.status === "error" && ( {row.error} )}
))}
)} {done && rows.length === 0 && (

没有找到包含 win_path 的项目。

)}
); } /* ── 检查更新区块 ─────────────────────────────────────────────────────────────── */ type UpdateState = | { status: "idle" } | { status: "checking" } | { status: "latest" } | { status: "available"; version: string; notes: string; downloadAndInstall: () => Promise } | { status: "downloading"; progress: number } | { status: "error"; message: string }; function UpdateSection() { const [appVersion, setAppVersion] = useState(""); const [state, setState] = useState({ status: "idle" }); useEffect(() => { getVersion().then(setAppVersion).catch(() => {}); }, []); const handleCheck = async () => { setState({ status: "checking" }); try { const update = await checkUpdate(); if (!update?.available) { setState({ status: "latest" }); return; } setState({ status: "available", version: update.version, notes: update.body ?? "", downloadAndInstall: async () => { let downloaded = 0; let total = 0; await update.downloadAndInstall((event) => { if (event.event === "Started") { total = event.data.contentLength ?? 0; setState({ status: "downloading", progress: 0 }); } else if (event.event === "Progress") { downloaded += event.data.chunkLength; setState({ status: "downloading", progress: total > 0 ? Math.round((downloaded / total) * 100) : 0 }); } else if (event.event === "Finished") { setState({ status: "idle" }); } }); await relaunch(); }, }); } catch (e) { setState({ status: "error", message: String(e) }); } }; return (

关于 & 更新

炼境

当前版本 v{appVersion}

{state.status === "available" && ( v{state.version} 可用 )} {state.status === "latest" && ( 已是最新版本 )} {state.status === "downloading" && ( 下载中 {state.progress}% )} {state.status === "available" ? ( ) : ( )}
{state.status === "error" && (

{state.message}

)}
); }