diff --git a/AGENTS.md b/AGENTS.md index f54fa6c..c3d8d94 100644 --- a/AGENTS.md +++ b/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 {} - -// 使用类型守卫处理 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` +- 使用类型守卫处理 `unknown` ### React 组件规范 -```typescript -interface DomainListProps { - domains: Domain[] - onSelect: (domain: Domain) => void -} - -function DomainList({ domains, onSelect }: DomainListProps) { - const [selectedId, setSelectedId] = useState(null) - - // Hooks 放在组件顶部 - // 事件处理函数使用 useCallback - const handleClick = useCallback((domain: Domain) => { - setSelectedId(domain.id) - onSelect(domain) - }, [onSelect]) - - return
...
-} - -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()( - persist( - (set) => ({ - domains: [], - addDomain: (domain) => - set((state) => ({ - domains: [...state.domains, domain], - })), - }), - { name: "domain-storage" }, - ), -); -``` +- 使用 `persist` 中间件持久化状态 +- 定义接口明确 state 和 actions 类型 ## 5. IPC 通信规范 -### Result 模式(所有 IPC 返回) - ```typescript type Result = { 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` +- 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
...
-} +// 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` 格式 +- 渲染进程检查 `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); - }); -}); -``` diff --git a/src/preload/index.ts b/src/preload/index.ts index 8736d2b..6486ded 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -2,24 +2,34 @@ import { contextBridge, ipcRenderer } from "electron"; import { electronAPI } from "@electron-toolkit/preload"; import type { SelfAPI } from "./index.d"; -// Static properties that are not IPC methods -const staticProps = { +// Plain object API - contextBridge cannot serialize Proxy objects +const api: SelfAPI = { platform: process.platform, -} as const; -// Proxy-based API that automatically routes method calls to IPC invoke -// Method name = IPC channel name, no manual mapping needed -const api: SelfAPI = new Proxy(staticProps as SelfAPI, { - get(_target, prop: string) { - // Return static property if exists - if (prop in staticProps) { - return (staticProps as Record)[prop]; - } - // Otherwise, auto-create IPC invoke function - // prop (method name) = IPC channel name - return (...args: unknown[]) => ipcRenderer.invoke(prop, ...args); - }, -}); + // Domain management + getDomains: () => ipcRenderer.invoke("getDomains"), + createDomain: (params) => ipcRenderer.invoke("createDomain", params), + updateDomain: (params) => ipcRenderer.invoke("updateDomain", params), + deleteDomain: (id) => ipcRenderer.invoke("deleteDomain", id), + testConnection: (id) => ipcRenderer.invoke("testConnection", id), + testDomainConnection: (params) => ipcRenderer.invoke("testDomainConnection", params), + + // Browse + getApps: (params) => ipcRenderer.invoke("getApps", params), + getAppDetail: (params) => ipcRenderer.invoke("getAppDetail", params), + getFileContent: (params) => ipcRenderer.invoke("getFileContent", params), + + // Deploy + deploy: (params) => ipcRenderer.invoke("deploy", params), + + // Download + download: (params) => ipcRenderer.invoke("download", params), + + // Version management + getVersions: (params) => ipcRenderer.invoke("getVersions", params), + deleteVersion: (id) => ipcRenderer.invoke("deleteVersion", id), + rollback: (params) => ipcRenderer.invoke("rollback", params), +}; // Use `contextBridge` APIs to expose Electron APIs to // renderer only if context isolation is enabled, otherwise @@ -29,9 +39,11 @@ if (process.contextIsolated) { contextBridge.exposeInMainWorld("electron", electronAPI); contextBridge.exposeInMainWorld("api", api); } catch (error) { - console.error(error); + console.error("[Preload] Failed to expose API:", error); } } else { - (window as Window & { electron: typeof electronAPI; api: SelfAPI }).electron = electronAPI; - (window as Window & { electron: typeof electronAPI; api: SelfAPI }).api = api; + // @ts-ignore - window is available in non-isolated context + window.electron = electronAPI; + // @ts-ignore + window.api = api; } diff --git a/src/renderer/src/components/DomainManager/DomainForm.tsx b/src/renderer/src/components/DomainManager/DomainForm.tsx index efbab84..32f43d8 100644 --- a/src/renderer/src/components/DomainManager/DomainForm.tsx +++ b/src/renderer/src/components/DomainManager/DomainForm.tsx @@ -4,7 +4,8 @@ */ import React from "react"; -import { Modal, Form, Input, Button, Space, message } from "antd"; +import { Modal, Input, Button, InputPassword } from "@lobehub/ui"; +import { Form, message } from "antd"; import { createStyles } from "antd-style"; import { useDomainStore } from "@renderer/stores"; import type { CreateDomainParams, UpdateDomainParams } from "@shared/types/ipc"; @@ -48,7 +49,12 @@ const DomainForm: React.FC = ({ open, onClose, domainId }) => { username: editingDomain.username, }); } else { - form.resetFields(); + form.setFieldsValue({ + name: "", + domain: "https://alicorn.cybozu.com", + username: "maxz", + password: "7ld7i8vd", + }); } } }, [open, editingDomain, form]); @@ -157,6 +163,7 @@ const DomainForm: React.FC = ({ open, onClose, domainId }) => { footer={null} width={520} destroyOnClose + maskClosable={false} >
@@ -205,13 +212,13 @@ const DomainForm: React.FC = ({ open, onClose, domainId }) => { label="密码" rules={isEdit ? [] : [{ required: true, message: "请输入密码" }]} > - - +
- +