This commit is contained in:
2026-03-13 18:15:01 +08:00
parent 4d92085957
commit 23edd0faab
3 changed files with 109 additions and 217 deletions

259
AGENTS.md
View File

@@ -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);
});
});
```