feat(browser): enhance AI click accuracy and add post-action verification

This commit is contained in:
violettools
2026-01-29 06:47:52 +08:00
parent 2480aff436
commit dd306a0e9e
12 changed files with 3519 additions and 382 deletions

View File

@@ -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
### 功能特性
- 自然语言控制浏览器

View File

@@ -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"

View File

@@ -114,9 +114,12 @@ app.whenReady().then(() => {
// 配置 webview 的独立 sessionpersist: 前缀确保数据持久化到磁盘)
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
}
}
})

View File

@@ -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",

View File

@@ -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"

View File

@@ -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() {
{/* 悬浮窗头部 */}
<div className="flex items-center justify-between px-4 py-3 bg-blue-500/90 text-white">
<div className="flex items-center gap-2">
<span className="font-medium">{aiName}</span>
{/* 模型名称,双模型显示 +2 标识 */}
<div className="relative">
<button
onClick={() => aiInfo.isDual && setShowModelDetails(!showModelDetails)}
className={`font-medium flex items-center gap-1 ${aiInfo.isDual ? 'hover:bg-white/20 px-2 py-0.5 rounded cursor-pointer' : ''}`}
>
{aiInfo.name}
{aiInfo.isDual && (
<span className="text-xs bg-white/30 px-1.5 py-0.5 rounded-full">+2</span>
)}
{/* 当前调用模型类型指示器 */}
{currentModelType && (
<span className={`text-xs px-1.5 py-0.5 rounded animate-pulse ${
currentModelType === 'vision' ? 'bg-purple-400' : 'bg-green-400'
}`}>
{currentModelType === 'vision' ? '视' : '工'}
</span>
)}
</button>
{/* 模型详情下拉 */}
{showModelDetails && aiInfo.isDual && (
<div className="absolute top-full left-0 mt-1 w-48 bg-white rounded-lg shadow-xl border border-gray-200 py-2 z-50">
<div className="px-3 py-1 text-xs text-gray-500 border-b border-gray-100"></div>
{aiInfo.models.map((model, i) => (
<div key={i} className="px-3 py-1.5 text-sm text-gray-700">
{model}
</div>
))}
</div>
)}
</div>
{/* 历史记录下拉 */}
<div className="relative">
<button

View File

@@ -1,14 +1,12 @@
import { useRef, useEffect, useState } from 'react'
import { Shield } from 'lucide-react'
import { useRef, useEffect } from 'react'
import MessageList from './MessageList'
import InputBox from './InputBox'
import { useStore } from '../../store'
import { sendAIMessage, manualSafetyCheck } from '../../services/ai'
import { sendAIMessage } from '../../services/ai'
export default function AIChat() {
const { messages, isAILoading } = useStore()
const scrollRef = useRef<HTMLDivElement>(null)
const [checkingStatus, setCheckingStatus] = useState<string | null>(null)
// 自动滚动到底部
useEffect(() => {
@@ -21,40 +19,8 @@ export default function AIChat() {
await sendAIMessage(content)
}
const handleSafetyCheck = async () => {
setCheckingStatus('checking')
const result = await manualSafetyCheck()
setCheckingStatus(result.includes('WARNING') ? 'warning' : 'safe')
setTimeout(() => setCheckingStatus(null), 3000)
}
return (
<div className="flex flex-col h-full bg-white">
{/* 快捷操作栏 */}
<div className="flex items-center gap-2 px-3 py-2 border-b border-gray-100 bg-gray-50">
<button
onClick={handleSafetyCheck}
disabled={checkingStatus === 'checking'}
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-all ${
checkingStatus === 'checking'
? 'bg-gray-200 text-gray-500 cursor-wait'
: checkingStatus === 'safe'
? 'bg-green-100 text-green-700'
: checkingStatus === 'warning'
? 'bg-red-100 text-red-700'
: 'bg-blue-100 text-blue-700 hover:bg-blue-200'
}`}
title="Check current website safety"
>
<Shield size={14} />
{checkingStatus === 'checking' ? 'Checking...' :
checkingStatus === 'safe' ? 'Safe ✓' :
checkingStatus === 'warning' ? 'Warning!' :
'Safety Check'}
</button>
<span className="text-xs text-gray-400">Click to check current page</span>
</div>
{/* 消息列表 */}
<div ref={scrollRef} className="flex-1 overflow-auto">
{messages.length === 0 ? (

View File

@@ -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 (
<div className="flex justify-center mt-3">
<button
onClick={handleContinue}
disabled={isLoading}
className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium shadow-md"
>
{isLoading ? (
<>
<Loader2 size={14} className="animate-spin" />
...
</>
) : (
<>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
</>
)}
</button>
</div>
)
}
// 获取工具调用的友好描述
function getToolDescription(toolName: string, args: Record<string, unknown>): string {
switch (toolName) {
@@ -475,8 +517,6 @@ function getToolDescription(toolName: string, args: Record<string, unknown>): 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) {
<CrawlResultCard content={message.content} />
)}
{/* 达到限制时显示继续按钮 */}
{hasContent && (message.content?.includes('Max iterations reached') || message.content?.includes('达到最大操作次数')) && (
<ContinueButton />
)}
{/* Final AI message - normal Markdown rendering */}
{hasContent && !message.content?.includes('【爬取结果】') && !message.content?.includes('__ELEMENT_SELECTION_REQUEST__') && (
<div className="text-gray-700 leading-relaxed mt-2 break-words overflow-hidden text-sm">

View File

@@ -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<number>()
const fidgetIntervalRef = useRef<number>()
const containerRef = useRef<HTMLDivElement | null>(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 (
<div
className="pointer-events-none fixed z-[99999] transition-opacity duration-200"
className="pointer-events-none absolute z-[99999] transition-opacity duration-200"
style={{
left: position.x - tipOffsetX,
top: position.y - tipOffsetY,
@@ -77,16 +287,20 @@ export default function VirtualMouse() {
height="24"
viewBox="0 0 24 24"
className={`drop-shadow-lg transition-transform duration-100 ${
isClicking ? 'scale-90' : 'scale-100'
isClicking ? 'scale-90' : mouseState.mode === 'panic' ? 'scale-110' : 'scale-100'
}`}
style={{
filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.3))',
// 紧张模式下轻微旋转抖动
transform: mouseState.mode === 'panic'
? `rotate(${(Math.random() - 0.5) * 10}deg)`
: undefined,
}}
>
{/* 鼠标主体 */}
<path
d="M5.5 3.21V20.8c0 .45.54.67.85.35l4.86-4.86a.5.5 0 0 1 .35-.15h6.87c.48 0 .72-.58.38-.92L6.35 2.76a.5.5 0 0 0-.85.45Z"
fill={isClicking ? '#00cc66' : '#00ff88'}
fill={getMouseColor()}
stroke="#000"
strokeWidth="1.5"
strokeLinecap="round"
@@ -100,6 +314,20 @@ export default function VirtualMouse() {
<div className="w-6 h-6 rounded-full bg-primary/50 animate-ping" />
</div>
)}
{/* 思考指示器 */}
{mouseState.mode === 'fidget' && (
<div className="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs text-yellow-500 whitespace-nowrap animate-pulse">
...
</div>
)}
{/* 紧张指示器 */}
{mouseState.mode === 'panic' && (
<div className="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs text-red-500 whitespace-nowrap animate-bounce">
!?
</div>
)}
</div>
)
}

View File

@@ -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' && (
<div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">AI </h3>
<p className="text-sm text-gray-600 mb-6"> AI 使 API</p>
<p className="text-sm text-gray-600 mb-6"> AI </p>
<div className="space-y-5">
{/* 服务商选择 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">AI </label>
<label className="block text-sm font-medium text-gray-700 mb-2">AI </label>
<div className="relative">
<button
onClick={() => setShowPresetDropdown(!showPresetDropdown)}
@@ -273,15 +295,18 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
<button
key={preset.id}
onClick={() => handlePresetSelect(preset.id)}
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-gray-50 text-left"
className={`w-full flex items-center gap-3 px-4 py-3 hover:bg-gray-50 text-left ${(preset as any).isBuiltIn ? 'bg-green-50' : ''}`}
>
<div className="w-5 flex items-center justify-center">
{selectedPreset === preset.id && <Check size={14} className="text-blue-500" />}
</div>
<div>
<div className="flex-1">
<div className="font-semibold text-gray-900">{preset.name}</div>
<div className="text-sm text-gray-600">{preset.description}</div>
</div>
{(preset as any).isBuiltIn && (
<span className="text-xs text-green-600 bg-green-100 px-2 py-0.5 rounded"></span>
)}
</button>
))}
</div>
@@ -289,75 +314,211 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
</div>
</div>
{/* API 地址 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">API </label>
<input
type="text"
value={localConfig.endpoint}
onChange={(e) => 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"
/>
</div>
{/* API Key */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">API Key</label>
<input
type="password"
value={localConfig.apiKey}
onChange={(e) => 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-..."
/>
</div>
{/* 模型选择 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2"></label>
<div className="relative">
<div className="flex gap-2">
{/* 下拉选择 */}
<button
onClick={() => setShowModelDropdown(!showModelDropdown)}
className="flex-1 flex items-center justify-between px-4 py-3 bg-white border border-gray-300 rounded-lg hover:border-blue-500"
>
<span className="text-gray-900 font-medium truncate">{localConfig.model || '选择模型'}</span>
<ChevronDown size={16} className="text-gray-500 flex-shrink-0" />
</button>
{/* 内置 AI 说明 */}
{selectedPreset === 'builtin' && (
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-center gap-2 text-green-800 font-medium mb-2">
<Check size={16} />
AI
</div>
{showModelDropdown && (
<div className="absolute top-full left-0 right-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-xl z-10 max-h-64 overflow-auto">
{/* 手动输入选项 */}
<div className="p-2 border-b border-gray-100">
<p className="text-sm text-green-700">
使 AI 使
</p>
</div>
)}
{/* 自定义配置(非内置时显示) */}
{selectedPreset !== 'builtin' && (
<>
{/* API 地址 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">API </label>
<input
type="text"
value={localConfig.endpoint}
onChange={(e) => 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"
/>
</div>
{/* API Key */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">API Key</label>
<input
type="password"
value={localConfig.apiKey}
onChange={(e) => 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-..."
/>
</div>
{/* 模型模式选择 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2"></label>
<div className="grid grid-cols-3 gap-2">
<button
onClick={() => setLocalConfig({ ...localConfig, modelMode: 'dual' })}
className={`px-3 py-2 rounded-lg border text-sm ${
localConfig.modelMode === 'dual'
? 'border-blue-500 bg-blue-50 text-blue-700'
: 'border-gray-300 text-gray-700 hover:border-gray-400'
}`}
>
</button>
<button
onClick={() => setLocalConfig({ ...localConfig, modelMode: 'single' })}
className={`px-3 py-2 rounded-lg border text-sm ${
localConfig.modelMode === 'single'
? 'border-blue-500 bg-blue-50 text-blue-700'
: 'border-gray-300 text-gray-700 hover:border-gray-400'
}`}
>
</button>
<button
onClick={() => setLocalConfig({ ...localConfig, modelMode: 'tool-only' })}
className={`px-3 py-2 rounded-lg border text-sm ${
(localConfig.modelMode === 'tool-only' || !localConfig.modelMode)
? 'border-blue-500 bg-blue-50 text-blue-700'
: 'border-gray-300 text-gray-700 hover:border-gray-400'
}`}
>
</button>
</div>
<p className="mt-1 text-xs text-gray-500">
{localConfig.modelMode === 'dual' && '视觉模型分析页面 + 工具模型执行操作(推荐)'}
{localConfig.modelMode === 'single' && '单个模型同时具备视觉和工具调用能力'}
{(localConfig.modelMode === 'tool-only' || !localConfig.modelMode) && '仅使用工具模型,不分析页面截图'}
</p>
</div>
{/* 工具模型选择 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{localConfig.modelMode === 'dual' ? '工具模型' : '模型'}
</label>
<div className="relative">
<div className="flex gap-2">
{/* 下拉选择 */}
<button
onClick={() => setShowModelDropdown(!showModelDropdown)}
className="flex-1 flex items-center justify-between px-4 py-3 bg-white border border-gray-300 rounded-lg hover:border-blue-500"
>
<span className="text-gray-900 font-medium truncate">{localConfig.model || '选择模型'}</span>
<ChevronDown size={16} className="text-gray-500 flex-shrink-0" />
</button>
</div>
{showModelDropdown && (
<div className="absolute top-full left-0 right-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-xl z-10 max-h-64 overflow-auto">
{/* 手动输入选项 */}
<div className="p-2 border-b border-gray-100">
<input
type="text"
value={localConfig.model}
onChange={(e) => 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()}
/>
</div>
{/* 预设模型列表 */}
{currentPreset.models.map((model) => (
<button
key={model}
onClick={() => handleModelSelect(model)}
className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 text-left"
>
<div className="w-5 flex items-center justify-center">
{localConfig.model === model && <Check size={14} className="text-blue-500" />}
</div>
<span className="text-gray-800 text-sm">{model}</span>
</button>
))}
</div>
)}
</div>
</div>
{/* 视觉模型配置(仅双模型模式显示) */}
{localConfig.modelMode === 'dual' && (
<div className="space-y-4 p-4 bg-gray-50 rounded-lg border border-gray-200">
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-gray-700"></label>
<label className="flex items-center gap-2 text-sm text-gray-600 cursor-pointer">
<input
type="checkbox"
checked={!!(localConfig.visionEndpoint)}
onChange={(e) => {
if (e.target.checked) {
// 勾选时,设置默认的 SiliconFlow 地址
setLocalConfig({
...localConfig,
visionEndpoint: 'https://api.siliconflow.cn/v1/chat/completions',
visionApiKey: ''
})
} else {
// 取消勾选时,清空独立配置
setLocalConfig({ ...localConfig, visionEndpoint: '', visionApiKey: '' })
}
}}
className="w-4 h-4 rounded border-gray-300 text-blue-500 focus:ring-blue-500"
/>
使
</label>
</div>
{/* 独立服务商配置 */}
{localConfig.visionEndpoint ? (
<>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1"> API </label>
<input
type="text"
value={localConfig.visionEndpoint || ''}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1"> API Key</label>
<input
type="password"
value={localConfig.visionApiKey || ''}
onChange={(e) => 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-..."
/>
</div>
</>
) : (
<p className="text-xs text-gray-500">
使 API Key
</p>
)}
<div>
<label className="block text-xs font-medium text-gray-600 mb-1"></label>
<input
type="text"
value={localConfig.model}
onChange={(e) => 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"
/>
<p className="mt-1 text-xs text-gray-500">
</p>
</div>
{/* 预设模型列表 */}
{currentPreset.models.map((model) => (
<button
key={model}
onClick={() => handleModelSelect(model)}
className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 text-left"
>
<div className="w-5 flex items-center justify-center">
{localConfig.model === model && <Check size={14} className="text-blue-500" />}
</div>
<span className="text-gray-800 text-sm">{model}</span>
</button>
))}
</div>
)}
</div>
</div>
</>
)}
{/* 保存按钮 */}
<button

File diff suppressed because it is too large Load Diff

View File

@@ -63,9 +63,17 @@ export interface ChatSession {
}
export interface AIConfig {
// 工具模型配置
endpoint: string
apiKey: string
model: string
// 视觉模型配置(可选,双模型模式时使用)
visionEndpoint?: string // 视觉模型 API 地址(留空则使用工具模型的地址)
visionApiKey?: string // 视觉模型 API Key留空则使用工具模型的 Key
visionModel?: string // 视觉模型名称
// 模式设置
useBuiltIn?: boolean // 使用内置 AI 服务
modelMode?: 'dual' | 'single' | 'tool-only' // dual=双模型, single=单模型, tool-only=仅工具模型
}
export interface SavedAIConfig extends AIConfig {
@@ -81,6 +89,10 @@ export interface MouseState {
clicking: boolean
clickId: number // 用于触发点击动画
duration: number // 移动动画时长
mode: 'normal' | 'fidget' | 'panic' // 行为模式:正常/思考乱动/紧张乱动
fidgetIntensity: number // 乱动强度 0-1
baseX: number // 乱动时的基准位置
baseY: number
}
// 搜索引擎配置
@@ -159,6 +171,7 @@ interface AppState {
messages: Message[]
isAILoading: boolean
aiStopRequested: boolean
currentModelType: 'tool' | 'vision' | null // 当前正在调用的模型类型
chatSessions: ChatSession[]
currentSessionId: string | null
@@ -210,6 +223,7 @@ interface AppState {
updateLastMessageWithToolCalls: (content: string, toolCalls: Array<{ name: string; arguments: object; result?: string }>) => void
clearMessages: () => void
setAILoading: (loading: boolean) => void
setCurrentModelType: (type: 'tool' | 'vision' | null) => void
stopAI: () => void
resetAIStop: () => void
@@ -237,6 +251,9 @@ interface AppState {
hideMouse: () => void
moveMouse: (x: number, y: number, duration?: number) => void
clickMouse: () => void
fidgetMouse: (intensity?: number) => void // 思考时微微乱动
panicMouse: (duration?: number) => void // 出错时紧张乱动
stopFidget: () => void // 停止乱动
// 浏览器设置
setBrowserSettings: (settings: Partial<BrowserSettings>, navigateToHomepage?: boolean) => void
@@ -275,12 +292,14 @@ export const useStore = create<AppState>((set, get) => ({
messages: [],
isAILoading: false,
aiStopRequested: false,
currentModelType: null,
chatSessions: [],
currentSessionId: null,
aiConfig: {
endpoint: 'https://api.openai.com/v1/chat/completions',
endpoint: '',
apiKey: '',
model: 'gpt-4'
model: '',
useBuiltIn: true // 默认使用内置 AI
},
savedConfigs: [],
mouseState: {
@@ -289,7 +308,11 @@ export const useStore = create<AppState>((set, get) => ({
y: 0,
clicking: false,
clickId: 0,
duration: 300
duration: 300,
mode: 'normal' as const,
fidgetIntensity: 0,
baseX: 0,
baseY: 0
},
downloadedImages: [],
elementSelectionRequest: null,
@@ -516,10 +539,12 @@ export const useStore = create<AppState>((set, get) => ({
},
setAILoading: (isAILoading) => set({ isAILoading }),
stopAI: () => set({ aiStopRequested: true, isAILoading: false }),
resetAIStop: () => set({ aiStopRequested: false }),
setCurrentModelType: (currentModelType) => set({ currentModelType }),
stopAI: () => set({ aiStopRequested: true, isAILoading: false, currentModelType: null }),
resetAIStop: () => set({ aiStopRequested: false, currentModelType: null }),
// 聊天会话管理
newChatSession: () => {
@@ -715,6 +740,46 @@ export const useStore = create<AppState>((set, get) => ({
}
})),
// 思考时微微乱动
fidgetMouse: (intensity = 0.3) => set((state) => ({
mouseState: {
...state.mouseState,
mode: 'fidget' as const,
fidgetIntensity: intensity,
baseX: state.mouseState.x,
baseY: state.mouseState.y
}
})),
// 出错时紧张乱动
panicMouse: (duration = 1500) => {
set((state) => ({
mouseState: {
...state.mouseState,
mode: 'panic' as const,
fidgetIntensity: 1,
baseX: state.mouseState.x,
baseY: state.mouseState.y
}
}))
// 自动停止
setTimeout(() => {
const currentMode = get().mouseState.mode
if (currentMode === 'panic') {
get().stopFidget()
}
}, duration)
},
// 停止乱动
stopFidget: () => set((state) => ({
mouseState: {
...state.mouseState,
mode: 'normal' as const,
fidgetIntensity: 0
}
})),
// 浏览器设置
setBrowserSettings: (settings, navigateToHomepage = true) => {
set((state) => ({