Merge branch 'main' into feat/aisdk-package

This commit is contained in:
MyPrototypeWhat 2025-08-15 11:11:20 +08:00
commit d05ed94702
23 changed files with 591 additions and 304 deletions

View File

@ -78,7 +78,7 @@
"node-stream-zip": "^1.15.0", "node-stream-zip": "^1.15.0",
"officeparser": "^4.2.0", "officeparser": "^4.2.0",
"os-proxy-config": "^1.1.2", "os-proxy-config": "^1.1.2",
"selection-hook": "^1.0.8", "selection-hook": "^1.0.9",
"turndown": "7.2.0" "turndown": "7.2.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -9,6 +9,7 @@ import { CancellationToken, UpdateInfo } from 'builder-util-runtime'
import { app, BrowserWindow, dialog } from 'electron' import { app, BrowserWindow, dialog } from 'electron'
import { AppUpdater as _AppUpdater, autoUpdater, Logger, NsisUpdater, UpdateCheckResult } from 'electron-updater' import { AppUpdater as _AppUpdater, autoUpdater, Logger, NsisUpdater, UpdateCheckResult } from 'electron-updater'
import path from 'path' import path from 'path'
import semver from 'semver'
import icon from '../../../build/icon.png?asset' import icon from '../../../build/icon.png?asset'
import { configManager } from './ConfigManager' import { configManager } from './ConfigManager'
@ -44,12 +45,6 @@ export default class AppUpdater {
// 检测到不需要更新时 // 检测到不需要更新时
autoUpdater.on('update-not-available', () => { autoUpdater.on('update-not-available', () => {
if (configManager.getTestPlan() && this.autoUpdater.channel !== UpgradeChannel.LATEST) {
logger.info('test plan is enabled, but update is not available, do not send update not available event')
// will not send update not available event, because will check for updates with latest channel
return
}
windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateNotAvailable) windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateNotAvailable)
}) })
@ -72,18 +67,24 @@ export default class AppUpdater {
this.autoUpdater = autoUpdater this.autoUpdater = autoUpdater
} }
private async _getPreReleaseVersionFromGithub(channel: UpgradeChannel) { private async _getReleaseVersionFromGithub(channel: UpgradeChannel) {
const headers = {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
'Accept-Language': 'en-US,en;q=0.9'
}
try { try {
logger.info(`get pre release version from github: ${channel}`) logger.info(`get release version from github: ${channel}`)
const responses = await fetch('https://api.github.com/repos/CherryHQ/cherry-studio/releases?per_page=8', { const responses = await fetch('https://api.github.com/repos/CherryHQ/cherry-studio/releases?per_page=8', {
headers: { headers
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
'Accept-Language': 'en-US,en;q=0.9'
}
}) })
const data = (await responses.json()) as GithubReleaseInfo[] const data = (await responses.json()) as GithubReleaseInfo[]
let mightHaveLatest = false
const release: GithubReleaseInfo | undefined = data.find((item: GithubReleaseInfo) => { const release: GithubReleaseInfo | undefined = data.find((item: GithubReleaseInfo) => {
if (!item.draft && !item.prerelease) {
mightHaveLatest = true
}
return item.prerelease && item.tag_name.includes(`-${channel}.`) return item.prerelease && item.tag_name.includes(`-${channel}.`)
}) })
@ -91,8 +92,29 @@ export default class AppUpdater {
return null return null
} }
logger.info(`prerelease url is ${release.tag_name}, set channel to ${channel}`) // if the release version is the same as the current version, return null
if (release.tag_name === app.getVersion()) {
return null
}
if (mightHaveLatest) {
logger.info(`might have latest release, get latest release`)
const latestReleaseResponse = await fetch(
'https://api.github.com/repos/CherryHQ/cherry-studio/releases/latest',
{
headers
}
)
const latestRelease = (await latestReleaseResponse.json()) as GithubReleaseInfo
if (semver.gt(latestRelease.tag_name, release.tag_name)) {
logger.info(
`latest release version is ${latestRelease.tag_name}, prerelease version is ${release.tag_name}, return null`
)
return null
}
}
logger.info(`release url is ${release.tag_name}, set channel to ${channel}`)
return `https://github.com/CherryHQ/cherry-studio/releases/download/${release.tag_name}` return `https://github.com/CherryHQ/cherry-studio/releases/download/${release.tag_name}`
} catch (error) { } catch (error) {
logger.error('Failed to get latest not draft version from github:', error as Error) logger.error('Failed to get latest not draft version from github:', error as Error)
@ -151,14 +173,14 @@ export default class AppUpdater {
return return
} }
const preReleaseUrl = await this._getPreReleaseVersionFromGithub(channel) const releaseUrl = await this._getReleaseVersionFromGithub(channel)
if (preReleaseUrl) { if (releaseUrl) {
logger.info(`prerelease url is ${preReleaseUrl}, set channel to ${channel}`) logger.info(`release url is ${releaseUrl}, set channel to ${channel}`)
this._setChannel(channel, preReleaseUrl) this._setChannel(channel, releaseUrl)
return return
} }
// if no prerelease url, use github latest to avoid error // if no prerelease url, use github latest to get release
this._setChannel(UpgradeChannel.LATEST, FeedUrl.GITHUB_LATEST) this._setChannel(UpgradeChannel.LATEST, FeedUrl.GITHUB_LATEST)
return return
} }
@ -195,17 +217,6 @@ export default class AppUpdater {
`update check result: ${this.updateCheckResult?.isUpdateAvailable}, channel: ${this.autoUpdater.channel}, currentVersion: ${this.autoUpdater.currentVersion}` `update check result: ${this.updateCheckResult?.isUpdateAvailable}, channel: ${this.autoUpdater.channel}, currentVersion: ${this.autoUpdater.currentVersion}`
) )
// if the update is not available, and the test plan is enabled, set the feed url to the github latest
if (
!this.updateCheckResult?.isUpdateAvailable &&
configManager.getTestPlan() &&
this.autoUpdater.channel !== UpgradeChannel.LATEST
) {
logger.info('test plan is enabled, but update is not available, set channel to latest')
this._setChannel(UpgradeChannel.LATEST, FeedUrl.GITHUB_LATEST)
this.updateCheckResult = await this.autoUpdater.checkForUpdates()
}
if (this.updateCheckResult?.isUpdateAvailable && !this.autoUpdater.autoDownload) { if (this.updateCheckResult?.isUpdateAvailable && !this.autoUpdater.autoDownload) {
// 如果 autoDownload 为 false则需要再调用下面的函数触发下 // 如果 autoDownload 为 false则需要再调用下面的函数触发下
// do not use await, because it will block the return of this function // do not use await, because it will block the return of this function

View File

@ -21,6 +21,27 @@ class BackupManager {
private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp') private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp')
private backupDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup') private backupDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup')
// 缓存实例,避免重复创建
private s3Storage: S3Storage | null = null
private webdavInstance: WebDav | null = null
// 缓存核心连接配置,用于检测连接配置是否变更
private cachedS3ConnectionConfig: {
endpoint: string
region: string
bucket: string
accessKeyId: string
secretAccessKey: string
root?: string
} | null = null
private cachedWebdavConnectionConfig: {
webdavHost: string
webdavUser?: string
webdavPass?: string
webdavPath?: string
} | null = null
constructor() { constructor() {
this.checkConnection = this.checkConnection.bind(this) this.checkConnection = this.checkConnection.bind(this)
this.backup = this.backup.bind(this) this.backup = this.backup.bind(this)
@ -87,6 +108,88 @@ class BackupManager {
} }
} }
/**
* fileName
*/
private isS3ConfigEqual(cachedConfig: typeof this.cachedS3ConnectionConfig, config: S3Config): boolean {
if (!cachedConfig) return false
return (
cachedConfig.endpoint === config.endpoint &&
cachedConfig.region === config.region &&
cachedConfig.bucket === config.bucket &&
cachedConfig.accessKeyId === config.accessKeyId &&
cachedConfig.secretAccessKey === config.secretAccessKey &&
cachedConfig.root === config.root
)
}
/**
* WebDAV fileName
*/
private isWebDavConfigEqual(cachedConfig: typeof this.cachedWebdavConnectionConfig, config: WebDavConfig): boolean {
if (!cachedConfig) return false
return (
cachedConfig.webdavHost === config.webdavHost &&
cachedConfig.webdavUser === config.webdavUser &&
cachedConfig.webdavPass === config.webdavPass &&
cachedConfig.webdavPath === config.webdavPath
)
}
/**
* S3Storage
*
*/
private getS3Storage(config: S3Config): S3Storage {
// 检查核心连接配置是否变更
const configChanged = !this.isS3ConfigEqual(this.cachedS3ConnectionConfig, config)
if (configChanged || !this.s3Storage) {
this.s3Storage = new S3Storage(config)
// 只缓存连接相关的配置字段
this.cachedS3ConnectionConfig = {
endpoint: config.endpoint,
region: config.region,
bucket: config.bucket,
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
root: config.root
}
logger.debug('[BackupManager] Created new S3Storage instance')
} else {
logger.debug('[BackupManager] Reusing existing S3Storage instance')
}
return this.s3Storage
}
/**
* WebDav
*
*/
private getWebDavInstance(config: WebDavConfig): WebDav {
// 检查核心连接配置是否变更
const configChanged = !this.isWebDavConfigEqual(this.cachedWebdavConnectionConfig, config)
if (configChanged || !this.webdavInstance) {
this.webdavInstance = new WebDav(config)
// 只缓存连接相关的配置字段
this.cachedWebdavConnectionConfig = {
webdavHost: config.webdavHost,
webdavUser: config.webdavUser,
webdavPass: config.webdavPass,
webdavPath: config.webdavPath
}
logger.debug('[BackupManager] Created new WebDav instance')
} else {
logger.debug('[BackupManager] Reusing existing WebDav instance')
}
return this.webdavInstance
}
async backup( async backup(
_: Electron.IpcMainInvokeEvent, _: Electron.IpcMainInvokeEvent,
fileName: string, fileName: string,
@ -322,7 +425,7 @@ class BackupManager {
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) { async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
const filename = webdavConfig.fileName || 'cherry-studio.backup.zip' const filename = webdavConfig.fileName || 'cherry-studio.backup.zip'
const backupedFilePath = await this.backup(_, filename, data, undefined, webdavConfig.skipBackupFile) const backupedFilePath = await this.backup(_, filename, data, undefined, webdavConfig.skipBackupFile)
const webdavClient = new WebDav(webdavConfig) const webdavClient = this.getWebDavInstance(webdavConfig)
try { try {
let result let result
if (webdavConfig.disableStream) { if (webdavConfig.disableStream) {
@ -349,7 +452,7 @@ class BackupManager {
async restoreFromWebdav(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) { async restoreFromWebdav(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) {
const filename = webdavConfig.fileName || 'cherry-studio.backup.zip' const filename = webdavConfig.fileName || 'cherry-studio.backup.zip'
const webdavClient = new WebDav(webdavConfig) const webdavClient = this.getWebDavInstance(webdavConfig)
try { try {
const retrievedFile = await webdavClient.getFileContents(filename) const retrievedFile = await webdavClient.getFileContents(filename)
const backupedFilePath = path.join(this.backupDir, filename) const backupedFilePath = path.join(this.backupDir, filename)
@ -377,7 +480,7 @@ class BackupManager {
listWebdavFiles = async (_: Electron.IpcMainInvokeEvent, config: WebDavConfig) => { listWebdavFiles = async (_: Electron.IpcMainInvokeEvent, config: WebDavConfig) => {
try { try {
const client = new WebDav(config) const client = this.getWebDavInstance(config)
const response = await client.getDirectoryContents() const response = await client.getDirectoryContents()
const files = Array.isArray(response) ? response : response.data const files = Array.isArray(response) ? response : response.data
@ -467,7 +570,7 @@ class BackupManager {
} }
async checkConnection(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) { async checkConnection(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) {
const webdavClient = new WebDav(webdavConfig) const webdavClient = this.getWebDavInstance(webdavConfig)
return await webdavClient.checkConnection() return await webdavClient.checkConnection()
} }
@ -477,13 +580,13 @@ class BackupManager {
path: string, path: string,
options?: CreateDirectoryOptions options?: CreateDirectoryOptions
) { ) {
const webdavClient = new WebDav(webdavConfig) const webdavClient = this.getWebDavInstance(webdavConfig)
return await webdavClient.createDirectory(path, options) return await webdavClient.createDirectory(path, options)
} }
async deleteWebdavFile(_: Electron.IpcMainInvokeEvent, fileName: string, webdavConfig: WebDavConfig) { async deleteWebdavFile(_: Electron.IpcMainInvokeEvent, fileName: string, webdavConfig: WebDavConfig) {
try { try {
const webdavClient = new WebDav(webdavConfig) const webdavClient = this.getWebDavInstance(webdavConfig)
return await webdavClient.deleteFile(fileName) return await webdavClient.deleteFile(fileName)
} catch (error: any) { } catch (error: any) {
logger.error('Failed to delete WebDAV file:', error) logger.error('Failed to delete WebDAV file:', error)
@ -525,7 +628,7 @@ class BackupManager {
logger.debug(`Starting S3 backup to ${filename}`) logger.debug(`Starting S3 backup to ${filename}`)
const backupedFilePath = await this.backup(_, filename, data, undefined, s3Config.skipBackupFile) const backupedFilePath = await this.backup(_, filename, data, undefined, s3Config.skipBackupFile)
const s3Client = new S3Storage(s3Config) const s3Client = this.getS3Storage(s3Config)
try { try {
const fileBuffer = await fs.promises.readFile(backupedFilePath) const fileBuffer = await fs.promises.readFile(backupedFilePath)
const result = await s3Client.putFileContents(filename, fileBuffer) const result = await s3Client.putFileContents(filename, fileBuffer)
@ -603,7 +706,7 @@ class BackupManager {
logger.debug(`Starting restore from S3: ${filename}`) logger.debug(`Starting restore from S3: ${filename}`)
const s3Client = new S3Storage(s3Config) const s3Client = this.getS3Storage(s3Config)
try { try {
const retrievedFile = await s3Client.getFileContents(filename) const retrievedFile = await s3Client.getFileContents(filename)
const backupedFilePath = path.join(this.backupDir, filename) const backupedFilePath = path.join(this.backupDir, filename)
@ -628,7 +731,7 @@ class BackupManager {
listS3Files = async (_: Electron.IpcMainInvokeEvent, s3Config: S3Config) => { listS3Files = async (_: Electron.IpcMainInvokeEvent, s3Config: S3Config) => {
try { try {
const s3Client = new S3Storage(s3Config) const s3Client = this.getS3Storage(s3Config)
const objects = await s3Client.listFiles() const objects = await s3Client.listFiles()
const files = objects const files = objects
@ -652,7 +755,7 @@ class BackupManager {
async deleteS3File(_: Electron.IpcMainInvokeEvent, fileName: string, s3Config: S3Config) { async deleteS3File(_: Electron.IpcMainInvokeEvent, fileName: string, s3Config: S3Config) {
try { try {
const s3Client = new S3Storage(s3Config) const s3Client = this.getS3Storage(s3Config)
return await s3Client.deleteFile(fileName) return await s3Client.deleteFile(fileName)
} catch (error: any) { } catch (error: any) {
logger.error('Failed to delete S3 file:', error) logger.error('Failed to delete S3 file:', error)
@ -661,7 +764,7 @@ class BackupManager {
} }
async checkS3Connection(_: Electron.IpcMainInvokeEvent, s3Config: S3Config) { async checkS3Connection(_: Electron.IpcMainInvokeEvent, s3Config: S3Config) {
const s3Client = new S3Storage(s3Config) const s3Client = this.getS3Storage(s3Config)
return await s3Client.checkConnection() return await s3Client.checkConnection()
} }
} }

View File

@ -112,3 +112,129 @@ export function MdiLightbulbOn(props: SVGProps<SVGSVGElement>) {
</svg> </svg>
) )
} }
export function BingLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg
fill="currentColor"
fill-rule="evenodd"
width="1em"
height="1em"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...props}>
<path d="M4.842.005a.966.966 0 01.604.142l2.62 1.813c.369.256.492.352.637.496.471.47.752 1.09.797 1.765l.008.847.003 1.441.004 13.002.144-.094 7.015-4.353.015.003.029.01c-.398-.17-.893-.339-1.655-.566l-.484-.146c-.584-.18-.71-.238-.921-.38a2.009 2.009 0 01-.37-.312 2.172 2.172 0 01-.41-.592L11.32 9.063c-.166-.444-.166-.49-.156-.63a.92.92 0 01.806-.864l.094-.01c.044-.005.22.023.29.044l.052.021c.06.026.16.075.313.154l3.63 1.908a6.626 6.626 0 013.292 4.531c.194.99.159 2.037-.102 3.012-.216.805-.639 1.694-1.054 2.213l-.08.099-.047.05c-.01.01-.013.01-.01.002l.043-.074-.072.114c-.011.031-.233.28-.38.425l-.17.161c-.22.202-.431.36-.832.62L13.544 23c-.941.6-1.86.912-2.913.992-.23.018-.854.008-1.074-.017a6.31 6.31 0 01-1.658-.412c-1.854-.738-3.223-2.288-3.705-4.195a8.077 8.077 0 01-.121-.57l-.046-.325a1.123 1.123 0 01-.014-.168l-.006-.029L4 11.617 4.01.866a.981.981 0 01.007-.111.943.943 0 01.825-.75z"></path>
</svg>
)
}
export function SearXNGLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 265 265" style={{ display: 'block' }} {...props}>
<g transform="translate(-40.921 -17.417)">
<circle
cx="142.2"
cy="122.9"
r="85"
fill="none"
stroke="currentColor"
strokeWidth="28.3465"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="11.3386"
/>
<path
d="M118.4 77.6c19.8-10.2 44-6.4 59.7 9.4s19.3 40 8.9 59.7"
fill="none"
stroke="currentColor"
strokeWidth="14.1732"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="11.3386"
/>
<path d="m184.2 202 37-38.6 81.8 78.3-37 38.6z" fill="currentColor" />
</g>
</svg>
)
}
export function TavilyLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="m16.44.964 4.921 7.79c.79 1.252-.108 2.883-1.588 2.883H17.76V23.3h-2.91V.088c.61 0 1.22.292 1.59.876z"
fill="currentColor"
/>
<path
d="M8.342 8.755 13.263.964a1.864 1.864 0 0 1 1.59-.876V23.3a4.87 4.87 0 0 0-.252-.006c-.99 0-1.907.311-2.658.842V11.637H9.93c-1.48 0-2.38-1.631-1.589-2.882z"
fill="currentColor"
/>
<path
d="M30.278 31H18.031a4.596 4.596 0 0 0 1.219-2.91h22.577c0 .61-.292 1.22-.875 1.59L33.16 34.6c-1.251.791-2.883-.108-2.883-1.588V31z"
fill="currentColor"
/>
<path
d="m33.16 21.581 7.79 4.921c.585.369.876.979.876 1.589H19.25a4.619 4.619 0 0 0-.858-2.91h11.887V23.17c0-1.48 1.631-2.38 2.882-1.589z"
fill="currentColor"
/>
<path
d="m8.24 34.25-7.107 7.108a1.864 1.864 0 0 0 1.742.504l8.989-2.03c1.443-.325 1.961-2.114.915-3.16l-1.423-1.423 5.356-5.356a2.805 2.805 0 0 0 0-3.966l-.074-.075L8.24 34.25z"
fill="currentColor"
/>
<path
d="m7.243 31.135 5.355-5.356a2.805 2.805 0 0 1 3.967 0l.074.074-8.397 8.397-7.108 7.108a1.864 1.864 0 0 1-.504-1.742l2.029-8.989c.325-1.444 2.115-1.961 3.161-.915l1.423 1.423z"
fill="currentColor"
/>
</svg>
)
}
export function ExaLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg
fill="currentColor"
fill-rule="evenodd"
width="1em"
height="1em"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...props}>
<title>Exa</title>
<path
clip-rule="evenodd"
d="M3 0h19v1.791L13.892 12 22 22.209V24H3V0zm9.62 10.348l6.589-8.557H6.03l6.59 8.557zM5.138 3.935v7.17h5.52l-5.52-7.17zm5.52 8.96h-5.52v7.17l5.52-7.17zM6.03 22.21l6.59-8.557 6.589 8.557H6.03z"></path>
</svg>
)
}
export function BochaLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg width="1em" height="1em" viewBox="0 0 135 116" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12.5754 13.8123C24.6109 7.94459 39.1223 12.9435 44.9955 24.9805L57.5355 50.6805C60.4695 56.6936 57.9756 63.9478 51.9652 66.8832C51.9627 66.8844 51.9602 66.8856 51.9577 66.8868C45.94 69.8206 38.6843 67.3212 35.7477 61.3027L12.5754 13.8123Z"
fill="currentColor"
/>
<path
opacity="0.64774"
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 38.3013C9.46916 28.836 24.813 28.836 34.2822 38.3013L55.2526 59.2631C59.9819 63.9904 59.9852 71.6582 55.2601 76.3896C55.2576 76.3921 55.2551 76.3946 55.2526 76.397C50.5181 81.1297 42.8461 81.1297 38.1116 76.397L0 38.3013Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M86.8777 18.0444C113.939 18.0444 135.876 39.9725 135.876 67.0222C135.876 80.2286 129.086 93.6477 120.585 102.457L117.065 98.2367C111.026 90.9998 108.882 81.2777 111.314 72.1702C111.755 70.5198 111.976 69.0033 111.976 67.6209C111.976 53.6689 100.661 42.3586 86.7029 42.3586C72.7452 42.3586 61.4303 53.6689 61.4303 67.6209C61.4303 81.5728 72.7452 92.8831 86.7029 92.8831C89.3159 92.8831 91.8363 92.4867 94.2071 91.7508C101.312 89.5455 109.054 91.3768 114.419 96.5322L120.585 102.457C111.83 110.626 99.7992 116 86.8777 116C59.8168 116 37.8796 94.0719 37.8796 67.0222C37.8796 39.9725 59.8168 18.0444 86.8777 18.0444Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M37.8796 0C51.2677 0 62.1208 10.8581 62.1208 24.2522V41.7389C62.1208 55.133 51.2677 65.9911 37.8796 65.9911V0Z"
fill="currentColor"
/>
</svg>
)
}

View File

@ -3,7 +3,14 @@ import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
import SelectProviderModelPopup from '@renderer/pages/settings/ProviderSettings/SelectProviderModelPopup' import SelectProviderModelPopup from '@renderer/pages/settings/ProviderSettings/SelectProviderModelPopup'
import { checkApi } from '@renderer/services/ApiService' import { checkApi } from '@renderer/services/ApiService'
import WebSearchService from '@renderer/services/WebSearchService' import WebSearchService from '@renderer/services/WebSearchService'
import { Model, PreprocessProvider, Provider, WebSearchProvider } from '@renderer/types' import {
isPreprocessProviderId,
isWebSearchProviderId,
Model,
PreprocessProvider,
Provider,
WebSearchProvider
} from '@renderer/types'
import { ApiKeyConnectivity, ApiKeyWithStatus, HealthStatus } from '@renderer/types/healthCheck' import { ApiKeyConnectivity, ApiKeyWithStatus, HealthStatus } from '@renderer/types/healthCheck'
import { formatApiKeys, splitApiKeyString } from '@renderer/utils/api' import { formatApiKeys, splitApiKeyString } from '@renderer/utils/api'
import { formatErrorMessage } from '@renderer/utils/error' import { formatErrorMessage } from '@renderer/utils/error'
@ -12,12 +19,11 @@ import { isEmpty } from 'lodash'
import { useCallback, useMemo, useState } from 'react' import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ApiKeyValidity, ApiProviderKind, ApiProviderUnion } from './types' import { ApiKeyValidity, ApiProvider, UpdateApiProviderFunc } from './types'
interface UseApiKeysProps { interface UseApiKeysProps {
provider: ApiProviderUnion provider: ApiProvider
updateProvider: (provider: Partial<ApiProviderUnion>) => void updateProvider: UpdateApiProviderFunc
providerKind: ApiProviderKind
} }
const logger = loggerService.withContext('ApiKeyListPopup') const logger = loggerService.withContext('ApiKeyListPopup')
@ -25,7 +31,7 @@ const logger = loggerService.withContext('ApiKeyListPopup')
/** /**
* API Keys hook * API Keys hook
*/ */
export function useApiKeys({ provider, updateProvider, providerKind }: UseApiKeysProps) { export function useApiKeys({ provider, updateProvider }: UseApiKeysProps) {
const { t } = useTranslation() const { t } = useTranslation()
// 连通性检查的 UI 状态管理 // 连通性检查的 UI 状态管理
@ -199,11 +205,13 @@ export function useApiKeys({ provider, updateProvider, providerKind }: UseApiKey
try { try {
const startTime = Date.now() const startTime = Date.now()
if (isLlmProvider(provider, providerKind) && model) { if (isLlmProvider(provider) && model) {
await checkApi({ ...provider, apiKey: keyToCheck }, model) await checkApi({ ...provider, apiKey: keyToCheck }, model)
} else { } else if (isWebSearchProvider(provider)) {
const result = await WebSearchService.checkSearch({ ...provider, apiKey: keyToCheck }) const result = await WebSearchService.checkSearch({ ...provider, apiKey: keyToCheck })
if (!result.valid) throw new Error(result.error) if (!result.valid) throw new Error(result.error)
} else {
// 不处理预处理供应商
} }
const latency = Date.now() - startTime const latency = Date.now() - startTime
@ -228,7 +236,7 @@ export function useApiKeys({ provider, updateProvider, providerKind }: UseApiKey
logger.error('failed to validate the connectivity of the api key', error) logger.error('failed to validate the connectivity of the api key', error)
} }
}, },
[keys, connectivityStates, updateConnectivityState, provider, providerKind] [keys, connectivityStates, updateConnectivityState, provider]
) )
// 检查单个 key 的连通性 // 检查单个 key 的连通性
@ -240,23 +248,23 @@ export function useApiKeys({ provider, updateProvider, providerKind }: UseApiKey
const currentState = connectivityStates.get(keyToCheck) const currentState = connectivityStates.get(keyToCheck)
if (currentState?.checking) return if (currentState?.checking) return
const model = isLlmProvider(provider, providerKind) ? await getModelForCheck(provider, t) : undefined const model = isLlmProvider(provider) ? await getModelForCheck(provider, t) : undefined
if (model === null) return if (model === null) return
await runConnectivityCheck(index, model) await runConnectivityCheck(index, model)
}, },
[provider, keys, connectivityStates, providerKind, t, runConnectivityCheck] [provider, keys, connectivityStates, t, runConnectivityCheck]
) )
// 检查所有 keys 的连通性 // 检查所有 keys 的连通性
const checkAllKeysConnectivity = useCallback(async () => { const checkAllKeysConnectivity = useCallback(async () => {
if (!provider || keys.length === 0) return if (!provider || keys.length === 0) return
const model = isLlmProvider(provider, providerKind) ? await getModelForCheck(provider, t) : undefined const model = isLlmProvider(provider) ? await getModelForCheck(provider, t) : undefined
if (model === null) return if (model === null) return
await Promise.allSettled(keys.map((_, index) => runConnectivityCheck(index, model))) await Promise.allSettled(keys.map((_, index) => runConnectivityCheck(index, model)))
}, [provider, keys, providerKind, t, runConnectivityCheck]) }, [provider, keys, t, runConnectivityCheck])
// 计算是否有 key 正在检查 // 计算是否有 key 正在检查
const isChecking = useMemo(() => { const isChecking = useMemo(() => {
@ -275,16 +283,18 @@ export function useApiKeys({ provider, updateProvider, providerKind }: UseApiKey
} }
} }
export function isLlmProvider(obj: any, kind: ApiProviderKind): obj is Provider { export function isLlmProvider(provider: ApiProvider): provider is Provider {
return kind === 'llm' && 'type' in obj && 'models' in obj return 'models' in provider
} }
export function isWebSearchProvider(obj: any, kind: ApiProviderKind): obj is WebSearchProvider { export function isWebSearchProvider(provider: ApiProvider): provider is WebSearchProvider {
return kind === 'websearch' && ('url' in obj || 'engines' in obj) return isWebSearchProviderId(provider.id)
} }
export function isPreprocessProvider(obj: any, kind: ApiProviderKind): obj is PreprocessProvider { export function isPreprocessProvider(provider: ApiProvider): provider is PreprocessProvider {
return kind === 'doc-preprocess' && ('quota' in obj || 'options' in obj) // NOTE: mistral 同时提供预处理和llm服务所以其llm provier可能被误判为预处理provider
// 后面需要使用更严格的判断方式
return isPreprocessProviderId(provider.id) && !isLlmProvider(provider)
} }
// 获取模型用于检查 // 获取模型用于检查

View File

@ -6,6 +6,7 @@ import { useProvider } from '@renderer/hooks/useProvider'
import { useWebSearchProvider } from '@renderer/hooks/useWebSearchProviders' import { useWebSearchProvider } from '@renderer/hooks/useWebSearchProviders'
import { SettingHelpText } from '@renderer/pages/settings' import { SettingHelpText } from '@renderer/pages/settings'
import { isProviderSupportAuth } from '@renderer/services/ProviderService' import { isProviderSupportAuth } from '@renderer/services/ProviderService'
import { PreprocessProviderId, WebSearchProviderId } from '@renderer/types'
import { ApiKeyWithStatus, HealthStatus } from '@renderer/types/healthCheck' import { ApiKeyWithStatus, HealthStatus } from '@renderer/types/healthCheck'
import { Button, Card, Flex, List, Popconfirm, Space, Tooltip, Typography } from 'antd' import { Button, Card, Flex, List, Popconfirm, Space, Tooltip, Typography } from 'antd'
import { Plus } from 'lucide-react' import { Plus } from 'lucide-react'
@ -15,19 +16,18 @@ import styled from 'styled-components'
import { isLlmProvider, useApiKeys } from './hook' import { isLlmProvider, useApiKeys } from './hook'
import ApiKeyItem from './item' import ApiKeyItem from './item'
import { ApiProviderKind, ApiProviderUnion } from './types' import { ApiProvider, UpdateApiProviderFunc } from './types'
interface ApiKeyListProps { interface ApiKeyListProps {
provider: ApiProviderUnion provider: ApiProvider
updateProvider: (provider: Partial<ApiProviderUnion>) => void updateProvider: UpdateApiProviderFunc
providerKind: ApiProviderKind
showHealthCheck?: boolean showHealthCheck?: boolean
} }
/** /**
* Api key CRUD * Api key CRUD
*/ */
export const ApiKeyList: FC<ApiKeyListProps> = ({ provider, updateProvider, providerKind, showHealthCheck = true }) => { export const ApiKeyList: FC<ApiKeyListProps> = ({ provider, updateProvider, showHealthCheck = true }) => {
const { t } = useTranslation() const { t } = useTranslation()
// 临时新项状态 // 临时新项状态
@ -42,7 +42,7 @@ export const ApiKeyList: FC<ApiKeyListProps> = ({ provider, updateProvider, prov
checkKeyConnectivity, checkKeyConnectivity,
checkAllKeysConnectivity, checkAllKeysConnectivity,
isChecking isChecking
} = useApiKeys({ provider, updateProvider, providerKind: providerKind }) } = useApiKeys({ provider, updateProvider })
// 创建一个临时新项 // 创建一个临时新项
const handleAddNew = () => { const handleAddNew = () => {
@ -73,7 +73,7 @@ export const ApiKeyList: FC<ApiKeyListProps> = ({ provider, updateProvider, prov
const shouldAutoFocus = () => { const shouldAutoFocus = () => {
if (provider.apiKey) return false if (provider.apiKey) return false
return isLlmProvider(provider, providerKind) && provider.enabled && !isProviderSupportAuth(provider) return isLlmProvider(provider) && provider.enabled && !isProviderSupportAuth(provider)
} }
// 合并真实 keys 和临时新项 // 合并真实 keys 和临时新项
@ -179,55 +179,33 @@ export const ApiKeyList: FC<ApiKeyListProps> = ({ provider, updateProvider, prov
interface SpecificApiKeyListProps { interface SpecificApiKeyListProps {
providerId: string providerId: string
providerKind: ApiProviderKind
showHealthCheck?: boolean showHealthCheck?: boolean
} }
export const LlmApiKeyList: FC<SpecificApiKeyListProps> = ({ providerId, providerKind, showHealthCheck = true }) => { type WebSearchApiKeyList = SpecificApiKeyListProps & {
providerId: WebSearchProviderId
}
type DocPreprocessApiKeyListProps = SpecificApiKeyListProps & {
providerId: PreprocessProviderId
}
export const LlmApiKeyList: FC<SpecificApiKeyListProps> = ({ providerId, showHealthCheck = true }) => {
const { provider, updateProvider } = useProvider(providerId) const { provider, updateProvider } = useProvider(providerId)
return ( return <ApiKeyList provider={provider} updateProvider={updateProvider} showHealthCheck={showHealthCheck} />
<ApiKeyList
provider={provider}
updateProvider={updateProvider}
providerKind={providerKind}
showHealthCheck={showHealthCheck}
/>
)
} }
export const WebSearchApiKeyList: FC<SpecificApiKeyListProps> = ({ export const WebSearchApiKeyList: FC<WebSearchApiKeyList> = ({ providerId, showHealthCheck = true }) => {
providerId,
providerKind,
showHealthCheck = true
}) => {
const { provider, updateProvider } = useWebSearchProvider(providerId) const { provider, updateProvider } = useWebSearchProvider(providerId)
return ( return <ApiKeyList provider={provider} updateProvider={updateProvider} showHealthCheck={showHealthCheck} />
<ApiKeyList
provider={provider}
updateProvider={updateProvider}
providerKind={providerKind}
showHealthCheck={showHealthCheck}
/>
)
} }
export const DocPreprocessApiKeyList: FC<SpecificApiKeyListProps> = ({ export const DocPreprocessApiKeyList: FC<DocPreprocessApiKeyListProps> = ({ providerId, showHealthCheck = true }) => {
providerId,
providerKind,
showHealthCheck = true
}) => {
const { provider, updateProvider } = usePreprocessProvider(providerId) const { provider, updateProvider } = usePreprocessProvider(providerId)
return ( return <ApiKeyList provider={provider} updateProvider={updateProvider} showHealthCheck={showHealthCheck} />
<ApiKeyList
provider={provider}
updateProvider={updateProvider}
providerKind={providerKind}
showHealthCheck={showHealthCheck}
/>
)
} }
const ListContainer = styled.div` const ListContainer = styled.div`

View File

@ -1,14 +1,13 @@
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import { isPreprocessProviderId, isWebSearchProviderId } from '@renderer/types'
import { Modal } from 'antd' import { Modal } from 'antd'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { DocPreprocessApiKeyList, LlmApiKeyList, WebSearchApiKeyList } from './list' import { DocPreprocessApiKeyList, LlmApiKeyList, WebSearchApiKeyList } from './list'
import { ApiProviderKind } from './types'
interface ShowParams { interface ShowParams {
providerId: string providerId: string
providerKind: ApiProviderKind
title?: string title?: string
showHealthCheck?: boolean showHealthCheck?: boolean
} }
@ -20,7 +19,7 @@ interface Props extends ShowParams {
/** /**
* API Key * API Key
*/ */
const PopupContainer: React.FC<Props> = ({ providerId, providerKind, title, resolve, showHealthCheck = true }) => { const PopupContainer: React.FC<Props> = ({ providerId, title, resolve, showHealthCheck = true }) => {
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const { t } = useTranslation() const { t } = useTranslation()
@ -33,17 +32,14 @@ const PopupContainer: React.FC<Props> = ({ providerId, providerKind, title, reso
} }
const ListComponent = useMemo(() => { const ListComponent = useMemo(() => {
switch (providerKind) { if (isWebSearchProviderId(providerId)) {
case 'llm': return <WebSearchApiKeyList providerId={providerId} showHealthCheck={showHealthCheck} />
return LlmApiKeyList
case 'websearch':
return WebSearchApiKeyList
case 'doc-preprocess':
return DocPreprocessApiKeyList
default:
return null
} }
}, [providerKind]) if (isPreprocessProviderId(providerId)) {
return <DocPreprocessApiKeyList providerId={providerId} showHealthCheck={showHealthCheck} />
}
return <LlmApiKeyList providerId={providerId} showHealthCheck={showHealthCheck} />
}, [providerId, showHealthCheck])
return ( return (
<Modal <Modal
@ -55,9 +51,7 @@ const PopupContainer: React.FC<Props> = ({ providerId, providerKind, title, reso
centered centered
width={600} width={600}
footer={null}> footer={null}>
{ListComponent && ( {ListComponent}
<ListComponent providerId={providerId} providerKind={providerKind} showHealthCheck={showHealthCheck} />
)}
</Modal> </Modal>
) )
} }

View File

@ -8,6 +8,12 @@ export type ApiKeyValidity = {
error?: string error?: string
} }
export type ApiProviderUnion = Provider | WebSearchProvider | PreprocessProvider export type ApiProvider = Provider | WebSearchProvider | PreprocessProvider
export type ApiProviderKind = 'llm' | 'websearch' | 'doc-preprocess' export type UpdateProviderFunc = (p: Partial<Provider>) => void
export type UpdateWebSearchProviderFunc = (p: Partial<WebSearchProvider>) => void
export type UpdatePreprocessProviderFunc = (p: Partial<PreprocessProvider>) => void
export type UpdateApiProviderFunc = UpdateProviderFunc | UpdateWebSearchProviderFunc | UpdatePreprocessProviderFunc

View File

@ -2724,7 +2724,7 @@ export function isSupportedThinkingTokenDoubaoModel(model?: Model): boolean {
const modelId = getLowerBaseModelName(model.id, '/') const modelId = getLowerBaseModelName(model.id, '/')
return DOUBAO_THINKING_MODEL_REGEX.test(modelId) || DOUBAO_THINKING_MODEL_REGEX.test(modelId) return DOUBAO_THINKING_MODEL_REGEX.test(modelId) || DOUBAO_THINKING_MODEL_REGEX.test(model.name)
} }
export function isClaudeReasoningModel(model?: Model): boolean { export function isClaudeReasoningModel(model?: Model): boolean {

View File

@ -1,8 +1,9 @@
import Doc2xLogo from '@renderer/assets/images/ocr/doc2x.png' import Doc2xLogo from '@renderer/assets/images/ocr/doc2x.png'
import MinerULogo from '@renderer/assets/images/ocr/mineru.jpg' import MinerULogo from '@renderer/assets/images/ocr/mineru.jpg'
import MistralLogo from '@renderer/assets/images/providers/mistral.png' import MistralLogo from '@renderer/assets/images/providers/mistral.png'
import { PreprocessProviderId } from '@renderer/types'
export function getPreprocessProviderLogo(providerId: string) { export function getPreprocessProviderLogo(providerId: PreprocessProviderId) {
switch (providerId) { switch (providerId) {
case 'doc2x': case 'doc2x':
return Doc2xLogo return Doc2xLogo
@ -15,7 +16,9 @@ export function getPreprocessProviderLogo(providerId: string) {
} }
} }
export const PREPROCESS_PROVIDER_CONFIG = { type PreprocessProviderConfig = { websites: { official: string; apiKey: string } }
export const PREPROCESS_PROVIDER_CONFIG: Record<PreprocessProviderId, PreprocessProviderConfig> = {
doc2x: { doc2x: {
websites: { websites: {
official: 'https://doc2x.noedgeai.com', official: 'https://doc2x.noedgeai.com',

View File

@ -1,24 +1,13 @@
import BochaLogo from '@renderer/assets/images/search/bocha.webp' import { WebSearchProvider, WebSearchProviderId } from '@renderer/types'
import ExaLogo from '@renderer/assets/images/search/exa.png'
import SearxngLogo from '@renderer/assets/images/search/searxng.svg'
import TavilyLogo from '@renderer/assets/images/search/tavily.png'
export function getWebSearchProviderLogo(providerId: string) { type WebSearchProviderConfig = {
switch (providerId) { websites: {
case 'tavily': official: string
return TavilyLogo apiKey?: string
case 'searxng':
return SearxngLogo
case 'exa':
return ExaLogo
case 'bocha':
return BochaLogo
default:
return undefined
} }
} }
export const WEB_SEARCH_PROVIDER_CONFIG = { export const WEB_SEARCH_PROVIDER_CONFIG: Record<WebSearchProviderId, WebSearchProviderConfig> = {
tavily: { tavily: {
websites: { websites: {
official: 'https://tavily.com', official: 'https://tavily.com',
@ -58,3 +47,46 @@ export const WEB_SEARCH_PROVIDER_CONFIG = {
} }
} }
} }
export const WEB_SEARCH_PROVIDERS: WebSearchProvider[] = [
{
id: 'tavily',
name: 'Tavily',
apiHost: 'https://api.tavily.com',
apiKey: ''
},
{
id: 'searxng',
name: 'Searxng',
apiHost: '',
basicAuthUsername: '',
basicAuthPassword: ''
},
{
id: 'exa',
name: 'Exa',
apiHost: 'https://api.exa.ai',
apiKey: ''
},
{
id: 'bocha',
name: 'Bocha',
apiHost: 'https://api.bochaai.com',
apiKey: ''
},
{
id: 'local-google',
name: 'Google',
url: 'https://www.google.com/search?q=%s'
},
{
id: 'local-bing',
name: 'Bing',
url: 'https://cn.bing.com/search?q=%s&ensearch=1'
},
{
id: 'local-baidu',
name: 'Baidu',
url: 'https://www.baidu.com/s?wd=%s'
}
] as const

View File

@ -4,10 +4,10 @@ import {
updatePreprocessProvider as _updatePreprocessProvider, updatePreprocessProvider as _updatePreprocessProvider,
updatePreprocessProviders as _updatePreprocessProviders updatePreprocessProviders as _updatePreprocessProviders
} from '@renderer/store/preprocess' } from '@renderer/store/preprocess'
import { PreprocessProvider } from '@renderer/types' import { PreprocessProvider, PreprocessProviderId } from '@renderer/types'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
export const usePreprocessProvider = (id: string) => { export const usePreprocessProvider = (id: PreprocessProviderId) => {
const dispatch = useDispatch() const dispatch = useDispatch()
const preprocessProviders = useSelector((state: RootState) => state.preprocess.providers) const preprocessProviders = useSelector((state: RootState) => state.preprocess.providers)
const provider = preprocessProviders.find((provider) => provider.id === id) const provider = preprocessProviders.find((provider) => provider.id === id)

View File

@ -11,7 +11,7 @@ import {
updateWebSearchProvider, updateWebSearchProvider,
updateWebSearchProviders updateWebSearchProviders
} from '@renderer/store/websearch' } from '@renderer/store/websearch'
import { WebSearchProvider } from '@renderer/types' import { WebSearchProvider, WebSearchProviderId } from '@renderer/types'
export const useDefaultWebSearchProvider = () => { export const useDefaultWebSearchProvider = () => {
const defaultProvider = useAppSelector((state) => state.websearch.defaultProvider) const defaultProvider = useAppSelector((state) => state.websearch.defaultProvider)
@ -49,7 +49,7 @@ export const useWebSearchProviders = () => {
} }
} }
export const useWebSearchProvider = (id: string) => { export const useWebSearchProvider = (id: WebSearchProviderId) => {
const providers = useAppSelector((state) => state.websearch.providers) const providers = useAppSelector((state) => state.websearch.providers)
const provider = providers.find((provider) => provider.id === id) const provider = providers.find((provider) => provider.id === id)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@ -60,7 +60,9 @@ export const useWebSearchProvider = (id: string) => {
return { return {
provider, provider,
updateProvider: (updates: Partial<WebSearchProvider>) => dispatch(updateWebSearchProvider({ id, ...updates })) updateProvider: (updates: Partial<WebSearchProvider>) => {
dispatch(updateWebSearchProvider({ id, ...updates }))
}
} }
} }

View File

@ -1,9 +1,11 @@
import { BaiduOutlined, GoogleOutlined } from '@ant-design/icons'
import { BingLogo, BochaLogo, ExaLogo, SearXNGLogo, TavilyLogo } from '@renderer/components/Icons'
import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel' import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel'
import { isWebSearchModel } from '@renderer/config/models' import { isWebSearchModel } from '@renderer/config/models'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useWebSearchProviders } from '@renderer/hooks/useWebSearchProviders' import { useWebSearchProviders } from '@renderer/hooks/useWebSearchProviders'
import WebSearchService from '@renderer/services/WebSearchService' import WebSearchService from '@renderer/services/WebSearchService'
import { Assistant, WebSearchProvider } from '@renderer/types' import { Assistant, WebSearchProvider, WebSearchProviderId } from '@renderer/types'
import { hasObjectKey } from '@renderer/utils' import { hasObjectKey } from '@renderer/utils'
import { Tooltip } from 'antd' import { Tooltip } from 'antd'
import { Globe } from 'lucide-react' import { Globe } from 'lucide-react'
@ -28,6 +30,33 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
const enableWebSearch = assistant?.webSearchProviderId || assistant.enableWebSearch const enableWebSearch = assistant?.webSearchProviderId || assistant.enableWebSearch
const WebSearchIcon = useCallback(
({ pid, size = 18 }: { pid?: WebSearchProviderId; size?: number }) => {
const iconColor = enableWebSearch ? 'var(--color-primary)' : 'var(--color-icon)'
switch (pid) {
case 'bocha':
return <BochaLogo width={size} height={size} color={iconColor} />
case 'exa':
// size微调视觉上和其他图标平衡一些
return <ExaLogo width={size - 2} height={size} color={iconColor} />
case 'tavily':
return <TavilyLogo width={size} height={size} color={iconColor} />
case 'searxng':
return <SearXNGLogo width={size} height={size} color={iconColor} />
case 'local-baidu':
return <BaiduOutlined size={size} style={{ color: iconColor, fontSize: size }} />
case 'local-bing':
return <BingLogo width={size} height={size} color={iconColor} />
case 'local-google':
return <GoogleOutlined size={size} style={{ color: iconColor, fontSize: size }} />
default:
return <Globe size={size} style={{ color: iconColor, fontSize: size }} />
}
},
[enableWebSearch]
)
const updateSelectedWebSearchProvider = useCallback( const updateSelectedWebSearchProvider = useCallback(
async (providerId?: WebSearchProvider['id']) => { async (providerId?: WebSearchProvider['id']) => {
// TODO: updateAssistant有性能问题会导致关闭快捷面板卡顿 // TODO: updateAssistant有性能问题会导致关闭快捷面板卡顿
@ -58,7 +87,7 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
? t('settings.tool.websearch.apikey') ? t('settings.tool.websearch.apikey')
: t('settings.tool.websearch.free') : t('settings.tool.websearch.free')
: t('chat.input.web_search.enable_content'), : t('chat.input.web_search.enable_content'),
icon: <Globe />, icon: <WebSearchIcon size={13} pid={p.id} />,
isSelected: p.id === assistant?.webSearchProviderId, isSelected: p.id === assistant?.webSearchProviderId,
disabled: !WebSearchService.isWebSearchEnabled(p.id), disabled: !WebSearchService.isWebSearchEnabled(p.id),
action: () => updateSelectedWebSearchProvider(p.id) action: () => updateSelectedWebSearchProvider(p.id)
@ -80,6 +109,7 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
return items return items
}, [ }, [
WebSearchIcon,
assistant.enableWebSearch, assistant.enableWebSearch,
assistant.model, assistant.model,
assistant?.webSearchProviderId, assistant?.webSearchProviderId,
@ -135,12 +165,7 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
mouseLeaveDelay={0} mouseLeaveDelay={0}
arrow> arrow>
<ToolbarButton type="text" onClick={handleOpenQuickPanel}> <ToolbarButton type="text" onClick={handleOpenQuickPanel}>
<Globe <WebSearchIcon pid={assistant.webSearchProviderId} />
size={18}
style={{
color: enableWebSearch ? 'var(--color-primary)' : 'var(--color-icon)'
}}
/>
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
) )

View File

@ -2,14 +2,14 @@ import { loggerService } from '@logger'
import { usePreprocessProvider } from '@renderer/hooks/usePreprocess' import { usePreprocessProvider } from '@renderer/hooks/usePreprocess'
import { getStoreSetting } from '@renderer/hooks/useSettings' import { getStoreSetting } from '@renderer/hooks/useSettings'
import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService' import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService'
import { KnowledgeBase } from '@renderer/types' import { KnowledgeBase, PreprocessProviderId } from '@renderer/types'
import { Tag } from 'antd' import { Tag } from 'antd'
import { FC, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
const logger = loggerService.withContext('QuotaTag') const logger = loggerService.withContext('QuotaTag')
const QuotaTag: FC<{ base: KnowledgeBase; providerId: string; quota?: number }> = ({ const QuotaTag: FC<{ base: KnowledgeBase; providerId: PreprocessProviderId; quota?: number }> = ({
base, base,
providerId, providerId,
quota: _quota quota: _quota

View File

@ -4,23 +4,14 @@ import { getPreprocessProviderLogo, PREPROCESS_PROVIDER_CONFIG } from '@renderer
import { usePreprocessProvider } from '@renderer/hooks/usePreprocess' import { usePreprocessProvider } from '@renderer/hooks/usePreprocess'
import { PreprocessProvider } from '@renderer/types' import { PreprocessProvider } from '@renderer/types'
import { formatApiKeys, hasObjectKey } from '@renderer/utils' import { formatApiKeys, hasObjectKey } from '@renderer/utils'
import { Avatar, Button, Divider, Flex, Input, InputNumber, Segmented, Tooltip } from 'antd' import { Avatar, Button, Divider, Flex, Input, Tooltip } from 'antd'
import Link from 'antd/es/typography/Link' import Link from 'antd/es/typography/Link'
import { List } from 'lucide-react' import { List } from 'lucide-react'
import { FC, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle, SettingTitle } from '..'
SettingDivider,
SettingHelpLink,
SettingHelpText,
SettingHelpTextRow,
SettingRow,
SettingRowTitle,
SettingSubtitle,
SettingTitle
} from '..'
interface Props { interface Props {
provider: PreprocessProvider provider: PreprocessProvider
@ -31,7 +22,7 @@ const PreprocessProviderSettings: FC<Props> = ({ provider: _provider }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [apiKey, setApiKey] = useState(preprocessProvider.apiKey || '') const [apiKey, setApiKey] = useState(preprocessProvider.apiKey || '')
const [apiHost, setApiHost] = useState(preprocessProvider.apiHost || '') const [apiHost, setApiHost] = useState(preprocessProvider.apiHost || '')
const [options, setOptions] = useState(preprocessProvider.options || {}) // const [options, setOptions] = useState(preprocessProvider.options || {})
const preprocessProviderConfig = PREPROCESS_PROVIDER_CONFIG[preprocessProvider.id] const preprocessProviderConfig = PREPROCESS_PROVIDER_CONFIG[preprocessProvider.id]
const apiKeyWebsite = preprocessProviderConfig?.websites?.apiKey const apiKeyWebsite = preprocessProviderConfig?.websites?.apiKey
@ -40,7 +31,7 @@ const PreprocessProviderSettings: FC<Props> = ({ provider: _provider }) => {
useEffect(() => { useEffect(() => {
setApiKey(preprocessProvider.apiKey ?? '') setApiKey(preprocessProvider.apiKey ?? '')
setApiHost(preprocessProvider.apiHost ?? '') setApiHost(preprocessProvider.apiHost ?? '')
setOptions(preprocessProvider.options ?? {}) // setOptions(preprocessProvider.options ?? {})
}, [preprocessProvider.apiKey, preprocessProvider.apiHost, preprocessProvider.options]) }, [preprocessProvider.apiKey, preprocessProvider.apiHost, preprocessProvider.options])
const onUpdateApiKey = () => { const onUpdateApiKey = () => {
@ -52,7 +43,6 @@ const PreprocessProviderSettings: FC<Props> = ({ provider: _provider }) => {
const openApiKeyList = async () => { const openApiKeyList = async () => {
await ApiKeyListPopup.show({ await ApiKeyListPopup.show({
providerId: preprocessProvider.id, providerId: preprocessProvider.id,
providerKind: 'doc-preprocess',
title: `${preprocessProvider.name} ${t('settings.provider.api.key.list.title')}`, title: `${preprocessProvider.name} ${t('settings.provider.api.key.list.title')}`,
showHealthCheck: false // FIXME: 目前还没有检查功能 showHealthCheck: false // FIXME: 目前还没有检查功能
}) })
@ -70,11 +60,11 @@ const PreprocessProviderSettings: FC<Props> = ({ provider: _provider }) => {
} }
} }
const onUpdateOptions = (key: string, value: any) => { // const onUpdateOptions = (key: string, value: any) => {
const newOptions = { ...options, [key]: value } // const newOptions = { ...options, [key]: value }
setOptions(newOptions) // setOptions(newOptions)
updateProvider({ options: newOptions }) // updateProvider({ options: newOptions })
} // }
return ( return (
<> <>
@ -145,7 +135,7 @@ const PreprocessProviderSettings: FC<Props> = ({ provider: _provider }) => {
)} )}
{/* 这部分看起来暂时用不上了 */} {/* 这部分看起来暂时用不上了 */}
{hasObjectKey(preprocessProvider, 'options') && preprocessProvider.id === 'system' && ( {/* {hasObjectKey(preprocessProvider, 'options') && preprocessProvider.id === 'system' && (
<> <>
<SettingDivider style={{ marginTop: 15, marginBottom: 12 }} /> <SettingDivider style={{ marginTop: 15, marginBottom: 12 }} />
<SettingRow> <SettingRow>
@ -177,7 +167,7 @@ const PreprocessProviderSettings: FC<Props> = ({ provider: _provider }) => {
/> />
</SettingRow> </SettingRow>
</> </>
)} )} */}
</> </>
) )
} }

View File

@ -1,4 +1,3 @@
import { isMac } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useDefaultPreprocessProvider, usePreprocessProviders } from '@renderer/hooks/usePreprocess' import { useDefaultPreprocessProvider, usePreprocessProviders } from '@renderer/hooks/usePreprocess'
import { PreprocessProvider } from '@renderer/types' import { PreprocessProvider } from '@renderer/types'
@ -40,8 +39,9 @@ const PreprocessSettings: FC = () => {
placeholder={t('settings.tool.preprocess.provider_placeholder')} placeholder={t('settings.tool.preprocess.provider_placeholder')}
options={preprocessProviders.map((p) => ({ options={preprocessProviders.map((p) => ({
value: p.id, value: p.id,
label: p.name, label: p.name
disabled: !isMac && p.id === 'system' // 在非 Mac 系统下禁用 system 选项 // 由于system字段实际未使用先注释掉
// disabled: !isMac && p.id === 'system' // 在非 Mac 系统下禁用 system 选项
}))} }))}
/> />
</div> </div>

View File

@ -128,7 +128,6 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
const openApiKeyList = async () => { const openApiKeyList = async () => {
await ApiKeyListPopup.show({ await ApiKeyListPopup.show({
providerId: provider.id, providerId: provider.id,
providerKind: 'llm',
title: `${fancyProviderName} ${t('settings.provider.api.key.list.title')}` title: `${fancyProviderName} ${t('settings.provider.api.key.list.title')}`
}) })
} }

View File

@ -1,9 +1,14 @@
import { CheckOutlined, ExportOutlined, LoadingOutlined } from '@ant-design/icons' import { CheckOutlined, ExportOutlined, LoadingOutlined } from '@ant-design/icons'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import BochaLogo from '@renderer/assets/images/search/bocha.webp'
import ExaLogo from '@renderer/assets/images/search/exa.png'
import SearxngLogo from '@renderer/assets/images/search/searxng.svg'
import TavilyLogo from '@renderer/assets/images/search/tavily.png'
import ApiKeyListPopup from '@renderer/components/Popups/ApiKeyListPopup/popup' import ApiKeyListPopup from '@renderer/components/Popups/ApiKeyListPopup/popup'
import { getWebSearchProviderLogo, WEB_SEARCH_PROVIDER_CONFIG } from '@renderer/config/webSearchProviders' import { WEB_SEARCH_PROVIDER_CONFIG } from '@renderer/config/webSearchProviders'
import { useWebSearchProvider } from '@renderer/hooks/useWebSearchProviders' import { useWebSearchProvider } from '@renderer/hooks/useWebSearchProviders'
import WebSearchService from '@renderer/services/WebSearchService' import WebSearchService from '@renderer/services/WebSearchService'
import { WebSearchProviderId } from '@renderer/types'
import { formatApiKeys, hasObjectKey } from '@renderer/utils' import { formatApiKeys, hasObjectKey } from '@renderer/utils'
import { Button, Divider, Flex, Form, Input, Space, Tooltip } from 'antd' import { Button, Divider, Flex, Form, Input, Space, Tooltip } from 'antd'
import Link from 'antd/es/typography/Link' import Link from 'antd/es/typography/Link'
@ -16,7 +21,7 @@ import { SettingDivider, SettingHelpLink, SettingHelpText, SettingHelpTextRow, S
const logger = loggerService.withContext('WebSearchProviderSetting') const logger = loggerService.withContext('WebSearchProviderSetting')
interface Props { interface Props {
providerId: string providerId: WebSearchProviderId
} }
const WebSearchProviderSetting: FC<Props> = ({ providerId }) => { const WebSearchProviderSetting: FC<Props> = ({ providerId }) => {
@ -74,7 +79,6 @@ const WebSearchProviderSetting: FC<Props> = ({ providerId }) => {
const openApiKeyList = async () => { const openApiKeyList = async () => {
await ApiKeyListPopup.show({ await ApiKeyListPopup.show({
providerId: provider.id, providerId: provider.id,
providerKind: 'websearch',
title: `${provider.name} ${t('settings.provider.api.key.list.title')}` title: `${provider.name} ${t('settings.provider.api.key.list.title')}`
}) })
} }
@ -132,6 +136,21 @@ const WebSearchProviderSetting: FC<Props> = ({ providerId }) => {
setBasicAuthPassword(provider.basicAuthPassword ?? '') setBasicAuthPassword(provider.basicAuthPassword ?? '')
}, [provider.apiKey, provider.apiHost, provider.basicAuthUsername, provider.basicAuthPassword]) }, [provider.apiKey, provider.apiHost, provider.basicAuthUsername, provider.basicAuthPassword])
const getWebSearchProviderLogo = (providerId: WebSearchProviderId) => {
switch (providerId) {
case 'tavily':
return TavilyLogo
case 'searxng':
return SearxngLogo
case 'exa':
return ExaLogo
case 'bocha':
return BochaLogo
default:
return undefined
}
}
return ( return (
<> <>
<SettingTitle> <SettingTitle>

View File

@ -40,6 +40,7 @@ export const createCitationCallbacks = (deps: CitationCallbacksDependencies) =>
status: MessageBlockStatus.SUCCESS status: MessageBlockStatus.SUCCESS
} }
blockManager.smartBlockUpdate(citationBlockId, changes, MessageBlockType.CITATION, true) blockManager.smartBlockUpdate(citationBlockId, changes, MessageBlockType.CITATION, true)
citationBlockId = null
} else { } else {
logger.error('[onExternalToolComplete] citationBlockId is null. Cannot update.') logger.error('[onExternalToolComplete] citationBlockId is null. Cannot update.')
} }

View File

@ -1,4 +1,5 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { WEB_SEARCH_PROVIDERS } from '@renderer/config/webSearchProviders'
import type { Model, WebSearchProvider } from '@renderer/types' import type { Model, WebSearchProvider } from '@renderer/types'
export interface SubscribeSource { export interface SubscribeSource {
key: number key: number
@ -42,48 +43,7 @@ export interface WebSearchState {
export const initialState: WebSearchState = { export const initialState: WebSearchState = {
defaultProvider: 'local-bing', defaultProvider: 'local-bing',
providers: [ providers: WEB_SEARCH_PROVIDERS,
{
id: 'tavily',
name: 'Tavily',
apiHost: 'https://api.tavily.com',
apiKey: ''
},
{
id: 'searxng',
name: 'Searxng',
apiHost: '',
basicAuthUsername: '',
basicAuthPassword: ''
},
{
id: 'exa',
name: 'Exa',
apiHost: 'https://api.exa.ai',
apiKey: ''
},
{
id: 'bocha',
name: 'Bocha',
apiHost: 'https://api.bochaai.com',
apiKey: ''
},
{
id: 'local-google',
name: 'Google',
url: 'https://www.google.com/search?q=%s'
},
{
id: 'local-bing',
name: 'Bing',
url: 'https://cn.bing.com/search?q=%s&ensearch=1'
},
{
id: 'local-baidu',
name: 'Baidu',
url: 'https://www.baidu.com/s?wd=%s'
}
],
searchWithTime: true, searchWithTime: true,
maxResults: 5, maxResults: 5,
excludeDomains: [], excludeDomains: [],
@ -111,7 +71,7 @@ const websearchSlice = createSlice({
updateWebSearchProviders: (state, action: PayloadAction<WebSearchProvider[]>) => { updateWebSearchProviders: (state, action: PayloadAction<WebSearchProvider[]>) => {
state.providers = action.payload state.providers = action.payload
}, },
updateWebSearchProvider: (state, action: PayloadAction<Partial<WebSearchProvider> & { id: string }>) => { updateWebSearchProvider: (state, action: PayloadAction<Partial<WebSearchProvider>>) => {
const index = state.providers.findIndex((provider) => provider.id === action.payload.id) const index = state.providers.findIndex((provider) => provider.id === action.payload.id)
if (index !== -1) { if (index !== -1) {
Object.assign(state.providers[index], action.payload) Object.assign(state.providers[index], action.payload)

View File

@ -609,8 +609,20 @@ export type KnowledgeBaseParams = {
} }
} }
export const PreprocessProviderIds = {
doc2x: 'doc2x',
mistral: 'mistral',
mineru: 'mineru'
} as const
export type PreprocessProviderId = keyof typeof PreprocessProviderIds
export const isPreprocessProviderId = (id: string): id is PreprocessProviderId => {
return Object.hasOwn(PreprocessProviderIds, id)
}
export interface PreprocessProvider { export interface PreprocessProvider {
id: string id: PreprocessProviderId
name: string name: string
apiKey?: string apiKey?: string
apiHost?: string apiHost?: string
@ -675,8 +687,24 @@ export type ExternalToolResult = {
memories?: MemoryItem[] memories?: MemoryItem[]
} }
export const WebSearchProviderIds = {
tavily: 'tavily',
searxng: 'searxng',
exa: 'exa',
bocha: 'bocha',
'local-google': 'local-google',
'local-bing': 'local-bing',
'local-baidu': 'local-baidu'
} as const
export type WebSearchProviderId = keyof typeof WebSearchProviderIds
export const isWebSearchProviderId = (id: string): id is WebSearchProviderId => {
return Object.hasOwn(WebSearchProviderIds, id)
}
export type WebSearchProvider = { export type WebSearchProvider = {
id: string id: WebSearchProviderId
name: string name: string
apiKey?: string apiKey?: string
apiHost?: string apiHost?: string

148
yarn.lock
View File

@ -75,10 +75,10 @@ __metadata:
linkType: hard linkType: hard
"@ai-sdk/amazon-bedrock@npm:^3.0.0": "@ai-sdk/amazon-bedrock@npm:^3.0.0":
version: 3.0.7 version: 3.0.8
resolution: "@ai-sdk/amazon-bedrock@npm:3.0.7" resolution: "@ai-sdk/amazon-bedrock@npm:3.0.8"
dependencies: dependencies:
"@ai-sdk/anthropic": "npm:2.0.3" "@ai-sdk/anthropic": "npm:2.0.4"
"@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.3" "@ai-sdk/provider-utils": "npm:3.0.3"
"@smithy/eventstream-codec": "npm:^4.0.1" "@smithy/eventstream-codec": "npm:^4.0.1"
@ -86,7 +86,7 @@ __metadata:
aws4fetch: "npm:^1.0.20" aws4fetch: "npm:^1.0.20"
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4 zod: ^3.25.76 || ^4
checksum: 10c0/96d118413db091fd325edc0a0900a7376ddeecb63de9d0bd00eca5f0f8ac86185eafe28740b19829a6cd873b304cc6ba6e3313bc435fdc6156abf9b705db1af4 checksum: 10c0/d7b303b8581e9d28e9ac375b3718ef3f7fff3353d18185870f0b90fd542eb9398d029768502981e9e45a6b64137a7029f591993afd0b18e9ef74525f625524f7
languageName: node languageName: node
linkType: hard linkType: hard
@ -102,15 +102,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@ai-sdk/anthropic@npm:2.0.3": "@ai-sdk/anthropic@npm:2.0.4":
version: 2.0.3 version: 2.0.4
resolution: "@ai-sdk/anthropic@npm:2.0.3" resolution: "@ai-sdk/anthropic@npm:2.0.4"
dependencies: dependencies:
"@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.3" "@ai-sdk/provider-utils": "npm:3.0.3"
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4 zod: ^3.25.76 || ^4
checksum: 10c0/733861197ae6c7fe93e9dc131a2176bd654eeee0b0575bb164f4259ecdb23d85039a0ef3ba4b1ad2de299c275b97d0fb1b0be0cff224713e81d0a31749ae1070 checksum: 10c0/2e5a997b6e2d9a2964c4681418643fd2f347df78ac1f9677a0cc6a3a3454920d05c663e35521d8922f0a382ec77a25e4b92204b3760a1da05876bf00d41adc39
languageName: node languageName: node
linkType: hard linkType: hard
@ -153,17 +153,17 @@ __metadata:
linkType: hard linkType: hard
"@ai-sdk/google-vertex@npm:^3.0.0": "@ai-sdk/google-vertex@npm:^3.0.0":
version: 3.0.8 version: 3.0.9
resolution: "@ai-sdk/google-vertex@npm:3.0.8" resolution: "@ai-sdk/google-vertex@npm:3.0.9"
dependencies: dependencies:
"@ai-sdk/anthropic": "npm:2.0.3" "@ai-sdk/anthropic": "npm:2.0.4"
"@ai-sdk/google": "npm:2.0.6" "@ai-sdk/google": "npm:2.0.6"
"@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.3" "@ai-sdk/provider-utils": "npm:3.0.3"
google-auth-library: "npm:^9.15.0" google-auth-library: "npm:^9.15.0"
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4 zod: ^3.25.76 || ^4
checksum: 10c0/3d0b89f64fcd9eafa7a1af5f21cf317fa3c3d2ca1fae55bc5ab22e969ddc3f6db6cb918b9bb27ec5b3f32eca518e0529877db2826abfe8fce64426bda3e0775f checksum: 10c0/c6584b877f9e20a10dd7d92fc4cb1b4a9838510aa89734cf1ff2faa74ba820b976d3359d4eadcb6035c8911973300efb157931fa0d1105abc8db36f94544cc88
languageName: node languageName: node
linkType: hard linkType: hard
@ -1285,38 +1285,38 @@ __metadata:
linkType: hard linkType: hard
"@babel/core@npm:^7.27.7": "@babel/core@npm:^7.27.7":
version: 7.28.0 version: 7.28.3
resolution: "@babel/core@npm:7.28.0" resolution: "@babel/core@npm:7.28.3"
dependencies: dependencies:
"@ampproject/remapping": "npm:^2.2.0" "@ampproject/remapping": "npm:^2.2.0"
"@babel/code-frame": "npm:^7.27.1" "@babel/code-frame": "npm:^7.27.1"
"@babel/generator": "npm:^7.28.0" "@babel/generator": "npm:^7.28.3"
"@babel/helper-compilation-targets": "npm:^7.27.2" "@babel/helper-compilation-targets": "npm:^7.27.2"
"@babel/helper-module-transforms": "npm:^7.27.3" "@babel/helper-module-transforms": "npm:^7.28.3"
"@babel/helpers": "npm:^7.27.6" "@babel/helpers": "npm:^7.28.3"
"@babel/parser": "npm:^7.28.0" "@babel/parser": "npm:^7.28.3"
"@babel/template": "npm:^7.27.2" "@babel/template": "npm:^7.27.2"
"@babel/traverse": "npm:^7.28.0" "@babel/traverse": "npm:^7.28.3"
"@babel/types": "npm:^7.28.0" "@babel/types": "npm:^7.28.2"
convert-source-map: "npm:^2.0.0" convert-source-map: "npm:^2.0.0"
debug: "npm:^4.1.0" debug: "npm:^4.1.0"
gensync: "npm:^1.0.0-beta.2" gensync: "npm:^1.0.0-beta.2"
json5: "npm:^2.2.3" json5: "npm:^2.2.3"
semver: "npm:^6.3.1" semver: "npm:^6.3.1"
checksum: 10c0/423302e7c721e73b1c096217880272e02020dfb697a55ccca60ad01bba90037015f84d0c20c6ce297cf33a19bb704bc5c2b3d3095f5284dfa592bd1de0b9e8c3 checksum: 10c0/e6b3eb830c4b93f5a442b305776df1cd2bb4fafa4612355366f67c764f3e54a69d45b84def77fb2d4fd83439102667b0a92c3ea2838f678733245b748c602a7b
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/generator@npm:^7.28.0": "@babel/generator@npm:^7.28.0, @babel/generator@npm:^7.28.3":
version: 7.28.0 version: 7.28.3
resolution: "@babel/generator@npm:7.28.0" resolution: "@babel/generator@npm:7.28.3"
dependencies: dependencies:
"@babel/parser": "npm:^7.28.0" "@babel/parser": "npm:^7.28.3"
"@babel/types": "npm:^7.28.0" "@babel/types": "npm:^7.28.2"
"@jridgewell/gen-mapping": "npm:^0.3.12" "@jridgewell/gen-mapping": "npm:^0.3.12"
"@jridgewell/trace-mapping": "npm:^0.3.28" "@jridgewell/trace-mapping": "npm:^0.3.28"
jsesc: "npm:^3.0.2" jsesc: "npm:^3.0.2"
checksum: 10c0/1b3d122268ea3df50fde707ad864d9a55c72621357d5cebb972db3dd76859c45810c56e16ad23123f18f80cc2692f5a015d2858361300f0f224a05dc43d36a92 checksum: 10c0/0ff58bcf04f8803dcc29479b547b43b9b0b828ec1ee0668e92d79f9e90f388c28589056637c5ff2fd7bcf8d153c990d29c448d449d852bf9d1bc64753ca462bc
languageName: node languageName: node
linkType: hard linkType: hard
@ -1350,16 +1350,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/helper-module-transforms@npm:^7.27.3": "@babel/helper-module-transforms@npm:^7.28.3":
version: 7.27.3 version: 7.28.3
resolution: "@babel/helper-module-transforms@npm:7.27.3" resolution: "@babel/helper-module-transforms@npm:7.28.3"
dependencies: dependencies:
"@babel/helper-module-imports": "npm:^7.27.1" "@babel/helper-module-imports": "npm:^7.27.1"
"@babel/helper-validator-identifier": "npm:^7.27.1" "@babel/helper-validator-identifier": "npm:^7.27.1"
"@babel/traverse": "npm:^7.27.3" "@babel/traverse": "npm:^7.28.3"
peerDependencies: peerDependencies:
"@babel/core": ^7.0.0 "@babel/core": ^7.0.0
checksum: 10c0/fccb4f512a13b4c069af51e1b56b20f54024bcf1591e31e978a30f3502567f34f90a80da6a19a6148c249216292a8074a0121f9e52602510ef0f32dbce95ca01 checksum: 10c0/549be62515a6d50cd4cfefcab1b005c47f89bd9135a22d602ee6a5e3a01f27571868ada10b75b033569f24dc4a2bb8d04bfa05ee75c16da7ade2d0db1437fcdb
languageName: node languageName: node
linkType: hard linkType: hard
@ -1391,24 +1391,24 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/helpers@npm:^7.27.6": "@babel/helpers@npm:^7.28.3":
version: 7.28.2 version: 7.28.3
resolution: "@babel/helpers@npm:7.28.2" resolution: "@babel/helpers@npm:7.28.3"
dependencies: dependencies:
"@babel/template": "npm:^7.27.2" "@babel/template": "npm:^7.27.2"
"@babel/types": "npm:^7.28.2" "@babel/types": "npm:^7.28.2"
checksum: 10c0/f3e7b21517e2699c4ca193663ecfb1bf1b2ae2762d8ba4a9f1786feaca0d6984537fc60bf2206e92c43640a6dada6b438f523cc1ad78610d0151aeb061b37f63 checksum: 10c0/03a8f94135415eec62d37be9c62c63908f2d5386c7b00e04545de4961996465775330e3eb57717ea7451e19b0e24615777ebfec408c2adb1df3b10b4df6bf1ce
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.0": "@babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.0, @babel/parser@npm:^7.28.3":
version: 7.28.0 version: 7.28.3
resolution: "@babel/parser@npm:7.28.0" resolution: "@babel/parser@npm:7.28.3"
dependencies: dependencies:
"@babel/types": "npm:^7.28.0" "@babel/types": "npm:^7.28.2"
bin: bin:
parser: ./bin/babel-parser.js parser: ./bin/babel-parser.js
checksum: 10c0/c2ef81d598990fa949d1d388429df327420357cb5200271d0d0a2784f1e6d54afc8301eb8bdf96d8f6c77781e402da93c7dc07980fcc136ac5b9d5f1fce701b5 checksum: 10c0/1f41eb82623b0ca0f94521b57f4790c6c457cd922b8e2597985b36bdec24114a9ccf54640286a760ceb60f11fe9102d192bf60477aee77f5d45f1029b9b72729
languageName: node languageName: node
linkType: hard linkType: hard
@ -1424,9 +1424,9 @@ __metadata:
linkType: hard linkType: hard
"@babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.10.4, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.6, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.24.4, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.24.8, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": "@babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.10.4, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.6, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.24.4, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.24.8, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
version: 7.28.2 version: 7.28.3
resolution: "@babel/runtime@npm:7.28.2" resolution: "@babel/runtime@npm:7.28.3"
checksum: 10c0/c20afe253629d53a405a610b12a62ac74d341a2c1e0fb202bbef0c118f6b5c84f94bf16039f58fd0483dd256901259930a43976845bdeb180cab1f882c21b6e0 checksum: 10c0/b360f82c2c5114f2a062d4d143d7b4ec690094764853937110585a9497977aed66c102166d0e404766c274e02a50ffb8f6d77fef7251ecf3f607f0e03e6397bc
languageName: node languageName: node
linkType: hard linkType: hard
@ -1441,22 +1441,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.27.3, @babel/traverse@npm:^7.28.0": "@babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.3":
version: 7.28.0 version: 7.28.3
resolution: "@babel/traverse@npm:7.28.0" resolution: "@babel/traverse@npm:7.28.3"
dependencies: dependencies:
"@babel/code-frame": "npm:^7.27.1" "@babel/code-frame": "npm:^7.27.1"
"@babel/generator": "npm:^7.28.0" "@babel/generator": "npm:^7.28.3"
"@babel/helper-globals": "npm:^7.28.0" "@babel/helper-globals": "npm:^7.28.0"
"@babel/parser": "npm:^7.28.0" "@babel/parser": "npm:^7.28.3"
"@babel/template": "npm:^7.27.2" "@babel/template": "npm:^7.27.2"
"@babel/types": "npm:^7.28.0" "@babel/types": "npm:^7.28.2"
debug: "npm:^4.3.1" debug: "npm:^4.3.1"
checksum: 10c0/32794402457827ac558173bcebdcc0e3a18fa339b7c41ca35621f9f645f044534d91bb923ff385f5f960f2e495f56ce18d6c7b0d064d2f0ccb55b285fa6bc7b9 checksum: 10c0/26e95b29a46925b7b41255e03185b7e65b2c4987e14bbee7bbf95867fb19c69181f301bbe1c7b201d4fe0cce6aa0cbea0282dad74b3a0fef3d9058f6c76fdcb3
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/types@npm:^7.25.4, @babel/types@npm:^7.27.1, @babel/types@npm:^7.28.0, @babel/types@npm:^7.28.1, @babel/types@npm:^7.28.2": "@babel/types@npm:^7.25.4, @babel/types@npm:^7.27.1, @babel/types@npm:^7.28.1, @babel/types@npm:^7.28.2":
version: 7.28.2 version: 7.28.2
resolution: "@babel/types@npm:7.28.2" resolution: "@babel/types@npm:7.28.2"
dependencies: dependencies:
@ -3973,8 +3973,8 @@ __metadata:
linkType: hard linkType: hard
"@modelcontextprotocol/sdk@npm:^1.17.0": "@modelcontextprotocol/sdk@npm:^1.17.0":
version: 1.17.2 version: 1.17.3
resolution: "@modelcontextprotocol/sdk@npm:1.17.2" resolution: "@modelcontextprotocol/sdk@npm:1.17.3"
dependencies: dependencies:
ajv: "npm:^6.12.6" ajv: "npm:^6.12.6"
content-type: "npm:^1.0.5" content-type: "npm:^1.0.5"
@ -3988,7 +3988,7 @@ __metadata:
raw-body: "npm:^3.0.0" raw-body: "npm:^3.0.0"
zod: "npm:^3.23.8" zod: "npm:^3.23.8"
zod-to-json-schema: "npm:^3.24.1" zod-to-json-schema: "npm:^3.24.1"
checksum: 10c0/9f0d00dc5f96a6bc78b775d2ce57d72270cf3b0fad08209f0061e30fff6e2005b7e6abbef98fbc030ba7e3aa5c78e5211f9b5c2a3d0d8f4455dfd47fef228720 checksum: 10c0/23df0949724279eaa620f2780e3c731dcf746311f3175e3cba602643aacf9ee6dbcf99daeab3fa848296fe9ac971456fc031c79c1b55870dd019baf0263fd080
languageName: node languageName: node
linkType: hard linkType: hard
@ -5927,21 +5927,21 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@tanstack/query-core@npm:5.85.2": "@tanstack/query-core@npm:5.85.3":
version: 5.85.2 version: 5.85.3
resolution: "@tanstack/query-core@npm:5.85.2" resolution: "@tanstack/query-core@npm:5.85.3"
checksum: 10c0/398386c0e56b0ab0916951468b6b6a2b9f1f713fa95757405171f1c99c7a33cd3427eb20a82f252e5abc63ccd93e8e77c7cc5bc30043401d0dd5a074d7f5ff4e checksum: 10c0/7db9e78c3648a3d5bc295fff14e7afdf061e38ca55b0004e3f6e6f1f44596f564bf8b59c97b2d1f7ce851792c8eac7008608ccd4de0dfcd6349a4e0943b7d247
languageName: node languageName: node
linkType: hard linkType: hard
"@tanstack/react-query@npm:^5.27.0": "@tanstack/react-query@npm:^5.27.0":
version: 5.85.2 version: 5.85.3
resolution: "@tanstack/react-query@npm:5.85.2" resolution: "@tanstack/react-query@npm:5.85.3"
dependencies: dependencies:
"@tanstack/query-core": "npm:5.85.2" "@tanstack/query-core": "npm:5.85.3"
peerDependencies: peerDependencies:
react: ^18 || ^19 react: ^18 || ^19
checksum: 10c0/8feb9a82d25589b7f3a9cc1e3f24ec2b29b35e86522ca94b3f94dc2649e4a4472da7782b2e1f0b7a48722d11e8c34cf77711b9fbad474b1b73478ae0f06c9ba0 checksum: 10c0/3588a6997c5f5302d999f98230894ad3ed5d24b0b063b62c34300e2cd3a715afc7edd97eb8a99408f2f7f85e76812fa87d035dbbc340c95adecade80d1e44ae0
languageName: node languageName: node
linkType: hard linkType: hard
@ -7871,7 +7871,7 @@ __metadata:
remove-markdown: "npm:^0.6.2" remove-markdown: "npm:^0.6.2"
rollup-plugin-visualizer: "npm:^5.12.0" rollup-plugin-visualizer: "npm:^5.12.0"
sass: "npm:^1.88.0" sass: "npm:^1.88.0"
selection-hook: "npm:^1.0.8" selection-hook: "npm:^1.0.9"
shiki: "npm:^3.9.1" shiki: "npm:^3.9.1"
strict-url-sanitise: "npm:^0.0.1" strict-url-sanitise: "npm:^0.0.1"
string-width: "npm:^7.2.0" string-width: "npm:^7.2.0"
@ -11001,9 +11001,9 @@ __metadata:
linkType: hard linkType: hard
"electron-to-chromium@npm:^1.5.199": "electron-to-chromium@npm:^1.5.199":
version: 1.5.200 version: 1.5.201
resolution: "electron-to-chromium@npm:1.5.200" resolution: "electron-to-chromium@npm:1.5.201"
checksum: 10c0/3f323c097d41c31da09632df30fa26a887c4805fb3a12196eaededc4337cc3b62530d2c80d18ed352c7732c8edfadefbc604ccefe6d4e83c1156c2ed1525bdee checksum: 10c0/83f415506e4f79ebe3bcf311526823fe73cd9d54938eff6d98504d222d7c1f7db21acbe98b30394349b1dc973ac85123f1afb5f2b495e166885e5ddd7cf086e6
languageName: node languageName: node
linkType: hard linkType: hard
@ -12111,14 +12111,14 @@ __metadata:
linkType: hard linkType: hard
"fdir@npm:^6.4.4, fdir@npm:^6.4.6": "fdir@npm:^6.4.4, fdir@npm:^6.4.6":
version: 6.4.6 version: 6.5.0
resolution: "fdir@npm:6.4.6" resolution: "fdir@npm:6.5.0"
peerDependencies: peerDependencies:
picomatch: ^3 || ^4 picomatch: ^3 || ^4
peerDependenciesMeta: peerDependenciesMeta:
picomatch: picomatch:
optional: true optional: true
checksum: 10c0/45b559cff889934ebb8bc498351e5acba40750ada7e7d6bde197768d2fa67c149be8ae7f8ff34d03f4e1eb20f2764116e56440aaa2f6689e9a4aa7ef06acafe9 checksum: 10c0/e345083c4306b3aed6cb8ec551e26c36bab5c511e99ea4576a16750ddc8d3240e63826cc624f5ae17ad4dc82e68a253213b60d556c11bfad064b7607847ed07f
languageName: node languageName: node
linkType: hard linkType: hard
@ -14398,8 +14398,8 @@ __metadata:
linkType: hard linkType: hard
"langsmith@npm:>=0.2.8 <0.4.0, langsmith@npm:^0.3.33, langsmith@npm:^0.3.46": "langsmith@npm:>=0.2.8 <0.4.0, langsmith@npm:^0.3.33, langsmith@npm:^0.3.46":
version: 0.3.59 version: 0.3.61
resolution: "langsmith@npm:0.3.59" resolution: "langsmith@npm:0.3.61"
dependencies: dependencies:
"@types/uuid": "npm:^10.0.0" "@types/uuid": "npm:^10.0.0"
chalk: "npm:^4.1.2" chalk: "npm:^4.1.2"
@ -14422,7 +14422,7 @@ __metadata:
optional: true optional: true
openai: openai:
optional: true optional: true
checksum: 10c0/e6c8c1f9f88a76116efa7fbb1af38be266759eb4e9a4c498e80e2d6cf5b456bbf0e607077cd14e0d864234685a0553b67f96bbc6b05f985b797cc41da8d6a682 checksum: 10c0/2aaf611bf8f7b2e44d9266415a9933b5f94986e02a2fa89be560b9118b118f9c8722d1a3c677f1bbeac17d3330706e7bbfeeeaa814587de89d8a18fd97c0a45b
languageName: node languageName: node
linkType: hard linkType: hard
@ -19795,7 +19795,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"selection-hook@npm:^1.0.8": "selection-hook@npm:^1.0.9":
version: 1.0.9 version: 1.0.9
resolution: "selection-hook@npm:1.0.9" resolution: "selection-hook@npm:1.0.9"
dependencies: dependencies: