- Move type definitions from src/renderer/src/types/ to src/shared/types/ - Add @shared/* path alias to tsconfig.node.json and tsconfig.web.json - Update all imports from @renderer/types/* to @shared/types/* - Update AGENTS.md with new directory structure and path alias This fixes architecture violation where main/preload processes imported from renderer directory. Types are now properly shared across all processes.
294 lines
7.0 KiB
Markdown
294 lines
7.0 KiB
Markdown
# AGENTS.md
|
||
|
||
Kintone Customize Manager - Electron + React 应用,用于管理 Kintone 自定义资源。
|
||
|
||
## 1. 构建命令
|
||
|
||
```bash
|
||
# 开发(HMR + 热重载)
|
||
npm run dev
|
||
|
||
# 类型检查
|
||
npx tsc --noEmit
|
||
|
||
# 构建
|
||
npm run build
|
||
|
||
# 打包
|
||
npm run package:win # Windows
|
||
npm run package:mac # macOS
|
||
npm run package:linux # Linux
|
||
|
||
# 代码质量
|
||
npm run lint # ESLint 检查
|
||
npm run format # Prettier 格式化
|
||
```
|
||
|
||
**注意**: 无测试框架,项目暂无测试文件。
|
||
|
||
## 2. 项目架构
|
||
|
||
```
|
||
src/
|
||
├── main/ # Electron 主进程
|
||
│ ├── index.ts # 入口,创建窗口
|
||
│ ├── ipc-handlers.ts # IPC 处理器(所有通信入口)
|
||
│ ├── storage.ts # 文件存储 + 密码加密
|
||
│ └── kintone-api.ts # Kintone REST API 封装
|
||
├── preload/ # Preload 脚本
|
||
│ ├── index.ts # 暴露 API 到渲染进程
|
||
│ └── index.d.ts # 类型声明
|
||
├── shared/ # 跨进程共享代码
|
||
│ └── types/ # 共享类型定义
|
||
└── renderer/ # React 渲染进程
|
||
└── src/
|
||
├── main.tsx # React 入口
|
||
├── App.tsx # 根组件
|
||
├── components/ # React 组件
|
||
└── stores/ # Zustand Stores
|
||
```
|
||
|
||
## 3. 路径别名
|
||
|
||
| 别名 | 路径 |
|
||
| ------------- | -------------------- |
|
||
| `@renderer/*` | `src/renderer/src/*` |
|
||
| `@main/*` | `src/main/*` |
|
||
| `@preload/*` | `src/preload/*` |
|
||
| `@shared/*` | `src/shared/*` |
|
||
## 4. 代码风格
|
||
|
||
### 导入顺序
|
||
|
||
```typescript
|
||
// 1. Node.js 内置模块
|
||
import { join } from "path";
|
||
import { app, BrowserWindow } from "electron";
|
||
|
||
// 2. 第三方库
|
||
import React, { useState, useEffect } from "react";
|
||
import { Button, Layout } from "antd";
|
||
|
||
// 3. 项目内部模块(别名)
|
||
import { useDomainStore } from "@renderer/stores";
|
||
|
||
// 4. 相对导入
|
||
import "./styles.css";
|
||
```
|
||
|
||
### 命名规范
|
||
|
||
- 组件文件/名: `PascalCase` (e.g., `DomainManager.tsx`)
|
||
- 工具函数文件: `camelCase` (e.g., `formatDate.ts`)
|
||
- Store 文件: `camelCase + Store` (e.g., `domainStore.ts`)
|
||
- 函数/变量: `camelCase` (e.g., `handleSubmit`)
|
||
- 常量: `UPPER_SNAKE_CASE` (e.g., `MAX_FILE_SIZE`)
|
||
- 类型/接口: `PascalCase` (e.g., `DomainConfig`)
|
||
|
||
### TypeScript 规范
|
||
|
||
```typescript
|
||
// 显式类型定义,避免 any
|
||
interface DomainConfig {
|
||
id: string;
|
||
name: string;
|
||
authType: "password" | "api_token"; // 使用字面量联合类型
|
||
}
|
||
|
||
// 异步函数返回 Promise
|
||
async function fetchDomains(): Promise<Domain[]> {}
|
||
|
||
// 使用类型守卫处理 unknown
|
||
function parseResponse(data: unknown): DomainConfig {
|
||
if (typeof data !== "object" || data === null) {
|
||
throw new Error("Invalid response");
|
||
}
|
||
return data as DomainConfig;
|
||
}
|
||
```
|
||
|
||
### React 组件规范
|
||
|
||
```typescript
|
||
interface DomainListProps {
|
||
domains: Domain[]
|
||
onSelect: (domain: Domain) => void
|
||
}
|
||
|
||
function DomainList({ domains, onSelect }: DomainListProps) {
|
||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||
|
||
// Hooks 放在组件顶部
|
||
// 事件处理函数使用 useCallback
|
||
const handleClick = useCallback((domain: Domain) => {
|
||
setSelectedId(domain.id)
|
||
onSelect(domain)
|
||
}, [onSelect])
|
||
|
||
return <div>...</div>
|
||
}
|
||
|
||
export default DomainList
|
||
```
|
||
|
||
### Zustand Store 规范
|
||
|
||
```typescript
|
||
import { create } from "zustand";
|
||
import { persist } from "zustand/middleware";
|
||
|
||
interface DomainState {
|
||
domains: Domain[];
|
||
addDomain: (domain: Domain) => void;
|
||
}
|
||
|
||
export const useDomainStore = create<DomainState>()(
|
||
persist(
|
||
(set) => ({
|
||
domains: [],
|
||
addDomain: (domain) =>
|
||
set((state) => ({
|
||
domains: [...state.domains, domain],
|
||
})),
|
||
}),
|
||
{ name: "domain-storage" },
|
||
),
|
||
);
|
||
```
|
||
|
||
## 5. IPC 通信规范
|
||
|
||
### Result 模式(所有 IPC 返回)
|
||
|
||
```typescript
|
||
type Result<T> = { success: true; data: T } | { success: false; error: string };
|
||
```
|
||
|
||
### Preload 暴露 API
|
||
|
||
```typescript
|
||
const api = {
|
||
// 双向通信使用 invoke
|
||
fetchDomains: () => ipcRenderer.invoke("fetch-domains"),
|
||
// 单向通信使用 send
|
||
notify: (message: string) => ipcRenderer.send("notify", message),
|
||
};
|
||
|
||
contextBridge.exposeInMainWorld("api", api);
|
||
```
|
||
|
||
### 类型定义
|
||
|
||
```typescript
|
||
// preload/index.d.ts
|
||
declare global {
|
||
interface Window {
|
||
api: ElectronAPI;
|
||
}
|
||
}
|
||
```
|
||
|
||
## 6. UI 组件规范
|
||
|
||
使用 **Ant Design 6** + **antd-style**(不用 Tailwind):
|
||
|
||
```typescript
|
||
import { createStyles } from 'antd-style'
|
||
|
||
const useStyles = createStyles(({ token, css }) => ({
|
||
container: css`
|
||
padding: ${token.paddingLG}px;
|
||
background: ${token.colorBgContainer};
|
||
border-radius: ${token.borderRadiusLG}px;
|
||
`
|
||
}))
|
||
|
||
function MyComponent() {
|
||
const { styles } = useStyles()
|
||
return <div className={styles.container}>...</div>
|
||
}
|
||
```
|
||
|
||
国际化:使用中文默认 `import zhCN from 'antd/locale/zh_CN'`
|
||
|
||
## 7. 安全规范
|
||
|
||
### 密码存储
|
||
|
||
```typescript
|
||
import { safeStorage } from "electron";
|
||
|
||
// 加密
|
||
const encrypted = safeStorage.encryptString(password);
|
||
// 解密
|
||
const decrypted = safeStorage.decryptString(encrypted);
|
||
```
|
||
|
||
### WebPreferences
|
||
|
||
必须启用:`contextIsolation: true`, `nodeIntegration: false`, `sandbox: false`
|
||
|
||
## 8. 错误处理
|
||
|
||
```typescript
|
||
// 主进程 IPC
|
||
ipcMain.handle("fetch-domains", async () => {
|
||
try {
|
||
const domains = await fetchDomainsFromApi();
|
||
return { success: true, data: domains };
|
||
} catch (error) {
|
||
return {
|
||
success: false,
|
||
error: error instanceof Error ? error.message : "Unknown error",
|
||
};
|
||
}
|
||
});
|
||
|
||
// 渲染进程调用
|
||
const result = await window.api.fetchDomains();
|
||
if (!result.success) {
|
||
message.error(result.error);
|
||
}
|
||
```
|
||
|
||
## 9. fnm 环境配置
|
||
|
||
所有 npm/npx 命令需加载 fnm 环境:
|
||
|
||
```bash
|
||
eval "$(fnm env --use-on-cd)" && npm run dev
|
||
```
|
||
|
||
## 10. 注意事项
|
||
|
||
1. **ESM Only**: LobeHub UI 仅支持 ESM
|
||
2. **React 19**: 使用 `@types/react@^19.0.0`
|
||
3. **CSS 方案**: 使用 `antd-style`,禁止 Tailwind
|
||
4. **禁止 `as any`**: 使用类型守卫或 `unknown`
|
||
5. **函数组件优先**: 禁止 class 组件
|
||
|
||
## 11. MVP Phase - Breaking Changes
|
||
|
||
**This is MVP phase - breaking changes are acceptable for better design.** However, you MUST:
|
||
|
||
- **Explain what will break and why**: Document which components/APIs/workflows will be affected
|
||
- **Compare old vs new approach**: Show the differences and improvements
|
||
- **Document the tradeoffs**: What are the pros and cons of this change
|
||
- **Ask for confirmation**: If the change is significant (affects multiple modules or core architecture)
|
||
|
||
**Examples of acceptable breaking changes during MVP**:
|
||
|
||
- Refactoring data structures for better type safety
|
||
- Changing IPC communication patterns
|
||
- Restructuring component hierarchy
|
||
- Modifying store architecture
|
||
- Updating API interfaces
|
||
|
||
**Process for breaking changes**:
|
||
|
||
1. Identify the change and its impact scope
|
||
2. Document the breaking change in code comments
|
||
3. Explain the reasoning and benefits
|
||
4. If significant, ask user for confirmation before implementing
|
||
5. Update related documentation after implementation
|