use rest-api-client
This commit is contained in:
351
AGENTS.md
351
AGENTS.md
@@ -1,81 +1,59 @@
|
||||
# AGENTS.md
|
||||
|
||||
Kintone Customize Manager 项目开发指南。
|
||||
Kintone Customize Manager - Electron + React 应用,用于管理 Kintone 自定义资源。
|
||||
|
||||
## 1. 构建命令
|
||||
|
||||
### 开发
|
||||
|
||||
```bash
|
||||
# 启动开发服务器(HMR + 热重载)
|
||||
# 开发(HMR + 热重载)
|
||||
npm run dev
|
||||
|
||||
# 类型检查
|
||||
npx tsc --noEmit
|
||||
```
|
||||
|
||||
### 构建
|
||||
|
||||
```bash
|
||||
# 构建生产版本
|
||||
# 构建
|
||||
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 格式化
|
||||
```
|
||||
|
||||
### 代码质量
|
||||
**注意**: 无测试框架,项目暂无测试文件。
|
||||
|
||||
```bash
|
||||
# ESLint 检查
|
||||
npm run lint
|
||||
|
||||
# 格式化代码
|
||||
npm run format
|
||||
```
|
||||
|
||||
## 2. 项目结构
|
||||
## 2. 项目架构
|
||||
|
||||
```
|
||||
kintone-customize-manager/
|
||||
├── src/
|
||||
│ ├── main/ # Electron 主进程
|
||||
│ │ ├── index.ts # 主进程入口
|
||||
│ │ ├── ipc-handlers.ts # IPC 通信处理
|
||||
│ │ ├── storage.ts # 文件系统操作
|
||||
│ │ ├── kintone-api.ts # Kintone API 封装
|
||||
│ │ ├── updater.ts # 自动更新逻辑
|
||||
│ │ └── config.ts # 配置管理
|
||||
│ ├── preload/ # Preload 脚本
|
||||
│ │ └── index.ts # 暴露 API 到渲染进程
|
||||
│ └── renderer/ # React 渲染进程
|
||||
│ └── src/
|
||||
│ ├── main.tsx # React 入口
|
||||
│ ├── App.tsx # 根组件
|
||||
│ ├── components/ # React 组件
|
||||
│ ├── hooks/ # 自定义 Hooks
|
||||
│ ├── stores/ # Zustand Stores
|
||||
│ ├── utils/ # 工具函数
|
||||
│ └── types/ # TypeScript 类型
|
||||
├── resources/ # 应用资源(图标等)
|
||||
└── build/ # 构建配置
|
||||
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 # 类型声明
|
||||
└── renderer/ # React 渲染进程
|
||||
└── src/
|
||||
├── main.tsx # React 入口
|
||||
├── App.tsx # 根组件
|
||||
├── components/ # React 组件
|
||||
├── stores/ # Zustand Stores
|
||||
└── types/ # TypeScript 类型
|
||||
```
|
||||
|
||||
## 3. 路径别名
|
||||
|
||||
| 别名 | 路径 |
|
||||
|------|------|
|
||||
| 别名 | 路径 |
|
||||
| ------------- | -------------------- |
|
||||
| `@renderer/*` | `src/renderer/src/*` |
|
||||
| `@main/*` | `src/main/*` |
|
||||
| `@preload/*` | `src/preload/*` |
|
||||
|
||||
使用示例:
|
||||
```typescript
|
||||
import { useStore } from '@renderer/stores'
|
||||
import { ipcHandler } from '@main/ipc-handlers'
|
||||
```
|
||||
| `@main/*` | `src/main/*` |
|
||||
| `@preload/*` | `src/preload/*` |
|
||||
|
||||
## 4. 代码风格
|
||||
|
||||
@@ -83,67 +61,54 @@ import { ipcHandler } from '@main/ipc-handlers'
|
||||
|
||||
```typescript
|
||||
// 1. Node.js 内置模块
|
||||
import { join } from 'path'
|
||||
import { app, BrowserWindow } from 'electron'
|
||||
import { join } from "path";
|
||||
import { app, BrowserWindow } from "electron";
|
||||
|
||||
// 2. 第三方库
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Button, Layout } from 'antd'
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button, Layout } from "antd";
|
||||
|
||||
// 3. 项目内部模块(使用别名)
|
||||
import { useDomainStore } from '@renderer/stores'
|
||||
import { formatDate } from '@renderer/utils'
|
||||
// 3. 项目内部模块(别名)
|
||||
import { useDomainStore } from "@renderer/stores";
|
||||
|
||||
// 4. 相对导入
|
||||
import './styles.css'
|
||||
import "./styles.css";
|
||||
```
|
||||
|
||||
### 命名规范
|
||||
|
||||
| 类型 | 规范 | 示例 |
|
||||
|------|------|------|
|
||||
| 组件文件 | PascalCase | `DomainManager.tsx` |
|
||||
| 工具函数文件 | camelCase | `formatDate.ts` |
|
||||
| Store 文件 | camelCase + Store | `domainStore.ts` |
|
||||
| 类型文件 | camelCase | `types.ts` |
|
||||
| 组件名 | PascalCase | `DomainManager` |
|
||||
| 函数/变量 | camelCase | `handleSubmit` |
|
||||
| 常量 | UPPER_SNAKE_CASE | `MAX_FILE_SIZE` |
|
||||
| 类型/接口 | PascalCase | `DomainConfig` |
|
||||
- 组件文件/名: `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
|
||||
domain: string
|
||||
username: string
|
||||
authType: 'password' | 'api_token'
|
||||
createdAt: string
|
||||
id: string;
|
||||
name: string;
|
||||
authType: "password" | "api_token"; // 使用字面量联合类型
|
||||
}
|
||||
|
||||
// 函数返回类型
|
||||
function createWindow(): void { }
|
||||
// 异步函数返回 Promise
|
||||
async function fetchDomains(): Promise<Domain[]> {}
|
||||
|
||||
// 异步函数
|
||||
async function fetchDomains(): Promise<Domain[]> { }
|
||||
|
||||
// 避免使用 any,使用 unknown 或具体类型
|
||||
// 使用类型守卫处理 unknown
|
||||
function parseResponse(data: unknown): DomainConfig {
|
||||
// 类型守卫
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
throw new Error('Invalid response')
|
||||
if (typeof data !== "object" || data === null) {
|
||||
throw new Error("Invalid response");
|
||||
}
|
||||
return data as DomainConfig
|
||||
return data as DomainConfig;
|
||||
}
|
||||
```
|
||||
|
||||
### React 组件规范
|
||||
|
||||
```typescript
|
||||
// 函数组件优先
|
||||
interface DomainListProps {
|
||||
domains: Domain[]
|
||||
onSelect: (domain: Domain) => void
|
||||
@@ -151,21 +116,15 @@ interface DomainListProps {
|
||||
|
||||
function DomainList({ domains, onSelect }: DomainListProps) {
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
|
||||
|
||||
// Hooks 放在组件顶部
|
||||
const { token } = theme.useToken()
|
||||
|
||||
// 事件处理函数使用 useCallback
|
||||
const handleClick = useCallback((domain: Domain) => {
|
||||
setSelectedId(domain.id)
|
||||
onSelect(domain)
|
||||
}, [onSelect])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* JSX */}
|
||||
</div>
|
||||
)
|
||||
|
||||
return <div>...</div>
|
||||
}
|
||||
|
||||
export default DomainList
|
||||
@@ -174,123 +133,65 @@ export default DomainList
|
||||
### Zustand Store 规范
|
||||
|
||||
```typescript
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
interface DomainState {
|
||||
domains: Domain[]
|
||||
currentDomain: Domain | null
|
||||
addDomain: (domain: Domain) => void
|
||||
removeDomain: (id: string) => void
|
||||
setCurrentDomain: (domain: Domain | null) => void
|
||||
domains: Domain[];
|
||||
addDomain: (domain: Domain) => void;
|
||||
}
|
||||
|
||||
export const useDomainStore = create<DomainState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
domains: [],
|
||||
currentDomain: null,
|
||||
addDomain: (domain) => set((state) => ({
|
||||
domains: [...state.domains, domain]
|
||||
})),
|
||||
removeDomain: (id) => set((state) => ({
|
||||
domains: state.domains.filter(d => d.id !== id)
|
||||
})),
|
||||
setCurrentDomain: (domain) => set({ currentDomain: domain })
|
||||
addDomain: (domain) =>
|
||||
set((state) => ({
|
||||
domains: [...state.domains, domain],
|
||||
})),
|
||||
}),
|
||||
{ name: 'domain-storage' }
|
||||
)
|
||||
)
|
||||
{ name: "domain-storage" },
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
## 5. 错误处理
|
||||
## 5. IPC 通信规范
|
||||
|
||||
### 主进程错误处理
|
||||
### Result 模式(所有 IPC 返回)
|
||||
|
||||
```typescript
|
||||
// IPC 处理错误
|
||||
ipcMain.handle('fetch-domains', async () => {
|
||||
try {
|
||||
const domains = await fetchDomainsFromApi()
|
||||
return { success: true, data: domains }
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch domains:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
})
|
||||
type Result<T> = { success: true; data: T } | { success: false; error: string };
|
||||
```
|
||||
|
||||
### 渲染进程错误处理
|
||||
|
||||
```typescript
|
||||
// 使用 Result 模式
|
||||
type Result<T> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; error: string }
|
||||
|
||||
async function handleFetch(): Promise<Result<Domain[]>> {
|
||||
try {
|
||||
const result = await window.api.fetchDomains()
|
||||
if (!result.success) {
|
||||
message.error(result.error)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error'
|
||||
message.error(errorMsg)
|
||||
return { success: false, error: errorMsg }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. IPC 通信规范
|
||||
|
||||
### Preload 暴露 API
|
||||
|
||||
```typescript
|
||||
// preload/index.ts
|
||||
const api = {
|
||||
// 使用 invoke 进行双向通信
|
||||
fetchDomains: () => ipcRenderer.invoke('fetch-domains'),
|
||||
|
||||
// 使用 send 进行单向通信
|
||||
notify: (message: string) => ipcRenderer.send('notify', message),
|
||||
|
||||
// 监听事件
|
||||
onUpdate: (callback: (info: UpdateInfo) => void) =>
|
||||
ipcRenderer.on('update-available', (_, info) => callback(info))
|
||||
}
|
||||
// 双向通信使用 invoke
|
||||
fetchDomains: () => ipcRenderer.invoke("fetch-domains"),
|
||||
// 单向通信使用 send
|
||||
notify: (message: string) => ipcRenderer.send("notify", message),
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld('api', api)
|
||||
contextBridge.exposeInMainWorld("api", api);
|
||||
```
|
||||
|
||||
### 类型定义
|
||||
|
||||
```typescript
|
||||
// preload/index.d.ts
|
||||
interface ElectronAPI {
|
||||
fetchDomains: () => Promise<Result<Domain[]>>
|
||||
notify: (message: string) => void
|
||||
onUpdate: (callback: (info: UpdateInfo) => void) => void
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: ElectronAPI
|
||||
api: ElectronAPI
|
||||
api: ElectronAPI;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. UI 组件规范
|
||||
## 6. UI 组件规范
|
||||
|
||||
### 使用 LobeHub UI + Ant Design
|
||||
使用 **Ant Design 6** + **antd-style**(不用 Tailwind):
|
||||
|
||||
```typescript
|
||||
// 使用 antd-style 进行样式
|
||||
import { createStyles } from 'antd-style'
|
||||
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
@@ -307,59 +208,85 @@ function MyComponent() {
|
||||
}
|
||||
```
|
||||
|
||||
### 国际化
|
||||
国际化:使用中文默认 `import zhCN from 'antd/locale/zh_CN'`
|
||||
|
||||
```typescript
|
||||
// 使用中文默认
|
||||
import zhCN from 'antd/locale/zh_CN'
|
||||
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<App />
|
||||
</ConfigProvider>
|
||||
```
|
||||
|
||||
## 8. 安全规范
|
||||
## 7. 安全规范
|
||||
|
||||
### 密码存储
|
||||
|
||||
```typescript
|
||||
// 使用 safeStorage 加密存储
|
||||
import { safeStorage } from 'electron'
|
||||
import { safeStorage } from "electron";
|
||||
|
||||
// 加密
|
||||
const encrypted = safeStorage.encryptString(password)
|
||||
|
||||
const encrypted = safeStorage.encryptString(password);
|
||||
// 解密
|
||||
const decrypted = safeStorage.decryptString(encrypted)
|
||||
const decrypted = safeStorage.decryptString(encrypted);
|
||||
```
|
||||
|
||||
### CSP 配置
|
||||
### WebPreferences
|
||||
|
||||
```html
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self';
|
||||
script-src 'self';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: https:;
|
||||
font-src 'self' data:;" />
|
||||
必须启用:`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 环境:
|
||||
所有 npm/npx 命令需加载 fnm 环境:
|
||||
|
||||
```bash
|
||||
# 方式一:使用 wrapper 脚本
|
||||
~/.config/opencode/node-fnm-wrapper.sh npm run dev
|
||||
|
||||
# 方式二:手动加载
|
||||
eval "$(fnm env --use-on-cd)" && npm run dev
|
||||
```
|
||||
|
||||
## 10. 注意事项
|
||||
|
||||
1. **ESM Only**: LobeHub UI 仅支持 ESM,确保 `tsconfig.json` 中 `"module": "ESNext"`
|
||||
2. **React 19**: 必须使用 `@types/react@^19.0.0` 和 `@types/react-dom@^19.0.0`
|
||||
3. **CSS 方案**: 使用 `antd-style`,不使用 Tailwind CSS
|
||||
4. **Context Isolation**: 必须启用 `contextIsolation: true`
|
||||
5. **禁止类型断言**: 避免使用 `as any`,优先使用类型守卫
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user