├── src ├── renderer │ ├── src │ │ ├── env.d.ts │ │ ├── hero.ts │ │ ├── assets │ │ │ └── main.css │ │ ├── main.tsx │ │ ├── utils │ │ │ └── index.ts │ │ ├── types │ │ │ └── index.ts │ │ └── App.tsx │ └── index.html ├── main │ ├── env.d.ts │ ├── utils │ │ ├── response.ts │ │ ├── index.ts │ │ ├── secrets.ts │ │ ├── paths.ts │ │ └── registryHelper.ts │ ├── index.ts │ ├── constant.ts │ ├── ipc │ │ ├── handle.ts │ │ ├── machine.ts │ │ └── index.ts │ └── services │ │ └── WindowService.ts └── preload │ ├── index.d.ts │ └── index.ts ├── .vscode ├── extensions.json ├── settings.json └── launch.json ├── resources └── icon.ico ├── .prettierignore ├── image-20250825160513863.png ├── tsconfig.json ├── .npmrc ├── .prettierrc ├── .editorconfig ├── .gitignore ├── tsconfig.node.json ├── tsconfig.web.json ├── packages └── shared │ └── IpcChannel.ts ├── electron.vite.config.ts ├── eslint.config.mjs ├── electron-builder.yml ├── package.json └── README.md /src/renderer/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /src/renderer/src/hero.ts: -------------------------------------------------------------------------------- 1 | import { heroui } from '@heroui/react' 2 | export default heroui() 3 | -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuxinle1996/account-switch-demo/HEAD/resources/icon.ico -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | pnpm-lock.yaml 4 | LICENSE.md 5 | tsconfig.json 6 | tsconfig.*.json 7 | -------------------------------------------------------------------------------- /image-20250825160513863.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuxinle1996/account-switch-demo/HEAD/image-20250825160513863.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }] 4 | } 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | electron_mirror=https://npmmirror.com/mirrors/electron/ 2 | electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ 3 | -------------------------------------------------------------------------------- /src/main/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv {} 4 | 5 | interface ImportMeta { 6 | readonly env: ImportMetaEnv 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "printWidth": 100, 5 | "trailingComma": "none", 6 | "plugins": ["prettier-plugin-tailwindcss"] 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /src/preload/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ElectronAPI } from '@electron-toolkit/preload' 2 | 3 | import type { WindowApiType } from './index' 4 | 5 | declare global { 6 | interface Window { 7 | electron: ElectronAPI 8 | api: WindowApiType 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .eslintcache 4 | 5 | # OS 6 | .DS_Store 7 | Thumbs.db 8 | 9 | # Env 10 | .env 11 | .env.* 12 | !.env.example 13 | !.env.test 14 | 15 | # Electron-vite 16 | dist 17 | out 18 | *.log* 19 | 20 | .cursor 21 | 22 | # Test 23 | _test.* 24 | -------------------------------------------------------------------------------- /src/renderer/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @plugin '@tailwindcss/typography'; 3 | @plugin '../hero.ts'; 4 | /* Note: You may need to change the path to fit your project structure */ 5 | @source '../../../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}'; 6 | @custom-variant dark (&:is(.dark *)); 7 | 8 | html { 9 | background-color: transparent; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/utils/response.ts: -------------------------------------------------------------------------------- 1 | import { IpcResponse } from '@types' 2 | 3 | export function success(data: T, message?: string): IpcResponse { 4 | return { 5 | success: true, 6 | data, 7 | message 8 | } 9 | } 10 | 11 | export function failure(message: string, flag?: string): IpcResponse { 12 | console.error('ipc failure:', flag ? `${flag}: ${message}` : message) 13 | return { 14 | success: false, 15 | data: null, 16 | message 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Electron 7 | 8 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { type BinaryLike, createHash } from 'node:crypto' 2 | 3 | /** 4 | * 判断是否为空 5 | * @param {any} param 6 | */ 7 | export function isEmpty(param: any) { 8 | return [undefined, null, ''].includes(param) 9 | } 10 | 11 | export function getHash(data: BinaryLike) { 12 | return createHash('sha256').update(data).digest() 13 | } 14 | 15 | export function toUrlSafeBase64(buffer: Buffer) { 16 | return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') 17 | } 18 | -------------------------------------------------------------------------------- /src/renderer/src/main.tsx: -------------------------------------------------------------------------------- 1 | import './assets/main.css' 2 | 3 | import { HeroUIProvider, ToastProvider } from '@heroui/react' 4 | import { StrictMode } from 'react' 5 | import { createRoot } from 'react-dom/client' 6 | 7 | import App from './App' 8 | 9 | const root = createRoot(document.getElementById('root')!) 10 | 11 | root.render( 12 | 13 | 14 | 15 | 22 | 23 | 24 | ) 25 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", 3 | "include": [ 4 | "electron.vite.config.*", 5 | "src/main/**/*", 6 | "src/preload/**/*", 7 | "src/renderer/src/types/*", 8 | "packages/shared/**/*", 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "types": ["electron-vite/node"], 13 | "baseUrl": ".", 14 | "moduleResolution": "bundler", 15 | "useUnknownInCatchVariables": false, 16 | "paths": { 17 | "@main/*": ["src/main/*"], 18 | "@types": ["src/renderer/src/types/index.ts"], 19 | "@shared/*": ["packages/shared/*"] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", 3 | "include": [ 4 | "src/preload/*.d.ts", 5 | "src/renderer/src/env.d.ts", 6 | "src/renderer/src/**/*", 7 | "src/renderer/src/**/*.tsx", 8 | "packages/shared/**/*" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "jsx": "react-jsx", 13 | "baseUrl": ".", 14 | "moduleResolution": "bundler", 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "useUnknownInCatchVariables": false, 18 | "paths": { 19 | "@renderer/*": ["src/renderer/src/*"], 20 | "@shared/*": ["packages/shared/*"], 21 | "@types": ["src/renderer/src/types/index.ts"], 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/shared/IpcChannel.ts: -------------------------------------------------------------------------------- 1 | export enum IpcChannel { 2 | /** ipc-获取应用信息 */ 3 | App_Info = 'app:info', 4 | /** ipc-重启应用 */ 5 | App_Reload = 'app:reload', 6 | /** ipc-获取配置 */ 7 | App_GetConfig = 'app:get-config', 8 | /** ipc-设置配置 */ 9 | App_SetConfig = 'app:set-config', 10 | /** ipc-secrets-加密 */ 11 | Secrets_Encrypt = 'secrets:encrypt', 12 | /** ipc-secrets-解密 */ 13 | Secrets_Decrypt = 'secrets:decrypt', 14 | /** ipc-获取机器ids */ 15 | App_GetMachineIds = 'app:get-machine-ids', 16 | /** ipc-设置随机机器ids */ 17 | App_SetRandomMachineIds = 'app:set-random-machine-ids', 18 | /** ipc-获取注册表guid */ 19 | App_GetRegistryMachineGuid = 'app:get-registry-machine-guid', 20 | /** ipc-设置随机注册表guid */ 21 | App_SetRandomRegistryMachineGuid = 'app:set-random-registry-machine-guid', 22 | /** ipc-生成登录链接 */ 23 | Augment_GenerateLoginUrl = 'augment:generate-login-url', 24 | /** ipc-校验code */ 25 | Augment_VerifyCode = 'augment:verify-code' 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "explorer.fileNesting.enabled": true, 3 | "explorer.fileNesting.patterns": { 4 | "package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .prettier*, prettier*, .editorconfig", 5 | "tsconfig.json": "tsconfig.node.json, tsconfig.web.json, vite.config.ts, electron.vite.config.ts" 6 | }, 7 | "[typescript]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | "[javascript]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "[json]": { 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | }, 16 | "editor.codeActionsOnSave": { 17 | "source.fixAll.eslint": "explicit", 18 | "source.fixAll.stylelint": "explicit", 19 | "source.organizeImports": "never" 20 | }, 21 | "editor.defaultFormatter": "esbenp.prettier-vscode", 22 | "cSpell.words": ["regedit", "heroui", "ahooks", "elecp"], 23 | "commentTranslate.source": "Bing" 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | /** 类名合并 */ 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)) 7 | } 8 | 9 | /** 10 | * 判断是否为空 11 | * @param {any} param 12 | */ 13 | export function isEmpty(param: any) { 14 | return [undefined, null, ''].includes(param) 15 | } 16 | 17 | /** 18 | * 生成枚举数据组 19 | * @param rest 参数, 参数数量必须为偶数, 奇数为id, 偶数为label 20 | * @returns 枚举数据组 21 | */ 22 | export function generatorDataGroup(...rest: any[]) { 23 | if (!rest) { 24 | return [] 25 | } 26 | 27 | if (rest.length % 2 !== 0) { 28 | throw new Error('参数数量不匹配') 29 | } 30 | const list: { id: any; label: any }[] = [] 31 | const map = {} 32 | for (let i = 0; i < rest.length; i += 2) { 33 | const id = rest[i] 34 | const label = rest[i + 1] 35 | list.push({ id, label }) 36 | Object.assign(map, { [id]: label }) 37 | } 38 | return [list, map] 39 | } 40 | -------------------------------------------------------------------------------- /src/main/utils/secrets.ts: -------------------------------------------------------------------------------- 1 | import { safeStorage } from 'electron' 2 | 3 | /** 4 | * 加密数据 5 | * @param {any} data 需要加密的数据 6 | * @returns {string} 加密后的数据JSON.stringify(Buffer) 7 | */ 8 | const encrypt = (data: any) => { 9 | try { 10 | const value = typeof data === 'string' ? data : JSON.stringify(data) 11 | const buffer = safeStorage.encryptString(value) 12 | return JSON.stringify(buffer) 13 | } catch (error: any) { 14 | console.error('encrypt error:', error.message) 15 | throw error 16 | } 17 | } 18 | 19 | /** 20 | * 解密数据 21 | * @param {number[]} data 需要解密的数据 22 | * @returns {string | null} 解密后的数据 23 | */ 24 | const decrypt = (data: number[]): string | null => { 25 | try { 26 | const result = safeStorage.decryptString(Buffer.from(data)) 27 | return result 28 | } catch (error: any) { 29 | console.error('decrypt error:', error.message) 30 | throw error 31 | } 32 | } 33 | 34 | const secrets = { 35 | encrypt, 36 | decrypt 37 | } 38 | 39 | export default secrets 40 | -------------------------------------------------------------------------------- /electron.vite.config.ts: -------------------------------------------------------------------------------- 1 | import tailwindcss from '@tailwindcss/vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite' 4 | import { resolve } from 'path' 5 | 6 | export default defineConfig({ 7 | main: { 8 | plugins: [externalizeDepsPlugin()], 9 | resolve: { 10 | alias: { 11 | '@main': resolve('src/main'), 12 | '@types': resolve('src/renderer/src/types'), 13 | '@shared': resolve('packages/shared') 14 | } 15 | } 16 | }, 17 | preload: { 18 | plugins: [externalizeDepsPlugin()], 19 | resolve: { 20 | alias: { 21 | '@shared': resolve('packages/shared'), 22 | '@types': resolve('src/renderer/src/types') 23 | } 24 | } 25 | }, 26 | renderer: { 27 | resolve: { 28 | alias: { 29 | '@shared': resolve('packages/shared'), 30 | '@renderer': resolve('src/renderer/src') 31 | } 32 | }, 33 | plugins: [react(), tailwindcss()] 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Main Process", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceRoot}", 9 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", 10 | "windows": { 11 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" 12 | }, 13 | "runtimeArgs": ["--sourcemap"], 14 | "env": { 15 | "REMOTE_DEBUGGING_PORT": "9222" 16 | } 17 | }, 18 | { 19 | "name": "Debug Renderer Process", 20 | "port": 9222, 21 | "request": "attach", 22 | "type": "chrome", 23 | "webRoot": "${workspaceFolder}/src/renderer", 24 | "timeout": 60000, 25 | "presentation": { 26 | "hidden": true 27 | } 28 | } 29 | ], 30 | "compounds": [ 31 | { 32 | "name": "Debug All", 33 | "configurations": ["Debug Main Process", "Debug Renderer Process"], 34 | "presentation": { 35 | "order": 1 36 | } 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer/src/types/index.ts: -------------------------------------------------------------------------------- 1 | /** 开发环境 */ 2 | export enum IDE { 3 | VSCode = 'Code', 4 | Cursor = 'Cursor', 5 | Windsurf = 'Windsurf' 6 | } 7 | 8 | /** ipc响应类型 */ 9 | export interface IpcResponse { 10 | success: boolean 11 | data: T 12 | message?: string 13 | } 14 | 15 | export interface AppInfo { 16 | /** 版本 */ 17 | version: string 18 | /** 是否打包 */ 19 | isPackaged: boolean 20 | /** 架构 */ 21 | arch: string 22 | /** 是否便携 */ 23 | isPortable: boolean 24 | /** 应用路径 */ 25 | appPath: string 26 | /** 资源路径 */ 27 | resourcesPath: string 28 | /** 可执行文件路径 */ 29 | exePath: string 30 | /** 用户数据路径 */ 31 | userDataPath: string 32 | /** 会话数据路径(和userDataPath一样) */ 33 | sessionPath: string 34 | /** 日志路径 */ 35 | logsPath: string 36 | /** 崩溃转储路径 */ 37 | crashDumpsPath: string 38 | /** 文件路径 */ 39 | filesPath: string 40 | /** (当前ide)扩展路径 如xxx/.vscode/extensions */ 41 | extensionsPath: string 42 | /** 会话备份路径 */ 43 | backupsPath: string 44 | } 45 | 46 | export interface MachineIds { 47 | machineId: string 48 | macMachineId: string 49 | devDeviceId: string 50 | sqmId: string 51 | } 52 | 53 | export enum DecryptType { 54 | /** 用户数据路径 */ 55 | UserDataPath = 'userDataPath', 56 | /** 秘钥文件 */ 57 | KeyFile = 'keyFile' 58 | } 59 | 60 | export interface Config { 61 | type?: DecryptType 62 | editor?: IDE 63 | } 64 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslintConfigPrettier from '@electron-toolkit/eslint-config-prettier' 2 | import tseslint from '@electron-toolkit/eslint-config-ts' 3 | import eslintReact from '@eslint-react/eslint-plugin' 4 | import eslintPluginReact from 'eslint-plugin-react' 5 | import reactHooks from 'eslint-plugin-react-hooks' 6 | import reactRefresh from 'eslint-plugin-react-refresh' 7 | import simpleImportSort from 'eslint-plugin-simple-import-sort' 8 | 9 | export default tseslint.config( 10 | tseslint.configs.recommended, 11 | eslintConfigPrettier, 12 | eslintPluginReact.configs.flat.recommended, 13 | eslintPluginReact.configs.flat['jsx-runtime'], 14 | eslintReact.configs['recommended-typescript'], 15 | { 16 | settings: { 17 | react: { 18 | version: 'detect' 19 | } 20 | } 21 | }, 22 | { 23 | plugins: { 24 | 'simple-import-sort': simpleImportSort 25 | }, 26 | rules: { 27 | 'simple-import-sort/imports': 'error', 28 | 'simple-import-sort/exports': 'error', 29 | '@typescript-eslint/no-explicit-any': 'off', 30 | '@typescript-eslint/no-unused-vars': 'warn' 31 | } 32 | }, 33 | { 34 | files: ['**/*.{ts,tsx}'], 35 | plugins: { 36 | 'react-hooks': reactHooks, 37 | 'react-refresh': reactRefresh 38 | }, 39 | rules: { 40 | ...reactHooks.configs.recommended.rules, 41 | ...reactRefresh.configs.vite.rules, 42 | '@typescript-eslint/explicit-function-return-type': 'off', 43 | '@eslint-react/hooks-extra/no-direct-set-state-in-use-effect': 'off', 44 | '@eslint-react/no-unstable-default-props': 'off', 45 | 'no-async-promise-executor': 'off', 46 | 'no-empty': 'off' 47 | } 48 | }, 49 | { ignores: ['**/node_modules', '**/dist', '**/out'] } 50 | ) 51 | -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { electronApp, optimizer } from '@electron-toolkit/utils' 2 | import { DecryptType, IDE } from '@types' 3 | import { app, BrowserWindow } from 'electron' 4 | import fs from 'fs-extra' 5 | 6 | import { APP_NAME } from './constant' 7 | import { registerIPCHandlers } from './ipc' 8 | import { windowService } from './services/WindowService' 9 | import { getConfigPath, readJsonFile, setUserDataPath, writeJsonFile } from './utils/paths' 10 | 11 | const initConfig = { 12 | type: DecryptType.UserDataPath, 13 | editor: IDE.VSCode 14 | } 15 | 16 | const configPath = getConfigPath() 17 | if (!fs.existsSync(configPath)) { 18 | writeJsonFile(configPath, initConfig) 19 | } 20 | const config = readJsonFile(configPath) 21 | if (config.type === DecryptType.UserDataPath) { 22 | setUserDataPath(config.editor) 23 | } else { 24 | setUserDataPath() // 为空时 设置为自身应用路径,开发环境(account-switch-demo-dev)/生产环境(account-switch-demo) 25 | } 26 | // setUserDataPath(xx)等同于下面 27 | // const idePath = path.join(app.getPath('appData'), xx) 28 | // app.setPath('userData', idePath) 29 | console.log('初始化时用户数据路径: ', app.getPath('userData')) 30 | 31 | app.whenReady().then(() => { 32 | setUserDataPath() // 修正回自身应用路径 33 | console.log('修正后的用户数据路径: ', app.getPath('userData')) 34 | electronApp.setAppUserModelId('com.dami.' + APP_NAME) 35 | 36 | windowService.createMainWindow() 37 | 38 | registerIPCHandlers() 39 | 40 | app.on('activate', function () { 41 | if (BrowserWindow.getAllWindows().length === 0) windowService.createMainWindow() 42 | }) 43 | }) 44 | 45 | app.on('browser-window-created', (_, window) => { 46 | optimizer.watchWindowShortcuts(window) 47 | }) 48 | 49 | app.on('window-all-closed', () => { 50 | if (process.platform !== 'darwin') { 51 | app.quit() 52 | } 53 | }) 54 | 55 | app.on('will-quit', () => { 56 | console.log('will-quit') 57 | }) 58 | -------------------------------------------------------------------------------- /src/main/constant.ts: -------------------------------------------------------------------------------- 1 | export const isMac = process.platform === 'darwin' 2 | export const isWin = process.platform === 'win32' 3 | export const isLinux = process.platform === 'linux' 4 | export const isDev = process.env.NODE_ENV === 'development' 5 | export const isPortable = isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env 6 | 7 | /** 应用名称 */ 8 | export const APP_NAME = 'account-switch-demo' 9 | /** 应用名称-开发环境 */ 10 | export const APP_NAME_DEV = 'account-switch-demo-dev' 11 | /** 协议名称 */ 12 | export const PROTOCOL_NAME = 'account-switch-demo' 13 | 14 | /** 标题栏样式-暗色 */ 15 | export const titleBarOverlayDark = { 16 | height: 40, 17 | color: 'rgba(255,255,255,0)', 18 | symbolColor: '#fff' 19 | } 20 | 21 | /** 标题栏样式-亮色 */ 22 | export const titleBarOverlayLight = { 23 | height: 40, 24 | color: 'rgba(255,255,255,0)', 25 | symbolColor: '#000' 26 | } 27 | 28 | /** augment版本 */ 29 | let AUGMENT_VERSION = '0.522.0' 30 | /** vscode版本 */ 31 | let VSCODE_VERSION = '1.102.5' 32 | /** cursor版本 */ 33 | let CURSOR_VERSION = '1.4.5' 34 | 35 | /** 设置augment版本 */ 36 | export const setAugmentVersion = (version: string | null) => { 37 | if (version) { 38 | AUGMENT_VERSION = version 39 | } 40 | } 41 | 42 | /** 设置vscode版本 */ 43 | export const setIdeVersion = (vscodeVersion: string | null, cursorVersion: string | null) => { 44 | if (vscodeVersion) { 45 | VSCODE_VERSION = vscodeVersion 46 | } 47 | if (cursorVersion) { 48 | CURSOR_VERSION = cursorVersion 49 | } 50 | } 51 | 52 | /** 获取augment UserAgent */ 53 | export const getAugmentUserAgent = () => { 54 | return `Augment.vscode-augment/${AUGMENT_VERSION} (win32; x64; 10.0.26100) vscode/${VSCODE_VERSION}` 55 | } 56 | 57 | /** 获取cursor UserAgent */ 58 | export const getCursorUserAgent = () => { 59 | return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Cursor/${CURSOR_VERSION} Chrome/132.0.6834.210 Electron/34.5.8 Safari/537.36` 60 | } 61 | -------------------------------------------------------------------------------- /src/main/utils/paths.ts: -------------------------------------------------------------------------------- 1 | import { APP_NAME, APP_NAME_DEV, isDev } from '@main/constant' 2 | import { IDE } from '@types' 3 | import { app } from 'electron' 4 | import fs from 'fs-extra' 5 | import path from 'path' 6 | 7 | /** 8 | * 确保目录存在 9 | * @param dir 目录路径 10 | */ 11 | export function makeSureDirExists(dir: string) { 12 | if (!fs.existsSync(dir)) { 13 | fs.mkdirSync(dir, { recursive: true }) 14 | } 15 | } 16 | 17 | /** 18 | * 获取指定项目用户数据目录 19 | * @param appName 项目名称 默认本应用名称 20 | * @returns 用户数据目录 21 | */ 22 | export function getUserDataPath(appName?: typeof APP_NAME | typeof APP_NAME_DEV | IDE) { 23 | if (!appName) { 24 | appName = isDev ? APP_NAME_DEV : APP_NAME 25 | } 26 | const userDataPath = path.join(app.getPath('appData'), appName) 27 | makeSureDirExists(userDataPath) 28 | return userDataPath 29 | } 30 | 31 | /** 32 | * 设置指定项目用户数据目录 33 | * @param appName 项目名称 默认本应用名称 34 | */ 35 | export function setUserDataPath(appName?: typeof APP_NAME | typeof APP_NAME_DEV | IDE) { 36 | if (!appName) { 37 | appName = isDev ? APP_NAME_DEV : APP_NAME 38 | } 39 | const userDataPath = getUserDataPath(appName) 40 | app.setPath('userData', userDataPath) 41 | } 42 | 43 | /** 44 | * 获取配置文件路径 45 | */ 46 | export function getConfigPath() { 47 | return path.join(getUserDataPath(), 'config.json') 48 | } 49 | 50 | /** 51 | * 获取IDE storage路径 52 | * @param ide 指定IDE 53 | */ 54 | export function getIDEStoragePath(ide: IDE) { 55 | return path.join(getUserDataPath(ide), 'User', 'globalStorage', 'storage.json') 56 | } 57 | 58 | /** 59 | * 读取json文件 60 | * @param filePath 文件路径 61 | * @param options 选项 62 | * @returns 文件内容 63 | */ 64 | export function readJsonFile(filePath: string, options?: fs.ReadOptions): any { 65 | return fs.readJSONSync(filePath, { throws: false, encoding: 'utf-8', ...options }) 66 | } 67 | 68 | /** 69 | * 写入json文件 70 | * @param filePath 文件路径 71 | * @param data 数据 72 | * @param options 选项 73 | */ 74 | export function writeJsonFile(filePath: string, data: object, options?: fs.WriteOptions) { 75 | fs.writeJSONSync(filePath, data, { encoding: 'utf-8', spaces: 4, ...options }) 76 | } 77 | -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: com.dami.account-switch-demo 2 | productName: 加密/解密演示 3 | icon: build/icon256x256.ico 4 | directories: 5 | buildResources: build 6 | files: 7 | - '!**/.vscode/*' 8 | - '!src/*' 9 | - '!electron.vite.config.{js,ts,mjs,cjs}' 10 | - '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' 11 | - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' 12 | - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' 13 | - '!**/node_modules/**/*.map' 14 | asarUnpack: 15 | - resources/** 16 | - node_modules/regedit/** 17 | win: 18 | executableName: account-switch-demo 19 | target: 20 | - target: nsis # 生成安装包 21 | - target: portable # 生成免安装单文件 22 | artifactName: ${productName}-${version}-${arch}-setup.${ext} 23 | # requestedExecutionLevel: highestAvailable # 提升权限 24 | nsis: 25 | oneClick: false # 禁用一键安装,显示向导 26 | perMachine: false # 禁用系统级安装,显示用户级安装 27 | allowElevation: true # 允许提升权限 28 | allowToChangeInstallationDirectory: true # 允许更改安装目录 29 | createDesktopShortcut: true # 创建桌面快捷方式 30 | createStartMenuShortcut: true # 创建开始菜单快捷方式 31 | artifactName: ${productName}-${version}-${arch}-setup.${ext} 32 | shortcutName: ${productName} 33 | runAfterFinish: true # 安装完成后运行应用 34 | guid: 'com.dami.account-switch-demo' # 应用唯一标识 35 | portable: 36 | artifactName: ${productName}-${version}-${arch}-portable.${ext} 37 | mac: 38 | entitlementsInherit: build/entitlements.mac.plist 39 | artifactName: ${productName}-${version}-${arch}.${ext} 40 | extendInfo: 41 | - NSCameraUsageDescription: Application requests access to the device's camera. 42 | - NSMicrophoneUsageDescription: Application requests access to the device's microphone. 43 | - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. 44 | - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. 45 | notarize: false 46 | target: 47 | - target: dmg 48 | - target: zip 49 | linux: 50 | artifactName: ${productName}-${version}-${arch}.${ext} 51 | target: 52 | - AppImage 53 | maintainer: electronjs.org 54 | category: Utility 55 | appImage: 56 | artifactName: ${name}-${version}.${ext} 57 | npmRebuild: false 58 | publish: 59 | provider: generic 60 | url: https://example.com/auto-updates 61 | electronDownload: 62 | mirror: https://npmmirror.com/mirrors/electron/ 63 | -------------------------------------------------------------------------------- /src/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { electronAPI } from '@electron-toolkit/preload' 2 | import { IpcChannel } from '@shared/IpcChannel' 3 | import { AppInfo, Config, IDE, IpcResponse, MachineIds } from '@types' 4 | import { contextBridge, ipcRenderer } from 'electron' 5 | 6 | // Custom APIs for renderer 7 | const api = { 8 | /** 获取应用信息 */ 9 | getAppInfo: () => ipcRenderer.invoke(IpcChannel.App_Info) as Promise, 10 | 11 | /** 重启应用 */ 12 | reload: () => ipcRenderer.invoke(IpcChannel.App_Reload), 13 | 14 | /** 获取配置 */ 15 | getConfig: () => ipcRenderer.invoke(IpcChannel.App_GetConfig) as Promise>, 16 | /** 设置配置 */ 17 | setConfig: (config: Config) => 18 | ipcRenderer.invoke(IpcChannel.App_SetConfig, config) as Promise>, 19 | 20 | secrets: { 21 | /** secrets-加密 */ 22 | encrypt: (data: any) => ipcRenderer.invoke(IpcChannel.Secrets_Encrypt, data), 23 | /** secrets-解密 */ 24 | decrypt: (data: number[]) => ipcRenderer.invoke(IpcChannel.Secrets_Decrypt, data) 25 | }, 26 | augment: { 27 | /** 生成登录链接 */ 28 | generateLoginUrl: (ide: IDE) => 29 | ipcRenderer.invoke(IpcChannel.Augment_GenerateLoginUrl, ide) as Promise, 30 | /** 校验code */ 31 | verifyCode: (code: string, tenantUrl: string, redirectUri: string) => 32 | ipcRenderer.invoke(IpcChannel.Augment_VerifyCode, code, tenantUrl, redirectUri) 33 | }, 34 | machine: { 35 | /** 获取机器ids */ 36 | getMachineIds: (ide: IDE) => 37 | ipcRenderer.invoke(IpcChannel.App_GetMachineIds, ide) as Promise>, 38 | /** 设置随机机器ids */ 39 | setRandomMachineIds: (ide: IDE) => 40 | ipcRenderer.invoke(IpcChannel.App_SetRandomMachineIds, ide) as Promise< 41 | IpcResponse 42 | >, 43 | /** 获取注册表guid */ 44 | getRegistryMachineGuid: () => 45 | ipcRenderer.invoke(IpcChannel.App_GetRegistryMachineGuid) as Promise>, 46 | /** 设置随机注册表guid */ 47 | setRandomRegistryMachineGuid: () => 48 | ipcRenderer.invoke(IpcChannel.App_SetRandomRegistryMachineGuid) as Promise< 49 | IpcResponse 50 | > 51 | } 52 | } 53 | 54 | if (process.contextIsolated) { 55 | try { 56 | contextBridge.exposeInMainWorld('electron', electronAPI) 57 | contextBridge.exposeInMainWorld('api', api) 58 | } catch (error) { 59 | console.error(error) 60 | } 61 | } else { 62 | // @ts-ignore (define in dts) 63 | window.electron = electronAPI 64 | // @ts-ignore (define in dts) 65 | window.api = api 66 | } 67 | 68 | export type WindowApiType = typeof api 69 | -------------------------------------------------------------------------------- /src/main/utils/registryHelper.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process' 2 | import { promisify } from 'util' 3 | 4 | const execAsync = promisify(exec) 5 | 6 | /** 7 | * 使用 Windows REG 命令行工具作为备用方案 8 | * 这个方法不依赖第三方库,直接使用系统命令 9 | */ 10 | export class WindowsRegistryHelper { 11 | /** 12 | * 读取注册表值 13 | * @param keyPath 注册表键路径 14 | * @param valueName 值名称 15 | * @returns 注册表值或 null 16 | */ 17 | static async getValue(keyPath: string, valueName: string): Promise { 18 | try { 19 | const command = `reg query "${keyPath}" /v "${valueName}"` 20 | console.log(`执行注册表读取命令: ${command}`) 21 | const { stdout } = await execAsync(command) 22 | // 解析输出,格式类似: 23 | // HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography 24 | // MachineGuid REG_SZ {12345678-1234-1234-1234-123456789012} 25 | const lines = stdout.split('\n') 26 | for (const line of lines) { 27 | if (line.includes(valueName)) { 28 | const parts = line.trim().split(/\s+/) 29 | if (parts.length >= 3 && parts[0] === valueName) { 30 | const value = parts.slice(2).join(' ').trim() 31 | return value 32 | } 33 | } 34 | } 35 | console.error(`未找到注册表值: ${valueName}`) 36 | return null 37 | } catch (error) { 38 | console.error( 39 | 'Windows Registry 读取失败:', 40 | JSON.stringify( 41 | { 42 | error: error.message, 43 | path: keyPath, 44 | name: valueName 45 | }, 46 | null, 47 | 2 48 | ) 49 | ) 50 | throw new Error(error.message) 51 | } 52 | } 53 | 54 | /** 55 | * 设置注册表值 56 | * @param keyPath 注册表键路径 57 | * @param valueName 值名称 58 | * @param value 值内容 59 | * @param valueType 值类型,默认 REG_SZ 60 | */ 61 | static async setValue( 62 | keyPath: string, 63 | valueName: string, 64 | value: string, 65 | valueType: string = 'REG_SZ' 66 | ) { 67 | try { 68 | const command = `reg add "${keyPath}" /v "${valueName}" /t ${valueType} /d "${value}" /f` 69 | console.log(`执行注册表写入命令: ${command}`) 70 | await execAsync(command) 71 | } catch (error) { 72 | console.error( 73 | 'Windows Registry 写入失败:', 74 | JSON.stringify( 75 | { 76 | error: error.message, 77 | path: keyPath, 78 | name: valueName, 79 | value 80 | }, 81 | null, 82 | 2 83 | ) 84 | ) 85 | throw new Error(error.message) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "account-switch-demo", 3 | "version": "1.0.0", 4 | "description": "加密/解密演示", 5 | "main": "./out/main/index.js", 6 | "author": "dami", 7 | "homepage": "https://github.com/yuxinle1996", 8 | "scripts": { 9 | "dev": "electron-vite dev", 10 | "start": "electron-vite preview", 11 | "fix": "eslint --fix .", 12 | "format": "prettier --write .", 13 | "lint": "eslint --cache .", 14 | "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", 15 | "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", 16 | "typecheck": "npm run typecheck:node && npm run typecheck:web", 17 | "build": "npm run typecheck && electron-vite build", 18 | "postinstall": "electron-builder install-app-deps", 19 | "build:unpack": "npm run build && electron-builder --dir", 20 | "build:win": "npm run build && electron-builder --win", 21 | "build:mac": "electron-vite build && electron-builder --mac", 22 | "build:linux": "electron-vite build && electron-builder --linux" 23 | }, 24 | "dependencies": { 25 | "@electron-toolkit/preload": "^3.0.2", 26 | "@electron-toolkit/utils": "^4.0.0", 27 | "@heroui/react": "^2.8.2", 28 | "axios": "^1.11.0", 29 | "chrome-launcher": "^1.2.0", 30 | "clsx": "^2.1.1", 31 | "dayjs": "^1.11.13", 32 | "electron-log": "^5.4.3", 33 | "framer-motion": "^12.23.12", 34 | "fs-extra": "^11.3.1", 35 | "lucide-react": "^0.541.0", 36 | "uuid": "^11.1.0" 37 | }, 38 | "devDependencies": { 39 | "@electron-toolkit/eslint-config-prettier": "^3.0.0", 40 | "@electron-toolkit/eslint-config-ts": "^3.1.0", 41 | "@electron-toolkit/tsconfig": "^1.0.1", 42 | "@electron/rebuild": "^4.0.1", 43 | "@eslint-react/eslint-plugin": "^1.52.6", 44 | "@tailwindcss/typography": "^0.5.16", 45 | "@tailwindcss/vite": "^4.1.12", 46 | "@types/node": "^22.16.5", 47 | "@types/react": "^19.1.11", 48 | "@types/react-dom": "^19.1.7", 49 | "@vitejs/plugin-react-swc": "^4.0.1", 50 | "electron": "^37.3.1", 51 | "electron-builder": "^26.0.12", 52 | "electron-vite": "^4.0.0", 53 | "eslint": "^9.34.0", 54 | "eslint-plugin-react": "^7.37.5", 55 | "eslint-plugin-react-hooks": "^5.2.0", 56 | "eslint-plugin-react-refresh": "^0.4.20", 57 | "eslint-plugin-simple-import-sort": "^12.1.1", 58 | "prettier": "^3.6.2", 59 | "prettier-plugin-tailwindcss": "^0.6.14", 60 | "react": "^19.1.1", 61 | "react-dom": "^19.1.1", 62 | "tailwind-merge": "^3.3.1", 63 | "tailwindcss": "^4.1.12", 64 | "typescript": "^5.9.2", 65 | "vite": "^7.1.3", 66 | "zod": "^4.1.1" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/ipc/handle.ts: -------------------------------------------------------------------------------- 1 | import { getHash, toUrlSafeBase64 } from '@main/utils' 2 | import { getConfigPath, getUserDataPath, readJsonFile, writeJsonFile } from '@main/utils/paths' 3 | import { failure, success } from '@main/utils/response' 4 | import { Config, DecryptType, IDE } from '@types' 5 | import axios from 'axios' 6 | import { randomBytes, randomUUID } from 'crypto' 7 | import dayjs from 'dayjs' 8 | import fs from 'fs-extra' 9 | import path from 'path' 10 | 11 | let oauthState: any = {} 12 | 13 | /** 读取配置 */ 14 | export function getConfig() { 15 | try { 16 | const configPath = getConfigPath() 17 | const config = readJsonFile(configPath) 18 | return success(config) 19 | } catch (error) { 20 | return failure(error.message) 21 | } 22 | } 23 | 24 | /** 设置配置 */ 25 | export function setConfig(config: Config) { 26 | try { 27 | const configPath = getConfigPath() 28 | const currentConfig: Config = readJsonFile(configPath) 29 | const newConfig = { ...currentConfig, ...config } 30 | writeJsonFile(configPath, newConfig) 31 | if (newConfig.type === DecryptType.KeyFile) { 32 | const ideKeyFilePath = path.join(getUserDataPath(newConfig.editor), 'Local State') 33 | // 将目标ide的key文件复制到自身应用用户数据路径 34 | const myKeyFilePath = path.join(getUserDataPath(), 'Local State') 35 | fs.copyFileSync(ideKeyFilePath, myKeyFilePath) 36 | } 37 | return success(newConfig) 38 | } catch (error) { 39 | return failure(error.message) 40 | } 41 | } 42 | 43 | /** 生成登录链接 */ 44 | export function generateLoginUrl(ide: IDE) { 45 | try { 46 | const codeVerifier = toUrlSafeBase64(randomBytes(32)) 47 | const codeChallenge = toUrlSafeBase64(getHash(Buffer.from(codeVerifier))) 48 | const state = randomUUID() 49 | const openUrl = `https://auth.augmentcode.com/authorize?response_type=code&code_challenge=${codeChallenge}&code_challenge_method=S256&client_id=augment-vscode-extension&redirect_uri=${ide}://augment.vscode-augment/auth/result&state=${state}&scope=email&prompt=login` 50 | 51 | oauthState = { 52 | /** 校验码(生成token时需要) */ 53 | creationTime: dayjs().add(10, 'm').format('YYYY-MM-DD HH:mm:ss'), 54 | codeVerifier, 55 | codeChallenge, 56 | state 57 | } 58 | // database.items.set('oauthState', oauthState) 59 | return success(openUrl) 60 | } catch (error: any) { 61 | return failure(error.message) 62 | } 63 | } 64 | 65 | /** 校验codeVerifier */ 66 | export async function verifyCode(code: string, tenantUrl: string, redirectUri: string) { 67 | try { 68 | // const oauthState = await database.items.get('oauthState') 69 | if (!oauthState || !oauthState.codeVerifier) return failure('已过期') 70 | if (dayjs(oauthState.creationTime).isBefore(dayjs())) { 71 | // database.items.delete('oauthState') 72 | oauthState = {} 73 | return failure('已过期') 74 | } 75 | tenantUrl = tenantUrl.endsWith('/') ? tenantUrl : tenantUrl + '/' 76 | const res = await axios.post(`${tenantUrl}token`, { 77 | grant_type: 'authorization_code', 78 | client_id: 'augment-vscode-extension', 79 | code_verifier: oauthState.codeVerifier, 80 | redirect_uri: redirectUri, 81 | code: code 82 | }) 83 | if (res.status !== 200) return failure(res.data?.error || '未知错误') 84 | const token = res.data?.access_token 85 | return success({ token }) 86 | } catch (error: any) { 87 | return failure(error?.response?.data?.error || error.message || '未知错误') 88 | } finally { 89 | // database.items.delete('oauthState') 90 | oauthState = {} 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/ipc/machine.ts: -------------------------------------------------------------------------------- 1 | import { failure, success } from '@main/utils/response' 2 | import { IDE, IpcResponse, MachineIds } from '@types' 3 | import crypto from 'crypto' 4 | import { v4 as uuidv4 } from 'uuid' 5 | 6 | import { getIDEStoragePath, readJsonFile, writeJsonFile } from '../utils/paths' 7 | import { WindowsRegistryHelper } from '../utils/registryHelper' 8 | 9 | const REGISTRY_PATH = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography' 10 | 11 | /** 12 | * 生成ids 13 | * @returns ids 14 | */ 15 | function generateMachineIds(): MachineIds { 16 | try { 17 | const machineId = crypto.createHash('sha256').update(crypto.randomBytes(32)).digest('hex') 18 | const macMachineId = crypto.createHash('sha512').update(crypto.randomBytes(64)).digest('hex') 19 | const devDeviceId = uuidv4() 20 | const sqmId = '{' + uuidv4().toUpperCase() + '}' 21 | 22 | return { 23 | machineId, 24 | macMachineId, 25 | devDeviceId, 26 | sqmId 27 | } 28 | } catch (error) { 29 | throw new Error(error.message) 30 | } 31 | } 32 | 33 | /** 34 | * 获取机器ids 35 | * @param ide ide 36 | * @returns 机器ids 37 | */ 38 | export function getMachineIds(ide: IDE): IpcResponse { 39 | try { 40 | const storagePath = getIDEStoragePath(ide) 41 | const storageObj = readJsonFile(storagePath) || {} 42 | return success({ 43 | machineId: storageObj['telemetry.machineId'], 44 | macMachineId: storageObj['telemetry.macMachineId'], 45 | devDeviceId: storageObj['telemetry.devDeviceId'], 46 | sqmId: storageObj['telemetry.sqmId'] 47 | }) 48 | } catch (error) { 49 | return failure(error.message) 50 | } 51 | } 52 | 53 | /** 54 | * 设置随机ids 55 | * @param ide ide 56 | * @returns 新的机器ids 57 | */ 58 | export function setRandomMachineIds(ide: IDE): IpcResponse { 59 | try { 60 | const machineIds = generateMachineIds() 61 | const storagePath = getIDEStoragePath(ide) 62 | const storageObj = readJsonFile(storagePath) || {} 63 | storageObj['telemetry.machineId'] = machineIds.machineId 64 | storageObj['telemetry.macMachineId'] = machineIds.macMachineId 65 | storageObj['telemetry.devDeviceId'] = machineIds.devDeviceId 66 | storageObj['telemetry.sqmId'] = machineIds.sqmId 67 | writeJsonFile(storagePath, storageObj) 68 | return success(machineIds) 69 | } catch (error) { 70 | return failure(error.message) 71 | } 72 | } 73 | 74 | /** 75 | * 获取注册表guid 76 | * @returns 注册表guid 77 | */ 78 | export async function getRegistryMachineGuid(): Promise> { 79 | try { 80 | const machineGuid = await WindowsRegistryHelper.getValue(REGISTRY_PATH, 'MachineGuid') 81 | if (!machineGuid) { 82 | throw new Error('MachineGuid 不存在') 83 | } 84 | return success(machineGuid) 85 | } catch (error) { 86 | if (error.message?.includes('Command failed')) { 87 | return failure('请以管理员身份运行') 88 | } 89 | return failure(error.message) 90 | } 91 | } 92 | 93 | /** 94 | * 设置随机注册表guid 95 | * @returns 新的注册表guid 96 | */ 97 | export async function setRandomRegistryMachineGuid(): Promise> { 98 | try { 99 | const value = uuidv4() 100 | await WindowsRegistryHelper.setValue(REGISTRY_PATH, 'MachineGuid', value) 101 | return success(value) 102 | } catch (error) { 103 | if (error.message?.includes('Command failed')) { 104 | return failure('请以管理员身份运行') 105 | } 106 | return failure(error.message) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/ipc/index.ts: -------------------------------------------------------------------------------- 1 | import { arch } from 'node:os' 2 | 3 | import { isWin } from '@main/constant' 4 | import secrets from '@main/utils/secrets' 5 | import { IpcChannel } from '@shared/IpcChannel' 6 | import { Config, IDE } from '@types' 7 | import { app, ipcMain } from 'electron' 8 | 9 | import { generateLoginUrl, getConfig, setConfig, verifyCode } from './handle' 10 | import { 11 | getMachineIds, 12 | getRegistryMachineGuid, 13 | setRandomMachineIds, 14 | setRandomRegistryMachineGuid 15 | } from './machine' 16 | 17 | export function registerIPCHandlers() { 18 | // 获取应用信息 19 | ipcMain.handle(IpcChannel.App_Info, () => ({ 20 | // 1.0.0 21 | version: app.getVersion(), 22 | // 是否被打包 23 | isPackaged: app.isPackaged, 24 | // x64 25 | arch: arch(), 26 | // 是否为便携版 27 | isPortable: isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env, 28 | // E:\\code\\electron\\account-switch-demo 29 | appPath: app.getAppPath(), 30 | // E:\\code\\electron\\account-switch-demo\\node_modules\\electron\\dist\\electron.exe 31 | exePath: app.getPath('exe'), 32 | // E:\\code\\electron\\account-switch-demo\\node_modules\\electron\\dist\\electron.exe 33 | // modulePath: app.getPath('module'), 34 | // C:\\Users\\dami 35 | // homePath: app.getPath('home'), 36 | // C:\\Users\\dami\\Desktop 37 | // desktopPath: app.getPath('desktop'), 38 | // C:\\Users\\dami\\Documents 39 | // documentsPath: app.getPath('documents'), 40 | // C:\\Users\\dami\\Downloads 41 | // downloadsPath: app.getPath('downloads'), 42 | // C:\\Users\\dami\\Music 43 | // musicPath: app.getPath('music'), 44 | // C:\\Users\\dami\\Pictures 45 | // picturesPath: app.getPath('pictures'), 46 | // C:\\Users\\dami\\Videos 47 | // videosPath: app.getPath('videos'), 48 | // C:\\Users\\dami\\AppData\\Roaming 49 | // dataPath: app.getPath('appData'), 50 | // C:\\Users\\dami\\AppData\\Roaming\\account-switch-demo-dev 51 | userDataPath: app.getPath('userData'), 52 | // C:\\Users\\dami\\AppData\\Roaming\\account-switch-demo-dev 53 | sessionPath: app.getPath('sessionData'), 54 | // C:\\Users\\dami\\AppData\\Roaming\\account-switch-demo-dev\\logs 55 | logsPath: app.getPath('logs'), 56 | // C:\\Users\\dami\\AppData\\Roaming\\account-switch-demo-dev\\Crashpad 57 | crashDumpsPath: app.getPath('crashDumps') 58 | // C:\\Users\\dami\\AppData\\Local\\Temp 59 | // tempPath: app.getPath('temp'), 60 | // C:\\Users\\dami\\AppData\\Roaming\\Microsoft\\Windows\\Recent 61 | // recentPath: app.getPath('recent'), 62 | })) 63 | 64 | // 重启应用 65 | ipcMain.handle(IpcChannel.App_Reload, () => { 66 | app.relaunch() 67 | app.exit(0) 68 | }) 69 | 70 | // 获取配置 71 | ipcMain.handle(IpcChannel.App_GetConfig, () => { 72 | return getConfig() 73 | }) 74 | 75 | // 设置配置 76 | ipcMain.handle(IpcChannel.App_SetConfig, (_, config: Config) => { 77 | return setConfig(config) 78 | }) 79 | 80 | // secrets-加密 81 | ipcMain.handle(IpcChannel.Secrets_Encrypt, (_, data: any) => { 82 | return secrets.encrypt(data) 83 | }) 84 | 85 | // secrets-解密 86 | ipcMain.handle(IpcChannel.Secrets_Decrypt, (_, data: number[]) => { 87 | return secrets.decrypt(data) 88 | }) 89 | 90 | // 生成登录链接 91 | ipcMain.handle(IpcChannel.Augment_GenerateLoginUrl, (_, ide: IDE) => { 92 | return generateLoginUrl(ide) 93 | }) 94 | 95 | // 校验code 96 | ipcMain.handle( 97 | IpcChannel.Augment_VerifyCode, 98 | (_, code: string, tenantUrl: string, redirectUri: string) => { 99 | return verifyCode(code, tenantUrl, redirectUri) 100 | } 101 | ) 102 | 103 | // 获取机器ids 104 | ipcMain.handle(IpcChannel.App_GetMachineIds, (_, ide: IDE) => { 105 | return getMachineIds(ide) 106 | }) 107 | 108 | // 设置随机机器ids 109 | ipcMain.handle(IpcChannel.App_SetRandomMachineIds, (_, ide: IDE) => { 110 | return setRandomMachineIds(ide) 111 | }) 112 | 113 | // 获取注册表guid 114 | ipcMain.handle(IpcChannel.App_GetRegistryMachineGuid, () => { 115 | return getRegistryMachineGuid() 116 | }) 117 | 118 | // 设置随机注册表guid 119 | ipcMain.handle(IpcChannel.App_SetRandomRegistryMachineGuid, () => { 120 | return setRandomRegistryMachineGuid() 121 | }) 122 | } 123 | -------------------------------------------------------------------------------- /src/renderer/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { addToast, Button, closeAll, Radio, RadioGroup, Tab, Tabs, Textarea } from '@heroui/react' 2 | import { useEffect, useState } from 'react' 3 | 4 | import { Config, DecryptType, IDE } from './types' 5 | 6 | const editorList = [ 7 | { 8 | label: 'VSCode', 9 | value: IDE.VSCode 10 | }, 11 | { 12 | label: 'Cursor', 13 | value: IDE.Cursor 14 | }, 15 | { 16 | label: 'Windsurf', 17 | value: IDE.Windsurf 18 | } 19 | ] 20 | 21 | function App() { 22 | const [init, setInit] = useState(false) 23 | const [data, setData] = useState('') 24 | const [result, setResult] = useState('') 25 | const [config, setConfig] = useState({}) 26 | 27 | async function getConfig() { 28 | try { 29 | const res = await window.api.getConfig() 30 | if (!res.success) { 31 | throw new Error(res.message) 32 | } 33 | setConfig(res.data) 34 | } catch (error) { 35 | addToast({ 36 | title: '获取配置失败', 37 | description: error.message?.replace('Error invoking remote method ', '') || error, 38 | color: 'danger' 39 | }) 40 | } finally { 41 | setInit(true) 42 | } 43 | } 44 | 45 | useEffect(() => { 46 | document.title = '加密/解密演示' 47 | getConfig() 48 | }, []) 49 | 50 | async function encrypt() { 51 | try { 52 | const res = await window.api.secrets.encrypt(data) 53 | setResult(res) 54 | } catch (error) { 55 | addToast({ 56 | title: '加密失败', 57 | description: error.message.replace('Error invoking remote method ', ''), 58 | color: 'danger' 59 | }) 60 | } 61 | } 62 | async function decrypt() { 63 | try { 64 | const obj = JSON.parse(data) 65 | const res = await window.api.secrets.decrypt(obj.data) 66 | setResult(res) 67 | } catch (error) { 68 | addToast({ 69 | title: '解密失败', 70 | description: error.message.replace('Error invoking remote method ', ''), 71 | color: 'danger' 72 | }) 73 | } 74 | } 75 | 76 | async function setConfigAndNotify(data: Config) { 77 | if (!init) return 78 | try { 79 | const res = await window.api.setConfig(data) 80 | if (!res.success) { 81 | throw new Error(res.message) 82 | } 83 | setConfig(res.data) 84 | closeAll() 85 | addToast({ 86 | title: '配置更新', 87 | description: '需要重启应用才能生效', 88 | color: 'primary', 89 | timeout: 3000, 90 | shouldShowTimeoutProgress: true, 91 | hideIcon: true, 92 | endContent: ( 93 | 96 | ) 97 | }) 98 | } catch (error) { 99 | addToast({ 100 | title: '配置更新失败', 101 | description: error.message.replace('Error invoking remote method ', ''), 102 | color: 'danger' 103 | }) 104 | } 105 | } 106 | 107 | return ( 108 |
109 |
110 |
111 |
112 | 115 | 118 | 129 |
130 |
131 | setConfigAndNotify({ type: value as DecryptType })} 136 | > 137 | 用户数据路径 138 | 秘钥文件 139 | 140 | setConfigAndNotify({ editor: value as IDE })} 144 | > 145 | {editorList.map((item) => ( 146 | 147 | ))} 148 | 149 |
150 |
151 |