8.2 KiB
8.2 KiB
AGENTS.md
Kintone Customize Manager 项目开发指南。
1. 构建命令
开发
# 启动开发服务器(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
代码质量
# ESLint 检查
npm run lint
# 格式化代码
npm run format
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/ # 构建配置
3. 路径别名
| 别名 | 路径 |
|---|---|
@renderer/* |
src/renderer/src/* |
@main/* |
src/main/* |
@preload/* |
src/preload/* |
使用示例:
import { useStore } from '@renderer/stores'
import { ipcHandler } from '@main/ipc-handlers'
4. 代码风格
导入顺序
// 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'
import { formatDate } from '@renderer/utils'
// 4. 相对导入
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 |
TypeScript 规范
// 显式类型定义
interface DomainConfig {
id: string
name: string
domain: string
username: string
authType: 'password' | 'api_token'
createdAt: string
}
// 函数返回类型
function createWindow(): void { }
// 异步函数
async function fetchDomains(): Promise<Domain[]> { }
// 避免使用 any,使用 unknown 或具体类型
function parseResponse(data: unknown): DomainConfig {
// 类型守卫
if (typeof data !== 'object' || data === null) {
throw new Error('Invalid response')
}
return data as DomainConfig
}
React 组件规范
// 函数组件优先
interface DomainListProps {
domains: Domain[]
onSelect: (domain: Domain) => void
}
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>
)
}
export default DomainList
Zustand Store 规范
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
}
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 })
}),
{ name: 'domain-storage' }
)
)
5. 错误处理
主进程错误处理
// 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'
}
}
})
渲染进程错误处理
// 使用 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
// 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))
}
contextBridge.exposeInMainWorld('api', api)
类型定义
// 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
}
}
7. UI 组件规范
使用 LobeHub UI + Ant Design
// 使用 antd-style 进行样式
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'
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>
8. 安全规范
密码存储
// 使用 safeStorage 加密存储
import { safeStorage } from 'electron'
// 加密
const encrypted = safeStorage.encryptString(password)
// 解密
const decrypted = safeStorage.decryptString(encrypted)
CSP 配置
<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:;" />
9. fnm 环境配置
所有 npm/npx 命令需要先加载 fnm 环境:
# 方式一:使用 wrapper 脚本
~/.config/opencode/node-fnm-wrapper.sh npm run dev
# 方式二:手动加载
eval "$(fnm env --use-on-cd)" && npm run dev
10. 注意事项
- ESM Only: LobeHub UI 仅支持 ESM,确保
tsconfig.json中"module": "ESNext" - React 19: 必须使用
@types/react@^19.0.0和@types/react-dom@^19.0.0 - CSS 方案: 使用
antd-style,不使用 Tailwind CSS - Context Isolation: 必须启用
contextIsolation: true - 禁止类型断言: 避免使用
as any,优先使用类型守卫