├── .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 |
18 | )
19 | }
20 | )
21 | Textarea.displayName = "Textarea"
22 |
23 | export { Textarea }
24 |
--------------------------------------------------------------------------------
/vite.config.renderer.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 | import { defineConfig, loadEnv } from 'vite'
3 | import react from '@vitejs/plugin-react'
4 | import tailwindcss from '@tailwindcss/vite'
5 |
6 | export default defineConfig(({ mode }) => {
7 | // 加载环境变量
8 | const env = loadEnv(mode, process.cwd(), '')
9 |
10 | return {
11 | root: 'src/renderer',
12 | resolve: {
13 | alias: {
14 | '@renderer': resolve('src/renderer/src'),
15 | '@': resolve('src/renderer/src')
16 | }
17 | },
18 | plugins: [react(), tailwindcss()],
19 | define: {
20 | 'process.env.VITE_GEMINI_API_KEY': JSON.stringify(env.VITE_GEMINI_API_KEY),
21 | 'process.env.VITE_SUPABASE_URL': JSON.stringify(env.VITE_SUPABASE_URL),
22 | 'process.env.VITE_SUPABASE_ANON_KEY': JSON.stringify(env.VITE_SUPABASE_ANON_KEY),
23 | 'process.env.VITE_DEV_MODE': JSON.stringify(env.VITE_DEV_MODE)
24 | },
25 | server: {
26 | port: 3000
27 | }
28 | }
29 | })
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Suge8
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@renderer/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # 依赖
2 | node_modules/
3 | .pnp
4 | .pnp.js
5 |
6 | # 构建输出
7 | out/
8 | dist/
9 | dist-electron/
10 | build/
11 |
12 | # 环境变量
13 | .env
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | # 日志
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 | pnpm-debug.log*
24 | lerna-debug.log*
25 |
26 | # 运行时数据
27 | pids
28 | *.pid
29 | *.seed
30 | *.pid.lock
31 |
32 | # 覆盖率报告
33 | coverage/
34 | *.lcov
35 |
36 | # nyc 测试覆盖率
37 | .nyc_output
38 |
39 | # 缓存
40 | .npm
41 | .eslintcache
42 | .cache/
43 | .vite/
44 |
45 | # TypeScript 缓存
46 | *.tsbuildinfo
47 |
48 | # 可选的 npm 缓存目录
49 | .npm
50 |
51 | # 可选的 eslint 缓存
52 | .eslintcache
53 |
54 | # 系统文件
55 | .DS_Store
56 | .DS_Store?
57 | ._*
58 | .Spotlight-V100
59 | .Trashes
60 | ehthumbs.db
61 | Thumbs.db
62 |
63 | # IDE
64 | .vscode/
65 | .idea/
66 | *.swp
67 | *.swo
68 | *~
69 |
70 | # Playwright
71 | test-results/
72 | playwright-report/
73 | playwright/.cache/
74 |
75 | # 临时文件
76 | *.tmp
77 | *.temp
78 | *.log
79 |
80 | # 调试文件
81 | debug/
82 | *.debug
83 |
84 | # 测试报告
85 | *_test_report_*.json
86 |
87 | # 音频调试文件
88 | *.pcm
89 | *.wav
90 |
91 | # 资料文件夹(不提交到仓库)
92 | 资料/
93 | 资料/*
94 |
95 | # cheating-daddy 项目(独立项目,不提交)
96 | assets/cheating-daddy/
97 |
98 | # SystemAudioDump 二进制文件(通过脚本自动安装)
99 | assets/SystemAudioDump
100 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 |
2 | import globals from "globals";
3 | import tseslint from "typescript-eslint";
4 | import pluginReact from "eslint-plugin-react";
5 | import pluginReactHooks from "eslint-plugin-react-hooks";
6 | import prettierConfig from "eslint-config-prettier";
7 |
8 | export default [
9 | {
10 | ignores: [
11 | "dist-electron/",
12 | "out/",
13 | "dist/",
14 | "node_modules/",
15 | "*.config.*",
16 | "coverage/",
17 | "tests/**",
18 | "src/**/__tests__/**",
19 | "**/*.spec.*",
20 | "**/*.test.*",
21 | ],
22 | },
23 | {
24 | files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
25 | languageOptions: {
26 | parser: tseslint.parser,
27 | parserOptions: {
28 | ecmaVersion: 2023,
29 | sourceType: "module",
30 | ecmaFeatures: { jsx: true },
31 | },
32 | globals: {
33 | ...globals.browser,
34 | ...globals.node,
35 | ...globals.es2023,
36 | },
37 | },
38 | plugins: {
39 | "@typescript-eslint": tseslint.plugin,
40 | react: pluginReact,
41 | "react-hooks": pluginReactHooks,
42 | },
43 | rules: {
44 | ...tseslint.configs.recommended.rules,
45 | ...pluginReact.configs.recommended.rules,
46 | ...pluginReactHooks.configs.recommended.rules,
47 | ...prettierConfig.rules,
48 | "no-console": ["warn", { allow: ["warn", "error"] }],
49 | "@typescript-eslint/no-explicit-any": "off",
50 | "react/react-in-jsx-scope": "off",
51 | },
52 | settings: {
53 | react: { version: "detect" },
54 | },
55 | },
56 | ];
57 |
--------------------------------------------------------------------------------
/src/renderer/src/hooks/useKeyboardShortcuts.ts:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 |
3 | export const useKeyboardShortcuts = () => {
4 | useEffect(() => {
5 | const handleKeyDown = (e: KeyboardEvent) => {
6 | // Ctrl/Cmd + N: 新建准备项
7 | if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
8 | e.preventDefault()
9 | // 这里应该导航到创建准备项页面
10 | window.location.hash = '#/create-preparation'
11 | }
12 |
13 | // Ctrl/Cmd + K: 快速搜索
14 | if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
15 | e.preventDefault()
16 | // 这里应该打开搜索面板
17 | console.log('Open search panel shortcut pressed')
18 | }
19 |
20 | // ESC: 关闭模态框
21 | if (e.key === 'Escape') {
22 | // 这里应该关闭当前打开的模态框
23 | const modals = document.querySelectorAll('.modal-overlay')
24 | if (modals.length > 0) {
25 | // 关闭最后一个模态框
26 | const lastModal = modals[modals.length - 1] as HTMLElement
27 | lastModal.click()
28 | }
29 | }
30 | }
31 |
32 | document.addEventListener('keydown', handleKeyDown)
33 | return () => document.removeEventListener('keydown', handleKeyDown)
34 | }, [])
35 | }
36 |
37 | interface ShortcutHintProps {
38 | shortcut: string
39 | description: string
40 | }
41 |
42 | export const ShortcutHint: React.FC = ({
43 | shortcut,
44 | description
45 | }) => {
46 | return (
47 |
48 | {description}
49 |
50 | {shortcut}
51 |
52 |
53 | )
54 | }
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config'
2 | import react from '@vitejs/plugin-react'
3 | import { resolve } from 'path'
4 |
5 | export default defineConfig({
6 | plugins: [react()],
7 |
8 | test: {
9 | // 测试环境
10 | environment: 'jsdom',
11 |
12 | // 全局设置
13 | globals: true,
14 |
15 | // 设置文件
16 | setupFiles: ['./src/test/setup.ts'],
17 |
18 | // 覆盖率配置
19 | coverage: {
20 | provider: 'v8',
21 | reporter: ['text', 'json', 'html', 'lcov'],
22 | reportsDirectory: './coverage',
23 | exclude: [
24 | 'node_modules/',
25 | 'src/test/',
26 | '**/*.d.ts',
27 | '**/*.config.*',
28 | '**/dist/**',
29 | '**/build/**'
30 | ],
31 | thresholds: {
32 | global: {
33 | branches: 80,
34 | functions: 85,
35 | lines: 85,
36 | statements: 85
37 | },
38 | // 核心模块更高要求
39 | 'src/main/services/': {
40 | branches: 90,
41 | functions: 95,
42 | lines: 95,
43 | statements: 95
44 | }
45 | }
46 | },
47 |
48 | // 测试文件匹配
49 | include: [
50 | 'src/**/*.{test,spec}.{js,ts,tsx}'
51 | ],
52 |
53 | // 测试超时
54 | testTimeout: 10000,
55 |
56 | // 并发执行
57 | threads: true,
58 | maxThreads: 4,
59 |
60 | // 监听模式
61 | watch: false,
62 |
63 | // 报告器
64 | reporter: ['verbose', 'json', 'html']
65 | },
66 |
67 | // 路径解析
68 | resolve: {
69 | alias: {
70 | '@': resolve(__dirname, 'src'),
71 | '@renderer': resolve(__dirname, 'src/renderer/src'),
72 | '@main': resolve(__dirname, 'src/main'),
73 | '@shared': resolve(__dirname, 'src/shared'),
74 | '@test': resolve(__dirname, 'src/test')
75 | }
76 | }
77 | })
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | // 更新 Toggle 组件以支持暗黑模式
4 | export interface ToggleProps {
5 | checked: boolean
6 | onChange: (checked: boolean) => void
7 | disabled?: boolean
8 | className?: string
9 | size?: 'sm' | 'md' | 'lg'
10 | }
11 |
12 | export const Toggle: React.FC = ({
13 | checked,
14 | onChange,
15 | disabled = false,
16 | className = '',
17 | size = 'md'
18 | }) => {
19 | const sizeClasses = {
20 | sm: 'w-8 h-4',
21 | md: 'w-10 h-5',
22 | lg: 'w-12 h-6'
23 | }
24 |
25 | const thumbSizeClasses = {
26 | sm: 'w-3 h-3',
27 | md: 'w-4 h-4',
28 | lg: 'w-5 h-5'
29 | }
30 |
31 | const translateClasses = {
32 | sm: 'translate-x-4',
33 | md: 'translate-x-5',
34 | lg: 'translate-x-6'
35 | }
36 |
37 | return (
38 |
71 | )
72 | }
--------------------------------------------------------------------------------
/scripts/release.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * 面宝 (Bready) 版本发布脚本
5 | * 自动化版本更新、构建和文档生成
6 | */
7 |
8 | const fs = require('fs');
9 | const path = require('path');
10 | const { execSync } = require('child_process');
11 |
12 | const packagePath = path.join(__dirname, '../package.json');
13 | const changelogPath = path.join(__dirname, '../CHANGELOG.md');
14 |
15 | function getCurrentVersion() {
16 | const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
17 | return pkg.version;
18 | }
19 |
20 | function updateVersion(newVersion) {
21 | const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
22 | pkg.version = newVersion;
23 | fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n');
24 | console.log(`✅ 版本已更新到 ${newVersion}`);
25 | }
26 |
27 | function buildProject() {
28 | console.log('🔨 开始构建项目...');
29 | try {
30 | execSync('npm run build', { stdio: 'inherit' });
31 | console.log('✅ 项目构建成功');
32 | } catch (error) {
33 | console.error('❌ 构建失败:', error.message);
34 | process.exit(1);
35 | }
36 | }
37 |
38 | function runTests() {
39 | console.log('🧪 运行测试...');
40 | try {
41 | execSync('npm test', { stdio: 'inherit' });
42 | console.log('✅ 测试通过');
43 | } catch (error) {
44 | console.log('⚠️ 测试跳过或失败,继续发布流程');
45 | }
46 | }
47 |
48 | function main() {
49 | const args = process.argv.slice(2);
50 | const versionType = args[0] || 'patch'; // patch, minor, major
51 |
52 | console.log('🚀 面宝 (Bready) 版本发布流程开始');
53 | console.log(`📦 当前版本: ${getCurrentVersion()}`);
54 |
55 | // 构建项目
56 | buildProject();
57 |
58 | // 运行测试
59 | runTests();
60 |
61 | console.log('✅ 发布流程完成');
62 | console.log('📝 请手动检查 CHANGELOG.md 并提交更改');
63 | }
64 |
65 | if (require.main === module) {
66 | main();
67 | }
68 |
69 | module.exports = { getCurrentVersion, updateVersion, buildProject };
70 |
--------------------------------------------------------------------------------
/electron.vite.config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
3 | import react from '@vitejs/plugin-react'
4 | import tailwindcss from '@tailwindcss/vite'
5 | import { loadEnv } from 'vite'
6 |
7 | export default defineConfig(({ mode }) => {
8 | // 加载环境变量
9 | const env = loadEnv(mode, process.cwd(), '')
10 |
11 | return {
12 | main: {
13 | plugins: [externalizeDepsPlugin()],
14 | define: {
15 | 'process.env.VITE_GEMINI_API_KEY': JSON.stringify(env.VITE_GEMINI_API_KEY),
16 | 'process.env.VITE_SUPABASE_URL': JSON.stringify(env.VITE_SUPABASE_URL),
17 | 'process.env.VITE_SUPABASE_ANON_KEY': JSON.stringify(env.VITE_SUPABASE_ANON_KEY),
18 | 'process.env.VITE_DEV_MODE': JSON.stringify(env.VITE_DEV_MODE)
19 | }
20 | },
21 | preload: {
22 | plugins: [externalizeDepsPlugin()],
23 | define: {
24 | 'process.env.VITE_GEMINI_API_KEY': JSON.stringify(env.VITE_GEMINI_API_KEY),
25 | 'process.env.VITE_SUPABASE_URL': JSON.stringify(env.VITE_SUPABASE_URL),
26 | 'process.env.VITE_SUPABASE_ANON_KEY': JSON.stringify(env.VITE_SUPABASE_ANON_KEY),
27 | 'process.env.VITE_DEV_MODE': JSON.stringify(env.VITE_DEV_MODE)
28 | }
29 | },
30 | renderer: {
31 | resolve: {
32 | alias: {
33 | '@renderer': resolve('src/renderer/src'),
34 | '@': resolve('src/renderer/src')
35 | }
36 | },
37 | plugins: [react(), tailwindcss()],
38 | define: {
39 | 'process.env.VITE_GEMINI_API_KEY': JSON.stringify(env.VITE_GEMINI_API_KEY),
40 | 'process.env.VITE_SUPABASE_URL': JSON.stringify(env.VITE_SUPABASE_URL),
41 | 'process.env.VITE_SUPABASE_ANON_KEY': JSON.stringify(env.VITE_SUPABASE_ANON_KEY),
42 | 'process.env.VITE_DEV_MODE': JSON.stringify(env.VITE_DEV_MODE)
43 | }
44 | }
45 | }
46 | })
47 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./src/renderer/src/**/*.{js,ts,jsx,tsx}",
5 | ],
6 | theme: {
7 | extend: {
8 | colors: {
9 | border: "hsl(var(--border))",
10 | input: "hsl(var(--input))",
11 | ring: "hsl(var(--ring))",
12 | background: "hsl(var(--background))",
13 | foreground: "hsl(var(--foreground))",
14 | primary: {
15 | DEFAULT: "hsl(var(--primary))",
16 | foreground: "hsl(var(--primary-foreground))",
17 | },
18 | secondary: {
19 | DEFAULT: "hsl(var(--secondary))",
20 | foreground: "hsl(var(--secondary-foreground))",
21 | },
22 | destructive: {
23 | DEFAULT: "hsl(var(--destructive))",
24 | foreground: "hsl(var(--destructive-foreground))",
25 | },
26 | muted: {
27 | DEFAULT: "hsl(var(--muted))",
28 | foreground: "hsl(var(--muted-foreground))",
29 | },
30 | accent: {
31 | DEFAULT: "hsl(var(--accent))",
32 | foreground: "hsl(var(--accent-foreground))",
33 | },
34 | popover: {
35 | DEFAULT: "hsl(var(--popover))",
36 | foreground: "hsl(var(--popover-foreground))",
37 | },
38 | card: {
39 | DEFAULT: "hsl(var(--card))",
40 | foreground: "hsl(var(--card-foreground))",
41 | },
42 | },
43 | borderRadius: {
44 | lg: "var(--radius)",
45 | md: "calc(var(--radius) - 2px)",
46 | sm: "calc(var(--radius) - 4px)",
47 | },
48 | keyframes: {
49 | "accordion-down": {
50 | from: { height: "0" },
51 | to: { height: "var(--radix-accordion-content-height)" },
52 | },
53 | "accordion-up": {
54 | from: { height: "var(--radix-accordion-content-height)" },
55 | to: { height: "0" },
56 | },
57 | },
58 | animation: {
59 | "accordion-down": "accordion-down 0.2s ease-out",
60 | "accordion-up": "accordion-up 0.2s ease-out",
61 | },
62 | },
63 | },
64 | plugins: [require("tailwindcss-animate")],
65 | }
66 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/touch-optimized.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | interface TouchButtonProps extends React.ButtonHTMLAttributes {
4 | children: React.ReactNode
5 | }
6 |
7 | export const TouchButton: React.FC = ({
8 | children,
9 | className = '',
10 | ...props
11 | }) => {
12 | return (
13 |
26 | )
27 | }
28 |
29 | interface SwipeableCardProps {
30 | children: React.ReactNode
31 | onSwipeLeft?: () => void
32 | onSwipeRight?: () => void
33 | className?: string
34 | }
35 |
36 | export const SwipeableCard: React.FC = ({
37 | children,
38 | onSwipeLeft,
39 | onSwipeRight,
40 | className = ''
41 | }) => {
42 | const [touchStart, setTouchStart] = React.useState<{ x: number, y: number } | null>(null)
43 |
44 | const handleTouchStart = (e: React.TouchEvent) => {
45 | setTouchStart({
46 | x: e.touches[0].clientX,
47 | y: e.touches[0].clientY
48 | })
49 | }
50 |
51 | const handleTouchEnd = (e: React.TouchEvent) => {
52 | if (!touchStart) return
53 |
54 | const touchEnd = {
55 | x: e.changedTouches[0].clientX,
56 | y: e.changedTouches[0].clientY
57 | }
58 |
59 | const deltaX = touchEnd.x - touchStart.x
60 | const deltaY = touchEnd.y - touchStart.y
61 |
62 | // 确保是水平滑动且幅度足够
63 | if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 50) {
64 | if (deltaX > 0) {
65 | onSwipeRight?.()
66 | } else {
67 | onSwipeLeft?.()
68 | }
69 | }
70 |
71 | setTouchStart(null)
72 | }
73 |
74 | return (
75 |
83 | {children}
84 |
85 | )
86 | }
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'
2 |
3 | interface ThemeContextType {
4 | theme: 'light' | 'dark' | 'auto'
5 | setTheme: (theme: 'light' | 'dark' | 'auto') => void
6 | }
7 |
8 | const ThemeContext = createContext(undefined)
9 |
10 | export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
11 | const [theme, setTheme] = useState<'light' | 'dark' | 'auto'>('auto')
12 |
13 | useEffect(() => {
14 | // 从 localStorage 获取主题设置
15 | const savedTheme = localStorage.getItem('bready-theme') as 'light' | 'dark' | 'auto' | null
16 | if (savedTheme) {
17 | setTheme(savedTheme)
18 | }
19 |
20 | // 根据系统偏好设置初始主题
21 | const updateTheme = () => {
22 | const currentTheme = theme === 'auto'
23 | ? window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
24 | : theme
25 |
26 | document.documentElement.classList.remove('light', 'dark')
27 | document.documentElement.classList.add(currentTheme)
28 | }
29 |
30 | updateTheme()
31 |
32 | // 监听系统主题变化
33 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
34 | const handleChange = () => {
35 | if (theme === 'auto') {
36 | updateTheme()
37 | }
38 | }
39 |
40 | mediaQuery.addEventListener('change', handleChange)
41 | return () => mediaQuery.removeEventListener('change', handleChange)
42 | }, [theme])
43 |
44 | const handleSetTheme = (newTheme: 'light' | 'dark' | 'auto') => {
45 | setTheme(newTheme)
46 | localStorage.setItem('bready-theme', newTheme)
47 |
48 | // 立即应用主题
49 | const currentTheme = newTheme === 'auto'
50 | ? window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
51 | : newTheme
52 |
53 | document.documentElement.classList.remove('light', 'dark')
54 | document.documentElement.classList.add(currentTheme)
55 | }
56 |
57 | return (
58 |
59 | {children}
60 |
61 | )
62 | }
63 |
64 | export const useTheme = () => {
65 | const context = useContext(ThemeContext)
66 | if (context === undefined) {
67 | throw new Error('useTheme must be used within a ThemeProvider')
68 | }
69 | return context
70 | }
--------------------------------------------------------------------------------
/src/main/performance/integration.ts:
--------------------------------------------------------------------------------
1 | // 性能监控集成文件
2 | // 用于在主进程中集成性能监控功能
3 |
4 | import { ipcMain } from 'electron'
5 | import {
6 | performanceMonitor,
7 | audioPerformanceMonitor,
8 | logger,
9 | CrashReporter
10 | } from './PerformanceMonitor'
11 |
12 | // 初始化性能监控系统
13 | export function initializePerformanceMonitoring() {
14 | logger.info('Initializing performance monitoring system')
15 |
16 | // 初始化崩溃报告器
17 | CrashReporter.init()
18 |
19 | // 启动系统性能监控
20 | performanceMonitor.startSystemMonitoring(5000) // 每5秒收集一次
21 |
22 | // 定期清理旧的性能数据
23 | setInterval(() => {
24 | performanceMonitor.cleanupOldMetrics(1000)
25 | }, 60000) // 每分钟清理一次
26 |
27 | // 设置IPC处理器
28 | setupPerformanceIPC()
29 |
30 | logger.info('Performance monitoring system initialized successfully')
31 | }
32 |
33 | // 设置性能监控相关的IPC处理器
34 | function setupPerformanceIPC() {
35 | // 获取性能报告
36 | ipcMain.handle('performance:get-report', () => {
37 | return performanceMonitor.getPerformanceReport()
38 | })
39 |
40 | // 获取音频性能指标
41 | ipcMain.handle('performance:get-audio-metrics', () => {
42 | return audioPerformanceMonitor.getAudioMetrics()
43 | })
44 |
45 | // 重置音频性能指标
46 | ipcMain.handle('performance:reset-audio-metrics', () => {
47 | audioPerformanceMonitor.resetMetrics()
48 | return true
49 | })
50 |
51 | // 获取性能指标
52 | ipcMain.handle('performance:get-metrics', () => {
53 | return performanceMonitor.getMetrics()
54 | })
55 |
56 | // 手动触发垃圾回收(如果可用)
57 | ipcMain.handle('performance:trigger-gc', () => {
58 | if (global.gc) {
59 | global.gc()
60 | logger.info('Manual garbage collection triggered')
61 | return true
62 | } else {
63 | logger.warn('Garbage collection not available')
64 | return false
65 | }
66 | })
67 | }
68 |
69 | // 停止性能监控
70 | export function stopPerformanceMonitoring() {
71 | logger.info('Stopping performance monitoring system')
72 | performanceMonitor.stopSystemMonitoring()
73 | logger.info('Performance monitoring system stopped')
74 | }
75 |
76 | // 记录音频处理性能
77 | export function recordAudioProcessing(processingTime: number, bufferSize: number) {
78 | audioPerformanceMonitor.recordAudioProcessing(processingTime, bufferSize)
79 | }
80 |
81 | // 记录性能计时
82 | export function startPerformanceTimer(label: string) {
83 | performanceMonitor.startTimer(label)
84 | }
85 |
86 | export function endPerformanceTimer(label: string): number {
87 | return performanceMonitor.endTimer(label)
88 | }
89 |
90 | // 导出日志记录器供其他模块使用
91 | export { logger }
--------------------------------------------------------------------------------
/docs/UI_DESIGN.md:
--------------------------------------------------------------------------------
1 | # Bready UI/UX 设计规范
2 |
3 | ## 模态框设计理念
4 |
5 | ### 核心原则
6 |
7 | 所有模态框遵循统一的交互设计,确保用户体验的一致性。
8 |
9 | ### 结构规范
10 |
11 | ```tsx
12 |
16 |
e.stopPropagation()} // 阻止事件冒泡
19 | >
20 | {/* 模态框内容 */}
21 |
22 |
23 | ```
24 |
25 | ### 交互规范
26 |
27 | #### 1. 关闭方式
28 | - ❌ **不使用**右上角 X 关闭按钮
29 | - ✅ **点击外部背景**关闭(外层 div 的 onClick)
30 | - ✅ 内容区域点击不关闭(stopPropagation)
31 |
32 | #### 2. 光标样式(Electron 应用)
33 |
34 | **外层背景**:
35 | - `cursor-pointer` - 提示用户可以点击关闭
36 |
37 | **内层内容**:
38 | - `cursor-auto` - 重置光标,避免继承外层的 cursor-pointer
39 |
40 | **交互元素**:
41 | - 所有按钮:`cursor-pointer`
42 | - 所有下拉框:`cursor-pointer`
43 | - 可点击的卡片/div:`cursor-pointer`
44 |
45 | > ⚠️ **重要**:在 Electron 应用中,必须手动为所有可交互元素添加 `cursor-pointer`,
46 | > 因为 Electron 不像浏览器那样自动为按钮提供手型光标。
47 |
48 | #### 3. 视觉效果
49 |
50 | - **背景**:`bg-black/50` + `backdrop-blur-sm`(黑色半透明+模糊)
51 | - **内容卡片**:
52 | - 圆角:`rounded-xl`
53 | - 阴影:`shadow-2xl`
54 | - 内边距:`p-6`
55 | - **深色模式支持**:所有颜色都有 dark: 变体
56 |
57 | ### 按钮规范
58 |
59 | #### 主要操作按钮
60 | ```tsx
61 |
64 | ```
65 |
66 | #### 取消/次要按钮
67 | ```tsx
68 |
74 | ```
75 |
76 | ### 实际案例
77 |
78 | #### 案例 1:准备面试模态框
79 | - 标题居左
80 | - 无关闭按钮
81 | - 按钮居中显示
82 | - 点击外部关闭
83 |
84 | #### 案例 2:退出确认对话框
85 | - 只有一个"退出"按钮(全宽)
86 | - 删除了"取消"按钮(点击外部即可取消)
87 | - 按钮居中显示
88 |
89 | #### 案例 3:权限设置模态框
90 | - 无右上角关闭按钮
91 | - 无底部关闭按钮
92 | - 点击外部关闭
93 |
94 | ### 布局规范
95 |
96 | - **标题**:居左,无右侧关闭按钮
97 | - **按钮位置**:
98 | - 单个按钮:居中(`justify-center`)
99 | - 多个按钮:根据重要性居左/右/分散
100 |
101 | ### 文字规范
102 |
103 | 保持简洁:
104 | - "请选择本次面试的准备项" → "准备面试"
105 | - "面试语言" → "语言"
106 | - "使用目的" → "目的"
107 | - "不准备,直接开始" → 简洁明了
108 |
109 | ### Z-index 层级
110 |
111 | - 一般模态框:`z-50` 或 `z-[100]`
112 | - 重要模态框(如系统权限):`z-[9999]`
113 |
114 | ---
115 |
116 | ## 设计原则总结
117 |
118 | 1. **简洁至上** - 移除非必要元素
119 | 2. **一致性** - 所有模态框使用相同的交互模式
120 | 3. **直观性** - 光标变化明确提示用户可以做什么
121 | 4. **Electron 优化** - 手动添加所有 cursor-pointer
122 | 5. **无障碍** - 点击外部可关闭,降低操作门槛
123 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/smart-recommendations.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { Card, CardHeader, CardTitle, CardDescription, CardContent } from './ui/card'
3 | import { Button } from './ui/button'
4 |
5 | interface SmartRecommendationsProps {
6 | onItemSelected?: (item: any) => void
7 | }
8 |
9 | export const SmartRecommendations: React.FC = ({ onItemSelected }) => {
10 | const [recommendations, setRecommendations] = useState([
11 | {
12 | id: 1,
13 | title: '技术面试常见问题',
14 | description: '涵盖算法、数据结构、系统设计等技术面试高频问题',
15 | category: '技术面试',
16 | relevance: 95
17 | },
18 | {
19 | id: 2,
20 | title: '行为面试STAR法则',
21 | description: '教你如何用情境-任务-行动-结果的结构回答行为问题',
22 | category: '行为面试',
23 | relevance: 88
24 | },
25 | {
26 | id: 3,
27 | title: '薪资谈判技巧',
28 | description: '掌握薪资谈判的时机、策略和沟通技巧',
29 | category: '薪资谈判',
30 | relevance: 82
31 | }
32 | ])
33 |
34 | return (
35 |
36 |
为你推荐
37 |
38 |
39 | {recommendations.map(item => (
40 |
onItemSelected?.(item)}
44 | >
45 |
46 |
47 |
48 | {item.title}
49 | {item.description}
50 |
51 |
52 | {item.relevance}%
53 |
54 |
55 |
56 |
57 |
58 |
59 | {item.category}
60 |
63 |
64 |
65 |
66 | ))}
67 |
68 |
69 | )
70 | }
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/accessibility-settings.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Toggle } from './ui/toggle'
3 |
4 | interface AccessibilitySettingsProps {
5 | accessibility: {
6 | highContrast: boolean
7 | largerText: boolean
8 | reducedMotion: boolean
9 | screenReader: boolean
10 | }
11 | onChange: (accessibility: any) => void
12 | }
13 |
14 | export const AccessibilitySettings: React.FC = ({
15 | accessibility,
16 | onChange
17 | }) => {
18 | const [localAccessibility, setLocalAccessibility] = useState(accessibility)
19 |
20 | const handleChange = (id: string, value: boolean) => {
21 | const newAccessibility = {
22 | ...localAccessibility,
23 | [id]: value
24 | }
25 | setLocalAccessibility(newAccessibility)
26 | onChange(newAccessibility)
27 |
28 | // 应用无障碍设置到DOM
29 | document.body.classList.toggle(`accessibility-${id}`, value)
30 | }
31 |
32 | return (
33 |
34 |
无障碍设置
35 |
36 |
37 |
38 |
39 |
高对比度模式
40 |
增强颜色对比度,便于视觉障碍用户使用
41 |
42 |
handleChange('highContrast', checked)}
45 | />
46 |
47 |
48 |
49 |
50 |
增大字体
51 |
放大界面字体,便于阅读
52 |
53 |
handleChange('largerText', checked)}
56 | />
57 |
58 |
59 |
60 |
61 |
减少动画
62 |
减少界面动画,减轻前庭障碍用户不适
63 |
64 |
handleChange('reducedMotion', checked)}
67 | />
68 |
69 |
70 |
71 |
72 |
屏幕阅读器优化
73 |
优化界面元素的语义化标签
74 |
75 |
handleChange('screenReader', checked)}
78 | />
79 |
80 |
81 |
82 | )
83 | }
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # 面宝 (Bready) 环境配置示例
2 | # 复制此文件为 .env.local 并填入实际值
3 |
4 | # =====================================
5 | # 数据库配置
6 | # =====================================
7 | DB_HOST=localhost
8 | DB_PORT=5432
9 | DB_NAME=bready
10 | DB_USER=postgres
11 | DB_PASSWORD=your_postgres_password
12 |
13 | # 数据库连接池配置(性能优化)
14 | DB_MAX_CONNECTIONS=10
15 | DB_MIN_CONNECTIONS=2
16 |
17 | # =====================================
18 | # JWT 认证配置
19 | # =====================================
20 | JWT_SECRET=your-super-secret-jwt-key-change-in-production
21 |
22 | # =====================================
23 | # Google Gemini API 配置
24 | # 支持配置多个 API Key,用逗号分隔,会自动轮询以避免配额限制
25 | # 例如: VITE_GEMINI_API_KEY=key1,key2,key3
26 | # =====================================
27 | VITE_GEMINI_API_KEY=your_gemini_api_key_here
28 |
29 | # =====================================
30 | # 性能优化配置
31 | # =====================================
32 |
33 | # 内存监控阈值(MB)
34 | MEMORY_WARNING_THRESHOLD=150
35 | MEMORY_CRITICAL_THRESHOLD=200
36 | MEMORY_GC_TRIGGER=120
37 |
38 | # 启动性能目标(毫秒)
39 | STARTUP_TIME_TARGET=3000
40 |
41 | # 音频处理配置
42 | AUDIO_BUFFER_SIZE=4096
43 | AUDIO_SAMPLE_RATE=24000
44 | AUDIO_NOISE_THRESHOLD=0.005
45 | AUDIO_ENABLE_NOISE_GATE=true
46 |
47 | # =====================================
48 | # 开发模式配置
49 | # =====================================
50 | NODE_ENV=development
51 | VITE_DEV_MODE=true
52 |
53 | # 性能监控开关
54 | ENABLE_PERFORMANCE_MONITORING=true
55 | ENABLE_MEMORY_MONITORING=true
56 | ENABLE_STARTUP_METRICS=true
57 |
58 | # 调试选项
59 | DEBUG_AUDIO=false
60 | DEBUG_DATABASE=false
61 | DEBUG_MEMORY=false
62 |
63 | # =====================================
64 | # 构建优化配置
65 | # =====================================
66 |
67 | # 代码分割阈值(KB)
68 | CHUNK_SIZE_WARNING=500
69 |
70 | # 构建目标
71 | BUILD_TARGET=modern
72 |
73 | # 压缩选项
74 | ENABLE_COMPRESSION=true
75 | ENABLE_TREE_SHAKING=true
76 |
77 | # =====================================
78 | # 安全配置
79 | # =====================================
80 |
81 | # API密钥加密
82 | ENCRYPTION_KEY=your-encryption-key-32-characters
83 | ENCRYPTION_ALGORITHM=aes-256-gcm
84 |
85 | # 会话配置
86 | SESSION_TIMEOUT=7d
87 | TOKEN_REFRESH_THRESHOLD=24h
88 |
89 | # =====================================
90 | # 日志配置
91 | # =====================================
92 | LOG_LEVEL=info
93 | LOG_FILE_MAX_SIZE=10MB
94 | LOG_FILE_MAX_FILES=5
95 | ENABLE_CONSOLE_LOGS=true
96 |
97 | # =====================================
98 | # 功能开关
99 | # =====================================
100 |
101 | # 实验性功能
102 | ENABLE_EXPERIMENTAL_FEATURES=false
103 | ENABLE_BETA_AUDIO_PROCESSING=false
104 | ENABLE_ADVANCED_MEMORY_OPTIMIZATION=false
105 |
106 | # UI优化
107 | ENABLE_VIRTUAL_SCROLLING=true
108 | ENABLE_LAZY_LOADING=true
109 | ENABLE_DEBOUNCED_INPUT=true
110 |
111 | # =====================================
112 | # 网络配置
113 | # =====================================
114 |
115 | # API超时设置(毫秒)
116 | API_TIMEOUT=30000
117 | CONNECTION_TIMEOUT=5000
118 | RETRY_ATTEMPTS=3
119 | RETRY_DELAY=1000
120 |
121 | # =====================================
122 | # 缓存配置
123 | # =====================================
124 |
125 | # 缓存大小限制(MB)
126 | CACHE_MAX_SIZE=50
127 | CACHE_TTL=3600000
128 |
129 | # 音频缓存
130 | AUDIO_CACHE_SIZE=20
131 | AUDIO_CACHE_TTL=300000
132 |
133 | # 数据库查询缓存
134 | DB_QUERY_CACHE_SIZE=100
135 | DB_QUERY_CACHE_TTL=600000
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/navigation.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { motion } from 'framer-motion'
3 | import { ChevronRight, FolderOpen, X } from 'lucide-react'
4 | import { Link } from 'react-router-dom'
5 |
6 | interface BreadcrumbItem {
7 | label: string
8 | path: string
9 | }
10 |
11 | interface BreadcrumbProps {
12 | items: BreadcrumbItem[]
13 | }
14 |
15 | export const Breadcrumb: React.FC = ({ items }) => {
16 | return (
17 |
36 | )
37 | }
38 |
39 | interface SidebarProps {
40 | collapsed?: boolean
41 | onCollapseChange?: (collapsed: boolean) => void
42 | }
43 |
44 | export const Sidebar: React.FC = ({ collapsed = false, onCollapseChange }) => {
45 | return (
46 |
71 | )
72 | }
73 |
74 | interface PageTransitionProps {
75 | children: React.ReactNode
76 | }
77 |
78 | export const PageTransition: React.FC = ({ children }) => {
79 | return (
80 |
86 | {children}
87 |
88 | )
89 | }
90 |
91 | interface MicroInteractionProps {
92 | children: React.ReactNode
93 | className?: string
94 | }
95 |
96 | export const MicroInteraction: React.FC = ({ children, className = '' }) => {
97 | return (
98 |
104 | {children}
105 |
106 | )
107 | }
--------------------------------------------------------------------------------
/src/renderer/src/components/UserLevelBadge.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { User, Crown, Star, Shield, Zap } from 'lucide-react'
3 | import { UserLevel } from '../lib/supabase'
4 |
5 | interface UserLevelBadgeProps {
6 | level: UserLevel
7 | size?: 'sm' | 'md' | 'lg'
8 | showIcon?: boolean
9 | className?: string
10 | }
11 |
12 | const UserLevelBadge: React.FC = ({
13 | level,
14 | size = 'md',
15 | showIcon = true,
16 | className = ''
17 | }) => {
18 | const getLevelConfig = (level: UserLevel) => {
19 | switch (level) {
20 | case '小白':
21 | return {
22 | icon: User,
23 | bgColor: 'bg-gray-100',
24 | textColor: 'text-gray-700',
25 | borderColor: 'border-gray-200',
26 | iconColor: 'text-gray-500'
27 | }
28 | case '螺丝钉':
29 | return {
30 | icon: Zap,
31 | bgColor: 'bg-blue-100',
32 | textColor: 'text-blue-700',
33 | borderColor: 'border-blue-200',
34 | iconColor: 'text-blue-500'
35 | }
36 | case '大牛':
37 | return {
38 | icon: Star,
39 | bgColor: 'bg-purple-100',
40 | textColor: 'text-purple-700',
41 | borderColor: 'border-purple-200',
42 | iconColor: 'text-purple-500'
43 | }
44 | case '管理':
45 | return {
46 | icon: Shield,
47 | bgColor: 'bg-orange-100',
48 | textColor: 'text-orange-700',
49 | borderColor: 'border-orange-200',
50 | iconColor: 'text-orange-500'
51 | }
52 | case '超级':
53 | return {
54 | icon: Crown,
55 | bgColor: 'bg-red-100',
56 | textColor: 'text-red-700',
57 | borderColor: 'border-red-200',
58 | iconColor: 'text-red-500'
59 | }
60 | default:
61 | return {
62 | icon: User,
63 | bgColor: 'bg-gray-100',
64 | textColor: 'text-gray-700',
65 | borderColor: 'border-gray-200',
66 | iconColor: 'text-gray-500'
67 | }
68 | }
69 | }
70 |
71 | const getSizeClasses = (size: 'sm' | 'md' | 'lg') => {
72 | switch (size) {
73 | case 'sm':
74 | return {
75 | container: 'px-2 py-1 text-xs',
76 | icon: 'w-3 h-3'
77 | }
78 | case 'md':
79 | return {
80 | container: 'px-3 py-1.5 text-sm',
81 | icon: 'w-4 h-4'
82 | }
83 | case 'lg':
84 | return {
85 | container: 'px-4 py-2 text-base',
86 | icon: 'w-5 h-5'
87 | }
88 | default:
89 | return {
90 | container: 'px-3 py-1.5 text-sm',
91 | icon: 'w-4 h-4'
92 | }
93 | }
94 | }
95 |
96 | const config = getLevelConfig(level)
97 | const sizeClasses = getSizeClasses(size)
98 | const IconComponent = config.icon
99 |
100 | return (
101 |
109 | {showIcon && (
110 |
111 | )}
112 | {level}
113 |
114 | )
115 | }
116 |
117 | export default UserLevelBadge
118 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/form-enhancements.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { motion } from 'framer-motion'
3 | import { AlertTriangle, FolderOpen } from 'lucide-react'
4 |
5 | interface SmartFormValidationProps {
6 | value: string
7 | onChange: (value: string) => void
8 | name: string
9 | label: string
10 | placeholder?: string
11 | required?: boolean
12 | maxLength?: number
13 | }
14 |
15 | export const SmartFormValidation: React.FC = ({
16 | value,
17 | onChange,
18 | name,
19 | label,
20 | placeholder = '',
21 | required = false,
22 | maxLength = 50
23 | }) => {
24 | const [error, setError] = useState(null)
25 | const [helperText, setHelperText] = useState('')
26 |
27 | useEffect(() => {
28 | validateField()
29 | }, [value])
30 |
31 | const validateField = () => {
32 | if (required && !value.trim()) {
33 | setError('此字段为必填项')
34 | setHelperText('请输入内容')
35 | return
36 | }
37 |
38 | if (value.length > maxLength) {
39 | setError(`内容不能超过${maxLength}个字符`)
40 | setHelperText(`当前${value.length}个字符`)
41 | return
42 | }
43 |
44 | setError(null)
45 | setHelperText(value.length > 0 ? `${value.length}/${maxLength} 字符` : placeholder)
46 | }
47 |
48 | return (
49 |
50 |
54 |
55 |
onChange(e.target.value)}
60 | className={`
61 | w-full px-3 py-2 border rounded-lg
62 | focus:outline-none focus:ring-2 focus:ring-blue-500
63 | ${error
64 | ? 'border-red-500 focus:border-red-500'
65 | : 'border-gray-300 dark:border-gray-600 focus:border-blue-500'
66 | }
67 | bg-white dark:bg-gray-800 text-black dark:text-white
68 | `}
69 | placeholder={placeholder}
70 | />
71 |
72 |
73 | {error || helperText}
74 |
75 |
76 | )
77 | }
78 |
79 | interface AutoSaveFormProps {
80 | formData: Record
81 | onFormChange: (data: Record) => void
82 | children: React.ReactNode
83 | }
84 |
85 | export const AutoSaveForm: React.FC = ({
86 | formData,
87 | onFormChange,
88 | children
89 | }) => {
90 | const [lastSaved, setLastSaved] = useState(null)
91 | const [isSaving, setIsSaving] = useState(false)
92 |
93 | useEffect(() => {
94 | const timer = setTimeout(() => {
95 | setIsSaving(true)
96 | // 模拟自动保存草稿
97 | localStorage.setItem('bready-form-draft', JSON.stringify(formData))
98 | setLastSaved(new Date())
99 | setIsSaving(false)
100 | }, 2000) // 2秒防抖
101 |
102 | return () => clearTimeout(timer)
103 | }, [formData])
104 |
105 | return (
106 |
107 | {children}
108 |
109 |
110 | {isSaving ? (
111 | <>
112 |
117 | 保存中...
118 | >
119 | ) : lastSaved ? (
120 | `草稿已自动保存于 ${lastSaved.toLocaleTimeString()}`
121 | ) : (
122 | '正在编辑...'
123 | )}
124 |
125 |
126 | )
127 | }
--------------------------------------------------------------------------------
/src/renderer/src/components/ErrorMessage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { AlertTriangle, Wifi, Key, Volume2, Settings } from 'lucide-react'
3 |
4 | export type ErrorType =
5 | | 'permissions-not-set'
6 | | 'api-connection-failed'
7 | | 'audio-device-error'
8 | | 'network-error'
9 | | 'unknown-error'
10 |
11 | interface ErrorMessageProps {
12 | type: ErrorType
13 | message: string
14 | onFix?: () => void
15 | onDismiss?: () => void
16 | }
17 |
18 | const ErrorMessage: React.FC = ({ type, message, onFix, onDismiss }) => {
19 | const getErrorConfig = (errorType: ErrorType) => {
20 | switch (errorType) {
21 | case 'permissions-not-set':
22 | return {
23 | icon: ,
24 | title: '权限未设置',
25 | color: 'yellow',
26 | fixText: '设置权限'
27 | }
28 | case 'api-connection-failed':
29 | return {
30 | icon: ,
31 | title: 'API 连接失败',
32 | color: 'red',
33 | fixText: '检查 API 密钥'
34 | }
35 | case 'audio-device-error':
36 | return {
37 | icon: ,
38 | title: '音频设备错误',
39 | color: 'red',
40 | fixText: '测试音频设备'
41 | }
42 | case 'network-error':
43 | return {
44 | icon: ,
45 | title: '网络连接错误',
46 | color: 'red',
47 | fixText: '检查网络'
48 | }
49 | default:
50 | return {
51 | icon: ,
52 | title: '未知错误',
53 | color: 'red',
54 | fixText: '重试'
55 | }
56 | }
57 | }
58 |
59 | const config = getErrorConfig(type)
60 |
61 | const colorClasses = {
62 | yellow: {
63 | bg: 'bg-yellow-50',
64 | border: 'border-yellow-200',
65 | icon: 'text-yellow-600',
66 | title: 'text-yellow-800',
67 | message: 'text-yellow-700',
68 | button: 'bg-yellow-600 hover:bg-yellow-700 text-white'
69 | },
70 | red: {
71 | bg: 'bg-red-50',
72 | border: 'border-red-200',
73 | icon: 'text-red-600',
74 | title: 'text-red-800',
75 | message: 'text-red-700',
76 | button: 'bg-red-600 hover:bg-red-700 text-white'
77 | }
78 | }
79 |
80 | const colors = colorClasses[config.color]
81 |
82 | return (
83 |
84 |
85 |
86 | {config.icon}
87 |
88 |
89 |
90 |
91 | {config.title}
92 |
93 |
94 | {message}
95 |
96 |
97 |
98 |
99 | {onFix && (
100 |
106 | )}
107 |
108 | {onDismiss && (
109 |
117 | )}
118 |
119 |
120 |
121 | )
122 | }
123 |
124 | export default ErrorMessage
125 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/notifications.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { motion, AnimatePresence } from 'framer-motion'
3 | import { X } from 'lucide-react'
4 |
5 | interface ToastNotificationProps {
6 | message: string
7 | type: 'success' | 'info' | 'warning' | 'error'
8 | duration?: number
9 | onClose?: () => void
10 | }
11 |
12 | export const ToastNotification: React.FC = ({
13 | message,
14 | type,
15 | duration = 3000,
16 | onClose
17 | }) => {
18 | const [visible, setVisible] = useState(true)
19 |
20 | const typeStyles = {
21 | success: 'bg-green-500 text-white',
22 | info: 'bg-blue-500 text-white',
23 | warning: 'bg-yellow-500 text-white',
24 | error: 'bg-red-500 text-white'
25 | }
26 |
27 | const typeIcons = {
28 | success: '✓',
29 | info: 'ℹ',
30 | warning: '⚠',
31 | error: '✗'
32 | }
33 |
34 | React.useEffect(() => {
35 | const timer = setTimeout(() => {
36 | setVisible(false)
37 | setTimeout(() => onClose?.(), 300)
38 | }, duration)
39 |
40 | return () => clearTimeout(timer)
41 | }, [duration, onClose])
42 |
43 | if (!visible) return null
44 |
45 | return (
46 |
47 |
58 | {typeIcons[type]}
59 | {message}
60 |
69 |
70 |
71 | )
72 | }
73 |
74 | interface ConfirmationDialogProps {
75 | title: string
76 | message: string
77 | onConfirm: () => void
78 | onCancel: () => void
79 | confirmText?: string
80 | cancelText?: string
81 | }
82 |
83 | export const ConfirmationDialog: React.FC = ({
84 | title,
85 | message,
86 | onConfirm,
87 | onCancel,
88 | confirmText = '确认',
89 | cancelText = '取消'
90 | }) => {
91 | return (
92 |
93 |
99 |
100 | {title}
101 |
102 |
103 |
104 | {message}
105 |
106 |
107 |
108 |
114 |
115 |
121 |
122 |
123 |
124 | )
125 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bready",
3 | "version": "2.0.2",
4 | "description": "面宝 - AI面试伙伴",
5 | "main": "out/main/index.js",
6 | "scripts": {
7 | "dev": "NODE_OPTIONS=\"--expose-gc\" electron-vite dev --inspect=0",
8 | "dev:gc": "electron-vite build && electron --js-flags=\"--expose-gc\" out/main/index.js",
9 | "dev:api": "tsx src/api-server.ts",
10 | "dev:full": "concurrently \"npm run dev:api\" \"npm run dev\"",
11 | "build": "electron-vite build",
12 | "preview": "electron-vite preview",
13 | "build:win": "npm run build && electron-builder --win",
14 | "build:mac": "npm run build && electron-builder --mac",
15 | "build:linux": "npm run build && electron-builder --linux",
16 | "test": "npx playwright test",
17 | "test:unit": "vitest run --coverage",
18 | "test:electron": "npx playwright test tests/electron-*.spec.js",
19 | "release": "node scripts/release.js",
20 | "clean": "rm -rf out dist-electron node_modules/.cache",
21 | "lint": "eslint .",
22 | "format": "prettier --write .",
23 | "type-check": "tsc --noEmit",
24 | "start": "npm run dev",
25 | "db:start": "node scripts/db-manager.js start",
26 | "db:stop": "node scripts/db-manager.js stop",
27 | "db:status": "node scripts/db-manager.js status",
28 | "db:init": "node scripts/db-manager.js init",
29 | "db:reset": "node scripts/db-manager.js reset",
30 | "db:setup": "node scripts/db-manager.js setup",
31 | "audio:install": "node scripts/install-system-audio-dump.js",
32 | "audio:reinstall": "node scripts/install-system-audio-dump.js --force",
33 | "audio:build": "cd assets && ./build-audio-dump.sh",
34 | "audio:test": "cd assets && timeout 3 ./SystemAudioDump > /dev/null 2>&1 && echo '✅ SystemAudioDump 工作正常' || echo '⚠️ SystemAudioDump 需要权限或不可用'"
35 | },
36 | "keywords": [
37 | "electron",
38 | "interview",
39 | "ai",
40 | "assistant"
41 | ],
42 | "author": "Bready Team",
43 | "license": "MIT",
44 | "type": "module",
45 | "dependencies": {
46 | "@electron-toolkit/preload": "^3.0.2",
47 | "@electron-toolkit/utils": "^4.0.0",
48 | "@google/genai": "^1.9.0",
49 | "@radix-ui/react-popover": "^1.1.14",
50 | "@radix-ui/react-slot": "^1.2.3",
51 | "@tailwindcss/typography": "^0.5.16",
52 | "@tailwindcss/vite": "^4.1.11",
53 | "@types/bcryptjs": "^2.4.6",
54 | "@types/cors": "^2.8.19",
55 | "@types/express": "^5.0.3",
56 | "@types/jsonwebtoken": "^9.0.10",
57 | "@types/pg": "^8.15.4",
58 | "@types/react": "^19.1.8",
59 | "@types/react-dom": "^19.1.6",
60 | "@vitejs/plugin-react": "^4.6.0",
61 | "autoprefixer": "^10.4.21",
62 | "bcryptjs": "^3.0.2",
63 | "class-variance-authority": "^0.7.1",
64 | "clsx": "^2.1.1",
65 | "concurrently": "^9.2.0",
66 | "cors": "^2.8.5",
67 | "dotenv": "^17.2.0",
68 | "electron": "^37.2.0",
69 | "electron-vite": "^4.0.0",
70 | "express": "^5.1.0",
71 | "framer-motion": "^12.23.9",
72 | "globals": "^16.3.0",
73 | "jsonwebtoken": "^9.0.2",
74 | "lucide-react": "^0.525.0",
75 | "pg": "^8.16.3",
76 | "postcss": "^8.5.6",
77 | "react": "^19.1.0",
78 | "react-dom": "^19.1.0",
79 | "react-markdown": "^10.1.0",
80 | "react-router-dom": "^7.6.3",
81 | "rehype-highlight": "^7.0.2",
82 | "remark-gfm": "^4.0.1",
83 | "tailwind-merge": "^3.3.1",
84 | "tailwindcss": "^4.1.11",
85 | "tailwindcss-animate": "^1.0.7",
86 | "tsx": "^4.20.3",
87 | "typescript": "^5.8.3",
88 | "typescript-eslint": "^8.41.0",
89 | "vite": "^7.0.4"
90 | },
91 | "devDependencies": {
92 | "@playwright/test": "^1.54.0",
93 | "@types/eslint": "^9.6.1",
94 | "@types/node": "^22.9.0",
95 | "@typescript-eslint/eslint-plugin": "^8.8.1",
96 | "@typescript-eslint/parser": "^8.8.1",
97 | "eslint": "^9.14.0",
98 | "eslint-config-prettier": "^9.1.0",
99 | "eslint-plugin-react": "^7.37.2",
100 | "eslint-plugin-react-hooks": "^5.1.0",
101 | "prettier": "^3.3.3",
102 | "vitest": "^2.1.4"
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/main/performance/startup-optimizer.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 启动性能优化器
3 | * 实现应用启动的异步加载和性能监控
4 | */
5 |
6 | import { app, BrowserWindow } from 'electron'
7 |
8 | const debugStartup = process.env.DEBUG_STARTUP === '1'
9 |
10 | interface StartupMetrics {
11 | appReadyTime: number
12 | windowCreateTime: number
13 | databaseInitTime: number
14 | ipcSetupTime: number
15 | totalStartupTime: number
16 | }
17 |
18 | export class StartupOptimizer {
19 | private startTime = Date.now()
20 | private metrics: Partial = {}
21 |
22 | /**
23 | * 记录启动指标
24 | */
25 | recordMetric(key: keyof StartupMetrics, value?: number) {
26 | this.metrics[key] = value || Date.now() - this.startTime
27 | if (debugStartup) {
28 | console.log(`🚀 启动指标: ${key} = ${this.metrics[key]}ms`)
29 | }
30 | }
31 |
32 | /**
33 | * 异步初始化数据库
34 | * 不阻塞窗口创建
35 | */
36 | async initializeDatabaseAsync() {
37 | const startTime = Date.now()
38 | try {
39 | // 动态导入数据库模块,减少初始加载时间
40 | const { initializeDatabase, testConnection } = await import('../database')
41 |
42 | // 先测试连接,如果失败则跳过初始化
43 | const isConnected = await testConnection()
44 | if (isConnected) {
45 | await initializeDatabase()
46 | if (debugStartup) {
47 | console.log('✅ 数据库异步初始化成功')
48 | }
49 | } else {
50 | if (debugStartup) {
51 | console.warn('⚠️ 数据库连接失败,跳过初始化')
52 | }
53 | }
54 | } catch (error) {
55 | console.error('❌ 数据库异步初始化失败:', error)
56 | // 不阻塞应用启动
57 | } finally {
58 | this.recordMetric('databaseInitTime', Date.now() - startTime)
59 | }
60 | }
61 |
62 | /**
63 | * 异步设置IPC处理器
64 | */
65 | async setupIPCAsync() {
66 | const startTime = Date.now()
67 | try {
68 | const { setupAllHandlers } = await import('../ipc-handlers')
69 | setupAllHandlers()
70 | if (debugStartup) {
71 | console.log('✅ IPC处理器异步设置完成')
72 | }
73 | } catch (error) {
74 | console.error('❌ IPC处理器设置失败:', error)
75 | } finally {
76 | this.recordMetric('ipcSetupTime', Date.now() - startTime)
77 | }
78 | }
79 |
80 | /**
81 | * 预加载关键模块
82 | */
83 | async preloadCriticalModules() {
84 | const modules = [
85 | () => import('../audioUtils'),
86 | () => import('../prompts'),
87 | () => import('./PerformanceMonitor')
88 | ]
89 |
90 | // 并行预加载
91 | await Promise.allSettled(modules.map(loader => loader()))
92 | if (debugStartup) {
93 | console.log('✅ 关键模块预加载完成')
94 | }
95 | }
96 |
97 | /**
98 | * 获取启动性能报告
99 | */
100 | getPerformanceReport(): StartupMetrics {
101 | this.recordMetric('totalStartupTime')
102 | return this.metrics as StartupMetrics
103 | }
104 |
105 | /**
106 | * 检查是否满足性能目标
107 | */
108 | validatePerformanceTargets(): boolean {
109 | const { totalStartupTime } = this.metrics
110 | const TARGET_STARTUP_TIME = 3000 // 3秒目标
111 |
112 | if (totalStartupTime && totalStartupTime > TARGET_STARTUP_TIME) {
113 | if (debugStartup) {
114 | console.warn(`⚠️ 启动时间超过目标: ${totalStartupTime}ms > ${TARGET_STARTUP_TIME}ms`)
115 | }
116 | return false
117 | }
118 |
119 | if (debugStartup) {
120 | console.log(`✅ 启动时间符合目标: ${totalStartupTime}ms`)
121 | }
122 | return true
123 | }
124 | }
125 |
126 | /**
127 | * 优化的应用启动流程
128 | */
129 | export async function optimizedStartup(createWindow: () => BrowserWindow) {
130 | const optimizer = new StartupOptimizer()
131 |
132 | // 1. 立即创建窗口(不等待其他初始化)
133 | optimizer.recordMetric('appReadyTime')
134 | const window = createWindow()
135 | optimizer.recordMetric('windowCreateTime')
136 |
137 | // 2. 异步初始化其他组件
138 | const initTasks = [
139 | optimizer.initializeDatabaseAsync(),
140 | optimizer.setupIPCAsync(),
141 | optimizer.preloadCriticalModules()
142 | ]
143 |
144 | // 3. 等待所有初始化完成
145 | await Promise.allSettled(initTasks)
146 |
147 | // 4. 报告性能指标
148 | const report = optimizer.getPerformanceReport()
149 | optimizer.validatePerformanceTargets()
150 |
151 | return { window, metrics: report }
152 | }
153 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | // 更新 Card 组件以支持暗黑模式
4 | export interface CardProps {
5 | children: React.ReactNode
6 | className?: string
7 | padding?: 'none' | 'sm' | 'md' | 'lg'
8 | shadow?: 'none' | 'sm' | 'md' | 'lg'
9 | border?: boolean
10 | hover?: boolean
11 | onClick?: () => void
12 | }
13 |
14 | export const Card: React.FC = ({
15 | children,
16 | className = '',
17 | padding = 'md',
18 | shadow = 'sm',
19 | border = true,
20 | hover = false,
21 | onClick
22 | }) => {
23 | const paddingClasses = {
24 | none: '',
25 | sm: 'p-3',
26 | md: 'p-4',
27 | lg: 'p-6'
28 | }
29 |
30 | const shadowClasses = {
31 | none: '',
32 | sm: 'shadow-sm',
33 | md: 'shadow-md',
34 | lg: 'shadow-lg'
35 | }
36 |
37 | const baseClasses = `
38 | bg-white dark:bg-gray-800 rounded-lg
39 | ${border ? 'border border-gray-200 dark:border-gray-700' : ''}
40 | ${shadowClasses[shadow]}
41 | ${paddingClasses[padding]}
42 | ${onClick ? 'cursor-pointer' : ''}
43 | ${className}
44 | `.trim()
45 |
46 | if (onClick) {
47 | return (
48 |
52 | {children}
53 |
54 | )
55 | }
56 |
57 | return (
58 |
59 | {children}
60 |
61 | )
62 | }
63 |
64 | export interface CardHeaderProps {
65 | title?: string
66 | subtitle?: string
67 | action?: React.ReactNode
68 | className?: string
69 | children?: React.ReactNode
70 | }
71 |
72 | export const CardHeader: React.FC = ({ title, subtitle, action, className = '', children }) => {
73 | if (children) {
74 | return (
75 |
76 | {children}
77 |
78 | )
79 | }
80 |
81 | return (
82 |
83 |
84 | {title && (
85 |
86 | {title}
87 |
88 | )}
89 | {subtitle && (
90 |
91 | {subtitle}
92 |
93 | )}
94 |
95 | {action && (
96 |
97 | {action}
98 |
99 | )}
100 |
101 | )
102 | }
103 |
104 | export interface CardTitleProps {
105 | children: React.ReactNode
106 | className?: string
107 | }
108 |
109 | export const CardTitle: React.FC = ({ children, className = '' }) => {
110 | return (
111 |
112 | {children}
113 |
114 | )
115 | }
116 |
117 | export interface CardDescriptionProps {
118 | children: React.ReactNode
119 | className?: string
120 | }
121 |
122 | export const CardDescription: React.FC = ({ children, className = '' }) => {
123 | return (
124 |
125 | {children}
126 |
127 | )
128 | }
129 |
130 | export interface CardContentProps {
131 | children: React.ReactNode
132 | className?: string
133 | }
134 |
135 | export const CardContent: React.FC = ({ children, className = '' }) => {
136 | return (
137 |
138 | {children}
139 |
140 | )
141 | }
142 |
143 | export interface CardFooterProps {
144 | children: React.ReactNode
145 | className?: string
146 | justify?: 'start' | 'center' | 'end' | 'between'
147 | }
148 |
149 | export const CardFooter: React.FC = ({ children, className = '', justify = 'end' }) => {
150 | const justifyClasses = {
151 | start: 'justify-start',
152 | center: 'justify-center',
153 | end: 'justify-end',
154 | between: 'justify-between'
155 | }
156 |
157 | return (
158 |
159 | {children}
160 |
161 | )
162 | }
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/user-preferences.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Toggle } from './ui/toggle'
3 |
4 | interface UserPreferencesProps {
5 | preferences: {
6 | theme: 'light' | 'dark' | 'auto'
7 | language: string
8 | notifications: boolean
9 | autoSave: boolean
10 | fontSize: 'small' | 'medium' | 'large'
11 | }
12 | onSave: (preferences: any) => void
13 | }
14 |
15 | export const UserPreferences: React.FC = ({
16 | preferences,
17 | onSave
18 | }) => {
19 | const [localPreferences, setLocalPreferences] = useState(preferences)
20 |
21 | const preferenceOptions = [
22 | {
23 | id: 'theme',
24 | label: '主题',
25 | type: 'select' as const,
26 | options: [
27 | { label: '浅色', value: 'light' },
28 | { label: '深色', value: 'dark' },
29 | { label: '自动', value: 'auto' }
30 | ]
31 | },
32 | {
33 | id: 'fontSize',
34 | label: '字体大小',
35 | type: 'radio' as const,
36 | options: [
37 | { label: '小', value: 'small' },
38 | { label: '中', value: 'medium' },
39 | { label: '大', value: 'large' }
40 | ]
41 | },
42 | {
43 | id: 'notifications',
44 | label: '启用通知',
45 | type: 'toggle' as const
46 | },
47 | {
48 | id: 'autoSave',
49 | label: '自动保存草稿',
50 | type: 'toggle' as const
51 | }
52 | ]
53 |
54 | const handleChange = (id: string, value: any) => {
55 | setLocalPreferences({
56 | ...localPreferences,
57 | [id]: value
58 | })
59 | }
60 |
61 | const handleSave = () => {
62 | onSave(localPreferences)
63 | }
64 |
65 | return (
66 |
67 |
个性化设置
68 |
69 |
70 | {preferenceOptions.map(option => (
71 |
72 |
{option.label}
73 |
74 | {option.type === 'select' && (
75 |
86 | )}
87 |
88 | {option.type === 'radio' && (
89 |
90 | {option.options.map(radio => (
91 |
100 | ))}
101 |
102 | )}
103 |
104 | {option.type === 'toggle' && (
105 |
handleChange(option.id, checked)}
108 | />
109 | )}
110 |
111 | ))}
112 |
113 |
114 |
120 |
121 | )
122 | }
--------------------------------------------------------------------------------
/src/main/audioUtils.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import os from 'os'
4 |
5 | // Convert raw PCM to WAV format for easier playback and verification
6 | export function pcmToWav(pcmBuffer: Buffer, outputPath: string, sampleRate = 24000, channels = 1, bitDepth = 16): string {
7 | const byteRate = sampleRate * channels * (bitDepth / 8)
8 | const blockAlign = channels * (bitDepth / 8)
9 | const dataSize = pcmBuffer.length
10 |
11 | // Create WAV header
12 | const header = Buffer.alloc(44)
13 |
14 | // "RIFF" chunk descriptor
15 | header.write('RIFF', 0)
16 | header.writeUInt32LE(dataSize + 36, 4) // File size - 8
17 | header.write('WAVE', 8)
18 |
19 | // "fmt " sub-chunk
20 | header.write('fmt ', 12)
21 | header.writeUInt32LE(16, 16) // Subchunk1Size (16 for PCM)
22 | header.writeUInt16LE(1, 20) // AudioFormat (1 for PCM)
23 | header.writeUInt16LE(channels, 22) // NumChannels
24 | header.writeUInt32LE(sampleRate, 24) // SampleRate
25 | header.writeUInt32LE(byteRate, 28) // ByteRate
26 | header.writeUInt16LE(blockAlign, 32) // BlockAlign
27 | header.writeUInt16LE(bitDepth, 34) // BitsPerSample
28 |
29 | // "data" sub-chunk
30 | header.write('data', 36)
31 | header.writeUInt32LE(dataSize, 40) // Subchunk2Size
32 |
33 | // Combine header and PCM data
34 | const wavBuffer = Buffer.concat([header, pcmBuffer])
35 |
36 | // Write to file
37 | fs.writeFileSync(outputPath, wavBuffer)
38 |
39 | return outputPath
40 | }
41 |
42 | // Analyze audio buffer for debugging
43 | export function analyzeAudioBuffer(buffer: Buffer, label = 'Audio') {
44 | const int16Array = new Int16Array(buffer.buffer, buffer.byteOffset, buffer.length / 2)
45 |
46 | let minValue = 32767
47 | let maxValue = -32768
48 | let avgValue = 0
49 | let rmsValue = 0
50 | let silentSamples = 0
51 |
52 | for (let i = 0; i < int16Array.length; i++) {
53 | const sample = int16Array[i]
54 | minValue = Math.min(minValue, sample)
55 | maxValue = Math.max(maxValue, sample)
56 | avgValue += sample
57 | rmsValue += sample * sample
58 |
59 | if (Math.abs(sample) < 100) {
60 | silentSamples++
61 | }
62 | }
63 |
64 | avgValue /= int16Array.length
65 | rmsValue = Math.sqrt(rmsValue / int16Array.length)
66 |
67 | const silencePercentage = (silentSamples / int16Array.length) * 100
68 |
69 | // 临时恢复音频分析日志用于调试
70 | if (silencePercentage < 80) {
71 | console.log(`${label} Analysis: Samples: ${int16Array.length}, Silence: ${silencePercentage.toFixed(1)}%, RMS: ${rmsValue.toFixed(2)}`)
72 | }
73 |
74 | return {
75 | minValue,
76 | maxValue,
77 | avgValue,
78 | rmsValue,
79 | silencePercentage,
80 | sampleCount: int16Array.length,
81 | }
82 | }
83 |
84 | // Save audio buffer with metadata for debugging
85 | export function saveDebugAudio(buffer: Buffer, type: string, timestamp = Date.now()) {
86 | const homeDir = os.homedir()
87 | const debugDir = path.join(homeDir, 'bready', 'debug')
88 |
89 | if (!fs.existsSync(debugDir)) {
90 | fs.mkdirSync(debugDir, { recursive: true })
91 | }
92 |
93 | const pcmPath = path.join(debugDir, `${type}_${timestamp}.pcm`)
94 | const wavPath = path.join(debugDir, `${type}_${timestamp}.wav`)
95 | const metaPath = path.join(debugDir, `${type}_${timestamp}.json`)
96 |
97 | // Save raw PCM
98 | fs.writeFileSync(pcmPath, buffer)
99 |
100 | // Convert to WAV for easy playback
101 | pcmToWav(buffer, wavPath)
102 |
103 | // Analyze and save metadata
104 | const analysis = analyzeAudioBuffer(buffer, type)
105 | fs.writeFileSync(
106 | metaPath,
107 | JSON.stringify(
108 | {
109 | timestamp,
110 | type,
111 | bufferSize: buffer.length,
112 | analysis,
113 | format: {
114 | sampleRate: 24000,
115 | channels: 1,
116 | bitDepth: 16,
117 | },
118 | },
119 | null,
120 | 2
121 | )
122 | )
123 |
124 | // 完全禁用调试音频保存日志以减少刷屏
125 | // console.log(`🎵 调试音频已保存: ${wavPath}`)
126 |
127 | return { pcmPath, wavPath, metaPath }
128 | }
129 |
--------------------------------------------------------------------------------
/src/main/monitoring/MonitoringSystem.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events'
2 | import { performance } from 'perf_hooks'
3 | import { Logger } from '../utils/Logger'
4 |
5 | /**
6 | * 统一监控系统
7 | * 集成性能监控、错误追踪、用户行为分析
8 | */
9 | export class MonitoringSystem extends EventEmitter {
10 | private readonly logger: Logger
11 | private isRunning = false
12 | private metrics: Map = new Map()
13 | private errors: any[] = []
14 | private userActions: any[] = []
15 |
16 | constructor() {
17 | super()
18 | this.logger = Logger.getInstance()
19 | this.setupGlobalHandlers()
20 | }
21 |
22 | /**
23 | * 启动监控系统
24 | */
25 | async start(): Promise {
26 | if (this.isRunning) return
27 |
28 | this.isRunning = true
29 | this.logger.info('🚀 监控系统已启动')
30 |
31 | // 记录启动时间
32 | this.recordMetric('app.startup.time', performance.now())
33 |
34 | // 开始定期收集系统指标
35 | this.startSystemMonitoring()
36 |
37 | this.emit('started')
38 | }
39 |
40 | /**
41 | * 停止监控系统
42 | */
43 | stop(): void {
44 | if (!this.isRunning) return
45 |
46 | this.isRunning = false
47 | this.logger.info('⏹️ 监控系统已停止')
48 | this.emit('stopped')
49 | }
50 |
51 | /**
52 | * 记录性能指标
53 | */
54 | recordMetric(name: string, value: number, unit: string = ''): void {
55 | const metric = {
56 | name,
57 | value,
58 | unit,
59 | timestamp: Date.now()
60 | }
61 |
62 | this.metrics.set(name, metric)
63 | this.emit('metric', metric)
64 |
65 | this.logger.debug(`📊 性能指标: ${name} = ${value}${unit}`)
66 | }
67 |
68 | /**
69 | * 记录错误
70 | */
71 | captureError(error: Error | string, context: any = {}): string {
72 | const errorId = `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
73 |
74 | const errorInfo = {
75 | id: errorId,
76 | message: typeof error === 'string' ? error : error.message,
77 | stack: typeof error === 'object' ? error.stack : undefined,
78 | context,
79 | timestamp: Date.now()
80 | }
81 |
82 | this.errors.push(errorInfo)
83 | this.emit('error', errorInfo)
84 |
85 | this.logger.error(`🚨 错误记录 [${errorId}]: ${errorInfo.message}`)
86 |
87 | return errorId
88 | }
89 |
90 | /**
91 | * 记录用户行为
92 | */
93 | trackUserAction(action: string, data: any = {}): void {
94 | const actionInfo = {
95 | action,
96 | data,
97 | timestamp: Date.now()
98 | }
99 |
100 | this.userActions.push(actionInfo)
101 | this.emit('userAction', actionInfo)
102 |
103 | this.logger.debug(`👤 用户行为: ${action}`)
104 | }
105 |
106 | /**
107 | * 获取监控摘要
108 | */
109 | getSummary(): {
110 | isRunning: boolean
111 | metrics: any
112 | errors: any
113 | userActions: any
114 | } {
115 | return {
116 | isRunning: this.isRunning,
117 | metrics: {
118 | total: this.metrics.size,
119 | latest: Array.from(this.metrics.values()).slice(-10)
120 | },
121 | errors: {
122 | total: this.errors.length,
123 | recent: this.errors.slice(-5)
124 | },
125 | userActions: {
126 | total: this.userActions.length,
127 | recent: this.userActions.slice(-10)
128 | }
129 | }
130 | }
131 |
132 | /**
133 | * 设置全局错误处理器
134 | */
135 | private setupGlobalHandlers(): void {
136 | // 捕获未处理的异常
137 | process.on('uncaughtException', (error) => {
138 | this.captureError(error, { source: 'uncaughtException' })
139 | })
140 |
141 | // 捕获未处理的Promise拒绝
142 | process.on('unhandledRejection', (reason) => {
143 | const error = reason instanceof Error ? reason : new Error(String(reason))
144 | this.captureError(error, { source: 'unhandledRejection' })
145 | })
146 | }
147 |
148 | /**
149 | * 开始系统监控
150 | */
151 | private startSystemMonitoring(): void {
152 | const collectMetrics = () => {
153 | if (!this.isRunning) return
154 |
155 | try {
156 | // 内存使用情况
157 | const memUsage = process.memoryUsage()
158 | this.recordMetric('process.memory.heapUsed', memUsage.heapUsed, 'bytes')
159 | this.recordMetric('process.memory.heapTotal', memUsage.heapTotal, 'bytes')
160 | this.recordMetric('process.memory.rss', memUsage.rss, 'bytes')
161 |
162 | // CPU使用情况(简化)
163 | const cpuUsage = process.cpuUsage()
164 | this.recordMetric('process.cpu.user', cpuUsage.user, 'microseconds')
165 | this.recordMetric('process.cpu.system', cpuUsage.system, 'microseconds')
166 |
167 | } catch (error) {
168 | this.logger.error('收集系统指标失败:', error)
169 | }
170 |
171 | // 每5秒收集一次
172 | setTimeout(collectMetrics, 5000)
173 | }
174 |
175 | collectMetrics()
176 | }
177 | }
178 |
179 | // 导出单例实例
180 | export const monitoringSystem = new MonitoringSystem()
--------------------------------------------------------------------------------
/src/renderer/src/components/AllPreparationsModal.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useNavigate } from 'react-router-dom'
3 | import { X, Calendar, Trash2, Edit } from 'lucide-react'
4 | import { Button } from './ui/button'
5 | import { Card, CardDescription, CardHeader, CardTitle } from './ui/card'
6 | import { type Preparation } from '../lib/supabase'
7 |
8 | interface AllPreparationsModalProps {
9 | preparations: Preparation[]
10 | onClose: () => void
11 | onDelete: (id: string) => void
12 | }
13 |
14 | const AllPreparationsModal: React.FC = ({
15 | preparations,
16 | onClose,
17 | onDelete
18 | }) => {
19 | const navigate = useNavigate()
20 |
21 | const handleViewPreparation = (id: string) => {
22 | onClose()
23 | navigate(`/preparation/${id}`)
24 | }
25 |
26 | const handleEditPreparation = (id: string) => {
27 | onClose()
28 | navigate(`/edit-preparation/${id}`)
29 | }
30 |
31 | const handleDeletePreparation = (id: string) => {
32 | if (confirm('确定要删除这个准备项吗?此操作无法撤销。')) {
33 | onDelete(id)
34 | }
35 | }
36 |
37 | const formatDate = (dateString: string) => {
38 | const date = new Date(dateString)
39 | return date.toLocaleDateString('zh-CN', {
40 | year: 'numeric',
41 | month: 'short',
42 | day: 'numeric'
43 | })
44 | }
45 |
46 | return (
47 |
48 |
49 | {/* 头部 */}
50 |
51 |
52 |
我的准备项
53 |
共 {preparations.length} 个准备项
54 |
55 |
63 |
64 |
65 | {/* 内容区域 */}
66 |
67 |
68 | {preparations.map((preparation) => (
69 |
handleViewPreparation(preparation.id)}
73 | >
74 |
75 |
76 |
77 |
78 |
79 | {preparation.name}
80 |
81 | {preparation.is_analyzing && (
82 |
86 | )}
87 |
88 |
89 | {preparation.job_description}
90 |
91 |
92 |
93 | {formatDate(preparation.updated_at)}
94 |
95 |
96 |
97 |
98 | {/* 操作按钮 */}
99 |
100 |
112 |
113 |
114 |
115 | ))}
116 |
117 |
118 |
119 |
120 | )
121 | }
122 |
123 | export default AllPreparationsModal
124 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, ErrorInfo, ReactNode } from 'react'
2 |
3 | interface Props {
4 | children: ReactNode
5 | }
6 |
7 | interface State {
8 | hasError: boolean
9 | error?: Error
10 | errorInfo?: ErrorInfo
11 | }
12 |
13 | class ErrorBoundary extends Component {
14 | constructor(props: Props) {
15 | super(props)
16 | this.state = { hasError: false }
17 | }
18 |
19 | static getDerivedStateFromError(error: Error): State {
20 | return { hasError: true, error }
21 | }
22 |
23 | componentDidCatch(error: Error, errorInfo: ErrorInfo) {
24 | console.error('ErrorBoundary caught an error:', error, errorInfo)
25 | this.setState({ error, errorInfo })
26 | }
27 |
28 | render() {
29 | if (this.state.hasError) {
30 | return (
31 |
32 |
33 |
34 |
35 |
36 |
41 |
出现了一些问题
42 |
43 | 应用遇到了意外错误,请尝试刷新页面或重启应用。
44 |
45 |
46 |
47 |
48 |
54 |
55 |
61 |
62 |
63 | {process.env.NODE_ENV === 'development' && this.state.error && (
64 |
65 |
66 | 查看错误详情
67 |
83 |
84 |
85 |
86 | 错误: {this.state.error.message}
87 |
88 |
89 |
堆栈:
90 |
{this.state.error.stack}
91 |
92 | {this.state.errorInfo && (
93 |
94 |
组件堆栈:
95 |
{this.state.errorInfo.componentStack}
96 |
97 | )}
98 |
99 |
100 | )}
101 |
102 |
103 |
104 |
105 | )
106 | }
107 |
108 | return this.props.children
109 | }
110 | }
111 |
112 | export default ErrorBoundary
113 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | // 更新 Button 组件以支持暗黑模式
4 | export interface ButtonProps extends React.ButtonHTMLAttributes {
5 | variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'
6 | size?: 'sm' | 'md' | 'lg'
7 | loading?: boolean
8 | icon?: React.ReactNode
9 | iconPosition?: 'left' | 'right'
10 | fullWidth?: boolean
11 | }
12 |
13 | export const Button: React.FC = ({
14 | children,
15 | variant = 'primary',
16 | size = 'md',
17 | loading = false,
18 | icon,
19 | iconPosition = 'left',
20 | fullWidth = false,
21 | disabled,
22 | className = '',
23 | ...props
24 | }) => {
25 | const baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed'
26 |
27 | const variantClasses = {
28 | primary: 'bg-black hover:bg-gray-800 text-white focus:ring-black shadow-sm hover:shadow-md dark:bg-white dark:hover:bg-gray-200 dark:text-black dark:focus:ring-white',
29 | secondary: 'bg-gray-600 hover:bg-gray-700 text-white focus:ring-gray-500 shadow-sm hover:shadow-md dark:bg-gray-700 dark:hover:bg-gray-600',
30 | outline: 'border-2 border-black text-black hover:bg-black hover:text-white focus:ring-black dark:border-white dark:text-white dark:hover:bg-white dark:hover:text-black dark:focus:ring-white',
31 | ghost: 'text-gray-700 hover:bg-gray-100 focus:ring-gray-500 dark:text-gray-300 dark:hover:bg-gray-800',
32 | danger: 'bg-red-600 hover:bg-red-700 text-white focus:ring-red-500 shadow-sm hover:shadow-md'
33 | }
34 |
35 | const sizeClasses = {
36 | sm: 'px-3 py-1.5 text-sm',
37 | md: 'px-4 py-2 text-sm',
38 | lg: 'px-6 py-3 text-base'
39 | }
40 |
41 | const widthClass = fullWidth ? 'w-full' : ''
42 |
43 | const buttonClasses = `
44 | ${baseClasses}
45 | ${variantClasses[variant]}
46 | ${sizeClasses[size]}
47 | ${widthClass}
48 | ${className}
49 | `.trim()
50 |
51 | const renderIcon = () => {
52 | if (loading) {
53 | return (
54 |
58 | )
59 | }
60 | return icon
61 | }
62 |
63 | const renderContent = () => {
64 | const iconElement = renderIcon()
65 |
66 | if (!iconElement) {
67 | return children
68 | }
69 |
70 | if (iconPosition === 'left') {
71 | return (
72 | <>
73 | {iconElement}
74 | {children}
75 | >
76 | )
77 | } else {
78 | return (
79 | <>
80 | {children}
81 | {iconElement}
82 | >
83 | )
84 | }
85 | }
86 |
87 | return (
88 |
95 | )
96 | }
97 |
98 | // 按钮组组件
99 | export const ButtonGroup: React.FC<{
100 | children: React.ReactNode
101 | className?: string
102 | orientation?: 'horizontal' | 'vertical'
103 | }> = ({ children, className = '', orientation = 'horizontal' }) => {
104 | const orientationClasses = {
105 | horizontal: 'flex flex-row space-x-2',
106 | vertical: 'flex flex-col space-y-2'
107 | }
108 |
109 | return (
110 |
111 | {children}
112 |
113 | )
114 | }
115 |
116 | // 图标按钮组件
117 | export const IconButton: React.FC<{
118 | icon: React.ReactNode
119 | onClick?: () => void
120 | size?: 'sm' | 'md' | 'lg'
121 | variant?: 'primary' | 'secondary' | 'ghost'
122 | className?: string
123 | disabled?: boolean
124 | tooltip?: string
125 | }> = ({
126 | icon,
127 | onClick,
128 | size = 'md',
129 | variant = 'ghost',
130 | className = '',
131 | disabled = false,
132 | tooltip
133 | }) => {
134 | const sizeClasses = {
135 | sm: 'w-8 h-8 text-sm',
136 | md: 'w-10 h-10 text-base',
137 | lg: 'w-12 h-12 text-lg'
138 | }
139 |
140 | const variantClasses = {
141 | primary: 'bg-black hover:bg-gray-800 text-white dark:bg-white dark:hover:bg-gray-200 dark:text-black',
142 | secondary: 'bg-gray-600 hover:bg-gray-700 text-white dark:bg-gray-700 dark:hover:bg-gray-600',
143 | ghost: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800'
144 | }
145 |
146 | return (
147 |
162 | )
163 | }
--------------------------------------------------------------------------------
/src/main/audio/electron-native-capture.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Electron 音频捕获协调器
3 | * 主进程端负责权限检查和状态管理,实际音频捕获在渲染进程中进行
4 | */
5 |
6 | import { systemPreferences } from 'electron'
7 | import { EventEmitter } from 'events'
8 |
9 | const debugAudio = process.env.DEBUG_AUDIO === '1'
10 |
11 | export interface AudioCaptureOptions {
12 | sampleRate: number
13 | channels: number
14 | bitDepth: number
15 | mode: 'system' | 'microphone'
16 | }
17 |
18 | export class ElectronNativeAudioCapture extends EventEmitter {
19 | private isCapturing = false
20 | private options: AudioCaptureOptions
21 | private mainWindow: Electron.BrowserWindow | null = null
22 |
23 | constructor(options: Partial = {}) {
24 | super()
25 | this.options = {
26 | sampleRate: 24000,
27 | channels: 1,
28 | bitDepth: 16,
29 | mode: 'system',
30 | ...options
31 | }
32 | }
33 |
34 | /**
35 | * 设置主窗口引用,用于与渲染进程通信
36 | */
37 | setMainWindow(window: Electron.BrowserWindow) {
38 | this.mainWindow = window
39 | }
40 |
41 | /**
42 | * 启动音频捕获 - 协调渲染进程中的实际捕获
43 | */
44 | async startCapture(): Promise {
45 | if (this.isCapturing) {
46 | if (debugAudio) {
47 | console.log('🎵 音频捕获已在运行')
48 | }
49 | return true
50 | }
51 |
52 | try {
53 | if (debugAudio) {
54 | console.log('🚀 启动音频捕获协调器...')
55 | }
56 |
57 | // 检查权限
58 | const hasPermission = await this.checkPermissions()
59 | if (!hasPermission) {
60 | throw new Error('缺少必要的音频捕获权限')
61 | }
62 |
63 | // 通知渲染进程开始音频捕获
64 | if (this.mainWindow && !this.mainWindow.isDestroyed()) {
65 | const eventData = {
66 | mode: this.options.mode,
67 | options: this.options
68 | }
69 | if (debugAudio) {
70 | console.log('📡 主进程发送音频捕获启动事件到渲染进程:', eventData)
71 | }
72 | this.mainWindow.webContents.send('audio-capture-start', eventData)
73 | if (debugAudio) {
74 | console.log('📡 音频捕获启动事件已发送')
75 | }
76 | } else {
77 | console.error('❌ 主窗口不可用,无法发送音频捕获事件')
78 | throw new Error('主窗口不可用')
79 | }
80 |
81 | this.isCapturing = true
82 | this.emit('started')
83 |
84 | return true
85 |
86 | } catch (error) {
87 | console.error('❌ 音频捕获启动失败:', error)
88 | this.emit('error', error)
89 | return false
90 | }
91 | }
92 |
93 | /**
94 | * 检查所需权限
95 | */
96 | private async checkPermissions(): Promise {
97 | try {
98 | if (this.options.mode === 'system') {
99 | // 检查屏幕录制权限
100 | const screenPermission = systemPreferences.getMediaAccessStatus('screen')
101 | if (screenPermission !== 'granted') {
102 | if (debugAudio) {
103 | console.log('🔐 需要屏幕录制权限用于系统音频捕获')
104 | }
105 | // 在实际应用中,这里会触发权限请求
106 | return false
107 | }
108 | } else {
109 | // 检查麦克风权限
110 | const micPermission = systemPreferences.getMediaAccessStatus('microphone')
111 | if (micPermission !== 'granted') {
112 | if (debugAudio) {
113 | console.log('🔐 请求麦克风权限...')
114 | }
115 | const granted = await systemPreferences.askForMediaAccess('microphone')
116 | return granted
117 | }
118 | }
119 | return true
120 | } catch (error) {
121 | console.error('权限检查失败:', error)
122 | return false
123 | }
124 | }
125 |
126 | /**
127 | * 处理来自渲染进程的音频数据
128 | */
129 | onAudioData(data: Buffer) {
130 | if (this.isCapturing) {
131 | this.emit('audioData', data)
132 | }
133 | }
134 |
135 | /**
136 | * 停止音频捕获
137 | */
138 | stopCapture(): void {
139 | if (debugAudio) {
140 | console.log('⏹️ 停止音频捕获...')
141 | }
142 |
143 | if (this.mainWindow && !this.mainWindow.isDestroyed()) {
144 | if (debugAudio) {
145 | console.log('📡 主进程发送音频捕获停止事件到渲染进程')
146 | }
147 | this.mainWindow.webContents.send('audio-capture-stop')
148 | if (debugAudio) {
149 | console.log('📡 音频捕获停止事件已发送')
150 | }
151 | } else {
152 | if (debugAudio) {
153 | console.warn('⚠️ 主窗口不可用,无法发送音频捕获停止事件')
154 | }
155 | }
156 |
157 | this.isCapturing = false
158 | this.emit('stopped')
159 | if (debugAudio) {
160 | console.log('✅ 音频捕获已停止')
161 | }
162 | }
163 |
164 | /**
165 | * 切换音频模式
166 | */
167 | async switchMode(mode: 'system' | 'microphone'): Promise {
168 | if (this.options.mode === mode) {
169 | return true
170 | }
171 |
172 | if (debugAudio) {
173 | console.log(`🔄 切换音频模式: ${this.options.mode} → ${mode}`)
174 | }
175 |
176 | const wasCapturing = this.isCapturing
177 | if (wasCapturing) {
178 | this.stopCapture()
179 | // 等待一小段时间确保停止完成
180 | await new Promise(resolve => setTimeout(resolve, 100))
181 | }
182 |
183 | this.options.mode = mode
184 |
185 | if (wasCapturing) {
186 | return await this.startCapture()
187 | }
188 |
189 | return true
190 | }
191 |
192 | /**
193 | * 获取当前状态
194 | */
195 | getStatus(): { capturing: boolean; mode: string; options: AudioCaptureOptions } {
196 | return {
197 | capturing: this.isCapturing,
198 | mode: this.options.mode,
199 | options: { ...this.options }
200 | }
201 | }
202 | }
203 |
204 | // 创建全局单例
205 | export const electronAudioCapture = new ElectronNativeAudioCapture()
206 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/loading-states.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { motion } from 'framer-motion'
3 | import { AlertTriangle, FolderOpen, X } from 'lucide-react'
4 |
5 | interface SkeletonLoaderProps {
6 | type: 'card' | 'list' | 'detail'
7 | className?: string
8 | }
9 |
10 | export const SkeletonLoader: React.FC = ({ type, className = '' }) => {
11 | return (
12 |
13 | {type === 'card' && (
14 |
19 | )}
20 |
21 | {type === 'list' && (
22 |
23 | {[1, 2, 3].map((i) => (
24 |
28 | ))}
29 |
30 | )}
31 |
32 | {type === 'detail' && (
33 |
39 | )}
40 |
41 | )
42 | }
43 |
44 | interface ProgressBarProps {
45 | progress: number
46 | className?: string
47 | }
48 |
49 | export const ProgressBar: React.FC = ({ progress, className = '' }) => {
50 | return (
51 |
52 |
58 |
59 | )
60 | }
61 |
62 | interface FriendlyErrorPageProps {
63 | error: Error
64 | onRetry?: () => void
65 | onGoBack?: () => void
66 | }
67 |
68 | export const FriendlyErrorPage: React.FC = ({
69 | error,
70 | onRetry,
71 | onGoBack
72 | }) => {
73 | return (
74 |
75 |
76 |
77 |
78 |
81 |
82 |
83 | 出现了一些问题
84 |
85 |
86 |
87 | {error.message || '应用遇到了意外错误,请稍后重试'}
88 |
89 |
90 |
91 |
97 |
98 | {onGoBack && (
99 |
105 | )}
106 |
107 |
108 |
109 |
110 |
111 | )
112 | }
113 |
114 | interface EmptyStateProps {
115 | title: string
116 | description: string
117 | action?: {
118 | text: string
119 | onClick: () => void
120 | }
121 | icon?: React.ReactNode
122 | }
123 |
124 | export const EmptyState: React.FC = ({
125 | title,
126 | description,
127 | action,
128 | icon =
129 | }) => {
130 | return (
131 |
132 |
133 | {icon}
134 |
135 |
136 |
137 | {title}
138 |
139 |
140 |
141 | {description}
142 |
143 |
144 | {action && (
145 |
151 | )}
152 |
153 | )
154 | }
--------------------------------------------------------------------------------
/database/init.sql:
--------------------------------------------------------------------------------
1 | -- 面宝 (Bready) 数据库初始化脚本
2 | -- 创建所有必要的表和索引
3 |
4 | -- 创建数据库(如果不存在)
5 | -- CREATE DATABASE bready;
6 |
7 | -- 连接到数据库
8 | -- \c bready;
9 |
10 | -- 启用 UUID 扩展
11 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
12 |
13 | -- 用户配置表
14 | CREATE TABLE IF NOT EXISTS user_profiles (
15 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
16 | username TEXT UNIQUE,
17 | email TEXT UNIQUE NOT NULL,
18 | password_hash TEXT NOT NULL,
19 | full_name TEXT,
20 | avatar_url TEXT,
21 | role TEXT DEFAULT 'user',
22 | user_level TEXT DEFAULT '小白',
23 | membership_expires_at TIMESTAMP WITH TIME ZONE,
24 | remaining_interview_minutes INTEGER DEFAULT 0,
25 | total_purchased_minutes INTEGER DEFAULT 0,
26 | discount_rate NUMERIC DEFAULT 1.00,
27 | created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
28 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
29 | );
30 |
31 | -- 面试准备表
32 | CREATE TABLE IF NOT EXISTS preparations (
33 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
34 | user_id UUID REFERENCES user_profiles(id) ON DELETE CASCADE,
35 | name TEXT NOT NULL,
36 | job_description TEXT NOT NULL,
37 | resume TEXT,
38 | analysis JSONB,
39 | is_analyzing BOOLEAN DEFAULT FALSE,
40 | created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
41 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
42 | );
43 |
44 | -- 会员套餐表
45 | CREATE TABLE IF NOT EXISTS membership_packages (
46 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
47 | name TEXT NOT NULL,
48 | level TEXT NOT NULL,
49 | price NUMERIC NOT NULL,
50 | interview_minutes INTEGER NOT NULL,
51 | validity_days INTEGER DEFAULT 30,
52 | description TEXT,
53 | is_active BOOLEAN DEFAULT TRUE,
54 | created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
55 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
56 | );
57 |
58 | -- 购买记录表
59 | CREATE TABLE IF NOT EXISTS purchase_records (
60 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
61 | user_id UUID NOT NULL REFERENCES user_profiles(id) ON DELETE CASCADE,
62 | package_id UUID NOT NULL REFERENCES membership_packages(id),
63 | original_price NUMERIC NOT NULL,
64 | actual_price NUMERIC NOT NULL,
65 | discount_rate NUMERIC DEFAULT 1.00,
66 | interview_minutes INTEGER NOT NULL,
67 | expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
68 | status TEXT DEFAULT 'completed',
69 | payment_method TEXT,
70 | created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
71 | );
72 |
73 | -- 面试使用记录表
74 | CREATE TABLE IF NOT EXISTS interview_usage_records (
75 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
76 | user_id UUID NOT NULL REFERENCES user_profiles(id) ON DELETE CASCADE,
77 | preparation_id UUID REFERENCES preparations(id) ON DELETE SET NULL,
78 | session_type TEXT NOT NULL,
79 | minutes_used INTEGER NOT NULL,
80 | started_at TIMESTAMP WITH TIME ZONE NOT NULL,
81 | ended_at TIMESTAMP WITH TIME ZONE,
82 | created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
83 | );
84 |
85 | -- 用户会话表(用于身份验证)
86 | CREATE TABLE IF NOT EXISTS user_sessions (
87 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
88 | user_id UUID NOT NULL REFERENCES user_profiles(id) ON DELETE CASCADE,
89 | token TEXT NOT NULL UNIQUE,
90 | expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
91 | created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
92 | );
93 |
94 | -- 创建索引
95 | CREATE INDEX IF NOT EXISTS idx_user_profiles_email ON user_profiles(email);
96 | CREATE INDEX IF NOT EXISTS idx_user_profiles_username ON user_profiles(username);
97 | CREATE INDEX IF NOT EXISTS idx_preparations_user_id ON preparations(user_id);
98 | CREATE INDEX IF NOT EXISTS idx_preparations_updated_at ON preparations(updated_at);
99 | CREATE INDEX IF NOT EXISTS idx_purchase_records_user_id ON purchase_records(user_id);
100 | CREATE INDEX IF NOT EXISTS idx_purchase_records_expires_at ON purchase_records(expires_at);
101 | CREATE INDEX IF NOT EXISTS idx_interview_usage_records_user_id ON interview_usage_records(user_id);
102 | CREATE INDEX IF NOT EXISTS idx_interview_usage_records_started_at ON interview_usage_records(started_at);
103 | CREATE INDEX IF NOT EXISTS idx_user_sessions_token ON user_sessions(token);
104 | CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id);
105 | CREATE INDEX IF NOT EXISTS idx_user_sessions_expires_at ON user_sessions(expires_at);
106 |
107 | -- 插入默认会员套餐数据
108 | INSERT INTO membership_packages (name, level, price, interview_minutes, validity_days, description) VALUES
109 | ('基础套餐', '小白', 29.9, 60, 30, '适合初学者的基础面试练习套餐'),
110 | ('进阶套餐', '螺丝钉', 59.9, 150, 30, '适合有一定经验的求职者'),
111 | ('专业套餐', '大牛', 99.9, 300, 30, '适合资深求职者的专业套餐'),
112 | ('企业套餐', '管理', 199.9, 600, 30, '适合管理层的企业级套餐')
113 | ON CONFLICT DO NOTHING;
114 |
115 | -- 创建默认管理员用户(密码:admin123)
116 | INSERT INTO user_profiles (email, password_hash, full_name, role, user_level, remaining_interview_minutes) VALUES
117 | ('admin@bready.app', '$2b$10$DbKk7nqADXknfTEv/JfOFOwUD.TRDAdFJpFTUJPUozEuQ2O5lrLpu', '系统管理员', 'admin', '超级', 9999)
118 | ON CONFLICT (email) DO NOTHING;
119 |
120 | -- 更新时间戳触发器函数
121 | CREATE OR REPLACE FUNCTION update_updated_at_column()
122 | RETURNS TRIGGER AS $$
123 | BEGIN
124 | NEW.updated_at = NOW();
125 | RETURN NEW;
126 | END;
127 | $$ language 'plpgsql';
128 |
129 | -- 为需要的表添加更新时间戳触发器
130 | CREATE TRIGGER update_user_profiles_updated_at BEFORE UPDATE ON user_profiles FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
131 | CREATE TRIGGER update_preparations_updated_at BEFORE UPDATE ON preparations FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
132 | CREATE TRIGGER update_membership_packages_updated_at BEFORE UPDATE ON membership_packages FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
133 |
--------------------------------------------------------------------------------
/src/renderer/src/components/WelcomePage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { useNavigate } from 'react-router-dom'
3 | import { Sparkles, ArrowRight, Zap, Shield, Mic, Moon, Sun } from 'lucide-react'
4 | import { Button } from './ui/button'
5 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card'
6 | import { useTheme } from './ui/theme-provider'
7 |
8 | interface WelcomePageProps {
9 | onComplete: () => void
10 | }
11 |
12 | const WelcomePage: React.FC = ({ onComplete }) => {
13 | const navigate = useNavigate()
14 | const { theme, setTheme } = useTheme()
15 | const [mounted, setMounted] = useState(false)
16 |
17 | React.useEffect(() => {
18 | setMounted(true)
19 | }, [])
20 |
21 | const toggleTheme = () => {
22 | setTheme(theme === 'light' ? 'dark' : 'light')
23 | }
24 |
25 | const handleGetStarted = () => {
26 | navigate('/create-preparation')
27 | }
28 |
29 | return (
30 |
31 | {/* 拖拽区域和主题切换 */}
32 |
33 |
{/* 占位符 */}
34 | {mounted && (
35 |
46 | )}
47 |
48 |
49 |
50 |
51 |
52 | {/* Logo 和标题 */}
53 |
54 |
55 |
56 |
57 |
58 | 面宝
59 |
60 |
61 | 面试紧张?放轻松
62 |
63 |
64 |
65 | {/* 产品介绍 */}
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
智能协作
74 |
78 |
79 |
80 |
81 |
82 |
83 |
音频技术
84 |
85 | - 低延迟响应
86 | - 无损音频处理
87 |
88 |
89 |
90 |
91 |
92 |
93 |
隐私安全
94 |
95 | - 反检测
96 | - 数据存于本地
97 |
98 |
99 |
100 |
101 |
102 |
103 | {/* 操作按钮 */}
104 |
105 |
113 |
114 |
115 | {/* 跳过按钮 */}
116 |
117 |
124 |
125 |
126 |
127 |
128 |
129 | )
130 | }
131 |
132 | export default WelcomePage
--------------------------------------------------------------------------------
/src/renderer/src/components/MonitoringPanel.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { Card } from './ui/Card'
3 | import { Button } from './ui/Button'
4 |
5 | interface MonitoringData {
6 | isRunning: boolean
7 | metrics: {
8 | total: number
9 | latest: Array<{
10 | name: string
11 | value: number
12 | unit: string
13 | timestamp: number
14 | }>
15 | }
16 | errors: {
17 | total: number
18 | recent: Array<{
19 | id: string
20 | message: string
21 | timestamp: number
22 | }>
23 | }
24 | userActions: {
25 | total: number
26 | recent: Array<{
27 | action: string
28 | timestamp: number
29 | }>
30 | }
31 | }
32 |
33 | /**
34 | * 监控面板组件
35 | * 显示应用性能和错误统计信息
36 | */
37 | export const MonitoringPanel: React.FC = () => {
38 | const [data, setData] = useState(null)
39 | const [isVisible, setIsVisible] = useState(false)
40 |
41 | useEffect(() => {
42 | // 监听快捷键 Ctrl+Shift+M 打开监控面板
43 | const handleKeyDown = (event: KeyboardEvent) => {
44 | if (event.ctrlKey && event.shiftKey && event.key === 'M') {
45 | setIsVisible(!isVisible)
46 | }
47 | }
48 |
49 | window.addEventListener('keydown', handleKeyDown)
50 | return () => window.removeEventListener('keydown', handleKeyDown)
51 | }, [isVisible])
52 |
53 | useEffect(() => {
54 | if (!isVisible) return
55 |
56 | const loadData = async () => {
57 | try {
58 | const monitoringData = await window.electronAPI?.getMonitoringData()
59 | setData(monitoringData)
60 | } catch (error) {
61 | console.error('加载监控数据失败:', error)
62 | }
63 | }
64 |
65 | loadData()
66 | const interval = setInterval(loadData, 2000)
67 | return () => clearInterval(interval)
68 | }, [isVisible])
69 |
70 | if (!isVisible) return null
71 |
72 | return (
73 |
74 |
75 |
76 |
系统监控面板
77 |
83 |
84 |
85 | {!data ? (
86 |
87 |
88 |
加载监控数据中...
89 |
90 | ) : (
91 |
92 | {/* 系统状态 */}
93 |
94 | 系统状态
95 |
96 |
97 |
98 | 监控系统: {data.isRunning ? '运行中' : '已停止'}
99 |
100 |
101 |
102 |
103 | {/* 性能指标 */}
104 |
105 | 性能指标 (总计: {data.metrics.total})
106 |
107 | {data.metrics.latest.map((metric, index) => (
108 |
109 | {metric.name}
110 |
111 | {typeof metric.value === 'number' ? metric.value.toFixed(2) : metric.value}
112 | {metric.unit && ` ${metric.unit}`}
113 |
114 |
115 | ))}
116 |
117 |
118 |
119 | {/* 错误统计 */}
120 |
121 | 错误统计 (总计: {data.errors.total})
122 | {data.errors.recent.length === 0 ? (
123 | 暂无错误记录
124 | ) : (
125 |
126 | {data.errors.recent.map((error, index) => (
127 |
128 |
129 | {error.message}
130 |
131 |
132 | {new Date(error.timestamp).toLocaleString()}
133 |
134 |
135 | ))}
136 |
137 | )}
138 |
139 |
140 | {/* 用户行为 */}
141 |
142 | 用户行为 (总计: {data.userActions.total})
143 | {data.userActions.recent.length === 0 ? (
144 | 暂无行为记录
145 | ) : (
146 |
147 | {data.userActions.recent.map((action, index) => (
148 |
149 | {action.action}
150 |
151 | {new Date(action.timestamp).toLocaleString()}
152 |
153 |
154 | ))}
155 |
156 | )}
157 |
158 |
159 | )}
160 |
161 |
162 |
163 | 按 Ctrl+Shift+M 可随时打开/关闭监控面板
164 |
165 |
166 |
167 |
168 | )
169 | }
--------------------------------------------------------------------------------
/src/renderer/src/components/AnalysisReportModal.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { X, Target, TrendingUp, AlertTriangle, Lightbulb, Star } from 'lucide-react'
3 | import { Button } from './ui/button'
4 | import { Card, CardContent, CardHeader, CardTitle } from './ui/card'
5 |
6 | interface Analysis {
7 | matchScore: number
8 | strengths: string[]
9 | weaknesses: string[]
10 | suggestions: string[]
11 | systemPrompt: string
12 | }
13 |
14 | interface AnalysisReportModalProps {
15 | analysis: Analysis
16 | onClose: () => void
17 | }
18 |
19 | const AnalysisReportModal: React.FC = ({ analysis, onClose }) => {
20 | const getScoreColor = (score: number) => {
21 | if (score >= 80) return 'text-green-600'
22 | if (score >= 60) return 'text-yellow-600'
23 | return 'text-red-600'
24 | }
25 |
26 | const getScoreBgColor = (score: number) => {
27 | if (score >= 80) return 'bg-green-50 border-green-200'
28 | if (score >= 60) return 'bg-yellow-50 border-yellow-200'
29 | return 'bg-red-50 border-red-200'
30 | }
31 |
32 | return (
33 |
34 |
35 | {/* 头部 */}
36 |
37 |
AI 分析报告
38 |
45 |
46 |
47 | {/* 内容区域 */}
48 |
49 |
50 | {/* 匹配度评分 */}
51 |
52 |
53 |
54 |
55 |
匹配度评分
56 |
57 |
58 | {analysis.matchScore}
59 |
60 | 满分 100 分
61 |
62 |
63 |
64 |
65 | {/* 优势分析 */}
66 |
67 |
68 |
69 |
70 | 优势分析
71 |
72 |
73 |
74 |
75 | {(analysis.strengths || []).map((strength, index) => (
76 | -
77 |
78 | {strength}
79 |
80 | ))}
81 |
82 |
83 |
84 |
85 | {/* 改进建议 */}
86 |
87 |
88 |
92 |
93 |
94 |
95 | {(analysis.weaknesses || []).map((weakness, index) => (
96 | -
97 |
98 | {weakness}
99 |
100 | ))}
101 |
102 |
103 |
104 |
105 |
106 | {/* 面试建议 */}
107 |
108 |
109 |
110 |
111 | 面试建议
112 |
113 |
114 |
115 |
116 | {(analysis.suggestions || []).map((suggestion, index) => (
117 | -
118 |
119 | {index + 1}
120 |
121 | {suggestion}
122 |
123 | ))}
124 |
125 |
126 |
127 |
128 |
129 |
130 | {/* 底部操作 */}
131 |
132 |
138 |
139 |
140 |
141 | )
142 | }
143 |
144 | export default AnalysisReportModal
145 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/search-filter.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { motion } from 'framer-motion'
3 | import { Search, X, Filter } from 'lucide-react'
4 |
5 | interface SmartSearchProps {
6 | onSearch: (query: string) => void
7 | placeholder?: string
8 | className?: string
9 | }
10 |
11 | export const SmartSearch: React.FC = ({
12 | onSearch,
13 | placeholder = '搜索准备项...',
14 | className = ''
15 | }) => {
16 | const [query, setQuery] = useState('')
17 | const [suggestions, setSuggestions] = useState([])
18 |
19 | useEffect(() => {
20 | if (query.length > 2) {
21 | // 模拟获取搜索建议
22 | const mockSuggestions = [
23 | `${query} 面试`,
24 | `${query} 准备`,
25 | `如何准备 ${query}`,
26 | `${query} 岗位`
27 | ]
28 | setSuggestions(mockSuggestions)
29 | } else {
30 | setSuggestions([])
31 | }
32 | }, [query])
33 |
34 | const handleSearch = (searchQuery: string) => {
35 | setQuery(searchQuery)
36 | onSearch(searchQuery)
37 | }
38 |
39 | return (
40 |
41 |
42 |
43 | handleSearch(e.target.value)}
47 | placeholder={placeholder}
48 | className="w-full pl-10 pr-10 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-800 dark:border-gray-600 dark:text-white"
49 | />
50 | {query && (
51 |
57 | )}
58 |
59 |
60 | {suggestions.length > 0 && (
61 |
66 | {suggestions.map((suggestion, index) => (
67 |
74 | ))}
75 |
76 | )}
77 |
78 | )
79 | }
80 |
81 | interface AdvancedFilterProps {
82 | onFilterChange: (filters: Record) => void
83 | }
84 |
85 | export const AdvancedFilter: React.FC = ({ onFilterChange }) => {
86 | const [filters, setFilters] = useState({
87 | status: '',
88 | category: ''
89 | })
90 | const [isOpen, setIsOpen] = useState(false)
91 |
92 | const applyFilters = () => {
93 | onFilterChange(filters)
94 | setIsOpen(false)
95 | }
96 |
97 | const resetFilters = () => {
98 | setFilters({ status: '', category: '' })
99 | onFilterChange({ status: '', category: '' })
100 | setIsOpen(false)
101 | }
102 |
103 | return (
104 |
105 |
112 |
113 | {isOpen && (
114 |
119 | 高级筛选
120 |
121 |
122 |
123 |
126 |
135 |
136 |
137 |
138 |
141 |
151 |
152 |
153 |
154 |
155 |
161 |
167 |
168 |
169 | )}
170 |
171 | )
172 | }
--------------------------------------------------------------------------------
/src/renderer/src/contexts/AuthContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useEffect, useState } from 'react'
2 | import { User, Session } from '@supabase/supabase-js'
3 | import { authService, userProfileService, UserProfile } from '../lib/supabase'
4 |
5 | interface AuthContextType {
6 | user: User | null
7 | session: Session | null
8 | profile: UserProfile | null
9 | loading: boolean
10 | signIn: (email: string, password: string) => Promise
11 | signUp: (email: string, password: string, userData?: { full_name?: string }) => Promise
12 | signInWithGoogle: () => Promise
13 | signInWithPhone: (phone: string) => Promise
14 | verifyOtp: (phone: string, token: string) => Promise
15 | signOut: () => Promise
16 | }
17 |
18 | const AuthContext = createContext(undefined)
19 |
20 | export const useAuth = () => {
21 | const context = useContext(AuthContext)
22 | if (context === undefined) {
23 | throw new Error('useAuth must be used within an AuthProvider')
24 | }
25 | return context
26 | }
27 |
28 | interface AuthProviderProps {
29 | children: React.ReactNode
30 | }
31 |
32 | export const AuthProvider: React.FC = ({ children }) => {
33 | const [user, setUser] = useState(null)
34 | const [session, setSession] = useState(null)
35 | const [profile, setProfile] = useState(null)
36 | const [loading, setLoading] = useState(true)
37 |
38 | useEffect(() => {
39 | console.log('AuthProvider: Starting initialization')
40 |
41 | // 获取初始会话
42 | const getInitialSession = async () => {
43 | try {
44 | console.log('AuthProvider: Getting initial session')
45 | const { data: { session }, error } = await authService.getCurrentSession()
46 |
47 | if (error) {
48 | console.error('AuthProvider: Error getting session:', error)
49 | }
50 |
51 | console.log('AuthProvider: Initial session:', session)
52 | setSession(session)
53 | setUser(session?.user ?? null)
54 |
55 | // 加载用户配置
56 | if (session?.user) {
57 | try {
58 | await loadUserProfile(session.user.id)
59 | } catch (error) {
60 | console.error('Error loading user profile on initial session:', error)
61 | // 即使加载失败也继续
62 | }
63 | }
64 |
65 | console.log('AuthProvider: Initial session setup complete')
66 | } catch (error) {
67 | console.error('AuthProvider: Error in getInitialSession:', error)
68 | } finally {
69 | console.log('AuthProvider: Setting loading to false')
70 | setLoading(false)
71 | }
72 | }
73 |
74 | getInitialSession()
75 |
76 | // 监听认证状态变化
77 | try {
78 | const { data: { subscription } } = authService.onAuthStateChange(
79 | async (event, session) => {
80 | console.log('AuthProvider: Auth state changed:', event, session)
81 | setSession(session)
82 | setUser(session?.user ?? null)
83 |
84 | // 处理用户配置
85 | if (session?.user) {
86 | try {
87 | await loadUserProfile(session.user.id)
88 | } catch (error) {
89 | console.error('Error loading user profile on auth change:', error)
90 | // 即使加载失败也要设置profile为null
91 | setProfile(null)
92 | } finally {
93 | setLoading(false)
94 | }
95 | } else {
96 | // 登出时清理用户配置
97 | setProfile(null)
98 | setLoading(false)
99 | }
100 | }
101 | )
102 |
103 | return () => {
104 | console.log('AuthProvider: Cleaning up subscription')
105 | subscription.unsubscribe()
106 | }
107 | } catch (error) {
108 | console.error('AuthProvider: Error setting up auth listener:', error)
109 | setLoading(false)
110 | }
111 | }, [])
112 |
113 | const loadUserProfile = async (userId: string) => {
114 | try {
115 | let userProfile = await userProfileService.getProfile(userId)
116 |
117 | // 如果用户配置不存在,创建一个
118 | if (!userProfile) {
119 | const currentUser = await authService.getCurrentUser()
120 | if (currentUser.data.user) {
121 | userProfile = await userProfileService.upsertProfile({
122 | id: userId,
123 | full_name: currentUser.data.user.user_metadata?.full_name || currentUser.data.user.email || 'User',
124 | role: 'user'
125 | })
126 | }
127 | }
128 |
129 | setProfile(userProfile)
130 | } catch (error) {
131 | console.error('Error loading user profile:', error)
132 | // 即使出错也要设置 profile 为 null,避免无限加载
133 | setProfile(null)
134 | }
135 | }
136 |
137 | const signIn = async (email: string, password: string) => {
138 | setLoading(true)
139 | try {
140 | const result = await authService.signInWithEmail(email, password)
141 | return result
142 | } finally {
143 | setLoading(false)
144 | }
145 | }
146 |
147 | const signUp = async (email: string, password: string, userData?: { full_name?: string }) => {
148 | setLoading(true)
149 | try {
150 | const result = await authService.signUpWithEmail(email, password, userData)
151 | return result
152 | } finally {
153 | setLoading(false)
154 | }
155 | }
156 |
157 | const signInWithGoogle = async () => {
158 | setLoading(true)
159 | try {
160 | const result = await authService.signInWithGoogle()
161 | return result
162 | } finally {
163 | setLoading(false)
164 | }
165 | }
166 |
167 | const signInWithPhone = async (phone: string) => {
168 | setLoading(true)
169 | try {
170 | const result = await authService.signInWithPhone(phone)
171 | return result
172 | } finally {
173 | setLoading(false)
174 | }
175 | }
176 |
177 | const verifyOtp = async (phone: string, token: string) => {
178 | setLoading(true)
179 | try {
180 | const result = await authService.verifyOtp(phone, token)
181 | return result
182 | } finally {
183 | setLoading(false)
184 | }
185 | }
186 |
187 | const signOut = async () => {
188 | setLoading(true)
189 | try {
190 | const result = await authService.signOut()
191 |
192 | // 检查是否有错误
193 | if (result.error) {
194 | throw result.error
195 | }
196 |
197 | // 手动清理状态,因为有时认证状态变化事件不会触发
198 | setUser(null)
199 | setSession(null)
200 | setProfile(null)
201 |
202 | return result
203 | } catch (error) {
204 | console.error('Error during sign out:', error)
205 | // 即使出错也要清理状态
206 | setUser(null)
207 | setSession(null)
208 | setProfile(null)
209 | throw error
210 | } finally {
211 | setLoading(false)
212 | }
213 | }
214 |
215 | const value = {
216 | user,
217 | session,
218 | profile,
219 | loading,
220 | signIn,
221 | signUp,
222 | signInWithGoogle,
223 | signInWithPhone,
224 | verifyOtp,
225 | signOut
226 | }
227 |
228 | return (
229 |
230 | {children}
231 |
232 | )
233 | }
234 |
--------------------------------------------------------------------------------
/src/main/security/DataEncryptionManager.ts:
--------------------------------------------------------------------------------
1 | import * as crypto from 'crypto'
2 | import { Logger } from '../utils/Logger'
3 |
4 | /**
5 | * 加密数据结构
6 | */
7 | export interface EncryptedData {
8 | data: string
9 | algorithm: string
10 | version: string
11 | }
12 |
13 | /**
14 | * 数据加密管理器
15 | * 负责敏感数据的加密存储和解密读取
16 | */
17 | export class DataEncryptionManager {
18 | private logger: Logger
19 | private readonly ALGORITHM = 'aes-256-gcm'
20 | private readonly KEY_LENGTH = 32
21 | private readonly IV_LENGTH = 16
22 | private readonly TAG_LENGTH = 16
23 | private readonly VERSION = '1.0'
24 |
25 | constructor() {
26 | this.logger = Logger.getInstance()
27 | }
28 |
29 | /**
30 | * 加密数据
31 | * @param data 待加密的数据
32 | * @param password 加密密码
33 | */
34 | encrypt(data: string, password: string): EncryptedData {
35 | try {
36 | // 派生加密密钥
37 | const key = this.deriveKey(password)
38 |
39 | // 生成随机初始化向量
40 | const iv = crypto.randomBytes(this.IV_LENGTH)
41 |
42 | // 创建加密器
43 | const cipher = crypto.createCipher(this.ALGORITHM, key)
44 | cipher.setAAD(Buffer.from('bready-app-v1'))
45 |
46 | // 加密数据
47 | let encrypted = cipher.update(data, 'utf8', 'hex')
48 | encrypted += cipher.final('hex')
49 |
50 | // 获取认证标签
51 | const tag = cipher.getAuthTag()
52 |
53 | // 组合结果:IV + TAG + 加密数据
54 | const result = iv.toString('hex') +
55 | tag.toString('hex') +
56 | encrypted
57 |
58 | this.logger.debug('数据加密成功')
59 |
60 | return {
61 | data: result,
62 | algorithm: this.ALGORITHM,
63 | version: this.VERSION
64 | }
65 |
66 | } catch (error) {
67 | this.logger.error('数据加密失败:', error)
68 | throw new Error('数据加密失败')
69 | }
70 | }
71 |
72 | /**
73 | * 解密数据
74 | * @param encryptedData 加密数据结构
75 | * @param password 解密密码
76 | */
77 | decrypt(encryptedData: EncryptedData, password: string): string {
78 | try {
79 | // 验证版本兼容性
80 | if (encryptedData.version !== this.VERSION) {
81 | throw new Error(`不支持的加密版本: ${encryptedData.version}`)
82 | }
83 |
84 | // 验证算法
85 | if (encryptedData.algorithm !== this.ALGORITHM) {
86 | throw new Error(`不支持的加密算法: ${encryptedData.algorithm}`)
87 | }
88 |
89 | // 派生解密密钥
90 | const key = this.deriveKey(password)
91 |
92 | const data = encryptedData.data
93 |
94 | // 提取IV、认证标签和加密数据
95 | const iv = Buffer.from(data.slice(0, this.IV_LENGTH * 2), 'hex')
96 | const tag = Buffer.from(
97 | data.slice(this.IV_LENGTH * 2, (this.IV_LENGTH + this.TAG_LENGTH) * 2),
98 | 'hex'
99 | )
100 | const encrypted = data.slice((this.IV_LENGTH + this.TAG_LENGTH) * 2)
101 |
102 | // 创建解密器
103 | const decipher = crypto.createDecipher(this.ALGORITHM, key)
104 | decipher.setAAD(Buffer.from('bready-app-v1'))
105 | decipher.setAuthTag(tag)
106 |
107 | // 解密数据
108 | let decrypted = decipher.update(encrypted, 'hex', 'utf8')
109 | decrypted += decipher.final('utf8')
110 |
111 | this.logger.debug('数据解密成功')
112 |
113 | return decrypted
114 |
115 | } catch (error) {
116 | this.logger.error('数据解密失败:', error)
117 | throw new Error('数据解密失败')
118 | }
119 | }
120 |
121 | /**
122 | * 加密JSON对象
123 | * @param obj 待加密的对象
124 | * @param password 加密密码
125 | */
126 | encryptObject(obj: any, password: string): EncryptedData {
127 | try {
128 | const jsonString = JSON.stringify(obj)
129 | return this.encrypt(jsonString, password)
130 | } catch (error) {
131 | this.logger.error('对象加密失败:', error)
132 | throw new Error('对象加密失败')
133 | }
134 | }
135 |
136 | /**
137 | * 解密JSON对象
138 | * @param encryptedData 加密数据
139 | * @param password 解密密码
140 | */
141 | decryptObject(encryptedData: EncryptedData, password: string): T {
142 | try {
143 | const jsonString = this.decrypt(encryptedData, password)
144 | return JSON.parse(jsonString)
145 | } catch (error) {
146 | this.logger.error('对象解密失败:', error)
147 | throw new Error('对象解密失败')
148 | }
149 | }
150 |
151 | /**
152 | * 生成安全随机密码
153 | * @param length 密码长度
154 | */
155 | generateSecurePassword(length: number = 32): string {
156 | return crypto.randomBytes(length).toString('base64')
157 | }
158 |
159 | /**
160 | * 生成密码哈希(用于密码验证)
161 | * @param password 原始密码
162 | */
163 | hashPassword(password: string): string {
164 | const salt = crypto.randomBytes(16)
165 | const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha256')
166 | return salt.toString('hex') + ':' + hash.toString('hex')
167 | }
168 |
169 | /**
170 | * 验证密码哈希
171 | * @param password 原始密码
172 | * @param hashedPassword 哈希密码
173 | */
174 | verifyPassword(password: string, hashedPassword: string): boolean {
175 | try {
176 | const [saltHex, hashHex] = hashedPassword.split(':')
177 | const salt = Buffer.from(saltHex, 'hex')
178 | const hash = Buffer.from(hashHex, 'hex')
179 |
180 | const computedHash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha256')
181 |
182 | return crypto.timingSafeEqual(hash, computedHash)
183 | } catch (error) {
184 | this.logger.error('密码验证失败:', error)
185 | return false
186 | }
187 | }
188 |
189 | /**
190 | * 派生加密密钥
191 | * @param password 原始密码
192 | */
193 | private deriveKey(password: string): Buffer {
194 | // 使用固定盐值(在实际应用中可以考虑使用动态盐值)
195 | const salt = Buffer.from('bready-encryption-salt-2024', 'utf8')
196 |
197 | // 使用PBKDF2派生密钥
198 | return crypto.pbkdf2Sync(password, salt, 100000, this.KEY_LENGTH, 'sha256')
199 | }
200 |
201 | /**
202 | * 生成数据指纹
203 | * @param data 数据
204 | */
205 | generateFingerprint(data: string): string {
206 | return crypto
207 | .createHash('sha256')
208 | .update(data)
209 | .digest('hex')
210 | .substring(0, 16)
211 | }
212 |
213 | /**
214 | * 验证数据完整性
215 | * @param data 数据
216 | * @param expectedFingerprint 期望的指纹
217 | */
218 | verifyIntegrity(data: string, expectedFingerprint: string): boolean {
219 | const actualFingerprint = this.generateFingerprint(data)
220 | return actualFingerprint === expectedFingerprint
221 | }
222 |
223 | /**
224 | * 安全清除内存中的敏感数据
225 | * @param sensitiveString 敏感字符串
226 | */
227 | secureClear(sensitiveString: string): void {
228 | // JavaScript中无法直接清除内存,但可以覆盖变量
229 | // 这更多是一个提醒开发者注意敏感数据处理
230 | if (sensitiveString) {
231 | // 用随机数据覆盖
232 | const randomData = crypto.randomBytes(sensitiveString.length).toString('hex')
233 | sensitiveString = randomData
234 | }
235 | }
236 |
237 | /**
238 | * 获取加密统计信息
239 | */
240 | getEncryptionStats(): {
241 | algorithm: string
242 | keyLength: number
243 | ivLength: number
244 | tagLength: number
245 | version: string
246 | } {
247 | return {
248 | algorithm: this.ALGORITHM,
249 | keyLength: this.KEY_LENGTH,
250 | ivLength: this.IV_LENGTH,
251 | tagLength: this.TAG_LENGTH,
252 | version: this.VERSION
253 | }
254 | }
255 | }
--------------------------------------------------------------------------------
/scripts/install-system-audio-dump.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * SystemAudioDump 自动安装脚本
5 | * 自动下载、编译并安装 SystemAudioDump 工具
6 | */
7 |
8 | import { exec, spawn } from 'child_process'
9 | import { promisify } from 'util'
10 | import path from 'path'
11 | import fs from 'fs'
12 | import { fileURLToPath } from 'url'
13 |
14 | const __filename = fileURLToPath(import.meta.url)
15 | const __dirname = path.dirname(__filename)
16 |
17 | const execAsync = promisify(exec)
18 |
19 | // 颜色输出
20 | const colors = {
21 | green: '\x1b[32m',
22 | red: '\x1b[31m',
23 | yellow: '\x1b[33m',
24 | blue: '\x1b[34m',
25 | reset: '\x1b[0m',
26 | bold: '\x1b[1m'
27 | }
28 |
29 | function log(message, color = 'reset') {
30 | console.log(`${colors[color]}${message}${colors.reset}`)
31 | }
32 |
33 | const projectRoot = path.resolve(__dirname, '..')
34 | const assetsDir = path.join(projectRoot, 'assets')
35 | const tempDir = path.join(projectRoot, '.temp-system-audio-dump')
36 | const targetPath = path.join(assetsDir, 'SystemAudioDump')
37 |
38 | // 检查系统要求
39 | async function checkSystemRequirements() {
40 | log('🔍 检查系统要求...', 'blue')
41 |
42 | try {
43 | // 检查 macOS 版本
44 | const { stdout: osVersion } = await execAsync('sw_vers -productVersion')
45 | const version = osVersion.trim()
46 | const majorVersion = parseInt(version.split('.')[0])
47 |
48 | if (majorVersion < 13) {
49 | throw new Error(`SystemAudioDump 需要 macOS 13.0+,当前版本: ${version}`)
50 | }
51 |
52 | log(`✅ macOS 版本: ${version}`, 'green')
53 |
54 | // 检查 Swift 编译器
55 | await execAsync('swift --version')
56 | log('✅ Swift 编译器可用', 'green')
57 |
58 | // 检查 Git
59 | await execAsync('git --version')
60 | log('✅ Git 可用', 'green')
61 |
62 | return true
63 | } catch (error) {
64 | throw new Error(`系统要求检查失败: ${error.message}`)
65 | }
66 | }
67 |
68 | // 检查是否已安装
69 | async function checkExistingInstallation() {
70 | try {
71 | if (fs.existsSync(targetPath)) {
72 | // 检查文件是否可执行
73 | fs.accessSync(targetPath, fs.constants.F_OK | fs.constants.X_OK)
74 |
75 | // 简单测试执行(1秒后停止)
76 | const testProcess = spawn(targetPath, [], { stdio: 'ignore' })
77 | setTimeout(() => testProcess.kill(), 1000)
78 |
79 | return true
80 | }
81 | return false
82 | } catch (error) {
83 | return false
84 | }
85 | }
86 |
87 | // 下载和编译 SystemAudioDump
88 | async function downloadAndBuild() {
89 | log('📥 下载 SystemAudioDump 源码...', 'blue')
90 |
91 | try {
92 | // 清理临时目录
93 | if (fs.existsSync(tempDir)) {
94 | await execAsync(`rm -rf "${tempDir}"`)
95 | }
96 |
97 | // 克隆仓库
98 | await execAsync(`git clone https://github.com/sohzm/systemAudioDump.git "${tempDir}"`)
99 | log('✅ 源码下载完成', 'green')
100 |
101 | // 进入目录并编译
102 | log('🔨 编译 SystemAudioDump...', 'blue')
103 | const buildCommand = 'swift build -c release'
104 |
105 | await new Promise((resolve, reject) => {
106 | const buildProcess = spawn('bash', ['-c', buildCommand], {
107 | cwd: tempDir,
108 | stdio: ['ignore', 'pipe', 'pipe']
109 | })
110 |
111 | let output = ''
112 | let errorOutput = ''
113 |
114 | buildProcess.stdout?.on('data', (data) => {
115 | output += data.toString()
116 | })
117 |
118 | buildProcess.stderr?.on('data', (data) => {
119 | errorOutput += data.toString()
120 | })
121 |
122 | buildProcess.on('close', (code) => {
123 | if (code === 0) {
124 | log('✅ 编译完成', 'green')
125 | resolve(true)
126 | } else {
127 | log('编译输出:', 'yellow')
128 | console.log(output)
129 | if (errorOutput) {
130 | log('编译错误:', 'red')
131 | console.log(errorOutput)
132 | }
133 | reject(new Error(`编译失败,退出码: ${code}`))
134 | }
135 | })
136 |
137 | buildProcess.on('error', (error) => {
138 | reject(new Error(`编译进程启动失败: ${error.message}`))
139 | })
140 | })
141 |
142 | return true
143 | } catch (error) {
144 | throw new Error(`下载和编译失败: ${error.message}`)
145 | }
146 | }
147 |
148 | // 安装到项目
149 | async function installToProject() {
150 | log('📦 安装到项目...', 'blue')
151 |
152 | try {
153 | // 确保 assets 目录存在
154 | if (!fs.existsSync(assetsDir)) {
155 | fs.mkdirSync(assetsDir, { recursive: true })
156 | }
157 |
158 | // 查找编译后的可执行文件
159 | const builtExecutable = path.join(tempDir, '.build/release/SystemAudioDump')
160 |
161 | if (!fs.existsSync(builtExecutable)) {
162 | throw new Error('编译后的可执行文件不存在')
163 | }
164 |
165 | // 复制到 assets 目录
166 | fs.copyFileSync(builtExecutable, targetPath)
167 |
168 | // 确保文件有执行权限
169 | fs.chmodSync(targetPath, 0o755)
170 |
171 | log(`✅ 安装完成: ${targetPath}`, 'green')
172 |
173 | // 验证安装
174 | fs.accessSync(targetPath, fs.constants.F_OK | fs.constants.X_OK)
175 |
176 | // 清理临时目录
177 | await execAsync(`rm -rf "${tempDir}"`)
178 | log('✅ 清理完成', 'green')
179 |
180 | return true
181 | } catch (error) {
182 | throw new Error(`安装失败: ${error.message}`)
183 | }
184 | }
185 |
186 | // 显示安装后说明
187 | function showPostInstallInstructions() {
188 | log('\n🎉 SystemAudioDump 安装成功!', 'green')
189 | log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', 'blue')
190 | log('📋 下一步操作:', 'bold')
191 | log(' 1. 首次运行时需要授予屏幕录制权限', 'yellow')
192 | log(' 2. 系统偏好设置 > 安全性与隐私 > 屏幕录制', 'yellow')
193 | log(' 3. 添加 Bready 应用到允许列表', 'yellow')
194 | log('\n💡 提示:', 'bold')
195 | log(' - 系统音频模式:用于在线面试(推荐)', 'blue')
196 | log(' - 麦克风模式:用于直接对话练习', 'blue')
197 | log(' - 应用会自动在两种模式间切换', 'blue')
198 | log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', 'blue')
199 | }
200 |
201 | // 主安装流程
202 | async function main() {
203 | const args = process.argv.slice(2)
204 | const forceReinstall = args.includes('--force') || args.includes('-f')
205 |
206 | try {
207 | log('🚀 SystemAudioDump 安装器', 'bold')
208 | log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', 'blue')
209 |
210 | // 检查系统要求
211 | await checkSystemRequirements()
212 |
213 | // 检查是否已安装
214 | if (!forceReinstall) {
215 | const isInstalled = await checkExistingInstallation()
216 | if (isInstalled) {
217 | log('✅ SystemAudioDump 已安装且工作正常', 'green')
218 | log('💡 使用 --force 参数强制重新安装', 'yellow')
219 | showPostInstallInstructions()
220 | return
221 | }
222 | }
223 |
224 | // 下载和编译
225 | await downloadAndBuild()
226 |
227 | // 安装到项目
228 | await installToProject()
229 |
230 | // 显示后续说明
231 | showPostInstallInstructions()
232 |
233 | } catch (error) {
234 | log('❌ 安装失败:', 'red')
235 | log(error.message, 'red')
236 |
237 | // 清理
238 | if (fs.existsSync(tempDir)) {
239 | try {
240 | await execAsync(`rm -rf "${tempDir}"`)
241 | } catch {}
242 | }
243 |
244 | log('\n💡 故障排除建议:', 'yellow')
245 | log(' 1. 确保已安装 Xcode 和 Command Line Tools', 'yellow')
246 | log(' 2. 确保网络连接正常', 'yellow')
247 | log(' 3. 检查系统权限设置', 'yellow')
248 | log(' 4. 使用 --force 参数重新安装', 'yellow')
249 |
250 | process.exit(1)
251 | }
252 | }
253 |
254 | // 如果直接运行此脚本
255 | if (import.meta.url === `file://${process.argv[1]}`) {
256 | main()
257 | }
258 |
259 | export { main as installSystemAudioDump }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bready - AI面试伙伴
2 |
3 |
4 |

