Files
Thing/AI_DEV_GUIDE.md
T
2026-07-22 22:26:40 +08:00

508 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/)