开发指南
这一页不是仓库里所有开发说明的逐字搬运版。
它的目标是把当前项目最重要、最容易踩坑、最影响代码质量的一组约束收敛到一页里,方便你在真正动手之前建立共识。
如果你还没有理解项目边界,建议先看 系统架构。
如果你还没有完成本地环境准备,先回到 快速开始。
开发原则
ClashMind 当前的开发原则可以概括成五句话:
- 前端负责交互,Rust 负责中控,AI sidecar 负责模型相关逻辑。
- 后端事实走 TanStack Query,本地交互走 Zustand。
- 前端不直连 Mihomo,统一经过 Rust IPC。
- 配置变更优先走校验、预览、确认,再考虑落盘。
- 代码组织优先服从边界清晰,而不是“哪里方便先写哪里”。
这些原则听上去很抽象,但它们几乎决定了你每次改代码时文件应该落在哪一层。
前端规范
使用 TypeScript 严格模式
前端默认遵循严格类型约束。
最简单的理解方式是:
- 不要用
any - 不要把后端返回值当成“随便长什么样都可以”
- 能在类型层约束的东西,尽量不要推迟到运行时再赌
项目里的 IPC 返回类型已经集中定义在 src/lib/tauri-api.ts。
这意味着你新增一个后端命令时,前端通常也要同步新增类型,而不是让调用方自己猜返回值。
组件、hooks 和 API 要分层
前端不是所有逻辑都堆在组件里。
比较推荐的职责划分是:
- 组件负责渲染与交互
- hooks 负责请求、缓存和动作封装
lib/tauri-api.ts负责统一的 IPC 函数入口
例如代理模块的 hook:
export function useProxies() {
return useQuery({ queryKey: PROXY_KEYS.all, queryFn: api.proxy.getAll });
}
export function useSwitchProxy() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ group, name }: { group: string; name: string }) =>
api.proxy.switch(group, name),
onSuccess: () => qc.invalidateQueries({ queryKey: PROXY_KEYS.all }),
});
}这段代码体现了前端的推荐模式:
- 组件不要直接
invoke - 组件通过 hook 获取数据和动作
- hook 通过统一
api对象访问 IPC
路径别名优先用 @/*
项目当前使用 @/* -> src/* 的路径别名。
这意味着在前端代码里,优先写:
import { api } from "@/lib/tauri-api";
import { useAppStore } from "@/stores/app-store";而不是满屏相对路径层层回退。
当一个模块开始频繁使用 ../../../../ 时,通常说明路径组织或职责边界已经需要重新审视。
Rust 后端规范
错误处理优先使用 thiserror
Rust 侧当前约定非常明确:
- 业务错误用
thiserror定义 - IPC 命令返回
Result<T, CustomError> - 错误类型需要能序列化给前端
这不是风格偏好,而是为了让 Tauri 命令错误在前端可读、可展示、可追踪。
禁止随手 unwrap() / expect()
这里的规则也很直接:
- 正常业务路径不要用
unwrap() - 需要向上抛的错误就明确返回
- 测试中可以更宽松,但生产逻辑应把失败路径写出来
这条约束在 sidecar 管理、文件操作、SQLite 和 Mihomo 通信上尤其重要。
因为这些地方几乎都属于外部系统边界,失败不是例外,而是常态之一。
Tauri 2 Sidecar 使用现代 API
项目明确使用 tauri_plugin_shell::ShellExt 管理 sidecar。
也就是说:
- 不要再用已经废弃或移除的旧 API 习惯
- 新增 sidecar 或 shell 逻辑时,优先沿用现有
core/sidecar.rs的模式
如果你准备改 sidecar 启停逻辑,先读 src-tauri/src/core/sidecar.rs,不要凭旧版 Tauri 教程直接写。
IPC 规则
前端不直连 Mihomo
这是项目里最重要的边界规则之一。
即使某个 Mihomo API 看起来“前端直接 fetch 一下也能成”,也不要这么做。
正确路径应当是:
- 前端通过
api.*发起invoke - Rust 命令负责参数整形与边界处理
- Rust 调用 Mihomo HTTP/WS 或操作本地文件
- 结果再回到前端
原因很实际:
- 地址和 secret 不必散落在前端
- 文件写入不会穿透到 UI 层
- 日志、重试、快照和权限更容易收口
IPC 类型统一放在 src/lib/tauri-api.ts
这个文件不是简单的工具文件。
它在当前项目里承担了三个角色:
- IPC 命令入口
- 前后端共享的 TypeScript 视图类型
- AI、代理、统计、配置、系统能力的统一门面
真实代码结构大致是这样:
export const api = {
ai: {
start: () => invoke("start_ai_service"),
status: () => invoke<boolean>("get_ai_status"),
chat: (params: AiChatParams) => invoke("ai_chat", { params }),
},
proxy: {
getAll: () => invoke<ProxiesResponse>("get_proxies"),
switch: (group: string, name: string) => invoke("switch_proxy", { group, name }),
},
config: {
read: (path: string) => invoke<string>("read_config", { path }),
write: (path: string, content: string) => invoke("write_config", { path, content }),
},
} as const;如果你要新增一个 IPC 能力,推荐顺序通常是:
- 先补 Rust 命令
- 再补前端类型
- 最后把
api门面和对应 hook 接起来
这样调用方就不需要自己知道具体命令名和参数细节。
AI 相关 IPC 也要遵循相同边界
AI 功能虽然复杂,但同样不能绕过统一出口。
例如:
- 启停 AI sidecar 通过
start_ai_service/stop_ai_service - 聊天通过
ai_chat - 模型列表通过
fetch_ai_models - 应用配置变更通过
apply_config_change
这保证了 AI 相关流程仍然受 Rust 统一约束,而不是变成前端和 AI sidecar 的直连通道。
不要引入第二套调用路径
如果某个功能已经有 invoke -> Rust -> sidecar/Mihomo 的正路,就不要为了“图快”在前端新开一条直连路径。
一旦出现两套入口,缓存、日志、权限和错误处理会立刻分叉。
状态管理规则
TanStack Query 管后端事实
下列数据优先放到 Query:
- 代理列表
- 配置文件内容
- 统计查询结果
- AI 设置
- 快照列表
- 连接状态
判断标准很简单:
如果这份数据来自 Rust、Mihomo、SQLite 或 AI service,而且存在“重新获取”这件事,那么它大概率应该进 Query。
Zustand 管客户端状态
下列状态更适合放到 Zustand:
- 主题
- 侧边栏折叠
- 当前页面
- 表单草稿
- AI 面板中的本地消息流状态
- 待确认配置 payload 的临时持有
全局应用 store 的真实结构可以作为参考:
interface AppState {
theme: Theme;
sidebarCollapsed: boolean;
currentPage: Page;
mihomoConfigDir: string;
apiAddress: string;
apiSecret: string;
httpPort: number;
socksPort: number;
language: "zh-CN" | "en-US";
}AI 会话 store 则更像一个本地状态机:
- 有消息数组
- 有流式进行中的消息 ID
- 有工具调用状态
- 有待应用的配置 payload
这正是 Zustand 擅长的状态类型。
不要把 Query 当成全局状态容器
Query 适合缓存后端数据,不适合拿来保存任意本地 UI 偏好。
例如主题、导航、表单展开状态这类东西,硬塞到 Query 里会让缓存语义变得很奇怪。
同样地,也不要把需要失效、刷新、重试的后端事实手搓进 Zustand。
命名与目录约定
文件名使用 kebab-case
前端和多数 TypeScript 文件,当前约定使用 kebab-case。
例如:
config-diff-preview.tsxuse-ai-settings.tsproxy-mode-switch.tsx
这样做有两个收益:
- 文件名风格统一
- 在跨平台文件系统上更稳定,避免大小写混用带来的问题
React 组件使用 PascalCase
文件名是 kebab-case,不代表导出组件也要同样命名。
组件本身保持 PascalCase 更符合 React 生态习惯。
例如:
- 文件名:
config-diff-preview.tsx - 组件名:
ConfigDiffPreview
目录优先按职责和业务切分
当前仓库的推荐落点是:
- 跨业务 UI:
src/components/ - 业务模块:
src/features/<domain>/ - 全局 hook:
src/hooks/ - 纯工具和 API:
src/lib/ - 本地状态:
src/stores/
Rust 侧同理:
- 命令定义在
cmd/ - 核心逻辑在
core/ - 数据采集在
collector/ - 数据库存取在
db/
如果你新增一段代码时犹豫该放哪,优先问自己:
“它是某个业务模块的一部分,还是一项跨模块能力?”
UI 与依赖约定
样式优先用 Tailwind CSS 4 原子类
当前项目默认使用 Tailwind CSS 4。
这意味着:
- 页面和组件样式优先通过原子类表达
- 主题和设计 token 优先沿用现有变量与体系
- 不要为小范围样式频繁新增单独 CSS 文件
组件能力优先复用现有基建
当前 UI 生态主要包括:
- Radix UI
- lucide-react
- framer-motion
- Monaco Editor
如果你需要弹层、下拉、开关、Tabs 之类的基础交互,优先看是否已有对应 Radix 组件封装。
不要为了一个小需求再引入第二套功能重复的组件库。
Radix UI 按需安装
当前约定不是“一次性装全家桶”。
而是需要哪个包,就明确引入哪个 @radix-ui/react-* 依赖。
这种方式更利于控制依赖体积,也更符合当前仓库已经建立起来的组件结构。
AI 开发时的额外边界
如果你改的是 AI 功能,除了普通前后端规范外,还要额外注意三条。
第一,不要让模型直接落盘或直接操作系统状态。
第二,不要跳过脱敏、Schema 校验和 diff 预览。
第三,不要把 Provider 特定逻辑散落到前端组件里。
比较理想的职责分布是:
- 前端只负责设置表单、聊天 UI 和 diff 展示
- Provider 细节留在
ai-service/src/providers/ - 工具定义留在
ai-service/src/tools/ - 安全逻辑留在
ai-service/src/safety/
这样当某个 Provider、Schema 或工具策略变化时,不需要把 UI 层也拖下水。
提交与检查建议
提交前至少做这几项检查
对前端或文档改动,最少建议执行:
pnpm lint
pnpm type-check如果你改了 Rust,最好再补:
cargo fmt --check
cargo clippy
cargo test如果你改了文档站,再补一次:
cd docs
npm run docs:build项目并不是要求你每次都把所有命令跑满,但至少应该针对改动层级做最基本的自检。
提交信息建议
仓库当前文档约定推荐使用语义前缀:
feat:fix:refactor:
这不是说当前仓库已经强制配置了 commit lint。
它更像是一条团队协作建议:
让变更目的在历史记录里更容易扫读。
变更描述优先写“为什么”
在提交说明或 PR 描述里,尽量回答这三个问题:
- 改了什么
- 为什么要这么改
- 如何验证
尤其是 IPC、状态管理和 AI 逻辑改动,如果不写“为什么”,后续维护者会很难判断这条边界是不是有意设计。
一个推荐的开发顺序
如果你准备加一个新功能,可以按下面这个顺序推进:
- 先确认功能属于前端、Rust 还是 AI sidecar 哪一层。
- 如果涉及后端能力,先定义 Rust 命令和返回结构。
- 在
src/lib/tauri-api.ts暴露前端入口。 - 为这个入口写 Query hook 或 Mutation hook。
- 最后再写组件和页面交互。
如果需求涉及 AI 配置变更,则再加两步:
- 在
ai-service/src/tools/补工具和 schema。 - 检查脱敏、预览和确认链路是否完整。
这套顺序的好处是,你会天然沿着边界开发,而不是先把 UI 写死,再回头补系统层。
新增功能时先找“现有模式”
在 ClashMind 里,最稳的开发方式不是从空白开始设计一套新写法。
更好的方式是先找一个最接近的现有 feature,沿着它的目录结构、hook 形态和 API 封装方式继续写。
继续阅读
到这里,你应该已经知道在这个仓库里哪些做法是推荐路径,哪些做法会越过边界。
如果你准备改 AI 逻辑,继续看 AI Sidecar。
如果你想回到整体系统视角,回看 系统架构。
如果你只是还没把项目跑起来,回到 快速开始。