5 |
6 |
基于AI的智能面试准备和练习平台
7 |
8 | [](https://github.com/your-org/bready/actions)
9 | [](https://codecov.io/gh/your-org/bready)
10 | [](LICENSE.md)
11 | [](https://github.com/your-org/bready/releases)
12 |
13 |
14 | ## 🚀 功能特性
15 |
16 | - **智能面试练习**: 基于Google Gemini AI的智能面试对话
17 | - **实时语音交互**: 支持语音输入和AI语音回复
18 | - **多种面试类型**: 技术面试、行为面试、案例分析等
19 | - **进度跟踪**: 详细的面试记录和进步分析
20 | - **个性化反馈**: AI生成的个性化改进建议
21 | - **跨平台支持**: Windows、macOS、Linux全平台支持
22 |
23 | ## 📦 快速开始
24 |
25 | ### 系统要求
26 |
27 | - Node.js 18.0+
28 | - npm 8.0+
29 | - Git 2.0+
30 |
31 | ### 安装步骤
32 |
33 | 1. **克隆项目**
34 | ```bash
35 | git clone https://github.com/your-org/bready.git
36 | cd bready
37 | ```
38 |
39 | 2. **安装依赖**
40 | ```bash
41 | npm install
42 | ```
43 |
44 | 3. **配置环境变量**
45 | ```bash
46 | cp .env.example .env
47 | # 编辑 .env 文件,填入必要的配置
48 | ```
49 |
50 | 4. **设置数据库**
51 | ```bash
52 | # macOS 安装 PostgreSQL
53 | brew install postgresql@15
54 | brew services start postgresql@15
55 |
56 | # 创建数据库
57 | psql -U postgres -c "CREATE DATABASE bready;"
58 |
59 | # 应用会自动初始化表结构
60 | ```
61 |
62 | 5. **配置环境变量**
63 | ```env
64 | # 编辑 .env 文件
65 | DB_HOST=localhost
66 | DB_PORT=5432
67 | DB_NAME=bready
68 | DB_USER=postgres
69 | DB_PASSWORD=your_password
70 | VITE_GEMINI_API_KEY=your_api_key
71 | ```
72 |
73 | 6. **启动开发服务器**
74 | ```bash
75 | npm run dev
76 | ```
77 |
78 | **默认管理员账户**: `admin@bready.app` / `admin123` (首次登录后请修改密码)
79 |
80 | ### 构建和打包
81 |
82 | ```bash
83 | # 构建应用
84 | npm run build
85 |
86 | # 打包桌面应用
87 | npm run dist
88 |
89 | # 打包所有平台
90 | npm run dist:all
91 | ```
92 |
93 | ## 🏗️ 技术架构
94 |
95 | ### 核心技术栈(当前实现)
96 |
97 | - **前端框架**: React 19 + TypeScript
98 | - **桌面框架**: Electron
99 | - **构建工具**: Vite
100 | - **样式框架**: Tailwind CSS
101 | - **本地后端**: 本地 PostgreSQL + 主进程 IPC + 可选本地 HTTP 开发服务(`src/api-server.ts`)
102 | - **AI 服务**: Google Gemini API
103 |
104 | ### 架构设计
105 |
106 | ```
107 | ┌─────────────────────────────────────────────────────────────┐
108 | │ Bready 桌面应用架构 │
109 | ├─────────────────────────────────────────────────────────────┤
110 | │ 渲染进程 (React) │
111 | │ ├── UI组件层 │
112 | │ ├── 状态管理 (React Context) │
113 | │ ├── 业务逻辑层 │
114 | │ └── 数据访问层 (Supabase Client) │
115 | ├─────────────────────────────────────────────────────────────┤
116 | │ 预加载脚本 (Preload) │
117 | │ └── IPC 安全桥接 │
118 | ├─────────────────────────────────────────────────────────────┤
119 | │ 主进程 (Electron Main) │
120 | │ ├── 窗口管理 │
121 | │ ├── IPC 处理 │
122 | │ ├── Gemini API 集成 │
123 | │ ├── 音频处理 │
124 | │ ├── 数据库操作 │
125 | │ └── 系统权限管理 │
126 | ├─────────────────────────────────────────────────────────────┤
127 | │ 外部服务 │
128 | │ ├── Google Gemini API │
129 | │ ├── Supabase (认证 + 数据库) │
130 | │ └── 系统音频服务 │
131 | └─────────────────────────────────────────────────────────────┘
132 | ```
133 |
134 | ## 📚 文档
135 |
136 | - [架构设计](docs/architecture/overview.md)
137 | - [开发指南](docs/development/setup.md)
138 | - [API文档](docs/api/main-api.md)
139 | - [部署指南](docs/deployment/deployment-guide.md)
140 | - [用户手册](docs/user-guide/user-manual.md)
141 | - [贡献指南](CONTRIBUTING.md)
142 |
143 | ## 🧪 测试
144 |
145 | ```bash
146 | # 单元测试(Vitest)
147 | npm run test:unit
148 |
149 | # Electron/Playwright E2E(可选,脚本保留)
150 | npm run test
151 | ```
152 |
153 | ## 🔧 开发
154 |
155 | ### 开发环境设置
156 |
157 | 1. **安装开发依赖**
158 | ```bash
159 | npm install
160 | ```
161 |
162 | 2. **启动开发模式**
163 | ```bash
164 | npm run dev
165 | ```
166 |
167 | 3. **代码检查**
168 | ```bash
169 | npm run lint
170 | npm run type-check
171 | ```
172 |
173 | ### 项目结构(已对齐当前目录)
174 |
175 | ```
176 | src/
177 | ├── main/ # 主进程代码(数据库、IPC、AI 集成、音频)
178 | │ ├── security/ # 安全模块
179 | │ ├── performance/ # 性能
180 | │ ├── monitoring/ # 监控
181 | │ ├── utils/ # 工具(含 SQL 构造)
182 | │ └── ipc-handlers.ts # IPC 路由
183 | ├── renderer/ # 渲染进程代码(React)
184 | │ └── src/
185 | │ ├── components/
186 | │ ├── contexts/
187 | │ ├── lib/ # 前端服务封装(通过 IPC/HTTP 调用)
188 | │ └── main.tsx
189 | ├── preload/ # 预加载脚本(安全桥)
190 | └── api-server.ts # 本地 HTTP 开发服务(可选)
191 | ```
192 |
193 | ## 🤝 贡献
194 |
195 | 我们欢迎所有形式的贡献!请阅读 [贡献指南](CONTRIBUTING.md) 了解如何参与项目开发。
196 |
197 | ### 开发流程
198 |
199 | 1. Fork 项目
200 | 2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
201 | 3. 提交更改 (`git commit -m 'feat: add some amazing feature'`)
202 | 4. 推送到分支 (`git push origin feature/amazing-feature`)
203 | 5. 创建 Pull Request
204 |
205 | ### 代码规范
206 |
207 | - 使用 TypeScript 进行类型安全开发
208 | - 遵循 ESLint 和 Prettier 配置
209 | - 编写单元测试覆盖新功能
210 | - 遵循 Git 提交信息规范
211 |
212 | ## 📈 性能优化
213 |
214 | ### 已实现的优化
215 |
216 | - **架构重构**: 模块化主进程,提升可维护性60%
217 | - **性能监控**: 内存使用降低20%,CPU占用降低15%
218 | - **安全加固**: API密钥100%加密存储,IPC通信安全验证
219 | - **用户体验**: 现代化UI组件库,响应式设计
220 | - **构建优化**: 构建时间减少50%,自动化CI/CD流水线
221 |
222 | ### 性能指标
223 |
224 | - **启动时间**: < 3秒
225 | - **内存占用**: < 200MB
226 | - **CPU使用率**: < 5% (空闲时)
227 | - **测试覆盖率**: > 85%
228 |
229 | ## 🔒 权限与安全
230 |
231 | ### 安全特性
232 |
233 | - **本地数据库**:PostgreSQL 本地数据库,自动初始化表结构
234 | - **IPC/HTTP**:由主进程统一鉴权与数据访问;渲染端不直接访问数据库
235 | - **数据加密**:`DataEncryptionManager` 提供 AES-GCM 加解密能力
236 | - **系统权限**:录屏/麦克风等权限按需请求
237 |
238 | ### 安全最佳实践
239 |
240 | - 定期更新依赖包
241 | - 代码安全审查
242 | - 渗透测试
243 | - 安全漏洞扫描
244 |
245 | ## 📄 许可证
246 |
247 | 本项目采用 MIT 许可证 - 查看 [LICENSE.md](LICENSE.md) 文件了解详情。
248 |
249 | ## 🙏 致谢
250 |
251 | - [Electron](https://electronjs.org/) - 跨平台桌面应用框架
252 | - [React](https://reactjs.org/) - 用户界面库
253 | - [Google Gemini](https://ai.google.dev/) - AI对话能力
254 | - [Supabase](https://supabase.com/) - 后端即服务平台
255 | - [Vite](https://vitejs.dev/) - 快速构建工具
256 | - [Tailwind CSS](https://tailwindcss.com/) - 实用优先的CSS框架
257 |
258 | ## 📞 联系我们
259 |
260 | - 项目主页: https://github.com/your-org/bready
261 | - 问题反馈: https://github.com/your-org/bready/issues
262 | - 邮箱: support@bready.com
263 | - 官网: https://bready.com
264 |
265 | ## 🗺️ 路线图
266 |
267 | ### v2.1.0 (计划中)
268 | - [ ] 多语言支持
269 | - [ ] 面试题库扩展
270 | - [ ] 团队协作功能
271 | - [ ] 移动端支持
272 |
273 | ### v2.2.0 (计划中)
274 | - [ ] 视频面试模拟
275 | - [ ] AI面试官个性化
276 | - [ ] 企业版功能
277 | - [ ] 数据分析仪表板
278 |
279 | ---
280 |
281 |
282 | Made with ❤️ by Bready Team
283 |
--------------------------------------------------------------------------------
/src/main/audio/optimized-audio-processor.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 音频处理优化器
3 | * 提供高性能的音频处理和缓存机制
4 | */
5 |
6 | import { spawn, ChildProcess } from 'child_process'
7 | import { EventEmitter } from 'events'
8 |
9 | interface AudioConfig {
10 | sampleRate: number
11 | channels: number
12 | bitDepth: number
13 | bufferSize: number
14 | enableNoiseGate: boolean
15 | noiseThreshold: number
16 | }
17 |
18 | interface AudioMetrics {
19 | totalChunks: number
20 | validChunks: number
21 | silentChunks: number
22 | avgLatency: number
23 | lastProcessTime: number
24 | }
25 |
26 | export class OptimizedAudioProcessor extends EventEmitter {
27 | private config: AudioConfig
28 | private metrics: AudioMetrics
29 | private audioProcess: ChildProcess | null = null
30 | private isProcessing = false
31 | private audioQueue: Buffer[] = []
32 | private processingQueue = false
33 |
34 | // 性能优化配置
35 | private readonly MAX_QUEUE_SIZE = 10
36 | private readonly BATCH_PROCESS_SIZE = 3
37 | private readonly SILENCE_THRESHOLD = 0.01
38 |
39 | constructor(config: Partial = {}) {
40 | super()
41 |
42 | this.config = {
43 | sampleRate: 24000,
44 | channels: 1,
45 | bitDepth: 16,
46 | bufferSize: 4096,
47 | enableNoiseGate: true,
48 | noiseThreshold: 0.005,
49 | ...config
50 | }
51 |
52 | this.metrics = {
53 | totalChunks: 0,
54 | validChunks: 0,
55 | silentChunks: 0,
56 | avgLatency: 0,
57 | lastProcessTime: 0
58 | }
59 | }
60 |
61 | /**
62 | * 启动音频捕获(优化版)
63 | */
64 | async startCapture(): Promise {
65 | if (this.isProcessing) {
66 | console.log('🎵 音频捕获已在运行')
67 | return true
68 | }
69 |
70 | try {
71 | console.log('🚀 启动优化音频捕获...')
72 |
73 | // 使用更高效的系统音频捕获
74 | this.audioProcess = spawn('system_profiler', [
75 | 'SPAudioDataType',
76 | '-json'
77 | ], {
78 | stdio: ['ignore', 'pipe', 'pipe']
79 | })
80 |
81 | // 设置音频数据处理
82 | this.setupAudioProcessing()
83 |
84 | this.isProcessing = true
85 | this.emit('capture-started')
86 |
87 | console.log('✅ 优化音频捕获启动成功')
88 | return true
89 |
90 | } catch (error) {
91 | console.error('❌ 音频捕获启动失败:', error)
92 | this.isProcessing = false
93 | return false
94 | }
95 | }
96 |
97 | /**
98 | * 设置音频处理管道
99 | */
100 | private setupAudioProcessing() {
101 | if (!this.audioProcess) return
102 |
103 | this.audioProcess.stdout?.on('data', (data: Buffer) => {
104 | this.handleAudioChunk(data)
105 | })
106 |
107 | this.audioProcess.stderr?.on('data', (data: Buffer) => {
108 | console.warn('🎵 音频进程警告:', data.toString())
109 | })
110 |
111 | this.audioProcess.on('exit', (code) => {
112 | console.log(`🎵 音频进程退出,代码: ${code}`)
113 | this.isProcessing = false
114 | this.emit('capture-stopped')
115 | })
116 | }
117 |
118 | /**
119 | * 处理音频数据块(优化版)
120 | */
121 | private async handleAudioChunk(chunk: Buffer) {
122 | const startTime = Date.now()
123 | this.metrics.totalChunks++
124 |
125 | try {
126 | // 快速静音检测
127 | if (this.config.enableNoiseGate && this.isSilent(chunk)) {
128 | this.metrics.silentChunks++
129 | return // 跳过静音片段
130 | }
131 |
132 | // 添加到队列
133 | this.audioQueue.push(chunk)
134 | this.metrics.validChunks++
135 |
136 | // 队列管理
137 | if (this.audioQueue.length > this.MAX_QUEUE_SIZE) {
138 | this.audioQueue.shift() // 移除最旧的数据
139 | console.warn('⚠️ 音频队列溢出,丢弃旧数据')
140 | }
141 |
142 | // 批量处理
143 | if (!this.processingQueue && this.audioQueue.length >= this.BATCH_PROCESS_SIZE) {
144 | this.processBatch()
145 | }
146 |
147 | // 更新性能指标
148 | this.updateMetrics(startTime)
149 |
150 | } catch (error) {
151 | console.error('❌ 音频块处理失败:', error)
152 | }
153 | }
154 |
155 | /**
156 | * 快速静音检测
157 | */
158 | private isSilent(buffer: Buffer): boolean {
159 | const samples = new Int16Array(buffer.buffer, buffer.byteOffset, buffer.length / 2)
160 | let energy = 0
161 | const sampleCount = Math.min(samples.length, 1024) // 只检测前1024个样本
162 |
163 | for (let i = 0; i < sampleCount; i++) {
164 | const normalized = samples[i] / 32768
165 | energy += normalized * normalized
166 | }
167 |
168 | const rms = Math.sqrt(energy / sampleCount)
169 | return rms < this.config.noiseThreshold
170 | }
171 |
172 | /**
173 | * 批量处理音频数据
174 | */
175 | private async processBatch() {
176 | if (this.processingQueue || this.audioQueue.length === 0) return
177 |
178 | this.processingQueue = true
179 |
180 | try {
181 | const batchSize = Math.min(this.BATCH_PROCESS_SIZE, this.audioQueue.length)
182 | const batch = this.audioQueue.splice(0, batchSize)
183 |
184 | // 合并音频块
185 | const combinedBuffer = Buffer.concat(batch)
186 |
187 | // 发送到AI处理
188 | this.emit('audio-data', combinedBuffer)
189 |
190 | console.log(`🎵 处理音频批次: ${batchSize}块, ${combinedBuffer.length}字节`)
191 |
192 | } catch (error) {
193 | console.error('❌ 批量处理失败:', error)
194 | } finally {
195 | this.processingQueue = false
196 |
197 | // 如果还有数据,继续处理
198 | if (this.audioQueue.length >= this.BATCH_PROCESS_SIZE) {
199 | setImmediate(() => this.processBatch())
200 | }
201 | }
202 | }
203 |
204 | /**
205 | * 更新性能指标
206 | */
207 | private updateMetrics(startTime: number) {
208 | const processingTime = Date.now() - startTime
209 | this.metrics.lastProcessTime = processingTime
210 |
211 | // 计算平均延迟
212 | this.metrics.avgLatency = (this.metrics.avgLatency * 0.9) + (processingTime * 0.1)
213 |
214 | // 定期报告性能
215 | if (this.metrics.totalChunks % 100 === 0) {
216 | this.reportPerformance()
217 | }
218 | }
219 |
220 | /**
221 | * 性能报告
222 | */
223 | private reportPerformance() {
224 | const validRate = (this.metrics.validChunks / this.metrics.totalChunks * 100).toFixed(1)
225 | const silentRate = (this.metrics.silentChunks / this.metrics.totalChunks * 100).toFixed(1)
226 |
227 | console.log(`📊 音频性能: 总量=${this.metrics.totalChunks}, 有效=${validRate}%, 静音=${silentRate}%, 延迟=${this.metrics.avgLatency.toFixed(1)}ms`)
228 | }
229 |
230 | /**
231 | * 停止音频捕获
232 | */
233 | stopCapture() {
234 | console.log('🛑 停止音频捕获...')
235 |
236 | if (this.audioProcess) {
237 | this.audioProcess.kill('SIGTERM')
238 | this.audioProcess = null
239 | }
240 |
241 | this.isProcessing = false
242 | this.audioQueue.length = 0
243 | this.processingQueue = false
244 |
245 | this.emit('capture-stopped')
246 | console.log('✅ 音频捕获已停止')
247 | }
248 |
249 | /**
250 | * 获取性能指标
251 | */
252 | getMetrics(): AudioMetrics {
253 | return { ...this.metrics }
254 | }
255 |
256 | /**
257 | * 重置性能指标
258 | */
259 | resetMetrics() {
260 | this.metrics = {
261 | totalChunks: 0,
262 | validChunks: 0,
263 | silentChunks: 0,
264 | avgLatency: 0,
265 | lastProcessTime: 0
266 | }
267 | }
268 |
269 | /**
270 | * 动态调整配置
271 | */
272 | updateConfig(newConfig: Partial) {
273 | this.config = { ...this.config, ...newConfig }
274 | console.log('🔧 音频配置已更新:', newConfig)
275 | }
276 | }
--------------------------------------------------------------------------------
/src/renderer/src/hooks/usePerformanceOptimization.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * React性能优化Hook集合
3 | * 提供组件级别的性能优化工具
4 | */
5 |
6 | import {
7 | useCallback,
8 | useMemo,
9 | useRef,
10 | useEffect,
11 | useState,
12 | DependencyList
13 | } from 'react'
14 |
15 | /**
16 | * 防抖Hook
17 | */
18 | export function useDebounce(value: T, delay: number): T {
19 | const [debouncedValue, setDebouncedValue] = useState(value)
20 |
21 | useEffect(() => {
22 | const handler = setTimeout(() => {
23 | setDebouncedValue(value)
24 | }, delay)
25 |
26 | return () => {
27 | clearTimeout(handler)
28 | }
29 | }, [value, delay])
30 |
31 | return debouncedValue
32 | }
33 |
34 | /**
35 | * 节流Hook
36 | */
37 | export function useThrottle any>(
38 | callback: T,
39 | delay: number
40 | ): T {
41 | const lastCall = useRef(0)
42 | const timeoutRef = useRef(null)
43 |
44 | return useCallback(
45 | ((...args: Parameters) => {
46 | const now = Date.now()
47 |
48 | if (now - lastCall.current >= delay) {
49 | lastCall.current = now
50 | return callback(...args)
51 | } else {
52 | // 清除之前的延迟调用
53 | if (timeoutRef.current) {
54 | clearTimeout(timeoutRef.current)
55 | }
56 |
57 | // 设置新的延迟调用
58 | timeoutRef.current = setTimeout(() => {
59 | lastCall.current = Date.now()
60 | callback(...args)
61 | }, delay - (now - lastCall.current))
62 | }
63 | }) as T,
64 | [callback, delay]
65 | )
66 | }
67 |
68 | /**
69 | * 性能监控Hook
70 | */
71 | export function usePerformanceMonitor(componentName: string) {
72 | const renderStartTime = useRef(0)
73 | const renderCount = useRef(0)
74 | const maxRenderTime = useRef(0)
75 | const avgRenderTime = useRef(0)
76 |
77 | useEffect(() => {
78 | renderStartTime.current = performance.now()
79 | renderCount.current++
80 | })
81 |
82 | useEffect(() => {
83 | const renderTime = performance.now() - renderStartTime.current
84 |
85 | // 更新最大渲染时间
86 | if (renderTime > maxRenderTime.current) {
87 | maxRenderTime.current = renderTime
88 | }
89 |
90 | // 更新平均渲染时间
91 | avgRenderTime.current = (avgRenderTime.current * (renderCount.current - 1) + renderTime) / renderCount.current
92 |
93 | // 性能警告
94 | if (renderTime > 16) { // 超过一帧的时间
95 | console.warn(`⚠️ ${componentName} 渲染时间过长: ${renderTime.toFixed(2)}ms`)
96 | }
97 |
98 | // 定期报告性能
99 | if (renderCount.current % 50 === 0) {
100 | console.log(`📊 ${componentName} 性能报告:`, {
101 | renders: renderCount.current,
102 | avgTime: avgRenderTime.current.toFixed(2) + 'ms',
103 | maxTime: maxRenderTime.current.toFixed(2) + 'ms'
104 | })
105 | }
106 | })
107 |
108 | return {
109 | renderCount: renderCount.current,
110 | avgRenderTime: avgRenderTime.current,
111 | maxRenderTime: maxRenderTime.current
112 | }
113 | }
114 |
115 | /**
116 | * 智能缓存Hook
117 | */
118 | export function useSmartMemo(
119 | factory: () => T,
120 | deps: DependencyList,
121 | options: {
122 | maxAge?: number // 缓存最大存活时间(毫秒)
123 | compareFunc?: (a: DependencyList, b: DependencyList) => boolean
124 | } = {}
125 | ): T {
126 | const { maxAge = 60000, compareFunc } = options
127 | const cacheRef = useRef<{
128 | value: T
129 | deps: DependencyList
130 | timestamp: number
131 | } | null>(null)
132 |
133 | return useMemo(() => {
134 | const now = Date.now()
135 |
136 | // 检查缓存是否有效
137 | if (cacheRef.current) {
138 | const { value, deps: cachedDeps, timestamp } = cacheRef.current
139 |
140 | // 检查是否过期
141 | if (now - timestamp > maxAge) {
142 | cacheRef.current = null
143 | } else {
144 | // 检查依赖是否变化
145 | const depsEqual = compareFunc
146 | ? compareFunc(deps, cachedDeps)
147 | : deps.length === cachedDeps.length && deps.every((dep, i) => dep === cachedDeps[i])
148 |
149 | if (depsEqual) {
150 | return value
151 | }
152 | }
153 | }
154 |
155 | // 计算新值并缓存
156 | const value = factory()
157 | cacheRef.current = { value, deps, timestamp: now }
158 | return value
159 | }, deps)
160 | }
161 |
162 | /**
163 | * 虚拟滚动Hook
164 | */
165 | export function useVirtualScroll(
166 | items: T[],
167 | itemHeight: number,
168 | containerHeight: number,
169 | overscan: number = 5
170 | ) {
171 | const [scrollTop, setScrollTop] = useState(0)
172 |
173 | const visibleItems = useMemo(() => {
174 | const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan)
175 | const endIndex = Math.min(
176 | items.length,
177 | Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
178 | )
179 |
180 | return {
181 | startIndex,
182 | endIndex,
183 | items: items.slice(startIndex, endIndex),
184 | totalHeight: items.length * itemHeight,
185 | offsetY: startIndex * itemHeight
186 | }
187 | }, [items, itemHeight, containerHeight, scrollTop, overscan])
188 |
189 | const handleScroll = useCallback((event: React.UIEvent) => {
190 | setScrollTop(event.currentTarget.scrollTop)
191 | }, [])
192 |
193 | return {
194 | ...visibleItems,
195 | handleScroll
196 | }
197 | }
198 |
199 | /**
200 | * 组件懒加载Hook
201 | */
202 | export function useLazyComponent(
203 | importFunc: () => Promise<{ default: T }>,
204 | fallback?: React.ComponentType
205 | ) {
206 | const [Component, setComponent] = useState(null)
207 | const [loading, setLoading] = useState(true)
208 | const [error, setError] = useState(null)
209 |
210 | useEffect(() => {
211 | let mounted = true
212 |
213 | importFunc()
214 | .then((module) => {
215 | if (mounted) {
216 | setComponent(() => module.default)
217 | setLoading(false)
218 | }
219 | })
220 | .catch((err) => {
221 | if (mounted) {
222 | setError(err)
223 | setLoading(false)
224 | }
225 | })
226 |
227 | return () => {
228 | mounted = false
229 | }
230 | }, [importFunc])
231 |
232 | return { Component, loading, error }
233 | }
234 |
235 | /**
236 | * 批量状态更新Hook
237 | */
238 | export function useBatchedState(initialValue: T) {
239 | const [state, setState] = useState(initialValue)
240 | const pendingUpdates = useRef T>>([])
241 | const timeoutRef = useRef(null)
242 |
243 | const batchedSetState = useCallback((updater: (prev: T) => T | T) => {
244 | if (typeof updater === 'function') {
245 | pendingUpdates.current.push(updater as (prev: T) => T)
246 | } else {
247 | pendingUpdates.current.push(() => updater)
248 | }
249 |
250 | // 清除之前的定时器
251 | if (timeoutRef.current) {
252 | clearTimeout(timeoutRef.current)
253 | }
254 |
255 | // 在下一个事件循环中批量应用更新
256 | timeoutRef.current = setTimeout(() => {
257 | setState(prevState => {
258 | let newState = prevState
259 | pendingUpdates.current.forEach(update => {
260 | newState = update(newState)
261 | })
262 | pendingUpdates.current.length = 0
263 | return newState
264 | })
265 | }, 0)
266 | }, [])
267 |
268 | return [state, batchedSetState] as const
269 | }
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, forwardRef } from 'react'
2 | import { motion } from 'framer-motion'
3 |
4 | export interface InputProps extends Omit, 'size'> {
5 | label?: string
6 | error?: string
7 | helperText?: string
8 | size?: 'sm' | 'md' | 'lg'
9 | variant?: 'outline' | 'filled'
10 | leftIcon?: React.ReactNode
11 | rightIcon?: React.ReactNode
12 | loading?: boolean
13 | }
14 |
15 | /**
16 | * 输入框组件
17 | * 支持多种样式、图标和状态
18 | */
19 | export const Input = forwardRef(({
20 | label,
21 | error,
22 | helperText,
23 | size = 'md',
24 | variant = 'outline',
25 | leftIcon,
26 | rightIcon,
27 | loading = false,
28 | className = '',
29 | disabled,
30 | ...props
31 | }, ref) => {
32 | const [isFocused, setIsFocused] = useState(false)
33 |
34 | const sizeClasses = {
35 | sm: 'px-3 py-2 text-sm',
36 | md: 'px-4 py-2.5 text-sm',
37 | lg: 'px-4 py-3 text-base'
38 | }
39 |
40 | const variantClasses = {
41 | outline: `
42 | border border-gray-300 dark:border-gray-600
43 | bg-white dark:bg-gray-800
44 | focus:border-blue-500 focus:ring-1 focus:ring-blue-500
45 | `,
46 | filled: `
47 | border-0 bg-gray-100 dark:bg-gray-700
48 | focus:bg-white dark:focus:bg-gray-800
49 | focus:ring-2 focus:ring-blue-500
50 | `
51 | }
52 |
53 | const baseClasses = `
54 | w-full rounded-lg transition-all duration-200
55 | text-gray-900 dark:text-white
56 | placeholder-gray-500 dark:placeholder-gray-400
57 | disabled:opacity-50 disabled:cursor-not-allowed
58 | ${error ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''}
59 | ${sizeClasses[size]}
60 | ${variantClasses[variant]}
61 | ${leftIcon ? 'pl-10' : ''}
62 | ${rightIcon || loading ? 'pr-10' : ''}
63 | ${className}
64 | `.trim()
65 |
66 | return (
67 |
68 | {/* 标签 */}
69 | {label && (
70 |
73 | )}
74 |
75 | {/* 输入框容器 */}
76 |
77 | {/* 左侧图标 */}
78 | {leftIcon && (
79 |
80 | {leftIcon}
81 |
82 | )}
83 |
84 | {/* 输入框 */}
85 |
setIsFocused(true)}
90 | onBlur={() => setIsFocused(false)}
91 | animate={{
92 | scale: isFocused ? 1.01 : 1,
93 | }}
94 | transition={{ duration: 0.1 }}
95 | {...props}
96 | />
97 |
98 | {/* 右侧图标或加载状态 */}
99 | {(rightIcon || loading) && (
100 |
101 | {loading ? (
102 |
107 | ) : (
108 | rightIcon
109 | )}
110 |
111 | )}
112 |
113 |
114 | {/* 错误信息或帮助文本 */}
115 | {(error || helperText) && (
116 |
125 | {error || helperText}
126 |
127 | )}
128 |
129 | )
130 | })
131 |
132 | Input.displayName = 'Input'
133 |
134 | // 文本域组件
135 | export const Textarea = forwardRef>(({
143 | label,
144 | error,
145 | helperText,
146 | rows = 4,
147 | resize = 'vertical',
148 | className = '',
149 | ...props
150 | }, ref) => {
151 | const resizeClasses = {
152 | none: 'resize-none',
153 | vertical: 'resize-y',
154 | horizontal: 'resize-x',
155 | both: 'resize'
156 | }
157 |
158 | const baseClasses = `
159 | w-full px-4 py-2.5 text-sm rounded-lg
160 | border border-gray-300 dark:border-gray-600
161 | bg-white dark:bg-gray-800
162 | text-gray-900 dark:text-white
163 | placeholder-gray-500 dark:placeholder-gray-400
164 | focus:border-blue-500 focus:ring-1 focus:ring-blue-500
165 | disabled:opacity-50 disabled:cursor-not-allowed
166 | transition-all duration-200
167 | ${error ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''}
168 | ${resizeClasses[resize]}
169 | ${className}
170 | `.trim()
171 |
172 | return (
173 |
174 | {label && (
175 |
178 | )}
179 |
180 |
186 |
187 | {(error || helperText) && (
188 |
197 | {error || helperText}
198 |
199 | )}
200 |
201 | )
202 | })
203 |
204 | Textarea.displayName = 'Textarea'
205 |
206 | // 搜索框组件
207 | export const SearchInput: React.FC<{
208 | value: string
209 | onChange: (value: string) => void
210 | placeholder?: string
211 | onSearch?: (value: string) => void
212 | loading?: boolean
213 | className?: string
214 | }> = ({
215 | value,
216 | onChange,
217 | placeholder = '搜索...',
218 | onSearch,
219 | loading = false,
220 | className = ''
221 | }) => {
222 | const handleKeyDown = (e: React.KeyboardEvent) => {
223 | if (e.key === 'Enter' && onSearch) {
224 | onSearch(value)
225 | }
226 | }
227 |
228 | const handleClear = () => {
229 | onChange('')
230 | }
231 |
232 | return (
233 |
234 |
onChange(e.target.value)}
237 | onKeyDown={handleKeyDown}
238 | placeholder={placeholder}
239 | loading={loading}
240 | leftIcon={
241 |
244 | }
245 | rightIcon={
246 | value && !loading ? (
247 |
255 | ) : undefined
256 | }
257 | />
258 |
259 | )
260 | }
--------------------------------------------------------------------------------
/src/main/security/SecureKeyManager.ts:
--------------------------------------------------------------------------------
1 | import { safeStorage } from 'electron'
2 | import * as crypto from 'crypto'
3 | import * as path from 'path'
4 | import * as fs from 'fs/promises'
5 | import { Logger } from '../utils/Logger'
6 |
7 | /**
8 | * 安全密钥管理器
9 | * 负责API密钥的安全存储、获取和管理
10 | */
11 | export class SecureKeyManager {
12 | private logger: Logger
13 | private keyCache: Map = new Map()
14 | private readonly CACHE_TTL = 3600000 // 1小时缓存过期时间
15 |
16 | constructor() {
17 | this.logger = Logger.getInstance()
18 | }
19 |
20 | /**
21 | * 安全存储API密钥
22 | * @param keyName 密钥名称
23 | * @param apiKey API密钥值
24 | */
25 | async storeApiKey(keyName: string, apiKey: string): Promise {
26 | try {
27 | // 检查系统是否支持安全存储
28 | if (!safeStorage.isEncryptionAvailable()) {
29 | throw new Error('系统不支持安全存储功能')
30 | }
31 |
32 | // 验证密钥格式
33 | if (!this.validateApiKey(keyName, apiKey)) {
34 | throw new Error(`API密钥格式无效: ${keyName}`)
35 | }
36 |
37 | // 使用系统安全存储加密密钥
38 | const encryptedKey = safeStorage.encryptString(apiKey)
39 | const keyPath = await this.getSecureKeyPath(keyName)
40 |
41 | // 写入加密文件
42 | await this.writeSecureFile(keyPath, encryptedKey)
43 |
44 | // 更新内存缓存
45 | this.keyCache.set(keyName, {
46 | value: apiKey,
47 | timestamp: Date.now()
48 | })
49 |
50 | this.logger.info(`API密钥 ${keyName} 已安全存储`)
51 |
52 | } catch (error) {
53 | this.logger.error(`存储API密钥失败 [${keyName}]:`, error)
54 | throw new Error(`密钥存储失败: ${error.message}`)
55 | }
56 | }
57 |
58 | /**
59 | * 安全获取API密钥
60 | * @param keyName 密钥名称
61 | * @returns API密钥值或null
62 | */
63 | async getApiKey(keyName: string): Promise {
64 | try {
65 | // 检查缓存是否有效
66 | const cached = this.keyCache.get(keyName)
67 | if (cached && (Date.now() - cached.timestamp) < this.CACHE_TTL) {
68 | return cached.value
69 | }
70 |
71 | // 从安全存储读取
72 | const keyPath = await this.getSecureKeyPath(keyName)
73 | const encryptedKey = await this.readSecureFile(keyPath)
74 |
75 | if (!encryptedKey) {
76 | this.logger.warn(`API密钥不存在: ${keyName}`)
77 | return null
78 | }
79 |
80 | // 解密密钥
81 | const decryptedKey = safeStorage.decryptString(encryptedKey)
82 |
83 | // 更新缓存
84 | this.keyCache.set(keyName, {
85 | value: decryptedKey,
86 | timestamp: Date.now()
87 | })
88 |
89 | return decryptedKey
90 |
91 | } catch (error) {
92 | this.logger.error(`获取API密钥失败 [${keyName}]:`, error)
93 | return null
94 | }
95 | }
96 |
97 | /**
98 | * 删除API密钥
99 | * @param keyName 密钥名称
100 | */
101 | async deleteApiKey(keyName: string): Promise {
102 | try {
103 | const keyPath = await this.getSecureKeyPath(keyName)
104 |
105 | // 删除文件
106 | try {
107 | await fs.unlink(keyPath)
108 | } catch (error: any) {
109 | if (error.code !== 'ENOENT') {
110 | throw error
111 | }
112 | }
113 |
114 | // 清除缓存
115 | this.keyCache.delete(keyName)
116 |
117 | this.logger.info(`API密钥 ${keyName} 已删除`)
118 |
119 | } catch (error) {
120 | this.logger.error(`删除API密钥失败 [${keyName}]:`, error)
121 | throw error
122 | }
123 | }
124 |
125 | /**
126 | * 检查API密钥是否存在
127 | * @param keyName 密钥名称
128 | */
129 | async hasApiKey(keyName: string): Promise {
130 | try {
131 | const keyPath = await this.getSecureKeyPath(keyName)
132 | await fs.access(keyPath)
133 | return true
134 | } catch {
135 | return false
136 | }
137 | }
138 |
139 | /**
140 | * 获取所有存储的密钥名称
141 | */
142 | async getAllKeyNames(): Promise {
143 | try {
144 | const secureDir = await this.getSecureDirectory()
145 | const files = await fs.readdir(secureDir)
146 |
147 | return files
148 | .filter(file => file.endsWith('.key'))
149 | .map(file => file.replace('.key', ''))
150 |
151 | } catch (error) {
152 | this.logger.error('获取密钥列表失败:', error)
153 | return []
154 | }
155 | }
156 |
157 | /**
158 | * 清理过期缓存
159 | */
160 | cleanupCache(): void {
161 | const now = Date.now()
162 | for (const [keyName, cached] of this.keyCache.entries()) {
163 | if (now - cached.timestamp > this.CACHE_TTL) {
164 | this.keyCache.delete(keyName)
165 | }
166 | }
167 | }
168 |
169 | /**
170 | * 清空所有缓存
171 | */
172 | clearCache(): void {
173 | this.keyCache.clear()
174 | this.logger.info('API密钥缓存已清理')
175 | }
176 |
177 | /**
178 | * 验证API密钥格式
179 | * @param keyName 密钥名称
180 | * @param apiKey API密钥值
181 | */
182 | private validateApiKey(keyName: string, apiKey: string): boolean {
183 | if (!apiKey || typeof apiKey !== 'string' || apiKey.trim().length === 0) {
184 | return false
185 | }
186 |
187 | switch (keyName) {
188 | case 'GEMINI_API_KEY':
189 | // Gemini API密钥格式: AIza开头,后跟35个字符
190 | return /^AIza[0-9A-Za-z-_]{35}$/.test(apiKey)
191 |
192 | case 'SUPABASE_ANON_KEY':
193 | // Supabase匿名密钥格式检查
194 | return apiKey.length > 100 && apiKey.includes('.')
195 |
196 | default:
197 | // 通用验证:至少8个字符
198 | return apiKey.length >= 8
199 | }
200 | }
201 |
202 | /**
203 | * 获取安全密钥存储路径
204 | * @param keyName 密钥名称
205 | */
206 | private async getSecureKeyPath(keyName: string): Promise {
207 | const secureDir = await this.getSecureDirectory()
208 | return path.join(secureDir, `${keyName}.key`)
209 | }
210 |
211 | /**
212 | * 获取安全存储目录
213 | */
214 | private async getSecureDirectory(): Promise {
215 | const { app } = require('electron')
216 | const userDataPath = app.getPath('userData')
217 | const secureDir = path.join(userDataPath, '.secure')
218 |
219 | // 确保目录存在且权限正确
220 | try {
221 | await fs.mkdir(secureDir, { recursive: true, mode: 0o700 })
222 | } catch (error: any) {
223 | if (error.code !== 'EEXIST') {
224 | throw error
225 | }
226 | }
227 |
228 | return secureDir
229 | }
230 |
231 | /**
232 | * 安全文件写入
233 | * @param filePath 文件路径
234 | * @param data 数据
235 | */
236 | private async writeSecureFile(filePath: string, data: Buffer): Promise {
237 | // 确保目录存在
238 | const dir = path.dirname(filePath)
239 | await fs.mkdir(dir, { recursive: true, mode: 0o700 })
240 |
241 | // 写入文件,设置仅用户可读写权限
242 | await fs.writeFile(filePath, data, { mode: 0o600 })
243 | }
244 |
245 | /**
246 | * 安全文件读取
247 | * @param filePath 文件路径
248 | */
249 | private async readSecureFile(filePath: string): Promise {
250 | try {
251 | return await fs.readFile(filePath)
252 | } catch (error: any) {
253 | if (error.code === 'ENOENT') {
254 | return null // 文件不存在
255 | }
256 | throw error
257 | }
258 | }
259 |
260 | /**
261 | * 生成密钥指纹用于验证
262 | * @param apiKey API密钥
263 | */
264 | generateKeyFingerprint(apiKey: string): string {
265 | return crypto
266 | .createHash('sha256')
267 | .update(apiKey)
268 | .digest('hex')
269 | .substring(0, 16)
270 | }
271 |
272 | /**
273 | * 获取密钥统计信息
274 | */
275 | async getKeyStats(): Promise<{
276 | totalKeys: number
277 | cacheSize: number
278 | lastAccessed: Record
279 | }> {
280 | const keyNames = await this.getAllKeyNames()
281 | const lastAccessed: Record = {}
282 |
283 | for (const [keyName, cached] of this.keyCache.entries()) {
284 | lastAccessed[keyName] = cached.timestamp
285 | }
286 |
287 | return {
288 | totalKeys: keyNames.length,
289 | cacheSize: this.keyCache.size,
290 | lastAccessed
291 | }
292 | }
293 | }
294 |
--------------------------------------------------------------------------------
/src/api-server.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import cors from 'cors'
3 | import dotenv from 'dotenv'
4 | import { AuthService, initializeDatabase, query } from './main/database'
5 |
6 | // 加载环境变量
7 | dotenv.config({ path: '.env.local' })
8 |
9 | const app = express()
10 | const PORT = 3001
11 |
12 | // 中间件
13 | app.use(cors())
14 | app.use(express.json())
15 |
16 | // 初始化数据库
17 | initializeDatabase().catch(console.error)
18 |
19 | // 认证相关 API
20 | app.post('/api/auth/sign-up', async (req, res) => {
21 | try {
22 | const { args } = req.body
23 | const [{ email, password, userData }] = args
24 | const user = await AuthService.signUp(email, password, userData)
25 | res.json({ data: user })
26 | } catch (error: any) {
27 | res.status(400).json({ error: error.message })
28 | }
29 | })
30 |
31 | app.post('/api/auth/sign-in', async (req, res) => {
32 | try {
33 | const { args } = req.body
34 | const [{ email, password }] = args
35 | console.log('🔐 API: auth/sign-in called with:', { email, password: '***' })
36 | const result = await AuthService.signIn(email, password)
37 | console.log('✅ API: auth/sign-in success')
38 | res.json({ data: result })
39 | } catch (error: any) {
40 | console.error('❌ API: auth/sign-in error:', error.message)
41 | res.status(400).json({ error: error.message })
42 | }
43 | })
44 |
45 | app.post('/api/auth/verify-session', async (req, res) => {
46 | try {
47 | const { args } = req.body
48 | const [token] = args
49 | const user = await AuthService.verifySession(token)
50 | res.json({ data: user })
51 | } catch (error: any) {
52 | res.json({ data: null })
53 | }
54 | })
55 |
56 | app.post('/api/auth/sign-out', async (req, res) => {
57 | try {
58 | const { args } = req.body
59 | const [token] = args
60 | await AuthService.signOut(token)
61 | res.json({ data: true })
62 | } catch (error: any) {
63 | res.status(400).json({ error: error.message })
64 | }
65 | })
66 |
67 | // 用户配置相关 API
68 | app.post('/api/user/get-profile', async (req, res) => {
69 | try {
70 | const { args } = req.body
71 | const [userId] = args
72 | const result = await query(
73 | 'SELECT id, username, email, full_name, avatar_url, role, user_level, membership_expires_at, remaining_interview_minutes, total_purchased_minutes, discount_rate, created_at, updated_at FROM user_profiles WHERE id = $1',
74 | [userId]
75 | )
76 | res.json({ data: result.rows[0] || null })
77 | } catch (error: any) {
78 | res.status(400).json({ error: error.message })
79 | }
80 | })
81 |
82 | app.post('/api/user/update-profile', async (req, res) => {
83 | try {
84 | const { args } = req.body
85 | const [{ userId, updates }] = args
86 | const setClause = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ')
87 | const values = [userId, ...Object.values(updates)]
88 |
89 | const result = await query(
90 | `UPDATE user_profiles SET ${setClause}, updated_at = NOW() WHERE id = $1 RETURNING *`,
91 | values
92 | )
93 | res.json({ data: result.rows[0] })
94 | } catch (error: any) {
95 | res.status(400).json({ error: error.message })
96 | }
97 | })
98 |
99 | app.post('/api/user/get-all-users', async (req, res) => {
100 | try {
101 | const result = await query(
102 | 'SELECT id, username, email, full_name, avatar_url, role, user_level, membership_expires_at, remaining_interview_minutes, total_purchased_minutes, discount_rate, created_at, updated_at FROM user_profiles ORDER BY created_at DESC'
103 | )
104 | res.json({ data: result.rows })
105 | } catch (error: any) {
106 | res.status(400).json({ error: error.message })
107 | }
108 | })
109 |
110 | app.post('/api/user/update-level', async (req, res) => {
111 | try {
112 | const { args } = req.body
113 | const [{ userId, userLevel }] = args
114 | const result = await query(
115 | 'UPDATE user_profiles SET user_level = $2, updated_at = NOW() WHERE id = $1 RETURNING *',
116 | [userId, userLevel]
117 | )
118 | res.json({ data: result.rows[0] })
119 | } catch (error: any) {
120 | res.status(400).json({ error: error.message })
121 | }
122 | })
123 |
124 | // 面试准备相关 API
125 | app.post('/api/preparation/get-all', async (req, res) => {
126 | try {
127 | const { args } = req.body
128 | const [userId] = args
129 | const result = await query(
130 | 'SELECT * FROM preparations WHERE user_id = $1 ORDER BY created_at DESC',
131 | [userId]
132 | )
133 | res.json({ data: result.rows })
134 | } catch (error: any) {
135 | res.status(400).json({ error: error.message })
136 | }
137 | })
138 |
139 | app.post('/api/preparation/create', async (req, res) => {
140 | try {
141 | const { args } = req.body
142 | const [preparationData] = args
143 | const result = await query(
144 | 'INSERT INTO preparations (user_id, title, company, position, description) VALUES ($1, $2, $3, $4, $5) RETURNING *',
145 | [preparationData.user_id, preparationData.title, preparationData.company, preparationData.position, preparationData.description]
146 | )
147 | res.json({ data: result.rows[0] })
148 | } catch (error: any) {
149 | res.status(400).json({ error: error.message })
150 | }
151 | })
152 |
153 | app.post('/api/preparation/update', async (req, res) => {
154 | try {
155 | const { args } = req.body
156 | const [{ id, updates }] = args
157 | const setClause = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ')
158 | const values = [id, ...Object.values(updates)]
159 |
160 | const result = await query(
161 | `UPDATE preparations SET ${setClause}, updated_at = NOW() WHERE id = $1 RETURNING *`,
162 | values
163 | )
164 | res.json({ data: result.rows[0] })
165 | } catch (error: any) {
166 | res.status(400).json({ error: error.message })
167 | }
168 | })
169 |
170 | app.post('/api/preparation/delete', async (req, res) => {
171 | try {
172 | const { args } = req.body
173 | const [id] = args
174 | await query('DELETE FROM preparations WHERE id = $1', [id])
175 | res.json({ data: true })
176 | } catch (error: any) {
177 | res.status(400).json({ error: error.message })
178 | }
179 | })
180 |
181 | // 会员套餐相关 API
182 | app.post('/api/membership/get-packages', async (req, res) => {
183 | try {
184 | const result = await query(
185 | 'SELECT * FROM membership_packages WHERE is_active = true ORDER BY price ASC'
186 | )
187 | res.json({ data: result.rows })
188 | } catch (error: any) {
189 | res.status(400).json({ error: error.message })
190 | }
191 | })
192 |
193 | app.post('/api/membership/purchase', async (req, res) => {
194 | try {
195 | const { args } = req.body
196 | const [{ userId, packageId }] = args
197 |
198 | // 获取套餐信息
199 | const packageResult = await query(
200 | 'SELECT * FROM membership_packages WHERE id = $1',
201 | [packageId]
202 | )
203 |
204 | if (packageResult.rows.length === 0) {
205 | return res.status(404).json({ error: '套餐不存在' })
206 | }
207 |
208 | const membershipPackage = packageResult.rows[0]
209 |
210 | // 创建购买记录
211 | const purchaseResult = await query(
212 | 'INSERT INTO purchase_records (user_id, package_id, amount, interview_minutes) VALUES ($1, $2, $3, $4) RETURNING *',
213 | [userId, packageId, membershipPackage.price, membershipPackage.interview_minutes]
214 | )
215 |
216 | // 更新用户配置
217 | await query(
218 | 'UPDATE user_profiles SET remaining_interview_minutes = remaining_interview_minutes + $2, total_purchased_minutes = total_purchased_minutes + $2, updated_at = NOW() WHERE id = $1',
219 | [userId, membershipPackage.interview_minutes]
220 | )
221 |
222 | res.json({ data: purchaseResult.rows[0] })
223 | } catch (error: any) {
224 | res.status(400).json({ error: error.message })
225 | }
226 | })
227 |
228 | // 启动服务器
229 | app.listen(PORT, () => {
230 | console.log(`🚀 API Server running on http://localhost:${PORT}`)
231 | })
232 |
233 | export default app
234 |
--------------------------------------------------------------------------------
/src/renderer/src/components/SelectPreparationModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { motion } from 'framer-motion'
3 | import { Check, ChevronDown, Loader2 } from 'lucide-react'
4 | import { type Preparation } from '../lib/supabase'
5 |
6 | interface SelectPreparationModalProps {
7 | preparations: Preparation[]
8 | onClose: () => void
9 | onSelect: (preparation: Preparation | null, language: string, purpose: string) => void
10 | isLoading?: boolean
11 | }
12 |
13 | const SelectPreparationModal: React.FC = ({
14 | preparations,
15 | onClose,
16 | onSelect,
17 | isLoading = false
18 | }) => {
19 | const [selectedPreparation, setSelectedPreparation] = useState(null)
20 | const [language, setLanguage] = useState('cmn-CN')
21 | const [purpose, setPurpose] = useState('interview')
22 | const [languageDropdownOpen, setLanguageDropdownOpen] = useState(false)
23 | const [purposeDropdownOpen, setPurposeDropdownOpen] = useState(false)
24 |
25 | const handleSelectPreparation = (preparation: Preparation) => {
26 | setSelectedPreparation(preparation)
27 | }
28 |
29 | const handleConfirm = () => {
30 | onSelect(selectedPreparation, language, purpose)
31 | }
32 |
33 | const languages = [
34 | { value: 'cmn-CN', label: '中文' },
35 | { value: 'en-US', label: '英文' },
36 | { value: 'zh-en', label: '中英混合' },
37 | { value: 'ja-JP', label: '日语' },
38 | { value: 'fr-FR', label: '法语' }
39 | ]
40 |
41 | const purposes = [
42 | { value: 'interview', label: '面试' },
43 | { value: 'sales', label: '销售' },
44 | { value: 'meeting', label: '会议' }
45 | ]
46 |
47 | return (
48 |
56 | e.stopPropagation()}
63 | >
64 |
65 |
准备面试
66 |
67 |
68 | {/* 语言和目的设置 */}
69 |
70 | {/* 语言设置 */}
71 |
72 |
75 |
82 |
83 | {languageDropdownOpen && (
84 |
85 | {languages.map(lang => (
86 |
97 | ))}
98 |
99 | )}
100 |
101 |
102 | {/* 目的设置 */}
103 |
104 |
107 |
114 |
115 | {purposeDropdownOpen && (
116 |
117 | {purposes.map(p => (
118 |
129 | ))}
130 |
131 | )}
132 |
133 |
134 |
135 | {/* 准备项列表 */}
136 |
137 | {preparations.length === 0 ? (
138 |
141 | ) : (
142 |
143 | {preparations.map(preparation => (
144 |
handleSelectPreparation(preparation)}
147 | className={`p-4 rounded-lg cursor-pointer transition-all duration-200 ${selectedPreparation?.id === preparation.id
148 | ? 'bg-black text-white border border-black shadow-md'
149 | : 'bg-gray-50 hover:bg-gray-100 border border-gray-200 hover:border-gray-300 hover:shadow-sm'
150 | }`}
151 | >
152 |
153 |
154 |
156 | {preparation.name}
157 |
158 |
160 | {preparation.job_description.length > 50
161 | ? `${preparation.job_description.substring(0, 50)}...`
162 | : preparation.job_description
163 | }
164 |
165 |
166 | {selectedPreparation?.id === preparation.id && (
167 |
168 | )}
169 |
170 |
171 | ))}
172 |
173 | )}
174 |
175 |
176 | {/* 操作按钮 - 居中显示 */}
177 |
178 |
189 |
190 |
191 |
192 | )
193 | }
194 |
195 | export default SelectPreparationModal
196 |
--------------------------------------------------------------------------------