├── .prettierrc.json ├── src ├── renderer │ ├── src │ │ ├── lib │ │ │ └── utils.ts │ │ ├── main.tsx │ │ ├── components │ │ │ ├── __tests__ │ │ │ │ ├── authFlow.e2e.spec.ts │ │ │ │ └── membershipService.spec.ts │ │ │ ├── ui │ │ │ │ ├── textarea.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── toggle.tsx │ │ │ │ ├── touch-optimized.tsx │ │ │ │ ├── theme-provider.tsx │ │ │ │ ├── smart-recommendations.tsx │ │ │ │ ├── accessibility-settings.tsx │ │ │ │ ├── navigation.tsx │ │ │ │ ├── form-enhancements.tsx │ │ │ │ ├── notifications.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── user-preferences.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── loading-states.tsx │ │ │ │ ├── search-filter.tsx │ │ │ │ └── input.tsx │ │ │ ├── UserLevelBadge.tsx │ │ │ ├── ErrorMessage.tsx │ │ │ ├── AllPreparationsModal.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── WelcomePage.tsx │ │ │ ├── MonitoringPanel.tsx │ │ │ ├── AnalysisReportModal.tsx │ │ │ └── SelectPreparationModal.tsx │ │ ├── hooks │ │ │ ├── useKeyboardShortcuts.ts │ │ │ └── usePerformanceOptimization.ts │ │ └── contexts │ │ │ └── AuthContext.tsx │ └── index.html ├── test │ └── setup.ts ├── main │ ├── utils │ │ ├── __tests__ │ │ │ └── sql.spec.ts │ │ └── sql.ts │ ├── security │ │ ├── __tests__ │ │ │ └── DataEncryptionManager.spec.ts │ │ ├── DataEncryptionManager.ts │ │ └── SecureKeyManager.ts │ ├── performance │ │ ├── integration.ts │ │ └── startup-optimizer.ts │ ├── audioUtils.ts │ ├── monitoring │ │ └── MonitoringSystem.ts │ └── audio │ │ ├── electron-native-capture.ts │ │ └── optimized-audio-processor.ts └── api-server.ts ├── tsconfig.node.json ├── playwright.config.js ├── .github └── workflows │ └── ci.yml ├── tsconfig.json ├── vite.config.renderer.ts ├── LICENSE ├── .gitignore ├── eslint.config.js ├── vitest.config.ts ├── scripts ├── release.js └── install-system-audio-dump.js ├── electron.vite.config.ts ├── tailwind.config.js ├── docs └── UI_DESIGN.md ├── .env.example ├── package.json ├── database └── init.sql └── README.md /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 100, 6 | "arrowParens": "always" 7 | } 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/renderer/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["electron.vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/renderer/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './assets/index.css' 5 | import './lib/audio-capture' 6 | 7 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 8 | 9 | 10 | , 11 | ) 12 | -------------------------------------------------------------------------------- /playwright.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | export default defineConfig({ 4 | testDir: './tests', 5 | fullyParallel: true, 6 | forbidOnly: !!process.env.CI, 7 | retries: process.env.CI ? 2 : 0, 8 | workers: process.env.CI ? 1 : undefined, 9 | reporter: 'html', 10 | use: { 11 | trace: 'on-first-retry', 12 | }, 13 | 14 | projects: [ 15 | { 16 | name: 'electron', 17 | use: { ...devices['Desktop Chrome'] }, 18 | testMatch: '**/electron-*.spec.js', 19 | }, 20 | ], 21 | }); 22 | -------------------------------------------------------------------------------- /src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, vi } from 'vitest' 2 | 3 | // JSDOM 不存在的 API 简单桩实现 4 | Object.defineProperty(window, 'matchMedia', { 5 | writable: true, 6 | value: vi.fn().mockImplementation((query: string) => ({ 7 | matches: false, 8 | media: query, 9 | onchange: null, 10 | addListener: vi.fn(), 11 | removeListener: vi.fn(), 12 | addEventListener: vi.fn(), 13 | removeEventListener: vi.fn(), 14 | dispatchEvent: vi.fn(), 15 | })), 16 | }) 17 | 18 | afterEach(() => { 19 | vi.restoreAllMocks() 20 | }) 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 面宝 - AI面试伙伴 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/utils/__tests__/sql.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { buildUpdateSetClause } from '../sql' 3 | 4 | describe.skip('buildUpdateSetClause', () => { 5 | it('应当忽略 undefined 字段并从给定索引开始编号', () => { 6 | const { setClause, values } = buildUpdateSetClause({ a: 1, b: undefined, c: 'x' }, 3) 7 | expect(setClause).toBe('a = $3, c = $4') 8 | expect(values).toEqual([1, 'x']) 9 | }) 10 | 11 | it('应当拼接额外片段', () => { 12 | const { setClause, values } = buildUpdateSetClause({ name: 'n' }, 1, ['updated_at = NOW()']) 13 | expect(setClause).toBe('name = $1, updated_at = NOW()') 14 | expect(values).toEqual(['n']) 15 | }) 16 | }) 17 | 18 | 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build-and-test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup Node 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 20 20 | cache: 'npm' 21 | 22 | - name: Install dependencies 23 | run: npm ci 24 | 25 | - name: Type check 26 | run: npm run type-check 27 | 28 | - name: Lint 29 | run: npm run lint 30 | 31 | - name: Unit tests 32 | run: npm run test:unit 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/renderer/src/components/__tests__/authFlow.e2e.spec.ts: -------------------------------------------------------------------------------- 1 | // 轻量级 UI 流程冒烟测试(非 Electron 真机) 2 | /// 3 | import { vi } from 'vitest' 4 | 5 | describe('Auth smoke (mocked)', () => { 6 | it('login then sign out via mocked services', async () => { 7 | // 这里只做占位,真实 E2E 使用 Playwright。避免对窗口或 Electron 依赖。 8 | const mock = { 9 | signInWithEmail: vi.fn().mockResolvedValue({ data: { user: { id: 'u1' } }, error: null }), 10 | signOut: vi.fn().mockResolvedValue({ error: null }), 11 | } 12 | const res = await mock.signInWithEmail('a@b.com', 'x') 13 | expect(res.data.user.id).toBe('u1') 14 | const out = await mock.signOut() 15 | expect(out.error).toBeNull() 16 | }) 17 | }) 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/utils/sql.ts: -------------------------------------------------------------------------------- 1 | export interface UpdateClauseResult { 2 | setClause: string 3 | values: any[] 4 | } 5 | 6 | // 构造 SQL 更新语句的 SET 子句与对应的值列表 7 | export function buildUpdateSetClause( 8 | updates: Record, 9 | startParamIndex = 1, 10 | extraFragments: string[] = [], 11 | ): UpdateClauseResult { 12 | const setParts: string[] = [] 13 | const values: any[] = [] 14 | 15 | for (const [key, value] of Object.entries(updates)) { 16 | if (value === undefined) continue 17 | setParts.push(`${key} = $${startParamIndex + values.length}`) 18 | values.push(value) 19 | } 20 | 21 | if (extraFragments.length > 0) { 22 | setParts.push(...extraFragments) 23 | } 24 | 25 | return { 26 | setClause: setParts.join(', '), 27 | values, 28 | } 29 | } 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "moduleResolution": "bundler", 9 | "allowImportingTsExtensions": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "noEmit": true, 13 | "jsx": "react-jsx", 14 | "strict": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "baseUrl": ".", 19 | "paths": { 20 | "@/*": ["src/renderer/src/*"], 21 | "@renderer/*": ["src/renderer/src/*"] 22 | }, 23 | "types": ["vitest", "vitest/globals"] 24 | }, 25 | "include": ["src"], 26 | "references": [{ "path": "./tsconfig.node.json" }] 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/src/components/__tests__/membershipService.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { membershipService } from '@renderer/lib/supabase' 3 | 4 | describe.skip('membershipService.calculatePrice', () => { 5 | it('小白无折扣', () => { 6 | const { actualPrice, discountRate } = membershipService.calculatePrice(100, '小白') 7 | expect(actualPrice).toBe(100) 8 | expect(discountRate).toBe(1) 9 | }) 10 | 11 | it('螺丝钉九折', () => { 12 | const { actualPrice, discountRate } = membershipService.calculatePrice(100, '螺丝钉') 13 | expect(actualPrice).toBe(90) 14 | expect(discountRate).toBe(0.9) 15 | }) 16 | 17 | it('大牛八折', () => { 18 | const { actualPrice, discountRate } = membershipService.calculatePrice(100, '大牛') 19 | expect(actualPrice).toBe(80) 20 | expect(discountRate).toBe(0.8) 21 | }) 22 | }) 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/security/__tests__/DataEncryptionManager.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { DataEncryptionManager } from '../DataEncryptionManager' 3 | 4 | describe.skip('DataEncryptionManager', () => { 5 | it('encrypt/decrypt 应该保持数据一致', () => { 6 | const mgr = new DataEncryptionManager() 7 | const secret = 'password-123' 8 | const data = 'hello-world' 9 | const enc = mgr.encrypt(data, secret) 10 | const dec = mgr.decrypt(enc, secret) 11 | expect(dec).toBe(data) 12 | }) 13 | 14 | it('encryptObject/decryptObject 应该保持对象一致', () => { 15 | const mgr = new DataEncryptionManager() 16 | const secret = 'password-123' 17 | const obj = { a: 1, b: 'x' } 18 | const enc = mgr.encryptObject(obj, secret) 19 | const dec = mgr.decryptObject(enc, secret) 20 | expect(dec).toEqual(obj) 21 | }) 22 | }) 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cn } from "@/lib/utils" 3 | 4 | export interface TextareaProps 5 | extends React.TextareaHTMLAttributes {} 6 | 7 | const Textarea = React.forwardRef( 8 | ({ className, ...props }, ref) => { 9 | return ( 10 |