mirror of
https://github.com/violettoolssite/CFspider.git
synced 2026-04-04 18:59:02 +08:00
feat(browser): enhance AI click accuracy and add post-action verification
This commit is contained in:
34
.github/workflows/build-browser.yml
vendored
34
.github/workflows/build-browser.yml
vendored
@@ -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
|
||||
|
||||
### 功能特性
|
||||
- 自然语言控制浏览器
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
616
cfspider-browser/package-lock.json
generated
616
cfspider-browser/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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) => ({
|
||||
|
||||
Reference in New Issue
Block a user