update
This commit is contained in:
259
AGENTS.md
259
AGENTS.md
@@ -66,13 +66,6 @@ src/
|
||||
| `@preload/*` | `src/preload/*` |
|
||||
| `@shared/*` | `src/shared/*` |
|
||||
|
||||
## 4. 代码风格
|
||||
| 别名 | 路径 |
|
||||
| ------------- | -------------------- |
|
||||
| `@renderer/*` | `src/renderer/src/*` |
|
||||
| `@main/*` | `src/main/*` |
|
||||
| `@preload/*` | `src/preload/*` |
|
||||
| `@shared/*` | `src/shared/*` |
|
||||
## 4. 代码风格
|
||||
|
||||
### 导入顺序
|
||||
@@ -104,179 +97,106 @@ import "./styles.css";
|
||||
|
||||
### 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;
|
||||
}
|
||||
```
|
||||
- 显式类型定义,避免 `any`
|
||||
- 使用字面量联合类型(如 `authType: "password" | "api_token"`)
|
||||
- 异步函数返回 `Promise<T>`
|
||||
- 使用类型守卫处理 `unknown`
|
||||
|
||||
### 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
|
||||
```
|
||||
- Hooks 放在组件顶部
|
||||
- 事件处理函数使用 `useCallback`
|
||||
- 使用 TypeScript 显式定义 props 类型
|
||||
|
||||
### 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" },
|
||||
),
|
||||
);
|
||||
```
|
||||
- 使用 `persist` 中间件持久化状态
|
||||
- 定义接口明确 state 和 actions 类型
|
||||
|
||||
## 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;
|
||||
}
|
||||
}
|
||||
```
|
||||
- IPC 调用使用 `invoke` 返回 `Result<T>`
|
||||
- Preload 通过 `contextBridge.exposeInMainWorld` 暴露 API
|
||||
|
||||
## 6. UI 组件规范
|
||||
|
||||
使用 **Ant Design 6** + **antd-style**(不用 Tailwind):
|
||||
**UI Kit 优先使用 LobeHub UI** (`@lobehub/ui`),其次使用 Ant Design 6 + antd-style:
|
||||
|
||||
```typescript
|
||||
import { createStyles } from 'antd-style'
|
||||
import { Button } from '@lobehub/ui';
|
||||
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>
|
||||
}
|
||||
// antd-style 仅用于自定义样式
|
||||
export const useStyles = createStyles(({ token, css }) => ({
|
||||
container: css`padding: ${token.paddingLG}px;`
|
||||
}));
|
||||
```
|
||||
|
||||
国际化:使用中文默认 `import zhCN from 'antd/locale/zh_CN'`
|
||||
- 国际化:使用中文默认 `import zhCN from 'antd/locale/zh_CN'`
|
||||
- 禁止使用 Tailwind
|
||||
|
||||
## 7. 安全规范
|
||||
|
||||
### 密码存储
|
||||
|
||||
```typescript
|
||||
import { safeStorage } from "electron";
|
||||
|
||||
// 加密
|
||||
const encrypted = safeStorage.encryptString(password);
|
||||
// 解密
|
||||
const decrypted = safeStorage.decryptString(encrypted);
|
||||
```
|
||||
|
||||
### WebPreferences
|
||||
|
||||
必须启用:`contextIsolation: true`, `nodeIntegration: false`, `sandbox: false`
|
||||
- 密码使用 `electron` 的 `safeStorage` 加密存储
|
||||
- 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);
|
||||
}
|
||||
```
|
||||
- 所有 IPC 返回 `Result<T>` 格式
|
||||
- 渲染进程检查 `result.success` 处理错误
|
||||
|
||||
## 9. fnm 环境配置
|
||||
|
||||
所有 npm/npx 命令需加载 fnm 环境:
|
||||
|
||||
```bash
|
||||
# 使用 fnm wrapper(推荐)
|
||||
~/.config/opencode/node-fnm-wrapper.sh npm run dev
|
||||
|
||||
# 或手动加载 fnm
|
||||
eval "$(fnm env --use-on-cd)" && npm run dev
|
||||
```
|
||||
|
||||
## 10. 注意事项
|
||||
## 10. 工具使用规范
|
||||
|
||||
### 使用 `edit_file` 进行代码编辑
|
||||
|
||||
推荐使用 `edit_file` 工具进行代码修改,它支持部分代码片段编辑,无需提供完整文件内容:
|
||||
|
||||
```typescript
|
||||
// 示例:替换单行代码
|
||||
edit_file({
|
||||
filePath: "src/main/index.ts",
|
||||
edits: [{
|
||||
pos: "42#XZ", // LINE#ID 格式
|
||||
op: "replace",
|
||||
lines: "new line content"
|
||||
}]
|
||||
})
|
||||
|
||||
// 示例:删除代码块
|
||||
edit_file({
|
||||
filePath: "src/main/index.ts",
|
||||
edits: [{
|
||||
pos: "40#AB",
|
||||
end: "45#CD",
|
||||
op: "replace",
|
||||
lines: null // null 表示删除
|
||||
}]
|
||||
})
|
||||
```
|
||||
|
||||
### LINE#ID 格式说明
|
||||
|
||||
- 格式:`{line_number}#{hash_id}`
|
||||
- hash_id 是每行唯一的两位标识符
|
||||
- 从 read 工具输出中获取正确的 LINE#ID
|
||||
|
||||
## 11. 注意事项
|
||||
|
||||
1. **ESM Only**: LobeHub UI 仅支持 ESM
|
||||
2. **React 19**: 使用 `@types/react@^19.0.0`
|
||||
@@ -284,7 +204,7 @@ eval "$(fnm env --use-on-cd)" && npm run dev
|
||||
4. **禁止 `as any`**: 使用类型守卫或 `unknown`
|
||||
5. **函数组件优先**: 禁止 class 组件
|
||||
|
||||
## 11. MVP Phase - Breaking Changes
|
||||
## 12. MVP Phase - Breaking Changes
|
||||
|
||||
**This is MVP phase - breaking changes are acceptable for better design.** However, you MUST:
|
||||
|
||||
@@ -309,7 +229,7 @@ eval "$(fnm env --use-on-cd)" && npm run dev
|
||||
4. If significant, ask user for confirmation before implementing
|
||||
5. Update related documentation after implementation
|
||||
|
||||
## 12. 测试规范
|
||||
## 13. 测试规范
|
||||
|
||||
### 测试框架
|
||||
|
||||
@@ -331,19 +251,6 @@ tests/
|
||||
2. 新增 API 功能时,应在 `src/main/__tests__/` 中添加对应测试
|
||||
3. 测试文件命名:`*.test.ts`
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
npm test
|
||||
|
||||
# 监听模式
|
||||
npm run test:watch
|
||||
|
||||
# 测试覆盖率
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
### Mock Electron API
|
||||
|
||||
测试中需要 Mock Electron 的 API(如 `safeStorage`、`app` 等),已在 `tests/mocks/electron.ts` 中提供。
|
||||
@@ -352,37 +259,3 @@ npm run test:coverage
|
||||
// tests/setup.ts 中自动 Mock
|
||||
vi.mock("electron", () => import("./mocks/electron"));
|
||||
```
|
||||
|
||||
### 测试示例
|
||||
|
||||
```typescript
|
||||
import { describe, expect, it, beforeAll } from "vitest";
|
||||
import { SelfKintoneClient, createKintoneClient } from "@main/kintone-api";
|
||||
import type { DomainWithPassword } from "@shared/types/domain";
|
||||
|
||||
describe("SelfKintoneClient", () => {
|
||||
let client: SelfKintoneClient;
|
||||
|
||||
beforeAll(() => {
|
||||
client = createKintoneClient({
|
||||
id: "test",
|
||||
name: "Test",
|
||||
domain: "example.cybozu.com",
|
||||
username: "user",
|
||||
password: "pass",
|
||||
authType: "password",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
it("should return the correct domain", () => {
|
||||
expect(client.getDomain()).toBe("example.cybozu.com");
|
||||
});
|
||||
|
||||
it("should test connection successfully", async () => {
|
||||
const result = await client.testConnection();
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user