Files
2026-07-13 13:20:22 +08:00

316 lines
16 KiB
Markdown

# File & Directory Structure
Rules for organizing files and directories across the entire Electron project.
## Repository Root
### Root Directory Rules
- **README translations** belong in `docs/readme/`, not at root. Only the main `readme.md` stays at root (GitHub convention)
- **Guide documents** (deployment, testing, WebUI, CDP, etc.) belong in `docs/guides/`
- **Contributor documentation** (dev setup, code style, file structure, PR workflow) belongs in `docs/contributing/`
- **Architecture documentation** belongs in `docs/architecture/` (research notes under `docs/architecture/research/`)
- **Feature specs / PRDs / design drafts** belong in `docs/specs/` (or `docs/prds/` for formal PRDs maintained by the product team)
- **Config files** (`tsconfig.json`, `package.json`, etc.) stay at root — Node.js/Electron ecosystem convention
- **New documentation** should be placed under the appropriate `docs/` subdirectory, not at project root
### Current Root Cleanup Targets
| Action | Files |
| ------------------------------------------ | ---------------------------------- |
| Move readme translations to `docs/readme/` | `readme_{ch,es,jp,ko,pt,tr,tw}.md` |
## Project Layout (`src/`)
AionUi is a multi-process Electron app with three core layers: **renderer**, **main process**, and **preload/shared**.
### Target Structure
```
src/
├── renderer/ # Renderer layer — React UI, no Node.js APIs
├── process/ # Main process layer — all Node.js / Electron business
│ ├── bridge/ # IPC handlers
│ ├── services/ # Business logic
│ ├── database/ # SQLite
│ ├── task/ # Agent/task management
│ ├── agent/ # AI platform connections
│ ├── channels/ # Multi-channel messaging
│ ├── extensions/ # Plugin system
│ ├── webserver/ # WebUI server
│ ├── worker/ # Background workers (fork)
│ └── i18n/ # Main-process i18n
├── common/ # Shared layer — cross-process types, adapters, utilities
├── preload.ts # IPC bridge — contextBridge between main ↔ renderer
└── index.ts # Main process entry point
```
### Current Structure
All main-process modules now live under `src/process/`. The `src/` root contains only the three core layers (`renderer/`, `process/`, `common/`), the entry files (`index.ts`, `preload.ts`), and the ambient type declaration (`types.d.ts`).
## Directory Naming — Two Conventions by Process
This project straddles two ecosystems. Each follows its own convention:
| Scope | Directory naming | Reason |
| ---------------------------------- | ---------------------------------------- | --------------------------------------------------------------------------- |
| **Renderer** (`src/renderer/`) | **PascalCase** for component/module dirs | React ecosystem — directory name = component name |
| **Everything else** | **lowercase** | Node.js ecosystem |
| **Categorical dirs** (everywhere) | **lowercase** | `components/`, `hooks/`, `utils/`, `services/` are categories, not entities |
| **Platform dirs** (renderer pages) | **lowercase** | Mirror `src/process/agent/<platform>/` naming for cross-process consistency |
### Quick test
> "Is this directory inside `src/renderer/` AND does it represent a specific component or feature module (not a category)?"
>
> **YES** → PascalCase. **NO** → lowercase.
>
> **Exception**: Platform directories (`acp/`, `codex/`, `gemini/`, `nanobot/`, `openclaw/`) always use lowercase, even inside renderer, to match `src/process/agent/`.
### Renderer examples
```
src/renderer/
├── components/ # categorical → lowercase
│ ├── SettingsModal/ # component → PascalCase
│ └── EmojiPicker/ # component → PascalCase
├── pages/ # categorical → lowercase
│ ├── settings/ # top-level page → lowercase (route segment)
│ │ ├── CssThemeSettings/ # feature module → PascalCase
│ │ └── McpManagement/ # feature module → PascalCase
│ └── conversation/ # top-level page → lowercase
│ ├── GroupedHistory/ # feature module → PascalCase
│ ├── Workspace/ # feature module → PascalCase
│ ├── acp/ # platform dir → lowercase (mirrors src/agent/acp/)
│ └── components/ # categorical → lowercase
└── hooks/ # categorical → lowercase
```
### Non-renderer examples
```
src/process/services/cron/ # lowercase
src/process/agent/acp/ # lowercase
src/process/channels/plugins/dingtalk/ # lowercase
```
## File Naming — Same Everywhere
| Content | Convention | Examples |
| ------------------------- | ------------------------------- | ------------------------------------- |
| React components, classes | PascalCase | `SettingsModal.tsx`, `CronService.ts` |
| Hooks | camelCase with `use` prefix | `useTheme.ts`, `useCronJobs.ts` |
| Utilities, helpers | camelCase | `formatDate.ts`, `cronUtils.ts` |
| Entry points | `index.ts` / `index.tsx` | Required for directory-based modules |
| Config, types, constants | camelCase | `types.ts`, `constants.ts` |
| Styles | kebab-case or `Name.module.css` | `chat-layout.css` |
## Process Boundary Rules
**Violating these causes runtime crashes.**
| Process | Can use | Cannot use |
| ---------------------------------- | ----------------------------- | -------------------------------- |
| **Main** (`src/process/`) | Node.js, Electron main APIs | DOM APIs, React |
| **Renderer** (`src/renderer/`) | DOM APIs, React, browser APIs | Node.js APIs, Electron main APIs |
| **Worker** (`src/process/worker/`) | Node.js APIs | DOM APIs, Electron APIs |
Cross-process communication MUST go through:
- Main ↔ Renderer: IPC via `src/preload.ts` + `src/process/bridge/*.ts`
- Main ↔ Worker: fork protocol via `src/process/worker/WorkerProtocol.ts`
## Main Process Naming
| Type | Pattern | Examples |
| ---------- | --------------------- | --------------------------------- |
| Bridge | `<domain>Bridge.ts` | `cronBridge.ts`, `webuiBridge.ts` |
| Service | `<Name>Service.ts` | `CronService.ts`, `McpService.ts` |
| Interface | `I<Name>Service.ts` | `IConversationService.ts` |
| Repository | `<Name>Repository.ts` | `SqliteConversationRepository.ts` |
## Service Testability Rules
### Pure Logic vs IO Separation
Services must separate **pure logic** from **IO operations**:
- **Pure logic** (data transformation, validation, formatting) → standalone functions, no `fs`/`db`/`net` imports
- **IO operations** (file read, DB query, HTTP call) → thin wrappers in service class or repository
- Service methods should receive IO results as parameters rather than calling IO internally
### Dependency Injection
Services and bridges that depend on external resources (DB, file system, other services) should accept dependencies as constructor/function parameters:
```typescript
// ❌ Hard to test — must mock the entire module
import { db } from '@process/database';
function getConversation(id: string) {
return db.query('SELECT * FROM conversations WHERE id = ?', id);
}
// ✅ Easy to test — inject the dependency
function getConversation(repo: IConversationRepository, id: string) {
return repo.findById(id);
}
```
For existing code using direct imports, `vi.mock()` is acceptable. For new code, prefer parameter injection.
## Test File Mapping
Test files must mirror the source file they test:
| Source | Test |
| -------------------------------------------- | ----------------------------------------------- |
| `src/process/services/CronService.ts` | `tests/unit/cronService.test.ts` |
| `src/process/bridge/fsBridge.ts` | `tests/unit/fsBridge.test.ts` |
| `src/renderer/utils/chat/latexDelimiters.ts` | `tests/unit/latexDelimiters.test.ts` |
| `src/renderer/hooks/ui/useAutoScroll.ts` | `tests/unit/useAutoScroll.dom.test.ts` |
| `src/process/extensions/ExtensionLoader.ts` | `tests/unit/extensions/extensionLoader.test.ts` |
When `tests/unit/` exceeds 10 direct children, group into subdirectories matching the source structure (e.g., `tests/unit/extensions/`). New source files with logic should be added to `vitest.config.ts``coverage.include`.
## Directory Size Limit
A single directory must not contain more than **10** direct children (files + subdirectories). When a directory approaches this limit, split its contents into subdirectories grouped by responsibility.
## UI Library & Icon Standards
- **Component library**: `@arco-design/web-react`. All new UI must use Arco components first.
- **Icon library**: `@icon-park/react`. All icons must come from this library.
- **No raw HTML for interactive elements**: Do not use native `<button>`, `<input>`, `<select>`, `<textarea>`, `<modal>`, etc. Use the corresponding Arco component (`Button`, `Input`, `Select`, `Modal`, etc.).
- **Layout tags are fine**: `<div>`, `<span>`, `<section>`, `<nav>`, `<main>`, and other pure layout/semantic tags may be used freely.
## CSS Conventions
- **Prefer UnoCSS utility classes**: Use atomic classes for simple styles (`flex items-center gap-8px`).
- **Complex/reusable styles**: Must use **CSS Modules** (`ComponentName.module.css`). Plain `.css` files are not allowed for component styles.
- **Semantic color tokens only**: Use colors from `uno.config.ts` (e.g., `text-t-primary`, `bg-base`, `border-b-base`) or CSS variables. **Hardcoded color values are forbidden** (e.g., `#86909C`, `rgb(0,0,0)`). Exception: theme preset files under `src/renderer/pages/settings/CssThemeSettings/presets/` may use hardcoded values since they define the theme tokens themselves.
- **No inline styles**: Do not use `style={{}}` except for dynamically computed values (e.g., calculated widths, positions).
- **Arco style overrides**: Co-locate in the component's CSS Module using `:global(.arco-xxx)`. Do not use a global override file.
- **Global styles**: Only allowed in `src/renderer/styles/` (themes, reset, layout base). No CSS files directly in `src/renderer/` root.
## Renderer Root Directory — Standard Layout
The renderer root must contain **at most 3 entry files + 7 directories = 10 items**.
```
src/renderer/
├── index.html # Vite HTML entry
├── main.tsx # React mount + app bootstrap
├── types.d.ts # Ambient type declarations
├── pages/ # Page-level modules (business code goes here)
├── components/ # Shared UI components (used across multiple pages)
├── hooks/ # Shared React hooks (supports business domain subdirs)
├── context/ # Global React contexts
├── services/ # Client-side services + i18n
├── utils/ # Utility functions + types + constants
├── styles/ # Global styles + theme configuration
└── assets/ # Static assets — Vite resolves to hashed URLs
```
**What does NOT belong at the renderer root:**
- CSS files → move to `styles/`
- Component files (`.tsx`) → move to `components/` or `pages/`
- Single-file directories (only 1 file inside) → merge into a related directory
## Renderer Component Rules
- **Single file** when self-contained; **directory** when it has sub-components/hooks
- Directory-based components must have `index.tsx` entry point
- **Single-file directory rule**: A directory containing only 1 file should be merged into its parent or a related directory
- Page-private code stays under `pages/<PageName>/`; move to shared only when a second consumer appears
### `src/renderer/components/` Structure
`components/` is for shared components used across multiple pages. It has two layers:
**Fixed layer:**
- `base/` — Generic UI primitives with no business logic. The only fixed subdirectory. Components here must not depend on app-specific context or domain logic.
**Business layer:**
- Create subdirectories by **business domain**, using lowercase naming (categorical directory rule)
- Create a domain subdirectory when **≥ 2** shared components belong to the same domain
- A single component may stay at the `components/` root temporarily until a second component in the same domain appears
**Constraints:**
- The `components/` root must not exceed **10** direct children (files + directories)
- Components used by only **one** page must live in `pages/<PageName>/components/`, not here
```
src/renderer/components/
├── base/ # UI primitives — AionModal, AionSelect, FlexFullContainer, etc.
├── chat/ # Conversation/message domain (example, not exhaustive)
├── agent/ # Agent selection/configuration domain
├── settings/ # Settings domain
├── layout/ # Window frame and layout
├── media/ # File preview, image viewer
└── index.ts # Public re-exports (optional)
```
> The business subdirectory list above is illustrative. New domains are created as needed following the same rules.
### `src/renderer/hooks/` — Grouping by Business Domain
When `hooks/` exceeds 10 direct children, group hooks into business domain subdirectories. Generic hooks with no clear domain stay at the root. The root must stay ≤ 10 direct children.
```
hooks/
├── agent/ # Agent/model related
├── chat/ # Chat/message input
├── file/ # File/workspace
├── mcp/ # MCP related
├── ui/ # Generic UI interaction
├── system/ # System-level (deep link, notification, theme, etc.)
└── index.ts # Public re-exports (optional)
```
> Domain names are recommendations. Create new domains as needed following the same pattern.
### `src/renderer/utils/` — Grouping by Business Domain
Same principle as `hooks/`. When `utils/` exceeds 10 direct children, group into domain subdirectories. The root must stay ≤ 10 direct children.
```
utils/
├── file/ # File handling
├── workspace/ # Workspace utilities
├── chat/ # Chat/message utilities
├── model/ # Model/agent utilities
├── theme/ # Theme/style utilities
├── ui/ # Generic UI utilities
└── ... # Ungrouped utilities at root
```
### Page Module Structure
```
PageName/ # PascalCase
├── index.tsx # Entry point (required)
├── components/ # lowercase (categorical)
├── hooks/ # lowercase (categorical)
├── contexts/ # lowercase (categorical)
├── utils/ # lowercase (categorical)
├── types.ts
└── constants.ts
```
### Page-Level Directory Naming
Inside a page module (e.g., `pages/conversation/`), three types of subdirectories exist:
| Type | Convention | Examples |
| ----------------------------------------------------- | ---------- | ---------------------------------------------------- |
| **Categorical** (standard role) | lowercase | `components/`, `hooks/`, `context/`, `utils/` |
| **Feature module** (business feature) | PascalCase | `GroupedHistory/`, `Workspace/`, `Preview/` |
| **Platform directory** (mirrors `src/process/agent/`) | lowercase | `acp/`, `codex/`, `gemini/`, `nanobot/`, `openclaw/` |
Platform directories are an exception to PascalCase. They use lowercase for cross-process naming consistency with `src/process/agent/<platform>/`.