508 lines
13 KiB
Markdown
508 lines
13 KiB
Markdown
# AI Agent 开发指南
|
||
|
||
本文档为 AI Agent 开发本项目提供详细的规范和模板,帮助快速理解项目结构、技术栈和开发流程。
|
||
|
||
## 项目概览
|
||
|
||
- **项目名称**: Thing
|
||
- **技术栈**: Tauri 2 + Vue 3 + Vite + Pinia + shadcn-vue + Rust
|
||
- **平台**: Windows
|
||
- **包管理**: Bun
|
||
|
||
## 核心架构
|
||
|
||
### 模块系统
|
||
|
||
项目采用统一的模块注册机制。每个模块通过 `src/modules/<name>/index.ts` 导出 `ModuleConfig` 配置,由 `src/modules/registry.ts` 中的 `moduleRegistry` 单例统一管理。
|
||
|
||
```
|
||
src/modules/<module-name>/
|
||
├── index.ts # 模块配置(ModuleConfig)
|
||
├── ModuleComponent.vue # 前端组件
|
||
src-tauri/src/ # Rust 后端逻辑(按功能分文件/模块目录)
|
||
```
|
||
|
||
#### 模块配置类型
|
||
|
||
```typescript
|
||
// src/types/module.ts
|
||
interface ModuleConfig {
|
||
id: string // 唯一标识
|
||
name: string // 显示名称
|
||
icon: string // 图标标识(对应 icons.ts 中的 key)
|
||
description: string // 模块描述
|
||
category: ModuleCategory // 分类:'network' | 'tool' | 'system' | 'media'
|
||
defaultEnabled?: boolean // 默认是否启用
|
||
builtin?: boolean // 是否内置模块(不可禁用)
|
||
loader?: () => Promise<{ default: Component }> // 懒加载
|
||
component?: Component // 直接组件引用(内置模块)
|
||
searchItems?: SearchIndexItem[] // 全局搜索项
|
||
process?: ModuleProcessConfig // 进程配置(需要子进程的模块)
|
||
lifecycle?: ModuleLifecycle // 生命周期钩子
|
||
order?: number // 排序权重
|
||
}
|
||
```
|
||
|
||
#### 注册新模块
|
||
|
||
1. 创建 `src/modules/<name>/index.ts`,导出 `moduleConfig`
|
||
2. 在 `src/modules/index.ts` 中添加导入
|
||
3. 在 `src/modules/icons.ts` 中添加图标映射
|
||
|
||
### IPC 通信
|
||
|
||
前端通过 Tauri API 调用 Rust 命令:
|
||
|
||
```typescript
|
||
import { invoke } from '@tauri-apps/api/core'
|
||
|
||
// 调用 Rust 命令
|
||
const result = await invoke('module_command', { param: 'value' })
|
||
```
|
||
|
||
### 状态管理
|
||
|
||
使用 Pinia 管理全局状态,每个模块可拥有独立的 store:
|
||
|
||
```typescript
|
||
// src/stores/moduleName.ts
|
||
import { defineStore } from 'pinia'
|
||
|
||
export const useModuleStore = defineStore('moduleName', {
|
||
state: () => ({
|
||
// 状态定义
|
||
}),
|
||
actions: {
|
||
// 操作方法
|
||
}
|
||
})
|
||
```
|
||
|
||
## 开发规范
|
||
|
||
### 命名规范
|
||
|
||
| 类型 | 格式 | 示例 |
|
||
|------|------|------|
|
||
| 组件 | PascalCase | `ProxyManager.vue` |
|
||
| 函数 | camelCase | `getSystemProxy()` |
|
||
| 文件 | kebab-case | `proxy-manager.ts` |
|
||
| 常量 | UPPER_CASE | `MAX_HISTORY_COUNT` |
|
||
| Rust 模块 | snake_case | `proxy.rs` |
|
||
| Tauri 命令 | snake_case | `get_system_proxy` |
|
||
|
||
### 代码风格
|
||
|
||
- **TypeScript**: 使用 ESLint,配置在 `.eslintrc.cjs`
|
||
- **Rust**: 使用 `cargo fmt` 格式化
|
||
- **提交信息**: Conventional Commits 格式
|
||
|
||
## 模块开发模板
|
||
|
||
### 前端模块模板
|
||
|
||
创建 `src/modules/<module-name>/` 目录,包含以下文件:
|
||
|
||
```typescript
|
||
// index.ts - 模块配置
|
||
import type { ModuleConfig } from '@/types/module'
|
||
|
||
export const moduleConfig: ModuleConfig = {
|
||
id: 'module-name',
|
||
name: '模块名称',
|
||
icon: 'module-name', // 需在 icons.ts 中添加映射
|
||
description: '模块描述',
|
||
category: 'tool', // 'network' | 'tool' | 'system' | 'media'
|
||
defaultEnabled: true,
|
||
loader: () => import('./ModuleComponent.vue'),
|
||
searchItems: [
|
||
{
|
||
title: '功能名称',
|
||
description: '功能描述',
|
||
keywords: ['关键词1', '关键词2']
|
||
}
|
||
],
|
||
order: 100
|
||
}
|
||
```
|
||
|
||
> 注册模块时,还需在 `src/modules/index.ts` 中添加导入,在 `src/modules/icons.ts` 中添加图标映射。
|
||
|
||
```vue
|
||
<!-- ModuleComponent.vue - 主组件 -->
|
||
<script setup lang="ts">
|
||
import { ref, onMounted, onUnmounted } from 'vue'
|
||
import { useModuleStore } from '@/stores/moduleName'
|
||
|
||
const store = useModuleStore()
|
||
|
||
// 组件逻辑
|
||
const data = ref(null)
|
||
|
||
const loadData = async () => {
|
||
// 调用 Rust 命令
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadData()
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
// 清理资源
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="module-container">
|
||
<!-- 模块内容 -->
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.module-container {
|
||
padding: 20px;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
### Rust 命令模板
|
||
|
||
在 `src-tauri/src/` 中创建模块文件(如 `src-tauri/src/my_module.rs` 或 `src-tauri/src/my_module/` 目录),并在 `lib.rs` 中注册:
|
||
|
||
```rust
|
||
// src-tauri/src/my_module.rs
|
||
use serde::Serialize;
|
||
use tauri::State;
|
||
|
||
#[derive(Serialize)]
|
||
pub struct CommandResult<T> {
|
||
pub success: bool,
|
||
pub data: Option<T>,
|
||
pub error: Option<String>,
|
||
}
|
||
|
||
#[tauri::command]
|
||
pub async fn module_get_data() -> CommandResult<Vec<String>> {
|
||
match fetch_data() {
|
||
Ok(data) => CommandResult {
|
||
success: true,
|
||
data: Some(data),
|
||
error: None,
|
||
},
|
||
Err(e) => CommandResult {
|
||
success: false,
|
||
data: None,
|
||
error: Some(e.to_string()),
|
||
},
|
||
}
|
||
}
|
||
|
||
#[tauri::command]
|
||
pub async fn module_set_data(data: String) -> CommandResult<bool> {
|
||
match save_data(&data) {
|
||
Ok(_) => CommandResult {
|
||
success: true,
|
||
data: Some(true),
|
||
error: None,
|
||
},
|
||
Err(e) => CommandResult {
|
||
success: false,
|
||
data: None,
|
||
error: Some(e.to_string()),
|
||
},
|
||
}
|
||
}
|
||
|
||
fn fetch_data() -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
||
// 实现逻辑
|
||
Ok(vec![])
|
||
}
|
||
|
||
fn save_data(data: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||
// 实现逻辑
|
||
Ok(())
|
||
}
|
||
```
|
||
|
||
### 注册 Rust 命令
|
||
|
||
在 `src-tauri/src/lib.rs` 中注册命令:
|
||
|
||
```rust
|
||
// lib.rs
|
||
mod my_module;
|
||
|
||
use my_module::{module_get_data, module_set_data};
|
||
|
||
#[tauri::command]
|
||
fn greet(name: &str) -> String {
|
||
format!("Hello, {}!", name)
|
||
}
|
||
|
||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||
pub fn run() {
|
||
tauri::Builder::default()
|
||
.invoke_handler(tauri::generate_handler![
|
||
greet,
|
||
module_get_data,
|
||
module_set_data
|
||
])
|
||
.run(tauri::generate_context!())
|
||
.expect("error while running tauri application");
|
||
}
|
||
```
|
||
|
||
## 常用 Tauri 插件
|
||
|
||
项目当前使用的 Tauri 插件(见 `Cargo.toml`):
|
||
|
||
| 插件 | 用途 |
|
||
|------|------|
|
||
| `tauri-plugin-autostart` | 开机自启 |
|
||
| `tauri-plugin-opener` | 打开文件/目录/URL(绕过 IPC scope 限制) |
|
||
| `tauri-plugin-dialog` | 文件/目录选择对话框 |
|
||
|
||
> 系统托盘(tray)和窗口效果(mica/acrylic)是 Tauri 2 内置能力,无需额外插件。日志系统为自建(`src-tauri/src/logger.rs`),未使用 `tauri-plugin-log`。
|
||
|
||
## 进程管理
|
||
|
||
对于需要管理外部子进程的模块(如 mihomo),使用内置的 `ProcessManager`。下载器模块使用进程内自建下载引擎(`src-tauri/src/download_engine/`),不涉及外部子进程:
|
||
|
||
### 架构
|
||
|
||
- **Rust 端** (`src-tauri/src/process_manager.rs`):`ProcessManager` 通过 `std::process::Command` 管理子进程,支持启动、停止、崩溃检测和自动重启
|
||
- **前端** (`src/stores/processStore.ts`):Pinia store,通过 `invoke` 调用 Rust 命令,通过 `listen` 接收进程状态变更事件
|
||
- **模块配置**:在模块的 `index.ts` 中通过 `ModuleProcessConfig` 声明进程信息
|
||
|
||
### 模块配置
|
||
|
||
```typescript
|
||
// src/modules/proxy/index.ts
|
||
export const moduleConfig: ModuleConfig = {
|
||
// ...
|
||
process: {
|
||
name: 'mihomo',
|
||
executable: '', // 运行时确定
|
||
args: ['-f', 'config.yaml'],
|
||
autoStart: false, // 模块启用时是否自动启动
|
||
restartOnCrash: true, // 崩溃后自动重启
|
||
maxRestarts: 3 // 最大重启次数(0 = 不限制)
|
||
}
|
||
}
|
||
```
|
||
|
||
### 前端 API
|
||
|
||
```typescript
|
||
import { useProcessStore } from '@/stores/processStore'
|
||
|
||
const processStore = useProcessStore()
|
||
|
||
// 通过模块 ID 启动进程(自动读取模块配置)
|
||
await processStore.startByModule('proxy')
|
||
|
||
// 停止进程
|
||
await processStore.stopByModule('proxy')
|
||
|
||
// 获取进程状态
|
||
const status = processStore.getProcessStatus('proxy')
|
||
|
||
// 监听状态变更(在 main.ts 中已初始化)
|
||
// processStore.initListener()
|
||
```
|
||
|
||
### Rust 命令
|
||
|
||
| 命令 | 参数 | 返回值 |
|
||
|------|------|--------|
|
||
| `start_process` | `StartProcessParams` | `ProcessInfo` |
|
||
| `stop_process` | `id: String` | `()` |
|
||
| `get_process_status` | `id: String` | `Option<ProcessInfo>` |
|
||
| `get_all_process_status` | - | `Vec<ProcessInfo>` |
|
||
| `stop_all_processes` | - | `()` |
|
||
|
||
### 事件
|
||
|
||
| 事件名 | 载荷 | 触发时机 |
|
||
|--------|------|----------|
|
||
| `process-status-changed` | `ProcessInfo` | 进程状态变更(崩溃、重启、停止) |
|
||
|
||
## 数据存储
|
||
|
||
### 配置文件
|
||
|
||
使用 JSON 格式存储在应用数据目录:
|
||
|
||
```rust
|
||
// 获取应用数据目录
|
||
let app_dir = app.path().app_data_dir()?;
|
||
let config_path = app_dir.join("config.json");
|
||
```
|
||
|
||
### SQLite
|
||
|
||
对于需要查询的数据(如剪贴板历史),使用 `rusqlite` crate:
|
||
|
||
```toml
|
||
# Cargo.toml
|
||
rusqlite = { version = "0.30", features = ["bundled"] }
|
||
```
|
||
|
||
## 权限配置
|
||
|
||
在 `src-tauri/capabilities/default.json` 中配置权限(实际项目配置):
|
||
|
||
```json
|
||
{
|
||
"$schema": "../gen/schemas/desktop-schema.json",
|
||
"identifier": "default",
|
||
"description": "Capability for the main window",
|
||
"windows": ["main"],
|
||
"permissions": [
|
||
"core:default",
|
||
"opener:default",
|
||
"opener:allow-reveal-item-in-dir",
|
||
"opener:allow-open-path",
|
||
"dialog:default",
|
||
"core:window:allow-minimize",
|
||
"core:window:allow-maximize",
|
||
"core:window:allow-close",
|
||
"core:window:allow-toggle-maximize",
|
||
"core:window:allow-hide",
|
||
"core:window:allow-show",
|
||
"core:window:allow-set-focus",
|
||
"core:window:allow-start-dragging",
|
||
"core:window:allow-set-effects",
|
||
"core:window:allow-set-background-color",
|
||
"core:window:allow-set-theme"
|
||
]
|
||
}
|
||
```
|
||
|
||
## 调试指南
|
||
|
||
### 前端调试
|
||
|
||
- 使用 Chrome DevTools:`Ctrl+Shift+I`
|
||
- 打印日志:`console.log()`
|
||
|
||
### Rust 调试
|
||
|
||
- 使用 `println!()` 输出到终端
|
||
- 使用 `dbg!()` 宏调试变量
|
||
- 使用 Visual Studio Code 的 Rust 调试插件
|
||
|
||
### 构建问题排查
|
||
|
||
1. **Rust 编译错误**:检查 `Cargo.toml` 依赖版本
|
||
2. **前端构建错误**:检查 `package.json` 依赖和 TypeScript 类型
|
||
3. **Tauri 配置错误**:检查 `tauri.conf.json` 路径和权限配置
|
||
|
||
## 常见问题
|
||
|
||
### Windows 路径问题
|
||
|
||
使用 `std::path::Path` 处理路径,避免硬编码分隔符:
|
||
|
||
```rust
|
||
let path = Path::new("data").join("config.json");
|
||
```
|
||
|
||
### Tauri 2 权限问题
|
||
|
||
确保在 `capabilities` 中声明所需权限,否则命令调用会失败。
|
||
|
||
### WebView2 兼容性
|
||
|
||
确保用户安装了 Microsoft Edge WebView2 Runtime:
|
||
- 安装包:https://developer.microsoft.com/zh-cn/microsoft-edge/webview2/
|
||
|
||
## 开发流程
|
||
|
||
1. **创建模块目录**:`src/modules/<module-name>/`,编写组件和 `index.ts` 配置
|
||
2. **注册模块**:在 `src/modules/index.ts` 中添加导入,在 `src/modules/icons.ts` 中添加图标映射
|
||
3. **实现 Rust 命令**:在 `src-tauri/src/` 中编写后端逻辑,在 `lib.rs` 中注册命令
|
||
4. **配置权限**:更新 `capabilities/default.json`(如需要)
|
||
5. **测试**:运行 `bun run tauri dev` 测试
|
||
6. **构建**:运行 `bun run tauri build` 构建生产版本
|
||
|
||
## 模板代码生成
|
||
|
||
### 快速生成模块
|
||
|
||
使用以下命令创建新模块结构:
|
||
|
||
```bash
|
||
# 创建前端模块目录
|
||
mkdir -p src/modules/new-module
|
||
|
||
# 创建 Rust 模块文件
|
||
touch src-tauri/src/new_module.rs
|
||
```
|
||
|
||
### 模板文件
|
||
|
||
复制以下模板快速开始:
|
||
|
||
**前端组件模板**:
|
||
|
||
```vue
|
||
<script setup lang="ts">
|
||
import { ref, onMounted } from 'vue'
|
||
|
||
const loading = ref(false)
|
||
const error = ref('')
|
||
|
||
const fetchData = async () => {
|
||
loading.value = true
|
||
try {
|
||
// 调用 Rust 命令
|
||
} catch (e) {
|
||
error.value = (e as Error).message
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchData()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div v-if="loading" class="flex items-center justify-center py-8">
|
||
<Loader2 class="animate-spin text-muted-foreground" :size="24" />
|
||
</div>
|
||
<div v-else-if="error" class="text-destructive p-4">{{ error }}</div>
|
||
<div v-else class="content p-5">
|
||
<!-- 内容 -->
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
**Rust 命令模板**:
|
||
|
||
```rust
|
||
#[tauri::command]
|
||
pub async fn new_module_action() -> Result<serde_json::Value, String> {
|
||
// 实现逻辑
|
||
Ok(serde_json::json!({}))
|
||
}
|
||
```
|
||
|
||
## 注意事项
|
||
|
||
1. **避免阻塞主线程**:Rust 命令使用 `async` 避免阻塞
|
||
2. **资源清理**:在组件卸载时清理定时器和事件监听器
|
||
3. **错误处理**:所有操作都应有错误处理和用户提示
|
||
4. **安全考虑**:避免执行未验证的用户输入作为命令参数
|
||
5. **性能优化**:对于高频更新的数据使用 `watch` 和 `computed`
|
||
|
||
## 参考资源
|
||
|
||
- [Tauri 2 Documentation](https://v2.tauri.app/)
|
||
- [Vue 3 Documentation](https://vuejs.org/)
|
||
- [Pinia Documentation](https://pinia.vuejs.org/)
|
||
- [shadcn-vue UI Documentation](https://shadcn-vue.com/docs)
|
||
- [Rust Documentation](https://doc.rust-lang.org/)
|