diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 0c5d48f..c892ebe 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -29,6 +29,10 @@ jobs: working-directory: cfspider-browser run: npm ci + - name: Generate icons + working-directory: cfspider-browser + run: node scripts/generate-icons.js + - name: Build Electron app working-directory: cfspider-browser run: npm run electron:build-win @@ -59,6 +63,26 @@ jobs: working-directory: cfspider-browser run: npm ci + - name: Generate icons + working-directory: cfspider-browser + run: node scripts/generate-icons.js + + - name: Generate macOS icns + working-directory: cfspider-browser + run: | + mkdir -p build/icon.iconset + cp build/icons/16x16.png build/icon.iconset/icon_16x16.png + cp build/icons/32x32.png build/icon.iconset/icon_16x16@2x.png + cp build/icons/32x32.png build/icon.iconset/icon_32x32.png + cp build/icons/64x64.png build/icon.iconset/icon_32x32@2x.png + cp build/icons/128x128.png build/icon.iconset/icon_128x128.png + cp build/icons/256x256.png build/icon.iconset/icon_128x128@2x.png + cp build/icons/256x256.png build/icon.iconset/icon_256x256.png + cp build/icons/512x512.png build/icon.iconset/icon_256x256@2x.png + cp build/icons/512x512.png build/icon.iconset/icon_512x512.png + cp build/icons/1024x1024.png build/icon.iconset/icon_512x512@2x.png + iconutil -c icns build/icon.iconset -o build/icon.icns + - name: Build Electron app working-directory: cfspider-browser run: npm run electron:build-mac @@ -91,6 +115,10 @@ jobs: working-directory: cfspider-browser run: npm ci + - name: Generate icons + working-directory: cfspider-browser + run: node scripts/generate-icons.js + - name: Build Electron app working-directory: cfspider-browser run: npm run electron:build-linux @@ -128,9 +156,9 @@ jobs: AI 驱动的智能浏览器,通过自然语言对话控制浏览器自动化。 ### 下载 - - **Windows**: cfspider-browser-Setup-*.exe - - **macOS**: cfspider-browser-*.dmg - - **Linux**: cfspider-browser-*.AppImage + - **Windows**: CFspider-Browser-Setup-*.exe + - **macOS**: CFspider-Browser-*.dmg + - **Linux**: CFspider-Browser-*.AppImage ### 功能特性 - 自然语言控制浏览器 diff --git a/cfspider-browser/electron-builder.json b/cfspider-browser/electron-builder.json index 64e7eb2..cb66f25 100644 --- a/cfspider-browser/electron-builder.json +++ b/cfspider-browser/electron-builder.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", "appId": "com.cfspider.browser", - "productName": "cfspider-智能浏览器", + "productName": "CFspider-Browser", "directories": { "output": "release", "buildResources": "build" diff --git a/cfspider-browser/electron/main.ts b/cfspider-browser/electron/main.ts index a6e5160..418ed5b 100644 --- a/cfspider-browser/electron/main.ts +++ b/cfspider-browser/electron/main.ts @@ -114,9 +114,12 @@ app.whenReady().then(() => { // 配置 webview 的独立 session(persist: 前缀确保数据持久化到磁盘) const webviewSession = session.fromPartition('persist:cfspider') - // 设置真实的 User-Agent - const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + // 设置最新版本的 Chrome User-Agent,避免网站兼容性警告 + const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' webviewSession.setUserAgent(userAgent) + + // 设置默认 session 的 User-Agent(某些情况会用到) + session.defaultSession.setUserAgent(userAgent) // 移除 X-Frame-Options 和 CSP 限制,允许在 webview 中加载任何网站 webviewSession.webRequest.onHeadersReceived((details, callback) => { @@ -425,15 +428,17 @@ ipcMain.handle('rules:save', async (_event, rules) => { ipcMain.handle('config:load', async () => { const fs = await import('fs/promises') const configPath = join(app.getPath('userData'), 'ai-config.json') - + try { const content = await fs.readFile(configPath, 'utf-8') return JSON.parse(content) } catch { + // 默认使用内置 AI return { - endpoint: 'https://api.openai.com/v1/chat/completions', + endpoint: '', apiKey: '', - model: 'gpt-4' + model: '', + useBuiltIn: true } } }) diff --git a/cfspider-browser/package-lock.json b/cfspider-browser/package-lock.json index 8ef383d..f365836 100644 --- a/cfspider-browser/package-lock.json +++ b/cfspider-browser/package-lock.json @@ -29,7 +29,9 @@ "electron": "^28.0.0", "electron-builder": "^24.9.1", "esbuild": "^0.19.10", + "png-to-ico": "^3.0.1", "postcss": "^8.4.32", + "sharp": "^0.34.5", "tailwindcss": "^3.3.6", "typescript": "^5.3.3", "vite": "^5.0.8", @@ -631,6 +633,17 @@ "node": ">= 10.0.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.19.12", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", @@ -1039,6 +1052,496 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3416,6 +3919,16 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmmirror.com/detect-node/-/detect-node-2.1.0.tgz", @@ -6401,6 +6914,51 @@ "node": ">=10.4.0" } }, + "node_modules/png-to-ico": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/png-to-ico/-/png-to-ico-3.0.1.tgz", + "integrity": "sha512-S8BOAoaGd9gT5uaemQ62arIY3Jzco7Uc7LwUTqRyqJDTsKqOAiyfyN4dSdT0D+Zf8XvgztgpRbM5wnQd7EgYwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^22.10.3", + "minimist": "^1.2.8", + "pngjs": "^7.0.0" + }, + "bin": { + "png-to-ico": "bin/cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/png-to-ico/node_modules/@types/node": { + "version": "22.19.7", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.7.tgz", + "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/png-to-ico/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", @@ -7139,6 +7697,64 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/cfspider-browser/package.json b/cfspider-browser/package.json index e6e5ac5..991f0be 100644 --- a/cfspider-browser/package.json +++ b/cfspider-browser/package.json @@ -44,7 +44,9 @@ "electron": "^28.0.0", "electron-builder": "^24.9.1", "esbuild": "^0.19.10", + "png-to-ico": "^3.0.1", "postcss": "^8.4.32", + "sharp": "^0.34.5", "tailwindcss": "^3.3.6", "typescript": "^5.3.3", "vite": "^5.0.8", @@ -52,7 +54,7 @@ }, "build": { "appId": "com.cfspider.browser", - "productName": "cfspider-智能浏览器", + "productName": "CFspider-Browser", "publish": null, "directories": { "output": "release" diff --git a/cfspider-browser/src/App.tsx b/cfspider-browser/src/App.tsx index ec6813e..9ab1739 100644 --- a/cfspider-browser/src/App.tsx +++ b/cfspider-browser/src/App.tsx @@ -5,21 +5,53 @@ import AIChat from './components/AIChat/AIChat' import SettingsModal from './components/Settings/SettingsModal' import { useStore } from './store' -// 从模型名称获取 AI 助手名称 -function getAIName(model: string): string { - if (!model) return 'AI 助手' +// 从模型名称获取简短的 AI 助手名称 +function getShortModelName(model: string): string { + if (!model) return '' const lowerModel = model.toLowerCase() if (lowerModel.includes('gpt-4')) return 'GPT-4' if (lowerModel.includes('gpt-3')) return 'GPT-3.5' if (lowerModel.includes('claude')) return 'Claude' if (lowerModel.includes('gemini')) return 'Gemini' + if (lowerModel.includes('deepseek-v3')) return 'DeepSeek-V3' + if (lowerModel.includes('deepseek-ocr')) return 'DeepSeek-OCR' if (lowerModel.includes('deepseek')) return 'DeepSeek' - if (lowerModel.includes('qwen')) return '通义千问' - if (lowerModel.includes('glm')) return 'ChatGLM' + if (lowerModel.includes('qwen')) return 'Qwen' + if (lowerModel.includes('glm')) return 'GLM' if (lowerModel.includes('llama')) return 'LLaMA' if (lowerModel.includes('mistral')) return 'Mistral' - // 显示模型名称的前部分 - return model.split('/').pop()?.split('-')[0] || 'AI 助手' + // 显示模型名称的后部分 + return model.split('/').pop()?.split(':')[0] || model +} + +// 从配置获取 AI 显示名称 +function getAIDisplayInfo(config: any): { name: string; isDual: boolean; models: string[] } { + // 使用内置 AI + if (config.useBuiltIn || (!config.endpoint && !config.apiKey)) { + return { + name: 'DeepSeek', + isDual: true, + models: ['DeepSeek-V3 (工具)', 'DeepSeek-OCR (视觉)'] + } + } + + // 双模型模式 + if (config.modelMode === 'dual' && config.visionModel) { + const toolName = getShortModelName(config.model) + const visionName = getShortModelName(config.visionModel) + return { + name: toolName, + isDual: true, + models: [`${toolName} (工具)`, `${visionName} (视觉)`] + } + } + + // 单模型模式 + return { + name: getShortModelName(config.model) || 'AI 助手', + isDual: false, + models: [config.model] + } } function App() { @@ -33,7 +65,9 @@ function App() { switchChatSession, deleteChatSession } = useStore() - const aiName = getAIName(aiConfig.model) + const aiInfo = getAIDisplayInfo(aiConfig) + const [showModelDetails, setShowModelDetails] = useState(false) + const { currentModelType, isAILoading } = useStore() useEffect(() => { // 并行加载所有配置 @@ -75,7 +109,37 @@ function App() { {/* 悬浮窗头部 */}
- {aiName} + {/* 模型名称,双模型显示 +2 标识 */} +
+ + {/* 模型详情下拉 */} + {showModelDetails && aiInfo.isDual && ( +
+
双模型配置
+ {aiInfo.models.map((model, i) => ( +
+ {model} +
+ ))} +
+ )} +
{/* 历史记录下拉 */}
- Click to check current page -
- {/* 消息列表 */}
{messages.length === 0 ? ( diff --git a/cfspider-browser/src/components/AIChat/MessageList.tsx b/cfspider-browser/src/components/AIChat/MessageList.tsx index f1c6522..51ed950 100644 --- a/cfspider-browser/src/components/AIChat/MessageList.tsx +++ b/cfspider-browser/src/components/AIChat/MessageList.tsx @@ -366,6 +366,48 @@ function ElementSelectionCard({ data }: { data: { id: string; purpose: string; s ) } +// 继续按钮组件 - 当达到操作限制时显示 +function ContinueButton() { + const [isLoading, setIsLoading] = useState(false) + const { isAILoading } = useStore() + + const handleContinue = async () => { + if (isLoading || isAILoading) return + setIsLoading(true) + try { + await sendAIMessage('继续') + } finally { + setIsLoading(false) + } + } + + if (isAILoading) return null + + return ( +
+ +
+ ) +} + // 获取工具调用的友好描述 function getToolDescription(toolName: string, args: Record): string { switch (toolName) { @@ -475,8 +517,6 @@ function getToolDescription(toolName: string, args: Record): st return '验证操作结果' case 'retry_with_alternative': return '尝试其他方法' - case 'check_website_safety': - return '检查网站安全' default: return toolName } @@ -571,6 +611,11 @@ export default function MessageList({ messages }: MessageListProps) { )} + {/* 达到限制时显示继续按钮 */} + {hasContent && (message.content?.includes('Max iterations reached') || message.content?.includes('达到最大操作次数')) && ( + + )} + {/* Final AI message - normal Markdown rendering */} {hasContent && !message.content?.includes('【爬取结果】') && !message.content?.includes('__ELEMENT_SELECTION_REQUEST__') && (
diff --git a/cfspider-browser/src/components/Browser/VirtualMouse.tsx b/cfspider-browser/src/components/Browser/VirtualMouse.tsx index 7aae42a..9af32af 100644 --- a/cfspider-browser/src/components/Browser/VirtualMouse.tsx +++ b/cfspider-browser/src/components/Browser/VirtualMouse.tsx @@ -1,34 +1,144 @@ -import { useEffect, useState, useRef } from 'react' +import { useEffect, useState, useRef, useCallback } from 'react' import { useStore } from '../../store' +// 当前位置 ref(用于乱动模式,避免闭包问题) +let currentPosRef = { x: 0, y: 0 } + +// 生成贝塞尔曲线控制点 +function generateBezierControlPoint( + start: { x: number; y: number }, + end: { x: number; y: number } +): { x: number; y: number } { + const distance = Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2)) + // 偏移量与距离成正比,但有最大限制 + const randomOffset = Math.min(distance * 0.4, 100) * (Math.random() * 0.6 + 0.4) + + // 计算中点 + const midX = (start.x + end.x) / 2 + const midY = (start.y + end.y) / 2 + + // 计算垂直方向的单位向量 + const dx = end.x - start.x + const dy = end.y - start.y + const length = Math.sqrt(dx * dx + dy * dy) || 1 + const perpX = -dy / length + const perpY = dx / length + + // 在垂直方向上添加随机偏移(随机选择左侧或右侧) + const side = Math.random() > 0.5 ? 1 : -1 + + return { + x: midX + perpX * randomOffset * side, + y: midY + perpY * randomOffset * side + } +} + +// 二次贝塞尔曲线插值 +function bezierInterpolate( + t: number, + p0: { x: number; y: number }, + p1: { x: number; y: number }, + p2: { x: number; y: number } +): { x: number; y: number } { + const u = 1 - t + return { + x: u * u * p0.x + 2 * u * t * p1.x + t * t * p2.x, + y: u * u * p0.y + 2 * u * t * p1.y + t * t * p2.y + } +} + +// 缓动函数:先快后慢 +function easeOutQuart(t: number): number { + return 1 - Math.pow(1 - t, 4) +} + export default function VirtualMouse() { const { mouseState } = useStore() const [position, setPosition] = useState({ x: -100, y: -100 }) const [isClicking, setIsClicking] = useState(false) const animationRef = useRef() + const fidgetIntervalRef = useRef() + const containerRef = useRef(null) + const controlPointRef = useRef<{ x: number; y: number } | null>(null) + const lastTargetRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }) - // 平滑移动到目标位置 + // 获取容器偏移 + useEffect(() => { + containerRef.current = document.getElementById('browser-container') as HTMLDivElement + }, []) + + // 生成随机乱动位置 + const generateFidgetPosition = useCallback((baseX: number, baseY: number, intensity: number, isPanic: boolean) => { + // 获取容器尺寸,用于计算屏幕范围 + const container = containerRef.current + const containerRect = container?.getBoundingClientRect() || { width: 800, height: 600 } + + if (isPanic) { + // panic 模式:在当前位置附近快速乱动 + const range = 40 + Math.random() * 60 + const angle = Math.random() * Math.PI * 2 + return { + x: baseX + Math.cos(angle) * range * intensity, + y: baseY + Math.sin(angle) * range * intensity + } + } else { + // fidget 模式(思考时):在整个屏幕范围内大幅度随机移动 + // 模拟真人思考时鼠标在屏幕上漫无目的地移动 + const margin = 50 // 边距 + const randomX = margin + Math.random() * (containerRect.width - margin * 2) + const randomY = margin + Math.random() * (containerRect.height - margin * 2) + return { x: randomX, y: randomY } + } + }, []) + + // 平滑移动到目标位置(贝塞尔曲线) useEffect(() => { if (!mouseState.visible) return + if (mouseState.mode !== 'normal') return // 乱动模式下不执行正常移动 - const targetX = mouseState.x - const targetY = mouseState.y + // 获取容器的位置 + const container = containerRef.current + const containerRect = container?.getBoundingClientRect() || { left: 0, top: 0 } + + // 目标位置 + const targetX = mouseState.x - containerRect.left + const targetY = mouseState.y - containerRect.top + + // 检查目标是否改变 + if (Math.abs(targetX - lastTargetRef.current.x) < 1 && + Math.abs(targetY - lastTargetRef.current.y) < 1) { + return // 目标没变,不需要移动 + } + const startX = position.x < 0 ? targetX : position.x const startY = position.y < 0 ? targetY : position.y const startTime = Date.now() const duration = mouseState.duration || 300 + + // 生成贝塞尔曲线控制点 + const start = { x: startX, y: startY } + const end = { x: targetX, y: targetY } + const control = generateBezierControlPoint(start, end) + controlPointRef.current = control + lastTargetRef.current = end const animate = () => { const elapsed = Date.now() - startTime const progress = Math.min(elapsed / duration, 1) - // 使用 easeOutCubic 缓动函数,使移动更自然 - const eased = 1 - Math.pow(1 - progress, 3) + // 使用缓动函数 + const eased = easeOutQuart(progress) - const currentX = startX + (targetX - startX) * eased - const currentY = startY + (targetY - startY) * eased + // 贝塞尔曲线插值 + const pos = bezierInterpolate(eased, start, control, end) - setPosition({ x: currentX, y: currentY }) + // 添加微小的随机抖动,模拟人手的不稳定 + const jitter = progress < 0.9 ? (Math.random() - 0.5) * 2 : 0 + + setPosition({ + x: pos.x + jitter, + y: pos.y + jitter + }) if (progress < 1) { animationRef.current = requestAnimationFrame(animate) @@ -45,7 +155,99 @@ export default function VirtualMouse() { cancelAnimationFrame(animationRef.current) } } - }, [mouseState.x, mouseState.y, mouseState.visible, mouseState.duration]) + }, [mouseState.x, mouseState.y, mouseState.visible, mouseState.duration, mouseState.mode]) + + // 同步位置到 ref(用于乱动模式读取最新位置) + useEffect(() => { + currentPosRef = { ...position } + }, [position]) + + // Fidget/Panic 乱动模式 + useEffect(() => { + if (!mouseState.visible) return + if (mouseState.mode === 'normal') { + // 停止乱动 + if (fidgetIntervalRef.current) { + clearTimeout(fidgetIntervalRef.current) + fidgetIntervalRef.current = undefined + } + return + } + + const isPanic = mouseState.mode === 'panic' + const intensity = mouseState.fidgetIntensity + + // 获取容器的位置 + const container = containerRef.current + const containerRect = container?.getBoundingClientRect() || { left: 0, top: 0 } + + // 基准位置 + const baseX = mouseState.baseX - containerRect.left + const baseY = mouseState.baseY - containerRect.top + + let isActive = true + + // 连续移动函数:完成一次移动后自动开始下一次 + const continuousMove = () => { + if (!isActive) return + + const newPos = generateFidgetPosition(baseX, baseY, intensity, isPanic) + + // 从当前实际位置开始(使用 ref 获取最新位置) + const startPos = { ...currentPosRef } + const startTime = Date.now() + + // panic 模式快速移动,fidget 模式慢速移动 + const moveDuration = isPanic ? 100 + Math.random() * 80 : 600 + Math.random() * 800 + + const animateMove = () => { + if (!isActive) return + + const elapsed = Date.now() - startTime + const progress = Math.min(elapsed / moveDuration, 1) + + // 使用更平滑的缓动函数 + const eased = isPanic + ? progress // panic 模式线性移动,更急促 + : easeOutQuart(progress) // fidget 模式平滑移动 + + const newX = startPos.x + (newPos.x - startPos.x) * eased + const newY = startPos.y + (newPos.y - startPos.y) * eased + + setPosition({ x: newX, y: newY }) + currentPosRef = { x: newX, y: newY } + + if (progress < 1) { + animationRef.current = requestAnimationFrame(animateMove) + } else { + // 动画完成,短暂停顿后开始下一次移动 + const pauseDuration = isPanic + ? 30 + Math.random() * 50 // panic 模式几乎不停顿 + : 200 + Math.random() * 500 // fidget 模式停顿一下再移动 + + fidgetIntervalRef.current = window.setTimeout(continuousMove, pauseDuration) + } + } + + if (animationRef.current) { + cancelAnimationFrame(animationRef.current) + } + animationRef.current = requestAnimationFrame(animateMove) + } + + // 立即开始 + continuousMove() + + return () => { + isActive = false + if (fidgetIntervalRef.current) { + clearTimeout(fidgetIntervalRef.current) + } + if (animationRef.current) { + cancelAnimationFrame(animationRef.current) + } + } + }, [mouseState.mode, mouseState.fidgetIntensity, mouseState.visible, mouseState.baseX, mouseState.baseY, generateFidgetPosition]) // 点击动画 useEffect(() => { @@ -62,9 +264,17 @@ export default function VirtualMouse() { const tipOffsetX = 5.5 const tipOffsetY = 3.21 + // 根据模式确定鼠标颜色 + const getMouseColor = () => { + if (isClicking) return '#00cc66' + if (mouseState.mode === 'panic') return '#ff6666' // 紧张时变红 + if (mouseState.mode === 'fidget') return '#ffaa00' // 思考时变橙 + return '#00ff88' // 正常绿色 + } + return (
{/* 鼠标主体 */}
)} + + {/* 思考指示器 */} + {mouseState.mode === 'fidget' && ( +
+ 思考中... +
+ )} + + {/* 紧张指示器 */} + {mouseState.mode === 'panic' && ( +
+ !? +
+ )}
) } diff --git a/cfspider-browser/src/components/Settings/SettingsModal.tsx b/cfspider-browser/src/components/Settings/SettingsModal.tsx index 387baf4..a7fa166 100644 --- a/cfspider-browser/src/components/Settings/SettingsModal.tsx +++ b/cfspider-browser/src/components/Settings/SettingsModal.tsx @@ -15,6 +15,7 @@ const COMMON_MODELS = [ // AI 服务商预设配置 const AI_PRESETS = [ + { id: 'builtin', name: '内置 AI', endpoint: '', models: [], description: '开箱即用,无需配置', isBuiltIn: true }, { id: 'custom', name: '自定义', endpoint: '', models: COMMON_MODELS, description: '自定义 API 地址,可选择常用模型' }, { id: 'ollama', name: 'Ollama', endpoint: 'http://localhost:11434/v1/chat/completions', models: ['llama3.2', 'llama3.1', 'qwen2.5', 'deepseek-r1', 'mistral', 'codellama', 'phi3'], description: '本地运行,无需 API Key' }, { id: 'deepseek', name: 'DeepSeek', endpoint: 'https://api.deepseek.com/v1/chat/completions', models: ['deepseek-chat', 'deepseek-coder', 'deepseek-reasoner'], description: '国产大模型,性价比高' }, @@ -66,10 +67,15 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { useEffect(() => { setLocalConfig(aiConfig) - const matched = AI_PRESETS.find(p => - p.endpoint && aiConfig.endpoint.includes(p.endpoint.replace('/chat/completions', '')) - ) - setSelectedPreset(matched?.id || 'custom') + // 如果使用内置 AI 或者没有配置,默认选中内置 + if (aiConfig.useBuiltIn || (!aiConfig.endpoint && !aiConfig.apiKey)) { + setSelectedPreset('builtin') + } else { + const matched = AI_PRESETS.find(p => + p.endpoint && aiConfig.endpoint.includes(p.endpoint.replace('/chat/completions', '')) + ) + setSelectedPreset(matched?.id || 'custom') + } }, [aiConfig]) useEffect(() => { @@ -82,12 +88,28 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { setSelectedPreset(presetId) setShowPresetDropdown(false) const preset = AI_PRESETS.find(p => p.id === presetId) - if (preset && preset.endpoint) { - setLocalConfig({ - ...localConfig, - endpoint: preset.endpoint, - model: preset.models[0] || '' - }) + if (preset) { + if ((preset as any).isBuiltIn) { + // 使用内置 AI + setLocalConfig({ + endpoint: '', + apiKey: '', + model: '', + useBuiltIn: true + }) + } else if (preset.endpoint) { + setLocalConfig({ + ...localConfig, + endpoint: preset.endpoint, + model: preset.models[0] || '', + useBuiltIn: false + }) + } else { + setLocalConfig({ + ...localConfig, + useBuiltIn: false + }) + } } } @@ -249,12 +271,12 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { {activeSection === 'ai' && (

AI 助手配置

-

配置 AI 助手使用的模型和 API

+

选择 AI 服务

{/* 服务商选择 */}
- +
))}
@@ -289,75 +314,211 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
- {/* API 地址 */} -
- - setLocalConfig({ ...localConfig, endpoint: e.target.value })} - className="w-full px-4 py-3 bg-white border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900" - placeholder="https://api.example.com/v1/chat/completions" - /> -
- - {/* API Key */} -
- - setLocalConfig({ ...localConfig, apiKey: e.target.value })} - className="w-full px-4 py-3 bg-white border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-gray-900" - placeholder="sk-..." - /> -
- - {/* 模型选择 */} -
- -
-
- {/* 下拉选择 */} - + {/* 内置 AI 说明 */} + {selectedPreset === 'builtin' && ( +
+
+ + 内置 AI 服务
- - {showModelDropdown && ( -
- {/* 手动输入选项 */} -
+

+ 使用内置 AI 服务,无需配置即可直接使用。 +

+
+ )} + + {/* 自定义配置(非内置时显示) */} + {selectedPreset !== 'builtin' && ( + <> + {/* API 地址 */} +
+ + setLocalConfig({ ...localConfig, endpoint: e.target.value })} + className="w-full px-4 py-3 bg-white border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900" + placeholder="https://api.example.com/v1/chat/completions" + /> +
+ + {/* API Key */} +
+ + setLocalConfig({ ...localConfig, apiKey: e.target.value })} + className="w-full px-4 py-3 bg-white border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-gray-900" + placeholder="sk-..." + /> +
+ + {/* 模型模式选择 */} +
+ +
+ + + +
+

+ {localConfig.modelMode === 'dual' && '视觉模型分析页面 + 工具模型执行操作(推荐)'} + {localConfig.modelMode === 'single' && '单个模型同时具备视觉和工具调用能力'} + {(localConfig.modelMode === 'tool-only' || !localConfig.modelMode) && '仅使用工具模型,不分析页面截图'} +

+
+ + {/* 工具模型选择 */} +
+ +
+
+ {/* 下拉选择 */} + +
+ + {showModelDropdown && ( +
+ {/* 手动输入选项 */} +
+ setLocalConfig({ ...localConfig, model: e.target.value })} + className="w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded text-sm text-gray-900 focus:outline-none focus:ring-1 focus:ring-blue-500" + placeholder="输入自定义模型名..." + onClick={(e) => e.stopPropagation()} + /> +
+ {/* 预设模型列表 */} + {currentPreset.models.map((model) => ( + + ))} +
+ )} +
+
+ + {/* 视觉模型配置(仅双模型模式显示) */} + {localConfig.modelMode === 'dual' && ( +
+
+ + +
+ + {/* 独立服务商配置 */} + {localConfig.visionEndpoint ? ( + <> +
+ + setLocalConfig({ ...localConfig, visionEndpoint: e.target.value })} + className="w-full px-3 py-2 bg-white border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900 text-sm" + placeholder="https://api.siliconflow.cn/v1/chat/completions" + /> +
+
+ + setLocalConfig({ ...localConfig, visionApiKey: e.target.value })} + className="w-full px-3 py-2 bg-white border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900 text-sm font-mono" + placeholder="sk-..." + /> +
+ + ) : ( +

+ 默认使用工具模型的 API 地址和 Key +

+ )} + +
+ setLocalConfig({ ...localConfig, model: e.target.value })} - className="w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded text-sm text-gray-900 focus:outline-none focus:ring-1 focus:ring-blue-500" - placeholder="输入自定义模型名..." - onClick={(e) => e.stopPropagation()} + value={localConfig.visionModel || ''} + onChange={(e) => setLocalConfig({ ...localConfig, visionModel: e.target.value })} + className="w-full px-3 py-2 bg-white border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900 text-sm" + placeholder="如 deepseek-ai/DeepSeek-OCR, gpt-4-vision-preview" /> +

+ 用于分析页面截图,提供更准确的操作上下文 +

- {/* 预设模型列表 */} - {currentPreset.models.map((model) => ( - - ))}
)} -
-
+ + )} {/* 保存按钮 */}