mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-03 19:30:04 +08:00
Merge branch 'main' of https://github.com/CherryHQ/cherry-studio into wip/data-refactor
This commit is contained in:
commit
6b503c4080
@ -79,7 +79,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": {
|
||||||
@ -216,7 +216,7 @@
|
|||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"macos-release": "^3.4.0",
|
"macos-release": "^3.4.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"mermaid": "^11.7.0",
|
"mermaid": "^11.9.0",
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
"motion": "^12.10.5",
|
"motion": "^12.10.5",
|
||||||
"notion-helper": "^1.3.22",
|
"notion-helper": "^1.3.22",
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { loggerService } from '@logger'
|
|||||||
import { fileStorage } from '@main/services/FileStorage'
|
import { fileStorage } from '@main/services/FileStorage'
|
||||||
import { FileMetadata, PreprocessProvider } from '@types'
|
import { FileMetadata, PreprocessProvider } from '@types'
|
||||||
import AdmZip from 'adm-zip'
|
import AdmZip from 'adm-zip'
|
||||||
import axios, { AxiosRequestConfig } from 'axios'
|
import { net } from 'electron'
|
||||||
|
|
||||||
import BasePreprocessProvider from './BasePreprocessProvider'
|
import BasePreprocessProvider from './BasePreprocessProvider'
|
||||||
|
|
||||||
@ -38,19 +38,24 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async validateFile(filePath: string): Promise<void> {
|
private async validateFile(filePath: string): Promise<void> {
|
||||||
const pdfBuffer = await fs.promises.readFile(filePath)
|
// 首先检查文件大小,避免读取大文件到内存
|
||||||
|
const stats = await fs.promises.stat(filePath)
|
||||||
|
const fileSizeBytes = stats.size
|
||||||
|
|
||||||
|
// 文件大小小于300MB
|
||||||
|
if (fileSizeBytes >= 300 * 1024 * 1024) {
|
||||||
|
const fileSizeMB = Math.round(fileSizeBytes / (1024 * 1024))
|
||||||
|
throw new Error(`PDF file size (${fileSizeMB}MB) exceeds the limit of 300MB`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有在文件大小合理的情况下才读取文件内容检查页数
|
||||||
|
const pdfBuffer = await fs.promises.readFile(filePath)
|
||||||
const doc = await this.readPdf(pdfBuffer)
|
const doc = await this.readPdf(pdfBuffer)
|
||||||
|
|
||||||
// 文件页数小于1000页
|
// 文件页数小于1000页
|
||||||
if (doc.numPages >= 1000) {
|
if (doc.numPages >= 1000) {
|
||||||
throw new Error(`PDF page count (${doc.numPages}) exceeds the limit of 1000 pages`)
|
throw new Error(`PDF page count (${doc.numPages}) exceeds the limit of 1000 pages`)
|
||||||
}
|
}
|
||||||
// 文件大小小于300MB
|
|
||||||
if (pdfBuffer.length >= 300 * 1024 * 1024) {
|
|
||||||
const fileSizeMB = Math.round(pdfBuffer.length / (1024 * 1024))
|
|
||||||
throw new Error(`PDF file size (${fileSizeMB}MB) exceeds the limit of 300MB`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async parseFile(sourceId: string, file: FileMetadata): Promise<{ processedFile: FileMetadata }> {
|
public async parseFile(sourceId: string, file: FileMetadata): Promise<{ processedFile: FileMetadata }> {
|
||||||
@ -160,11 +165,23 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
|
|||||||
* @returns 预上传响应的url和uid
|
* @returns 预上传响应的url和uid
|
||||||
*/
|
*/
|
||||||
private async preupload(): Promise<PreuploadResponse> {
|
private async preupload(): Promise<PreuploadResponse> {
|
||||||
const config = this.createAuthConfig()
|
|
||||||
const endpoint = `${this.provider.apiHost}/api/v2/parse/preupload`
|
const endpoint = `${this.provider.apiHost}/api/v2/parse/preupload`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.post<ApiResponse<PreuploadResponse>>(endpoint, null, config)
|
const response = await net.fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${this.provider.apiKey}`
|
||||||
|
},
|
||||||
|
body: null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as ApiResponse<PreuploadResponse>
|
||||||
|
|
||||||
if (data.code === 'success' && data.data) {
|
if (data.code === 'success' && data.data) {
|
||||||
return data.data
|
return data.data
|
||||||
@ -178,17 +195,29 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传文件
|
* 上传文件(使用流式上传)
|
||||||
* @param filePath 文件路径
|
* @param filePath 文件路径
|
||||||
* @param url 预上传响应的url
|
* @param url 预上传响应的url
|
||||||
*/
|
*/
|
||||||
private async putFile(filePath: string, url: string): Promise<void> {
|
private async putFile(filePath: string, url: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const fileStream = fs.createReadStream(filePath)
|
// 获取文件大小用于设置 Content-Length
|
||||||
const response = await axios.put(url, fileStream)
|
const stats = await fs.promises.stat(filePath)
|
||||||
|
const fileSize = stats.size
|
||||||
|
|
||||||
if (response.status !== 200) {
|
// 创建可读流
|
||||||
throw new Error(`HTTP status ${response.status}: ${response.statusText}`)
|
const fileStream = fs.createReadStream(filePath)
|
||||||
|
|
||||||
|
const response = await net.fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: fileStream as any, // TypeScript 类型转换,net.fetch 支持 ReadableStream
|
||||||
|
headers: {
|
||||||
|
'Content-Length': fileSize.toString()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to upload file ${filePath}: ${error instanceof Error ? error.message : String(error)}`)
|
logger.error(`Failed to upload file ${filePath}: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
@ -197,16 +226,25 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getStatus(uid: string): Promise<StatusResponse> {
|
private async getStatus(uid: string): Promise<StatusResponse> {
|
||||||
const config = this.createAuthConfig()
|
|
||||||
const endpoint = `${this.provider.apiHost}/api/v2/parse/status?uid=${uid}`
|
const endpoint = `${this.provider.apiHost}/api/v2/parse/status?uid=${uid}`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get<ApiResponse<StatusResponse>>(endpoint, config)
|
const response = await net.fetch(endpoint, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.provider.apiKey}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (response.data.code === 'success' && response.data.data) {
|
if (!response.ok) {
|
||||||
return response.data.data
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as ApiResponse<StatusResponse>
|
||||||
|
if (data.code === 'success' && data.data) {
|
||||||
|
return data.data
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`API returned error: ${response.data.message || JSON.stringify(response.data)}`)
|
throw new Error(`API returned error: ${data.message || JSON.stringify(data)}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to get status for uid ${uid}: ${error instanceof Error ? error.message : String(error)}`)
|
logger.error(`Failed to get status for uid ${uid}: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
@ -221,13 +259,6 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
|
|||||||
*/
|
*/
|
||||||
private async convertFile(uid: string, filePath: string): Promise<void> {
|
private async convertFile(uid: string, filePath: string): Promise<void> {
|
||||||
const fileName = path.parse(filePath).name
|
const fileName = path.parse(filePath).name
|
||||||
const config = {
|
|
||||||
...this.createAuthConfig(),
|
|
||||||
headers: {
|
|
||||||
...this.createAuthConfig().headers,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
uid,
|
uid,
|
||||||
@ -239,10 +270,22 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
|
|||||||
const endpoint = `${this.provider.apiHost}/api/v2/convert/parse`
|
const endpoint = `${this.provider.apiHost}/api/v2/convert/parse`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<ApiResponse<any>>(endpoint, payload, config)
|
const response = await net.fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${this.provider.apiKey}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
|
||||||
if (response.data.code !== 'success') {
|
if (!response.ok) {
|
||||||
throw new Error(`API returned error: ${response.data.message || JSON.stringify(response.data)}`)
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as ApiResponse<any>
|
||||||
|
if (data.code !== 'success') {
|
||||||
|
throw new Error(`API returned error: ${data.message || JSON.stringify(data)}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to convert file ${filePath}: ${error instanceof Error ? error.message : String(error)}`)
|
logger.error(`Failed to convert file ${filePath}: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
@ -256,16 +299,25 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
|
|||||||
* @returns 解析后的文件信息
|
* @returns 解析后的文件信息
|
||||||
*/
|
*/
|
||||||
private async getParsedFile(uid: string): Promise<ParsedFileResponse> {
|
private async getParsedFile(uid: string): Promise<ParsedFileResponse> {
|
||||||
const config = this.createAuthConfig()
|
|
||||||
const endpoint = `${this.provider.apiHost}/api/v2/convert/parse/result?uid=${uid}`
|
const endpoint = `${this.provider.apiHost}/api/v2/convert/parse/result?uid=${uid}`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get<ApiResponse<ParsedFileResponse>>(endpoint, config)
|
const response = await net.fetch(endpoint, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.provider.apiKey}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (response.status === 200 && response.data.data) {
|
if (!response.ok) {
|
||||||
return response.data.data
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as ApiResponse<ParsedFileResponse>
|
||||||
|
if (data.data) {
|
||||||
|
return data.data
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`HTTP status ${response.status}: ${response.statusText}`)
|
throw new Error(`No data in response`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
@ -295,8 +347,12 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 下载文件
|
// 下载文件
|
||||||
const response = await axios.get(url, { responseType: 'arraybuffer' })
|
const response = await net.fetch(url, { method: 'GET' })
|
||||||
fs.writeFileSync(zipPath, response.data)
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
const arrayBuffer = await response.arrayBuffer()
|
||||||
|
fs.writeFileSync(zipPath, Buffer.from(arrayBuffer))
|
||||||
|
|
||||||
// 确保提取目录存在
|
// 确保提取目录存在
|
||||||
if (!fs.existsSync(extractPath)) {
|
if (!fs.existsSync(extractPath)) {
|
||||||
@ -318,14 +374,6 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private createAuthConfig(): AxiosRequestConfig {
|
|
||||||
return {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${this.provider.apiKey}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public checkQuota(): Promise<number> {
|
public checkQuota(): Promise<number> {
|
||||||
throw new Error('Method not implemented.')
|
throw new Error('Method not implemented.')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { loggerService } from '@logger'
|
|||||||
import { fileStorage } from '@main/services/FileStorage'
|
import { fileStorage } from '@main/services/FileStorage'
|
||||||
import { FileMetadata, PreprocessProvider } from '@types'
|
import { FileMetadata, PreprocessProvider } from '@types'
|
||||||
import AdmZip from 'adm-zip'
|
import AdmZip from 'adm-zip'
|
||||||
import axios from 'axios'
|
import { net } from 'electron'
|
||||||
|
|
||||||
import BasePreprocessProvider from './BasePreprocessProvider'
|
import BasePreprocessProvider from './BasePreprocessProvider'
|
||||||
|
|
||||||
@ -95,7 +95,7 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
|
|||||||
|
|
||||||
public async checkQuota() {
|
public async checkQuota() {
|
||||||
try {
|
try {
|
||||||
const quota = await fetch(`${this.provider.apiHost}/api/v4/quota`, {
|
const quota = await net.fetch(`${this.provider.apiHost}/api/v4/quota`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -179,8 +179,12 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 下载ZIP文件
|
// 下载ZIP文件
|
||||||
const response = await axios.get(zipUrl, { responseType: 'arraybuffer' })
|
const response = await net.fetch(zipUrl, { method: 'GET' })
|
||||||
fs.writeFileSync(zipPath, Buffer.from(response.data))
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
const arrayBuffer = await response.arrayBuffer()
|
||||||
|
fs.writeFileSync(zipPath, Buffer.from(arrayBuffer))
|
||||||
logger.info(`Downloaded ZIP file: ${zipPath}`)
|
logger.info(`Downloaded ZIP file: ${zipPath}`)
|
||||||
|
|
||||||
// 确保提取目录存在
|
// 确保提取目录存在
|
||||||
@ -236,7 +240,7 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(endpoint, {
|
const response = await net.fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -271,7 +275,7 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
|
|||||||
try {
|
try {
|
||||||
const fileBuffer = await fs.promises.readFile(filePath)
|
const fileBuffer = await fs.promises.readFile(filePath)
|
||||||
|
|
||||||
const response = await fetch(uploadUrl, {
|
const response = await net.fetch(uploadUrl, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: fileBuffer,
|
body: fileBuffer,
|
||||||
headers: {
|
headers: {
|
||||||
@ -316,7 +320,7 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
|
|||||||
const endpoint = `${this.provider.apiHost}/api/v4/extract-results/batch/${batchId}`
|
const endpoint = `${this.provider.apiHost}/api/v4/extract-results/batch/${batchId}`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(endpoint, {
|
const response = await net.fetch(endpoint, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
|
import { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
|
||||||
import { KnowledgeBaseParams } from '@types'
|
import { KnowledgeBaseParams } from '@types'
|
||||||
import axios from 'axios'
|
import { net } from 'electron'
|
||||||
|
|
||||||
import BaseReranker from './BaseReranker'
|
import BaseReranker from './BaseReranker'
|
||||||
|
|
||||||
@ -15,7 +15,17 @@ export default class GeneralReranker extends BaseReranker {
|
|||||||
const requestBody = this.getRerankRequestBody(query, searchResults)
|
const requestBody = this.getRerankRequestBody(query, searchResults)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.post(url, requestBody, { headers: this.defaultHeaders() })
|
const response = await net.fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.defaultHeaders(),
|
||||||
|
body: JSON.stringify(requestBody)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
const rerankResults = this.extractRerankResult(data)
|
const rerankResults = this.extractRerankResult(data)
|
||||||
return this.getRerankResult(searchResults, rerankResults)
|
return this.getRerankResult(searchResults, rerankResults)
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||||
import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js'
|
import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js'
|
||||||
|
import { net } from 'electron'
|
||||||
|
|
||||||
const WEB_SEARCH_TOOL: Tool = {
|
const WEB_SEARCH_TOOL: Tool = {
|
||||||
name: 'brave_web_search',
|
name: 'brave_web_search',
|
||||||
@ -159,7 +160,7 @@ async function performWebSearch(apiKey: string, query: string, count: number = 1
|
|||||||
url.searchParams.set('count', Math.min(count, 20).toString()) // API limit
|
url.searchParams.set('count', Math.min(count, 20).toString()) // API limit
|
||||||
url.searchParams.set('offset', offset.toString())
|
url.searchParams.set('offset', offset.toString())
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await net.fetch(url.toString(), {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
'Accept-Encoding': 'gzip',
|
'Accept-Encoding': 'gzip',
|
||||||
@ -192,7 +193,7 @@ async function performLocalSearch(apiKey: string, query: string, count: number =
|
|||||||
webUrl.searchParams.set('result_filter', 'locations')
|
webUrl.searchParams.set('result_filter', 'locations')
|
||||||
webUrl.searchParams.set('count', Math.min(count, 20).toString())
|
webUrl.searchParams.set('count', Math.min(count, 20).toString())
|
||||||
|
|
||||||
const webResponse = await fetch(webUrl, {
|
const webResponse = await net.fetch(webUrl.toString(), {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
'Accept-Encoding': 'gzip',
|
'Accept-Encoding': 'gzip',
|
||||||
@ -225,7 +226,7 @@ async function getPoisData(apiKey: string, ids: string[]): Promise<BravePoiRespo
|
|||||||
checkRateLimit()
|
checkRateLimit()
|
||||||
const url = new URL('https://api.search.brave.com/res/v1/local/pois')
|
const url = new URL('https://api.search.brave.com/res/v1/local/pois')
|
||||||
ids.filter(Boolean).forEach((id) => url.searchParams.append('ids', id))
|
ids.filter(Boolean).forEach((id) => url.searchParams.append('ids', id))
|
||||||
const response = await fetch(url, {
|
const response = await net.fetch(url.toString(), {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
'Accept-Encoding': 'gzip',
|
'Accept-Encoding': 'gzip',
|
||||||
@ -244,7 +245,7 @@ async function getDescriptionsData(apiKey: string, ids: string[]): Promise<Brave
|
|||||||
checkRateLimit()
|
checkRateLimit()
|
||||||
const url = new URL('https://api.search.brave.com/res/v1/local/descriptions')
|
const url = new URL('https://api.search.brave.com/res/v1/local/descriptions')
|
||||||
ids.filter(Boolean).forEach((id) => url.searchParams.append('ids', id))
|
ids.filter(Boolean).forEach((id) => url.searchParams.append('ids', id))
|
||||||
const response = await fetch(url, {
|
const response = await net.fetch(url.toString(), {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
'Accept-Encoding': 'gzip',
|
'Accept-Encoding': 'gzip',
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||||
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||||
|
import { net } from 'electron'
|
||||||
import * as z from 'zod/v4'
|
import * as z from 'zod/v4'
|
||||||
|
|
||||||
const logger = loggerService.withContext('DifyKnowledgeServer')
|
const logger = loggerService.withContext('DifyKnowledgeServer')
|
||||||
@ -134,7 +135,7 @@ class DifyKnowledgeServer {
|
|||||||
private async performListKnowledges(difyKey: string, apiHost: string): Promise<McpResponse> {
|
private async performListKnowledges(difyKey: string, apiHost: string): Promise<McpResponse> {
|
||||||
try {
|
try {
|
||||||
const url = `${apiHost.replace(/\/$/, '')}/datasets`
|
const url = `${apiHost.replace(/\/$/, '')}/datasets`
|
||||||
const response = await fetch(url, {
|
const response = await net.fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${difyKey}`
|
Authorization: `Bearer ${difyKey}`
|
||||||
@ -186,7 +187,7 @@ class DifyKnowledgeServer {
|
|||||||
try {
|
try {
|
||||||
const url = `${apiHost.replace(/\/$/, '')}/datasets/${id}/retrieve`
|
const url = `${apiHost.replace(/\/$/, '')}/datasets/${id}/retrieve`
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await net.fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${difyKey}`,
|
Authorization: `Bearer ${difyKey}`,
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||||
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||||
|
import { net } from 'electron'
|
||||||
import { JSDOM } from 'jsdom'
|
import { JSDOM } from 'jsdom'
|
||||||
import TurndownService from 'turndown'
|
import TurndownService from 'turndown'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
@ -16,7 +17,7 @@ export type RequestPayload = z.infer<typeof RequestPayloadSchema>
|
|||||||
export class Fetcher {
|
export class Fetcher {
|
||||||
private static async _fetch({ url, headers }: RequestPayload): Promise<Response> {
|
private static async _fetch({ url, headers }: RequestPayload): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await net.fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent':
|
'User-Agent':
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
|||||||
@ -6,9 +6,10 @@ import { generateUserAgent } from '@main/utils/systemInfo'
|
|||||||
import { FeedUrl, UpgradeChannel } from '@shared/config/constant'
|
import { FeedUrl, UpgradeChannel } from '@shared/config/constant'
|
||||||
import { IpcChannel } from '@shared/IpcChannel'
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
import { CancellationToken, UpdateInfo } from 'builder-util-runtime'
|
import { CancellationToken, UpdateInfo } from 'builder-util-runtime'
|
||||||
import { app, BrowserWindow, dialog } from 'electron'
|
import { app, BrowserWindow, dialog, net } 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 net.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 net.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
|
||||||
|
|||||||
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { AxiosRequestConfig } from 'axios'
|
import { net } from 'electron'
|
||||||
import axios from 'axios'
|
|
||||||
import { app, safeStorage } from 'electron'
|
import { app, safeStorage } from 'electron'
|
||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
@ -86,7 +85,8 @@ class CopilotService {
|
|||||||
*/
|
*/
|
||||||
public getUser = async (_: Electron.IpcMainInvokeEvent, token: string): Promise<UserResponse> => {
|
public getUser = async (_: Electron.IpcMainInvokeEvent, token: string): Promise<UserResponse> => {
|
||||||
try {
|
try {
|
||||||
const config: AxiosRequestConfig = {
|
const response = await net.fetch(CONFIG.API_URLS.GITHUB_USER, {
|
||||||
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Connection: 'keep-alive',
|
Connection: 'keep-alive',
|
||||||
'user-agent': 'Visual Studio Code (desktop)',
|
'user-agent': 'Visual Studio Code (desktop)',
|
||||||
@ -95,12 +95,16 @@ class CopilotService {
|
|||||||
'Sec-Fetch-Dest': 'empty',
|
'Sec-Fetch-Dest': 'empty',
|
||||||
authorization: `token ${token}`
|
authorization: `token ${token}`
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await axios.get(CONFIG.API_URLS.GITHUB_USER, config)
|
const data = await response.json()
|
||||||
return {
|
return {
|
||||||
login: response.data.login,
|
login: data.login,
|
||||||
avatar: response.data.avatar_url
|
avatar: data.avatar_url
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to get user information:', error as Error)
|
logger.error('Failed to get user information:', error as Error)
|
||||||
@ -118,16 +122,23 @@ class CopilotService {
|
|||||||
try {
|
try {
|
||||||
this.updateHeaders(headers)
|
this.updateHeaders(headers)
|
||||||
|
|
||||||
const response = await axios.post<AuthResponse>(
|
const response = await net.fetch(CONFIG.API_URLS.GITHUB_DEVICE_CODE, {
|
||||||
CONFIG.API_URLS.GITHUB_DEVICE_CODE,
|
method: 'POST',
|
||||||
{
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
client_id: CONFIG.GITHUB_CLIENT_ID,
|
client_id: CONFIG.GITHUB_CLIENT_ID,
|
||||||
scope: 'read:user'
|
scope: 'read:user'
|
||||||
},
|
})
|
||||||
{ headers: this.headers }
|
})
|
||||||
)
|
|
||||||
|
|
||||||
return response.data
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await response.json()) as AuthResponse
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to get auth message:', error as Error)
|
logger.error('Failed to get auth message:', error as Error)
|
||||||
throw new CopilotServiceError('无法获取GitHub授权信息', error)
|
throw new CopilotServiceError('无法获取GitHub授权信息', error)
|
||||||
@ -150,17 +161,25 @@ class CopilotService {
|
|||||||
await this.delay(currentDelay)
|
await this.delay(currentDelay)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<TokenResponse>(
|
const response = await net.fetch(CONFIG.API_URLS.GITHUB_ACCESS_TOKEN, {
|
||||||
CONFIG.API_URLS.GITHUB_ACCESS_TOKEN,
|
method: 'POST',
|
||||||
{
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
client_id: CONFIG.GITHUB_CLIENT_ID,
|
client_id: CONFIG.GITHUB_CLIENT_ID,
|
||||||
device_code,
|
device_code,
|
||||||
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
|
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
|
||||||
},
|
})
|
||||||
{ headers: this.headers }
|
})
|
||||||
)
|
|
||||||
|
|
||||||
const { access_token } = response.data
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as TokenResponse
|
||||||
|
const { access_token } = data
|
||||||
if (access_token) {
|
if (access_token) {
|
||||||
return { access_token }
|
return { access_token }
|
||||||
}
|
}
|
||||||
@ -205,16 +224,19 @@ class CopilotService {
|
|||||||
const encryptedToken = await fs.readFile(this.tokenFilePath)
|
const encryptedToken = await fs.readFile(this.tokenFilePath)
|
||||||
const access_token = safeStorage.decryptString(Buffer.from(encryptedToken))
|
const access_token = safeStorage.decryptString(Buffer.from(encryptedToken))
|
||||||
|
|
||||||
const config: AxiosRequestConfig = {
|
const response = await net.fetch(CONFIG.API_URLS.COPILOT_TOKEN, {
|
||||||
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
...this.headers,
|
...this.headers,
|
||||||
authorization: `token ${access_token}`
|
authorization: `token ${access_token}`
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await axios.get<CopilotTokenResponse>(CONFIG.API_URLS.COPILOT_TOKEN, config)
|
return (await response.json()) as CopilotTokenResponse
|
||||||
|
|
||||||
return response.data
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to get Copilot token:', error as Error)
|
logger.error('Failed to get Copilot token:', error as Error)
|
||||||
throw new CopilotServiceError('无法获取Copilot令牌,请重新授权', error)
|
throw new CopilotServiceError('无法获取Copilot令牌,请重新授权', error)
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { FileMetadata } from '@types'
|
|||||||
import * as crypto from 'crypto'
|
import * as crypto from 'crypto'
|
||||||
import {
|
import {
|
||||||
dialog,
|
dialog,
|
||||||
|
net,
|
||||||
OpenDialogOptions,
|
OpenDialogOptions,
|
||||||
OpenDialogReturnValue,
|
OpenDialogReturnValue,
|
||||||
SaveDialogOptions,
|
SaveDialogOptions,
|
||||||
@ -509,7 +510,7 @@ class FileStorage {
|
|||||||
isUseContentType?: boolean
|
isUseContentType?: boolean
|
||||||
): Promise<FileMetadata> => {
|
): Promise<FileMetadata> => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url)
|
const response = await net.fetch(url)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`)
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import {
|
|||||||
} from '@modelcontextprotocol/sdk/types.js'
|
} from '@modelcontextprotocol/sdk/types.js'
|
||||||
import { nanoid } from '@reduxjs/toolkit'
|
import { nanoid } from '@reduxjs/toolkit'
|
||||||
import type { GetResourceResponse, MCPCallToolResponse, MCPPrompt, MCPResource, MCPServer, MCPTool } from '@types'
|
import type { GetResourceResponse, MCPCallToolResponse, MCPPrompt, MCPResource, MCPServer, MCPTool } from '@types'
|
||||||
import { app } from 'electron'
|
import { app, net } from 'electron'
|
||||||
import { EventEmitter } from 'events'
|
import { EventEmitter } from 'events'
|
||||||
import { memoize } from 'lodash'
|
import { memoize } from 'lodash'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
@ -205,7 +205,7 @@ class McpService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(url, { ...init, headers })
|
return net.fetch(typeof url === 'string' ? url : url.toString(), { ...init, headers })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
requestInit: {
|
requestInit: {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import path from 'node:path'
|
|||||||
|
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { NUTSTORE_HOST } from '@shared/config/nutstore'
|
import { NUTSTORE_HOST } from '@shared/config/nutstore'
|
||||||
|
import { net } from 'electron'
|
||||||
import { XMLParser } from 'fast-xml-parser'
|
import { XMLParser } from 'fast-xml-parser'
|
||||||
import { isNil, partial } from 'lodash'
|
import { isNil, partial } from 'lodash'
|
||||||
import { type FileStat } from 'webdav'
|
import { type FileStat } from 'webdav'
|
||||||
@ -62,7 +63,7 @@ export async function getDirectoryContents(token: string, target: string): Promi
|
|||||||
let currentUrl = `${NUTSTORE_HOST}${target}`
|
let currentUrl = `${NUTSTORE_HOST}${target}`
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const response = await fetch(currentUrl, {
|
const response = await net.fetch(currentUrl, {
|
||||||
method: 'PROPFIND',
|
method: 'PROPFIND',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Basic ${token}`,
|
Authorization: `Basic ${token}`,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
|
import { net } from 'electron'
|
||||||
|
|
||||||
const logger = loggerService.withContext('IpService')
|
const logger = loggerService.withContext('IpService')
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ export async function getIpCountry(): Promise<string> {
|
|||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 5000)
|
const timeoutId = setTimeout(() => controller.abort(), 5000)
|
||||||
|
|
||||||
const ipinfo = await fetch('https://ipinfo.io/json', {
|
const ipinfo = await net.fetch('https://ipinfo.io/json', {
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent':
|
'User-Agent':
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
|
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
|
||||||
<title>Cherry Studio</title>
|
<title>Cherry Studio Quick Assistant</title>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
html,
|
html,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|||||||
import { AihubmixAPIClient } from '../AihubmixAPIClient'
|
import { AihubmixAPIClient } from '../AihubmixAPIClient'
|
||||||
import { AnthropicAPIClient } from '../anthropic/AnthropicAPIClient'
|
import { AnthropicAPIClient } from '../anthropic/AnthropicAPIClient'
|
||||||
import { ApiClientFactory } from '../ApiClientFactory'
|
import { ApiClientFactory } from '../ApiClientFactory'
|
||||||
|
import { AwsBedrockAPIClient } from '../aws/AwsBedrockAPIClient'
|
||||||
import { GeminiAPIClient } from '../gemini/GeminiAPIClient'
|
import { GeminiAPIClient } from '../gemini/GeminiAPIClient'
|
||||||
import { VertexAPIClient } from '../gemini/VertexAPIClient'
|
import { VertexAPIClient } from '../gemini/VertexAPIClient'
|
||||||
import { NewAPIClient } from '../NewAPIClient'
|
import { NewAPIClient } from '../NewAPIClient'
|
||||||
@ -54,6 +55,19 @@ vi.mock('../openai/OpenAIResponseAPIClient', () => ({
|
|||||||
vi.mock('../ppio/PPIOAPIClient', () => ({
|
vi.mock('../ppio/PPIOAPIClient', () => ({
|
||||||
PPIOAPIClient: vi.fn().mockImplementation(() => ({}))
|
PPIOAPIClient: vi.fn().mockImplementation(() => ({}))
|
||||||
}))
|
}))
|
||||||
|
vi.mock('../aws/AwsBedrockAPIClient', () => ({
|
||||||
|
AwsBedrockAPIClient: vi.fn().mockImplementation(() => ({}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock the models config to prevent circular dependency issues
|
||||||
|
vi.mock('@renderer/config/models', () => ({
|
||||||
|
findTokenLimit: vi.fn(),
|
||||||
|
isReasoningModel: vi.fn(),
|
||||||
|
SYSTEM_MODELS: {
|
||||||
|
silicon: [],
|
||||||
|
defaultModel: []
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
describe('ApiClientFactory', () => {
|
describe('ApiClientFactory', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -144,6 +158,15 @@ describe('ApiClientFactory', () => {
|
|||||||
expect(client).toBeDefined()
|
expect(client).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should create AwsBedrockAPIClient for aws-bedrock type', () => {
|
||||||
|
const provider = createTestProvider('aws-bedrock', 'aws-bedrock')
|
||||||
|
|
||||||
|
const client = ApiClientFactory.create(provider)
|
||||||
|
|
||||||
|
expect(AwsBedrockAPIClient).toHaveBeenCalledWith(provider)
|
||||||
|
expect(client).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
// 测试默认情况
|
// 测试默认情况
|
||||||
it('should create OpenAIAPIClient as default for unknown type', () => {
|
it('should create OpenAIAPIClient as default for unknown type', () => {
|
||||||
const provider = createTestProvider('unknown', 'unknown-type')
|
const provider = createTestProvider('unknown', 'unknown-type')
|
||||||
|
|||||||
@ -2,19 +2,23 @@ import { BedrockClient, ListFoundationModelsCommand, ListInferenceProfilesComman
|
|||||||
import {
|
import {
|
||||||
BedrockRuntimeClient,
|
BedrockRuntimeClient,
|
||||||
ConverseCommand,
|
ConverseCommand,
|
||||||
ConverseStreamCommand,
|
InvokeModelCommand,
|
||||||
InvokeModelCommand
|
InvokeModelWithResponseStreamCommand
|
||||||
} from '@aws-sdk/client-bedrock-runtime'
|
} from '@aws-sdk/client-bedrock-runtime'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { GenericChunk } from '@renderer/aiCore/middleware/schemas'
|
import { GenericChunk } from '@renderer/aiCore/middleware/schemas'
|
||||||
import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
|
import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
|
||||||
|
import { findTokenLimit, isReasoningModel } from '@renderer/config/models'
|
||||||
import {
|
import {
|
||||||
getAwsBedrockAccessKeyId,
|
getAwsBedrockAccessKeyId,
|
||||||
getAwsBedrockRegion,
|
getAwsBedrockRegion,
|
||||||
getAwsBedrockSecretAccessKey
|
getAwsBedrockSecretAccessKey
|
||||||
} from '@renderer/hooks/useAwsBedrock'
|
} from '@renderer/hooks/useAwsBedrock'
|
||||||
|
import { getAssistantSettings } from '@renderer/services/AssistantService'
|
||||||
import { estimateTextTokens } from '@renderer/services/TokenService'
|
import { estimateTextTokens } from '@renderer/services/TokenService'
|
||||||
import {
|
import {
|
||||||
|
Assistant,
|
||||||
|
EFFORT_RATIO,
|
||||||
GenerateImageParams,
|
GenerateImageParams,
|
||||||
MCPCallToolResponse,
|
MCPCallToolResponse,
|
||||||
MCPTool,
|
MCPTool,
|
||||||
@ -23,7 +27,13 @@ import {
|
|||||||
Provider,
|
Provider,
|
||||||
ToolCallResponse
|
ToolCallResponse
|
||||||
} from '@renderer/types'
|
} from '@renderer/types'
|
||||||
import { ChunkType, MCPToolCreatedChunk, TextDeltaChunk } from '@renderer/types/chunk'
|
import {
|
||||||
|
ChunkType,
|
||||||
|
MCPToolCreatedChunk,
|
||||||
|
TextDeltaChunk,
|
||||||
|
ThinkingDeltaChunk,
|
||||||
|
ThinkingStartChunk
|
||||||
|
} from '@renderer/types/chunk'
|
||||||
import { Message } from '@renderer/types/newMessage'
|
import { Message } from '@renderer/types/newMessage'
|
||||||
import {
|
import {
|
||||||
AwsBedrockSdkInstance,
|
AwsBedrockSdkInstance,
|
||||||
@ -33,6 +43,7 @@ import {
|
|||||||
AwsBedrockSdkRawOutput,
|
AwsBedrockSdkRawOutput,
|
||||||
AwsBedrockSdkTool,
|
AwsBedrockSdkTool,
|
||||||
AwsBedrockSdkToolCall,
|
AwsBedrockSdkToolCall,
|
||||||
|
AwsBedrockStreamChunk,
|
||||||
SdkModel
|
SdkModel
|
||||||
} from '@renderer/types/sdk'
|
} from '@renderer/types/sdk'
|
||||||
import { convertBase64ImageToAwsBedrockFormat } from '@renderer/utils/aws-bedrock-utils'
|
import { convertBase64ImageToAwsBedrockFormat } from '@renderer/utils/aws-bedrock-utils'
|
||||||
@ -103,46 +114,65 @@ export class AwsBedrockAPIClient extends BaseApiClient<
|
|||||||
override async createCompletions(payload: AwsBedrockSdkParams): Promise<AwsBedrockSdkRawOutput> {
|
override async createCompletions(payload: AwsBedrockSdkParams): Promise<AwsBedrockSdkRawOutput> {
|
||||||
const sdk = await this.getSdkInstance()
|
const sdk = await this.getSdkInstance()
|
||||||
|
|
||||||
// 转换消息格式到AWS SDK原生格式
|
// 转换消息格式(用于 InvokeModelWithResponseStreamCommand)
|
||||||
const awsMessages = payload.messages.map((msg) => ({
|
const awsMessages = payload.messages.map((msg) => ({
|
||||||
role: msg.role,
|
role: msg.role,
|
||||||
content: msg.content.map((content) => {
|
content: msg.content.map((content) => {
|
||||||
if (content.text) {
|
if (content.text) {
|
||||||
return { text: content.text }
|
return { type: 'text', text: content.text }
|
||||||
}
|
}
|
||||||
if (content.image) {
|
if (content.image) {
|
||||||
|
// 处理图片数据,将 Uint8Array 或数字数组转换为 base64 字符串
|
||||||
|
let base64Data = ''
|
||||||
|
if (content.image.source.bytes) {
|
||||||
|
if (typeof content.image.source.bytes === 'string') {
|
||||||
|
// 如果已经是字符串,直接使用
|
||||||
|
base64Data = content.image.source.bytes
|
||||||
|
} else {
|
||||||
|
// 如果是数组或 Uint8Array,转换为 base64
|
||||||
|
const uint8Array = new Uint8Array(Object.values(content.image.source.bytes))
|
||||||
|
const binaryString = Array.from(uint8Array)
|
||||||
|
.map((byte) => String.fromCharCode(byte))
|
||||||
|
.join('')
|
||||||
|
base64Data = btoa(binaryString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
image: {
|
type: 'image',
|
||||||
format: content.image.format,
|
source: {
|
||||||
source: content.image.source
|
type: 'base64',
|
||||||
|
media_type: `image/${content.image.format}`,
|
||||||
|
data: base64Data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (content.toolResult) {
|
if (content.toolResult) {
|
||||||
return {
|
return {
|
||||||
toolResult: {
|
type: 'tool_result',
|
||||||
toolUseId: content.toolResult.toolUseId,
|
tool_use_id: content.toolResult.toolUseId,
|
||||||
content: content.toolResult.content,
|
content: content.toolResult.content
|
||||||
status: content.toolResult.status
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (content.toolUse) {
|
if (content.toolUse) {
|
||||||
return {
|
return {
|
||||||
toolUse: {
|
type: 'tool_use',
|
||||||
toolUseId: content.toolUse.toolUseId,
|
id: content.toolUse.toolUseId,
|
||||||
name: content.toolUse.name,
|
name: content.toolUse.name,
|
||||||
input: content.toolUse.input
|
input: content.toolUse.input
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 返回符合AWS SDK ContentBlock类型的对象
|
return { type: 'text', text: 'Unknown content type' }
|
||||||
return { text: 'Unknown content type' }
|
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
logger.info('Creating completions with model ID:', { modelId: payload.modelId })
|
logger.info('Creating completions with model ID:', { modelId: payload.modelId })
|
||||||
|
|
||||||
|
const excludeKeys = ['modelId', 'messages', 'system', 'maxTokens', 'temperature', 'topP', 'stream', 'tools']
|
||||||
|
const additionalParams = Object.keys(payload)
|
||||||
|
.filter((key) => !excludeKeys.includes(key))
|
||||||
|
.reduce((acc, key) => ({ ...acc, [key]: payload[key] }), {})
|
||||||
|
|
||||||
const commonParams = {
|
const commonParams = {
|
||||||
modelId: payload.modelId,
|
modelId: payload.modelId,
|
||||||
messages: awsMessages as any,
|
messages: awsMessages as any,
|
||||||
@ -162,10 +192,18 @@ export class AwsBedrockAPIClient extends BaseApiClient<
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (payload.stream) {
|
if (payload.stream) {
|
||||||
const command = new ConverseStreamCommand(commonParams)
|
// 根据模型类型选择正确的 API 格式
|
||||||
|
const requestBody = this.createRequestBodyForModel(commonParams, additionalParams)
|
||||||
|
|
||||||
|
const command = new InvokeModelWithResponseStreamCommand({
|
||||||
|
modelId: commonParams.modelId,
|
||||||
|
body: JSON.stringify(requestBody),
|
||||||
|
contentType: 'application/json',
|
||||||
|
accept: 'application/json'
|
||||||
|
})
|
||||||
|
|
||||||
const response = await sdk.client.send(command)
|
const response = await sdk.client.send(command)
|
||||||
// 直接返回AWS Bedrock流式响应的异步迭代器
|
return this.createInvokeModelStreamIterator(response)
|
||||||
return this.createStreamIterator(response)
|
|
||||||
} else {
|
} else {
|
||||||
const command = new ConverseCommand(commonParams)
|
const command = new ConverseCommand(commonParams)
|
||||||
const response = await sdk.client.send(command)
|
const response = await sdk.client.send(command)
|
||||||
@ -177,32 +215,236 @@ export class AwsBedrockAPIClient extends BaseApiClient<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async *createStreamIterator(response: any): AsyncIterable<AwsBedrockSdkRawChunk> {
|
/**
|
||||||
try {
|
* 根据模型类型创建请求体
|
||||||
if (response.stream) {
|
*/
|
||||||
for await (const chunk of response.stream) {
|
private createRequestBodyForModel(commonParams: any, additionalParams: any): any {
|
||||||
logger.debug('AWS Bedrock chunk received:', chunk)
|
const modelId = commonParams.modelId.toLowerCase()
|
||||||
|
|
||||||
// AWS Bedrock的流式响应格式转换为标准格式
|
// Claude 系列模型使用 Anthropic API 格式
|
||||||
if (chunk.contentBlockDelta?.delta?.text) {
|
if (modelId.includes('claude')) {
|
||||||
yield {
|
return {
|
||||||
contentBlockDelta: {
|
anthropic_version: 'bedrock-2023-05-31',
|
||||||
delta: { text: chunk.contentBlockDelta.delta.text }
|
max_tokens: commonParams.inferenceConfig.maxTokens,
|
||||||
|
temperature: commonParams.inferenceConfig.temperature,
|
||||||
|
top_p: commonParams.inferenceConfig.topP,
|
||||||
|
messages: commonParams.messages,
|
||||||
|
...(commonParams.system && commonParams.system[0]?.text ? { system: commonParams.system[0].text } : {}),
|
||||||
|
...(commonParams.toolConfig?.tools ? { tools: commonParams.toolConfig.tools } : {}),
|
||||||
|
...additionalParams
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenAI 系列模型
|
||||||
|
if (modelId.includes('gpt') || modelId.includes('openai')) {
|
||||||
|
const messages: any[] = []
|
||||||
|
|
||||||
|
// 添加系统消息
|
||||||
|
if (commonParams.system && commonParams.system[0]?.text) {
|
||||||
|
messages.push({
|
||||||
|
role: 'system',
|
||||||
|
content: commonParams.system[0].text
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换消息格式
|
||||||
|
for (const message of commonParams.messages) {
|
||||||
|
const content: any[] = []
|
||||||
|
for (const part of message.content) {
|
||||||
|
if (part.text) {
|
||||||
|
content.push({ type: 'text', text: part.text })
|
||||||
|
} else if (part.image) {
|
||||||
|
content.push({
|
||||||
|
type: 'image_url',
|
||||||
|
image_url: {
|
||||||
|
url: `data:image/${part.image.format};base64,${part.image.source.bytes}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
messages.push({
|
||||||
|
role: message.role,
|
||||||
|
content: content.length === 1 && content[0].type === 'text' ? content[0].text : content
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseBody: any = {
|
||||||
|
model: commonParams.modelId,
|
||||||
|
messages: messages,
|
||||||
|
max_tokens: commonParams.inferenceConfig.maxTokens,
|
||||||
|
temperature: commonParams.inferenceConfig.temperature,
|
||||||
|
top_p: commonParams.inferenceConfig.topP,
|
||||||
|
stream: true,
|
||||||
|
...(commonParams.toolConfig?.tools ? { tools: commonParams.toolConfig.tools } : {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenAI 模型的 thinking 参数格式
|
||||||
|
if (additionalParams.reasoning_effort) {
|
||||||
|
baseBody.reasoning_effort = additionalParams.reasoning_effort
|
||||||
|
delete additionalParams.reasoning_effort
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseBody,
|
||||||
|
...additionalParams
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Llama 系列模型
|
||||||
|
if (modelId.includes('llama')) {
|
||||||
|
const baseBody: any = {
|
||||||
|
prompt: this.convertMessagesToPrompt(commonParams.messages, commonParams.system),
|
||||||
|
max_gen_len: commonParams.inferenceConfig.maxTokens,
|
||||||
|
temperature: commonParams.inferenceConfig.temperature,
|
||||||
|
top_p: commonParams.inferenceConfig.topP
|
||||||
|
}
|
||||||
|
|
||||||
|
// Llama 模型的 thinking 参数格式
|
||||||
|
if (additionalParams.thinking_mode) {
|
||||||
|
baseBody.thinking_mode = additionalParams.thinking_mode
|
||||||
|
delete additionalParams.thinking_mode
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseBody,
|
||||||
|
...additionalParams
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Amazon Titan 系列模型
|
||||||
|
if (modelId.includes('titan')) {
|
||||||
|
const textGenerationConfig: any = {
|
||||||
|
maxTokenCount: commonParams.inferenceConfig.maxTokens,
|
||||||
|
temperature: commonParams.inferenceConfig.temperature,
|
||||||
|
topP: commonParams.inferenceConfig.topP
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将 thinking 相关参数添加到 textGenerationConfig 中
|
||||||
|
if (additionalParams.thinking) {
|
||||||
|
textGenerationConfig.thinking = additionalParams.thinking
|
||||||
|
delete additionalParams.thinking
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputText: this.convertMessagesToPrompt(commonParams.messages, commonParams.system),
|
||||||
|
textGenerationConfig: {
|
||||||
|
...textGenerationConfig,
|
||||||
|
...Object.keys(additionalParams).reduce((acc, key) => {
|
||||||
|
if (['thinking_tokens', 'reasoning_mode'].includes(key)) {
|
||||||
|
acc[key] = additionalParams[key]
|
||||||
|
delete additionalParams[key]
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {} as any)
|
||||||
|
},
|
||||||
|
...additionalParams
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cohere Command 系列模型
|
||||||
|
if (modelId.includes('cohere') || modelId.includes('command')) {
|
||||||
|
const baseBody: any = {
|
||||||
|
message: this.convertMessagesToPrompt(commonParams.messages, commonParams.system),
|
||||||
|
max_tokens: commonParams.inferenceConfig.maxTokens,
|
||||||
|
temperature: commonParams.inferenceConfig.temperature,
|
||||||
|
p: commonParams.inferenceConfig.topP
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cohere 模型的 thinking 参数格式
|
||||||
|
if (additionalParams.thinking) {
|
||||||
|
baseBody.thinking = additionalParams.thinking
|
||||||
|
delete additionalParams.thinking
|
||||||
|
}
|
||||||
|
if (additionalParams.reasoning_tokens) {
|
||||||
|
baseBody.reasoning_tokens = additionalParams.reasoning_tokens
|
||||||
|
delete additionalParams.reasoning_tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseBody,
|
||||||
|
...additionalParams
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认使用通用格式
|
||||||
|
const baseBody: any = {
|
||||||
|
prompt: this.convertMessagesToPrompt(commonParams.messages, commonParams.system),
|
||||||
|
max_tokens: commonParams.inferenceConfig.maxTokens,
|
||||||
|
temperature: commonParams.inferenceConfig.temperature,
|
||||||
|
top_p: commonParams.inferenceConfig.topP
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseBody,
|
||||||
|
...additionalParams
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将消息转换为简单的 prompt 格式
|
||||||
|
*/
|
||||||
|
private convertMessagesToPrompt(messages: any[], system?: any[]): string {
|
||||||
|
let prompt = ''
|
||||||
|
|
||||||
|
// 添加系统消息
|
||||||
|
if (system && system[0]?.text) {
|
||||||
|
prompt += `System: ${system[0].text}\n\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加对话消息
|
||||||
|
for (const message of messages) {
|
||||||
|
const role = message.role === 'assistant' ? 'Assistant' : 'Human'
|
||||||
|
let content = ''
|
||||||
|
|
||||||
|
for (const part of message.content) {
|
||||||
|
if (part.text) {
|
||||||
|
content += part.text
|
||||||
|
} else if (part.image) {
|
||||||
|
content += '[Image]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt += `${role}: ${content}\n\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt += 'Assistant:'
|
||||||
|
return prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
private async *createInvokeModelStreamIterator(response: any): AsyncIterable<AwsBedrockSdkRawChunk> {
|
||||||
|
try {
|
||||||
|
if (response.body) {
|
||||||
|
for await (const event of response.body) {
|
||||||
|
if (event.chunk) {
|
||||||
|
const chunk: AwsBedrockStreamChunk = JSON.parse(new TextDecoder().decode(event.chunk.bytes))
|
||||||
|
|
||||||
|
// 转换为标准格式
|
||||||
|
if (chunk.type === 'content_block_delta') {
|
||||||
|
yield {
|
||||||
|
contentBlockDelta: {
|
||||||
|
delta: chunk.delta,
|
||||||
|
contentBlockIndex: chunk.index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (chunk.type === 'message_start') {
|
||||||
|
yield { messageStart: chunk }
|
||||||
|
} else if (chunk.type === 'message_stop') {
|
||||||
|
yield { messageStop: chunk }
|
||||||
|
} else if (chunk.type === 'content_block_start') {
|
||||||
|
yield {
|
||||||
|
contentBlockStart: {
|
||||||
|
start: chunk.content_block,
|
||||||
|
contentBlockIndex: chunk.index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (chunk.type === 'content_block_stop') {
|
||||||
|
yield {
|
||||||
|
contentBlockStop: {
|
||||||
|
contentBlockIndex: chunk.index
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chunk.messageStart) {
|
|
||||||
yield { messageStart: chunk.messageStart }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chunk.messageStop) {
|
|
||||||
yield { messageStop: chunk.messageStop }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chunk.metadata) {
|
|
||||||
yield { metadata: chunk.metadata }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -485,6 +727,38 @@ export class AwsBedrockAPIClient extends BaseApiClient<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取推理预算token(对所有支持推理的模型)
|
||||||
|
const budgetTokens = this.getBudgetToken(assistant, model)
|
||||||
|
|
||||||
|
// 构建基础自定义参数
|
||||||
|
const customParams: Record<string, any> =
|
||||||
|
coreRequest.callType === 'chat' ? this.getCustomParameters(assistant) : {}
|
||||||
|
|
||||||
|
// 根据模型类型添加 thinking 参数
|
||||||
|
if (budgetTokens) {
|
||||||
|
const modelId = model.id.toLowerCase()
|
||||||
|
|
||||||
|
if (modelId.includes('claude')) {
|
||||||
|
// Claude 模型使用 Anthropic 格式
|
||||||
|
customParams.thinking = { type: 'enabled', budget_tokens: budgetTokens }
|
||||||
|
} else if (modelId.includes('gpt') || modelId.includes('openai')) {
|
||||||
|
// OpenAI 模型格式
|
||||||
|
customParams.reasoning_effort = assistant?.settings?.reasoning_effort
|
||||||
|
} else if (modelId.includes('llama')) {
|
||||||
|
// Llama 模型格式
|
||||||
|
customParams.thinking_mode = true
|
||||||
|
customParams.thinking_tokens = budgetTokens
|
||||||
|
} else if (modelId.includes('titan')) {
|
||||||
|
// Titan 模型格式
|
||||||
|
customParams.thinking = { enabled: true }
|
||||||
|
customParams.thinking_tokens = budgetTokens
|
||||||
|
} else if (modelId.includes('cohere') || modelId.includes('command')) {
|
||||||
|
// Cohere 模型格式
|
||||||
|
customParams.thinking = { enabled: true }
|
||||||
|
customParams.reasoning_tokens = budgetTokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const payload: AwsBedrockSdkParams = {
|
const payload: AwsBedrockSdkParams = {
|
||||||
modelId: model.id,
|
modelId: model.id,
|
||||||
messages:
|
messages:
|
||||||
@ -497,9 +771,7 @@ export class AwsBedrockAPIClient extends BaseApiClient<
|
|||||||
topP: this.getTopP(assistant, model),
|
topP: this.getTopP(assistant, model),
|
||||||
stream: streamOutput !== false,
|
stream: streamOutput !== false,
|
||||||
tools: tools.length > 0 ? tools : undefined,
|
tools: tools.length > 0 ? tools : undefined,
|
||||||
// 只在对话场景下应用自定义参数,避免影响翻译、总结等其他业务逻辑
|
...customParams
|
||||||
// 注意:用户自定义参数总是应该覆盖其他参数
|
|
||||||
...(coreRequest.callType === 'chat' ? this.getCustomParameters(assistant) : {})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeout = this.getTimeout(model)
|
const timeout = this.getTimeout(model)
|
||||||
@ -511,6 +783,7 @@ export class AwsBedrockAPIClient extends BaseApiClient<
|
|||||||
getResponseChunkTransformer(): ResponseChunkTransformer<AwsBedrockSdkRawChunk> {
|
getResponseChunkTransformer(): ResponseChunkTransformer<AwsBedrockSdkRawChunk> {
|
||||||
return () => {
|
return () => {
|
||||||
let hasStartedText = false
|
let hasStartedText = false
|
||||||
|
let hasStartedThinking = false
|
||||||
let accumulatedJson = ''
|
let accumulatedJson = ''
|
||||||
const toolCalls: Record<number, AwsBedrockSdkToolCall> = {}
|
const toolCalls: Record<number, AwsBedrockSdkToolCall> = {}
|
||||||
|
|
||||||
@ -570,6 +843,24 @@ export class AwsBedrockAPIClient extends BaseApiClient<
|
|||||||
} as TextDeltaChunk)
|
} as TextDeltaChunk)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理thinking增量
|
||||||
|
if (
|
||||||
|
rawChunk.contentBlockDelta?.delta?.type === 'thinking_delta' &&
|
||||||
|
rawChunk.contentBlockDelta?.delta?.thinking
|
||||||
|
) {
|
||||||
|
if (!hasStartedThinking) {
|
||||||
|
controller.enqueue({
|
||||||
|
type: ChunkType.THINKING_START
|
||||||
|
} as ThinkingStartChunk)
|
||||||
|
hasStartedThinking = true
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.enqueue({
|
||||||
|
type: ChunkType.THINKING_DELTA,
|
||||||
|
text: rawChunk.contentBlockDelta.delta.thinking
|
||||||
|
} as ThinkingDeltaChunk)
|
||||||
|
}
|
||||||
|
|
||||||
// 处理内容块停止事件 - 参考 Anthropic 的 content_block_stop 处理
|
// 处理内容块停止事件 - 参考 Anthropic 的 content_block_stop 处理
|
||||||
if (rawChunk.contentBlockStop) {
|
if (rawChunk.contentBlockStop) {
|
||||||
const blockIndex = rawChunk.contentBlockStop.contentBlockIndex || 0
|
const blockIndex = rawChunk.contentBlockStop.contentBlockIndex || 0
|
||||||
@ -708,4 +999,49 @@ export class AwsBedrockAPIClient extends BaseApiClient<
|
|||||||
extractMessagesFromSdkPayload(sdkPayload: AwsBedrockSdkParams): AwsBedrockSdkMessageParam[] {
|
extractMessagesFromSdkPayload(sdkPayload: AwsBedrockSdkParams): AwsBedrockSdkMessageParam[] {
|
||||||
return sdkPayload.messages || []
|
return sdkPayload.messages || []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 AWS Bedrock 的推理工作量预算token
|
||||||
|
* @param assistant - The assistant
|
||||||
|
* @param model - The model
|
||||||
|
* @returns The budget tokens for reasoning effort
|
||||||
|
*/
|
||||||
|
private getBudgetToken(assistant: Assistant, model: Model): number | undefined {
|
||||||
|
try {
|
||||||
|
if (!isReasoningModel(model)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const { maxTokens } = getAssistantSettings(assistant)
|
||||||
|
const reasoningEffort = assistant?.settings?.reasoning_effort
|
||||||
|
|
||||||
|
if (reasoningEffort === undefined) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const effortRatio = EFFORT_RATIO[reasoningEffort]
|
||||||
|
const tokenLimits = findTokenLimit(model.id)
|
||||||
|
|
||||||
|
if (tokenLimits) {
|
||||||
|
// 使用模型特定的 token 限制
|
||||||
|
const budgetTokens = Math.max(
|
||||||
|
1024,
|
||||||
|
Math.floor(
|
||||||
|
Math.min(
|
||||||
|
(tokenLimits.max - tokenLimits.min) * effortRatio + tokenLimits.min,
|
||||||
|
(maxTokens || DEFAULT_MAX_TOKENS) * effortRatio
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return budgetTokens
|
||||||
|
} else {
|
||||||
|
// 对于没有特定限制的模型,使用简化计算
|
||||||
|
const budgetTokens = Math.max(1024, Math.floor((maxTokens || DEFAULT_MAX_TOKENS) * effortRatio))
|
||||||
|
return budgetTokens
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Failed to calculate budget tokens for reasoning effort:', error as Error)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
isGPT5SeriesModel,
|
isGPT5SeriesModel,
|
||||||
isGrokReasoningModel,
|
isGrokReasoningModel,
|
||||||
isNotSupportSystemMessageModel,
|
isNotSupportSystemMessageModel,
|
||||||
|
isOpenAIReasoningModel,
|
||||||
isQwenAlwaysThinkModel,
|
isQwenAlwaysThinkModel,
|
||||||
isQwenMTModel,
|
isQwenMTModel,
|
||||||
isQwenReasoningModel,
|
isQwenReasoningModel,
|
||||||
@ -146,7 +147,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
|||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
// Don't disable reasoning for models that require it
|
// Don't disable reasoning for models that require it
|
||||||
if (isGrokReasoningModel(model)) {
|
if (isGrokReasoningModel(model) || isOpenAIReasoningModel(model)) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
return { reasoning: { enabled: false, exclude: true } }
|
return { reasoning: { enabled: false, exclude: true } }
|
||||||
@ -524,12 +525,13 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1. 处理系统消息
|
// 1. 处理系统消息
|
||||||
let systemMessage = { role: 'system', content: assistant.prompt || '' }
|
const systemMessage = { role: 'system', content: assistant.prompt || '' }
|
||||||
|
|
||||||
if (isSupportedReasoningEffortOpenAIModel(model)) {
|
if (isSupportedReasoningEffortOpenAIModel(model)) {
|
||||||
systemMessage = {
|
if (isSupportDeveloperRoleProvider(this.provider)) {
|
||||||
role: isSupportDeveloperRoleProvider(this.provider) ? 'developer' : 'system',
|
systemMessage.role = 'developer'
|
||||||
content: `Formatting re-enabled${systemMessage ? '\n' + systemMessage.content : ''}`
|
} else {
|
||||||
|
systemMessage.role = 'system'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -58,12 +58,9 @@ export const CodeBlockView: React.FC<Props> = memo(({ children, language, onSave
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { codeEditor, codeExecution, codeImageTools, codeCollapsible, codeWrappable } = useSettings()
|
const { codeEditor, codeExecution, codeImageTools, codeCollapsible, codeWrappable } = useSettings()
|
||||||
|
|
||||||
const [viewState, setViewState] = useState(() => {
|
const [viewState, setViewState] = useState({
|
||||||
const initialMode = SPECIAL_VIEWS.includes(language) ? 'special' : 'source'
|
mode: 'special' as ViewMode,
|
||||||
return {
|
previousMode: 'special' as ViewMode
|
||||||
mode: initialMode as ViewMode,
|
|
||||||
previousMode: initialMode as ViewMode
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
const { mode: viewMode } = viewState
|
const { mode: viewMode } = viewState
|
||||||
|
|
||||||
@ -99,18 +96,10 @@ export const CodeBlockView: React.FC<Props> = memo(({ children, language, onSave
|
|||||||
|
|
||||||
const hasSpecialView = useMemo(() => SPECIAL_VIEWS.includes(language), [language])
|
const hasSpecialView = useMemo(() => SPECIAL_VIEWS.includes(language), [language])
|
||||||
|
|
||||||
// TODO: 考虑移除
|
|
||||||
const isInSpecialView = useMemo(() => {
|
const isInSpecialView = useMemo(() => {
|
||||||
return hasSpecialView && viewMode === 'special'
|
return hasSpecialView && viewMode === 'special'
|
||||||
}, [hasSpecialView, viewMode])
|
}, [hasSpecialView, viewMode])
|
||||||
|
|
||||||
// 不支持特殊视图时回退到 source
|
|
||||||
useEffect(() => {
|
|
||||||
if (!hasSpecialView && viewMode !== 'source') {
|
|
||||||
setViewMode('source')
|
|
||||||
}
|
|
||||||
}, [hasSpecialView, viewMode, setViewMode])
|
|
||||||
|
|
||||||
const [expandOverride, setExpandOverride] = useState(!codeCollapsible)
|
const [expandOverride, setExpandOverride] = useState(!codeCollapsible)
|
||||||
const [unwrapOverride, setUnwrapOverride] = useState(!codeWrappable)
|
const [unwrapOverride, setUnwrapOverride] = useState(!codeWrappable)
|
||||||
|
|
||||||
@ -298,11 +287,14 @@ export const CodeBlockView: React.FC<Props> = memo(({ children, language, onSave
|
|||||||
|
|
||||||
// 根据视图模式和语言选择组件,优先展示特殊视图,fallback是源代码视图
|
// 根据视图模式和语言选择组件,优先展示特殊视图,fallback是源代码视图
|
||||||
const renderContent = useMemo(() => {
|
const renderContent = useMemo(() => {
|
||||||
const showSpecialView = specialView && ['special', 'split'].includes(viewMode)
|
const showSpecialView = !!specialView && ['special', 'split'].includes(viewMode)
|
||||||
const showSourceView = !specialView || viewMode !== 'special'
|
const showSourceView = !specialView || viewMode !== 'special'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SplitViewWrapper className="split-view-wrapper" $viewMode={viewMode}>
|
<SplitViewWrapper
|
||||||
|
className="split-view-wrapper"
|
||||||
|
$isSpecialView={showSpecialView && !showSourceView}
|
||||||
|
$isSplitView={showSpecialView && showSourceView}>
|
||||||
{showSpecialView && specialView}
|
{showSpecialView && specialView}
|
||||||
{showSourceView && sourceView}
|
{showSourceView && sourceView}
|
||||||
</SplitViewWrapper>
|
</SplitViewWrapper>
|
||||||
@ -373,7 +365,7 @@ const CodeHeader = styled.div<{ $isInSpecialView: boolean }>`
|
|||||||
background-color: ${(props) => (props.$isInSpecialView ? 'transparent' : 'var(--color-background-mute)')};
|
background-color: ${(props) => (props.$isInSpecialView ? 'transparent' : 'var(--color-background-mute)')};
|
||||||
`
|
`
|
||||||
|
|
||||||
const SplitViewWrapper = styled.div<{ $viewMode?: ViewMode }>`
|
const SplitViewWrapper = styled.div<{ $isSpecialView: boolean; $isSplitView: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
@ -383,13 +375,13 @@ const SplitViewWrapper = styled.div<{ $viewMode?: ViewMode }>`
|
|||||||
|
|
||||||
&:not(:has(+ [class*='Container'])) {
|
&:not(:has(+ [class*='Container'])) {
|
||||||
// 特殊视图的 header 会隐藏,所以全都使用圆角
|
// 特殊视图的 header 会隐藏,所以全都使用圆角
|
||||||
border-radius: ${(props) => (props.$viewMode === 'special' ? '8px' : '0 0 8px 8px')};
|
border-radius: ${(props) => (props.$isSpecialView ? '8px' : '0 0 8px 8px')};
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在 split 模式下添加中间分隔线
|
// 在 split 模式下添加中间分隔线
|
||||||
${(props) =>
|
${(props) =>
|
||||||
props.$viewMode === 'split' &&
|
props.$isSplitView &&
|
||||||
css`
|
css`
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取模型用于检查
|
// 获取模型用于检查
|
||||||
|
|||||||
@ -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`
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -66,6 +66,9 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
|||||||
const prevSearchTextRef = useRef('')
|
const prevSearchTextRef = useRef('')
|
||||||
const prevSymbolRef = useRef('')
|
const prevSymbolRef = useRef('')
|
||||||
|
|
||||||
|
// 无匹配项自动关闭的定时器
|
||||||
|
const noMatchTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
|
||||||
// 处理搜索,过滤列表
|
// 处理搜索,过滤列表
|
||||||
const list = useMemo(() => {
|
const list = useMemo(() => {
|
||||||
if (!ctx.isVisible && !ctx.symbol) return []
|
if (!ctx.isVisible && !ctx.symbol) return []
|
||||||
@ -128,12 +131,44 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
|||||||
prevSymbolRef.current = ctx.symbol
|
prevSymbolRef.current = ctx.symbol
|
||||||
|
|
||||||
return newList
|
return newList
|
||||||
}, [ctx.isVisible, ctx.list, ctx.symbol, searchText])
|
}, [ctx.isVisible, ctx.symbol, ctx.list, searchText])
|
||||||
|
|
||||||
const canForwardAndBackward = useMemo(() => {
|
const canForwardAndBackward = useMemo(() => {
|
||||||
return list.some((item) => item.isMenu) || historyPanel.length > 0
|
return list.some((item) => item.isMenu) || historyPanel.length > 0
|
||||||
}, [list, historyPanel])
|
}, [list, historyPanel])
|
||||||
|
|
||||||
|
// 智能关闭逻辑:当有搜索文本但无匹配项时,延迟关闭面板
|
||||||
|
useEffect(() => {
|
||||||
|
const _searchText = searchText.replace(/^[/@]/, '')
|
||||||
|
|
||||||
|
// 清除之前的定时器(无论面板是否可见都要清理)
|
||||||
|
if (noMatchTimeoutRef.current) {
|
||||||
|
clearTimeout(noMatchTimeoutRef.current)
|
||||||
|
noMatchTimeoutRef.current = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 面板不可见时不设置新定时器
|
||||||
|
if (!ctx.isVisible) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有在有搜索文本但无匹配项时才设置延迟关闭
|
||||||
|
if (_searchText && _searchText.length > 0 && list.length === 0) {
|
||||||
|
noMatchTimeoutRef.current = setTimeout(() => {
|
||||||
|
ctx.close('no-matches')
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理函数
|
||||||
|
return () => {
|
||||||
|
if (noMatchTimeoutRef.current) {
|
||||||
|
clearTimeout(noMatchTimeoutRef.current)
|
||||||
|
noMatchTimeoutRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- ctx对象引用不稳定,使用具体属性避免过度重渲染
|
||||||
|
}, [ctx.isVisible, searchText, list.length, ctx.close])
|
||||||
|
|
||||||
const clearSearchText = useCallback(
|
const clearSearchText = useCallback(
|
||||||
(includeSymbol = false) => {
|
(includeSymbol = false) => {
|
||||||
const textArea = document.querySelector('.inputbar textarea') as HTMLTextAreaElement
|
const textArea = document.querySelector('.inputbar textarea') as HTMLTextAreaElement
|
||||||
@ -275,7 +310,7 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
|||||||
const newSearchText = textBeforeCursor.slice(lastSymbolIndex)
|
const newSearchText = textBeforeCursor.slice(lastSymbolIndex)
|
||||||
setSearchText(newSearchText)
|
setSearchText(newSearchText)
|
||||||
} else {
|
} else {
|
||||||
handleClose('delete-symbol')
|
ctx.close('delete-symbol')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -292,6 +292,7 @@ export const CLAUDE_SUPPORTED_WEBSEARCH_REGEX = new RegExp(
|
|||||||
// 模型类型到支持的reasoning_effort的映射表
|
// 模型类型到支持的reasoning_effort的映射表
|
||||||
export const MODEL_SUPPORTED_REASONING_EFFORT: ReasoningEffortConfig = {
|
export const MODEL_SUPPORTED_REASONING_EFFORT: ReasoningEffortConfig = {
|
||||||
default: ['low', 'medium', 'high'] as const,
|
default: ['low', 'medium', 'high'] as const,
|
||||||
|
o: ['low', 'medium', 'high'] as const,
|
||||||
gpt5: ['minimal', 'low', 'medium', 'high'] as const,
|
gpt5: ['minimal', 'low', 'medium', 'high'] as const,
|
||||||
grok: ['low', 'high'] as const,
|
grok: ['low', 'high'] as const,
|
||||||
gemini: ['low', 'medium', 'high', 'auto'] as const,
|
gemini: ['low', 'medium', 'high', 'auto'] as const,
|
||||||
@ -307,7 +308,8 @@ export const MODEL_SUPPORTED_REASONING_EFFORT: ReasoningEffortConfig = {
|
|||||||
// 模型类型到支持选项的映射表
|
// 模型类型到支持选项的映射表
|
||||||
export const MODEL_SUPPORTED_OPTIONS: ThinkingOptionConfig = {
|
export const MODEL_SUPPORTED_OPTIONS: ThinkingOptionConfig = {
|
||||||
default: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.default] as const,
|
default: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.default] as const,
|
||||||
gpt5: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.gpt5] as const,
|
o: MODEL_SUPPORTED_REASONING_EFFORT.o,
|
||||||
|
gpt5: [...MODEL_SUPPORTED_REASONING_EFFORT.gpt5] as const,
|
||||||
grok: MODEL_SUPPORTED_REASONING_EFFORT.grok,
|
grok: MODEL_SUPPORTED_REASONING_EFFORT.grok,
|
||||||
gemini: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini] as const,
|
gemini: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini] as const,
|
||||||
gemini_pro: MODEL_SUPPORTED_REASONING_EFFORT.gemini_pro,
|
gemini_pro: MODEL_SUPPORTED_REASONING_EFFORT.gemini_pro,
|
||||||
@ -320,28 +322,28 @@ export const MODEL_SUPPORTED_OPTIONS: ThinkingOptionConfig = {
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const getThinkModelType = (model: Model): ThinkingModelType => {
|
export const getThinkModelType = (model: Model): ThinkingModelType => {
|
||||||
|
let thinkingModelType: ThinkingModelType = 'default'
|
||||||
if (isGPT5SeriesModel(model)) {
|
if (isGPT5SeriesModel(model)) {
|
||||||
return 'gpt5'
|
thinkingModelType = 'gpt5'
|
||||||
}
|
} else if (isSupportedReasoningEffortOpenAIModel(model)) {
|
||||||
if (isSupportedThinkingTokenGeminiModel(model)) {
|
thinkingModelType = 'o'
|
||||||
|
} else if (isSupportedThinkingTokenGeminiModel(model)) {
|
||||||
if (GEMINI_FLASH_MODEL_REGEX.test(model.id)) {
|
if (GEMINI_FLASH_MODEL_REGEX.test(model.id)) {
|
||||||
return 'gemini'
|
thinkingModelType = 'gemini'
|
||||||
} else {
|
} else {
|
||||||
return 'gemini_pro'
|
thinkingModelType = 'gemini_pro'
|
||||||
}
|
}
|
||||||
}
|
} else if (isSupportedReasoningEffortGrokModel(model)) thinkingModelType = 'grok'
|
||||||
if (isSupportedReasoningEffortGrokModel(model)) return 'grok'
|
else if (isSupportedThinkingTokenQwenModel(model)) {
|
||||||
if (isSupportedThinkingTokenQwenModel(model)) {
|
|
||||||
if (isQwenAlwaysThinkModel(model)) {
|
if (isQwenAlwaysThinkModel(model)) {
|
||||||
return 'qwen_thinking'
|
thinkingModelType = 'qwen_thinking'
|
||||||
}
|
}
|
||||||
return 'qwen'
|
thinkingModelType = 'qwen'
|
||||||
}
|
} else if (isSupportedThinkingTokenDoubaoModel(model)) thinkingModelType = 'doubao'
|
||||||
if (isSupportedThinkingTokenDoubaoModel(model)) return 'doubao'
|
else if (isSupportedThinkingTokenHunyuanModel(model)) thinkingModelType = 'hunyuan'
|
||||||
if (isSupportedThinkingTokenHunyuanModel(model)) return 'hunyuan'
|
else if (isSupportedReasoningEffortPerplexityModel(model)) thinkingModelType = 'perplexity'
|
||||||
if (isSupportedReasoningEffortPerplexityModel(model)) return 'perplexity'
|
else if (isSupportedThinkingTokenZhipuModel(model)) thinkingModelType = 'zhipu'
|
||||||
if (isSupportedThinkingTokenZhipuModel(model)) return 'zhipu'
|
return thinkingModelType
|
||||||
return 'default'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFunctionCallingModel(model?: Model): boolean {
|
export function isFunctionCallingModel(model?: Model): boolean {
|
||||||
@ -2469,7 +2471,7 @@ export function isVisionModel(model: Model): boolean {
|
|||||||
|
|
||||||
export function isOpenAIReasoningModel(model: Model): boolean {
|
export function isOpenAIReasoningModel(model: Model): boolean {
|
||||||
const modelId = getLowerBaseModelName(model.id, '/')
|
const modelId = getLowerBaseModelName(model.id, '/')
|
||||||
return isSupportedReasoningEffortOpenAIModel(model) || modelId.includes('o1') || modelId.includes('gpt-5-chat')
|
return isSupportedReasoningEffortOpenAIModel(model) || modelId.includes('o1')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isOpenAILLMModel(model: Model): boolean {
|
export function isOpenAILLMModel(model: Model): boolean {
|
||||||
@ -2721,7 +2723,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 {
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -1246,7 +1246,7 @@ export const isSupportArrayContentProvider = (provider: Provider) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const NOT_SUPPORT_DEVELOPER_ROLE_PROVIDERS = ['poe'] as const satisfies SystemProviderId[]
|
const NOT_SUPPORT_DEVELOPER_ROLE_PROVIDERS = ['poe', 'qiniu'] as const satisfies SystemProviderId[]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断提供商是否支持 developer 作为 message role。 Only for OpenAI API.
|
* 判断提供商是否支持 developer 作为 message role。 Only for OpenAI API.
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import { RootState } from '@renderer/store'
|
import { RootState } from '@renderer/store'
|
||||||
|
import { syncPreprocessProvider as _syncPreprocessProvider } from '@renderer/store/knowledge'
|
||||||
import {
|
import {
|
||||||
setDefaultPreprocessProvider as _setDefaultPreprocessProvider,
|
setDefaultPreprocessProvider as _setDefaultPreprocessProvider,
|
||||||
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)
|
||||||
@ -17,7 +18,14 @@ export const usePreprocessProvider = (id: string) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
provider,
|
provider,
|
||||||
updateProvider: (updates: Partial<PreprocessProvider>) => dispatch(_updatePreprocessProvider({ id, ...updates }))
|
updateProvider: (updates: Partial<PreprocessProvider>) => {
|
||||||
|
const payload = { id, ...updates }
|
||||||
|
dispatch(_updatePreprocessProvider(payload))
|
||||||
|
// 将更新同步到所有知识库中的引用
|
||||||
|
if (updates.apiHost || updates.apiKey || updates.model) {
|
||||||
|
dispatch(_syncPreprocessProvider(payload))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -293,7 +293,7 @@ export const getFileFieldLabel = (key: string): string => {
|
|||||||
|
|
||||||
const builtInMcpDescriptionKeyMap = {
|
const builtInMcpDescriptionKeyMap = {
|
||||||
'@cherry/mcp-auto-install': 'settings.mcp.builtinServersDescriptions.mcp_auto_install',
|
'@cherry/mcp-auto-install': 'settings.mcp.builtinServersDescriptions.mcp_auto_install',
|
||||||
'@cherry/memory': 'settings.mcp.builtinServersDescriptions.mcp_auto_install',
|
'@cherry/memory': 'settings.mcp.builtinServersDescriptions.memory',
|
||||||
'@cherry/sequentialthinking': 'settings.mcp.builtinServersDescriptions.sequentialthinking',
|
'@cherry/sequentialthinking': 'settings.mcp.builtinServersDescriptions.sequentialthinking',
|
||||||
'@cherry/brave-search': 'settings.mcp.builtinServersDescriptions.brave_search',
|
'@cherry/brave-search': 'settings.mcp.builtinServersDescriptions.brave_search',
|
||||||
'@cherry/fetch': 'settings.mcp.builtinServersDescriptions.fetch',
|
'@cherry/fetch': 'settings.mcp.builtinServersDescriptions.fetch',
|
||||||
|
|||||||
@ -2715,6 +2715,17 @@
|
|||||||
"title": "Launch",
|
"title": "Launch",
|
||||||
"totray": "Minimize to Tray on Launch"
|
"totray": "Minimize to Tray on Launch"
|
||||||
},
|
},
|
||||||
|
"math": {
|
||||||
|
"engine": {
|
||||||
|
"label": "Math engine",
|
||||||
|
"none": "None"
|
||||||
|
},
|
||||||
|
"single_dollar": {
|
||||||
|
"label": "Enable $...$",
|
||||||
|
"tip": "Render math equations quoted by single dollar signs $...$. Default is enabled."
|
||||||
|
},
|
||||||
|
"title": "Math Settings"
|
||||||
|
},
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
@ -2945,10 +2956,6 @@
|
|||||||
"title": "Input Settings"
|
"title": "Input Settings"
|
||||||
},
|
},
|
||||||
"markdown_rendering_input_message": "Markdown render input message",
|
"markdown_rendering_input_message": "Markdown render input message",
|
||||||
"math_engine": {
|
|
||||||
"label": "Math engine",
|
|
||||||
"none": "None"
|
|
||||||
},
|
|
||||||
"metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",
|
"metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",
|
||||||
"model": {
|
"model": {
|
||||||
"title": "Model Settings"
|
"title": "Model Settings"
|
||||||
@ -2960,6 +2967,7 @@
|
|||||||
"none": "None"
|
"none": "None"
|
||||||
},
|
},
|
||||||
"prompt": "Show prompt",
|
"prompt": "Show prompt",
|
||||||
|
"show_message_outline": "Show message outline",
|
||||||
"title": "Message Settings",
|
"title": "Message Settings",
|
||||||
"use_serif_font": "Use serif font"
|
"use_serif_font": "Use serif font"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2715,6 +2715,17 @@
|
|||||||
"title": "起動",
|
"title": "起動",
|
||||||
"totray": "起動時にトレイに最小化"
|
"totray": "起動時にトレイに最小化"
|
||||||
},
|
},
|
||||||
|
"math": {
|
||||||
|
"engine": {
|
||||||
|
"label": "数式エンジン",
|
||||||
|
"none": "なし"
|
||||||
|
},
|
||||||
|
"single_dollar": {
|
||||||
|
"label": "$...$ を有効にする",
|
||||||
|
"tip": "単一のドル記号 $...$ で囲まれた数式をレンダリングします。デフォルトで有効です。"
|
||||||
|
},
|
||||||
|
"title": "数式設定"
|
||||||
|
},
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"actions": "操作",
|
"actions": "操作",
|
||||||
"active": "有効",
|
"active": "有効",
|
||||||
@ -2945,10 +2956,6 @@
|
|||||||
"title": "入力設定"
|
"title": "入力設定"
|
||||||
},
|
},
|
||||||
"markdown_rendering_input_message": "Markdownで入力メッセージをレンダリング",
|
"markdown_rendering_input_message": "Markdownで入力メッセージをレンダリング",
|
||||||
"math_engine": {
|
|
||||||
"label": "数式エンジン",
|
|
||||||
"none": "なし"
|
|
||||||
},
|
|
||||||
"metrics": "最初のトークンまでの時間 {{time_first_token_millsec}}ms | トークン速度 {{token_speed}} tok/sec",
|
"metrics": "最初のトークンまでの時間 {{time_first_token_millsec}}ms | トークン速度 {{token_speed}} tok/sec",
|
||||||
"model": {
|
"model": {
|
||||||
"title": "モデル設定"
|
"title": "モデル設定"
|
||||||
@ -2960,6 +2967,7 @@
|
|||||||
"none": "表示しない"
|
"none": "表示しない"
|
||||||
},
|
},
|
||||||
"prompt": "プロンプト表示",
|
"prompt": "プロンプト表示",
|
||||||
|
"show_message_outline": "メッセージの概要を表示します",
|
||||||
"title": "メッセージ設定",
|
"title": "メッセージ設定",
|
||||||
"use_serif_font": "セリフフォントを使用"
|
"use_serif_font": "セリフフォントを使用"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2715,6 +2715,17 @@
|
|||||||
"title": "Запуск",
|
"title": "Запуск",
|
||||||
"totray": "Свернуть в трей при запуске"
|
"totray": "Свернуть в трей при запуске"
|
||||||
},
|
},
|
||||||
|
"math": {
|
||||||
|
"engine": {
|
||||||
|
"label": "Математический движок",
|
||||||
|
"none": "Нет"
|
||||||
|
},
|
||||||
|
"single_dollar": {
|
||||||
|
"label": "Включить $...$",
|
||||||
|
"tip": "Отображать математические формулы, заключенные в одиночные символы доллара $...$. По умолчанию включено."
|
||||||
|
},
|
||||||
|
"title": "Настройки математических формул"
|
||||||
|
},
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"actions": "Действия",
|
"actions": "Действия",
|
||||||
"active": "Активен",
|
"active": "Активен",
|
||||||
@ -2945,10 +2956,6 @@
|
|||||||
"title": "Настройки ввода"
|
"title": "Настройки ввода"
|
||||||
},
|
},
|
||||||
"markdown_rendering_input_message": "Отображение ввода в формате Markdown",
|
"markdown_rendering_input_message": "Отображение ввода в формате Markdown",
|
||||||
"math_engine": {
|
|
||||||
"label": "Математический движок",
|
|
||||||
"none": "Нет"
|
|
||||||
},
|
|
||||||
"metrics": "{{time_first_token_millsec}}ms до первого токена | {{token_speed}} tok/sec",
|
"metrics": "{{time_first_token_millsec}}ms до первого токена | {{token_speed}} tok/sec",
|
||||||
"model": {
|
"model": {
|
||||||
"title": "Настройки модели"
|
"title": "Настройки модели"
|
||||||
@ -2960,6 +2967,7 @@
|
|||||||
"none": "Не показывать"
|
"none": "Не показывать"
|
||||||
},
|
},
|
||||||
"prompt": "Показывать подсказки",
|
"prompt": "Показывать подсказки",
|
||||||
|
"show_message_outline": "Показать наброски сообщения",
|
||||||
"title": "Настройки сообщений",
|
"title": "Настройки сообщений",
|
||||||
"use_serif_font": "Использовать serif шрифт"
|
"use_serif_font": "Использовать serif шрифт"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2715,6 +2715,17 @@
|
|||||||
"title": "启动",
|
"title": "启动",
|
||||||
"totray": "启动时最小化到托盘"
|
"totray": "启动时最小化到托盘"
|
||||||
},
|
},
|
||||||
|
"math": {
|
||||||
|
"engine": {
|
||||||
|
"label": "数学公式引擎",
|
||||||
|
"none": "无"
|
||||||
|
},
|
||||||
|
"single_dollar": {
|
||||||
|
"label": "启用 $...$",
|
||||||
|
"tip": "渲染单个美元符号 $...$ 包裹的数学公式,默认启用。"
|
||||||
|
},
|
||||||
|
"title": "数学公式设置"
|
||||||
|
},
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"actions": "操作",
|
"actions": "操作",
|
||||||
"active": "启用",
|
"active": "启用",
|
||||||
@ -2945,10 +2956,6 @@
|
|||||||
"title": "输入设置"
|
"title": "输入设置"
|
||||||
},
|
},
|
||||||
"markdown_rendering_input_message": "Markdown 渲染输入消息",
|
"markdown_rendering_input_message": "Markdown 渲染输入消息",
|
||||||
"math_engine": {
|
|
||||||
"label": "数学公式引擎",
|
|
||||||
"none": "无"
|
|
||||||
},
|
|
||||||
"metrics": "首字时延 {{time_first_token_millsec}} ms | 每秒 {{token_speed}} tokens",
|
"metrics": "首字时延 {{time_first_token_millsec}} ms | 每秒 {{token_speed}} tokens",
|
||||||
"model": {
|
"model": {
|
||||||
"title": "模型设置"
|
"title": "模型设置"
|
||||||
@ -2960,6 +2967,7 @@
|
|||||||
"none": "不显示"
|
"none": "不显示"
|
||||||
},
|
},
|
||||||
"prompt": "显示提示词",
|
"prompt": "显示提示词",
|
||||||
|
"show_message_outline": "显示消息大纲",
|
||||||
"title": "消息设置",
|
"title": "消息设置",
|
||||||
"use_serif_font": "使用衬线字体"
|
"use_serif_font": "使用衬线字体"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2715,6 +2715,17 @@
|
|||||||
"title": "啟動",
|
"title": "啟動",
|
||||||
"totray": "啟動時最小化到系统匣"
|
"totray": "啟動時最小化到系统匣"
|
||||||
},
|
},
|
||||||
|
"math": {
|
||||||
|
"engine": {
|
||||||
|
"label": "數學公式引擎",
|
||||||
|
"none": "無"
|
||||||
|
},
|
||||||
|
"single_dollar": {
|
||||||
|
"label": "啟用 $...$",
|
||||||
|
"tip": "渲染單個美元符號 $...$ 包裹的數學公式,默認啟用。"
|
||||||
|
},
|
||||||
|
"title": "數學公式設定"
|
||||||
|
},
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"actions": "操作",
|
"actions": "操作",
|
||||||
"active": "啟用",
|
"active": "啟用",
|
||||||
@ -2945,10 +2956,6 @@
|
|||||||
"title": "輸入設定"
|
"title": "輸入設定"
|
||||||
},
|
},
|
||||||
"markdown_rendering_input_message": "Markdown 渲染輸入訊息",
|
"markdown_rendering_input_message": "Markdown 渲染輸入訊息",
|
||||||
"math_engine": {
|
|
||||||
"label": "數學公式引擎",
|
|
||||||
"none": "無"
|
|
||||||
},
|
|
||||||
"metrics": "首字延遲 {{time_first_token_millsec}} ms | 每秒 {{token_speed}} tokens",
|
"metrics": "首字延遲 {{time_first_token_millsec}} ms | 每秒 {{token_speed}} tokens",
|
||||||
"model": {
|
"model": {
|
||||||
"title": "模型設定"
|
"title": "模型設定"
|
||||||
@ -2960,6 +2967,7 @@
|
|||||||
"none": "不顯示"
|
"none": "不顯示"
|
||||||
},
|
},
|
||||||
"prompt": "提示詞顯示",
|
"prompt": "提示詞顯示",
|
||||||
|
"show_message_outline": "顯示消息大綱",
|
||||||
"title": "訊息設定",
|
"title": "訊息設定",
|
||||||
"use_serif_font": "使用襯線字型"
|
"use_serif_font": "使用襯線字型"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { HStack } from '@renderer/components/Layout'
|
|||||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||||
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
|
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
|
||||||
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { getAssistantById } from '@renderer/services/AssistantService'
|
import { getAssistantById } from '@renderer/services/AssistantService'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import { isGenerating, locateToMessage } from '@renderer/services/MessagesService'
|
import { isGenerating, locateToMessage } from '@renderer/services/MessagesService'
|
||||||
@ -10,6 +11,7 @@ import NavigationService from '@renderer/services/NavigationService'
|
|||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
||||||
import { Topic } from '@renderer/types'
|
import { Topic } from '@renderer/types'
|
||||||
|
import { classNames } from '@renderer/utils'
|
||||||
import { Button, Divider, Empty } from 'antd'
|
import { Button, Divider, Empty } from 'antd'
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
import { Forward } from 'lucide-react'
|
import { Forward } from 'lucide-react'
|
||||||
@ -26,6 +28,7 @@ const TopicMessages: FC<Props> = ({ topic, ...props }) => {
|
|||||||
const navigate = NavigationService.navigate!
|
const navigate = NavigationService.navigate!
|
||||||
const { handleScroll, containerRef } = useScrollPosition('TopicMessages')
|
const { handleScroll, containerRef } = useScrollPosition('TopicMessages')
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const { messageStyle } = useSettings()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
topic && dispatch(loadTopicMessagesThunk(topic.id))
|
topic && dispatch(loadTopicMessagesThunk(topic.id))
|
||||||
@ -48,9 +51,9 @@ const TopicMessages: FC<Props> = ({ topic, ...props }) => {
|
|||||||
return (
|
return (
|
||||||
<MessageEditingProvider>
|
<MessageEditingProvider>
|
||||||
<MessagesContainer {...props} ref={containerRef} onScroll={handleScroll}>
|
<MessagesContainer {...props} ref={containerRef} onScroll={handleScroll}>
|
||||||
<ContainerWrapper>
|
<ContainerWrapper className={messageStyle}>
|
||||||
{topic?.messages.map((message) => (
|
{topic?.messages.map((message) => (
|
||||||
<div key={message.id} style={{ position: 'relative' }}>
|
<MessageWrapper key={message.id} className={classNames([messageStyle, message.role])}>
|
||||||
<MessageItem message={message} topic={topic} hideMenuBar={true} />
|
<MessageItem message={message} topic={topic} hideMenuBar={true} />
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
@ -60,7 +63,7 @@ const TopicMessages: FC<Props> = ({ topic, ...props }) => {
|
|||||||
icon={<Forward size={16} />}
|
icon={<Forward size={16} />}
|
||||||
/>
|
/>
|
||||||
<Divider style={{ margin: '8px auto 15px' }} variant="dashed" />
|
<Divider style={{ margin: '8px auto 15px' }} variant="dashed" />
|
||||||
</div>
|
</MessageWrapper>
|
||||||
))}
|
))}
|
||||||
{isEmpty && <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}
|
{isEmpty && <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}
|
||||||
{!isEmpty && (
|
{!isEmpty && (
|
||||||
@ -91,4 +94,11 @@ const ContainerWrapper = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const MessageWrapper = styled.div`
|
||||||
|
position: relative;
|
||||||
|
&.bubble.user {
|
||||||
|
padding-top: 26px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export default TopicMessages
|
export default TopicMessages
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const GenerateImageButton: FC<Props> = ({ model, ToolbarButton, assistant, onEna
|
|||||||
mouseLeaveDelay={0}
|
mouseLeaveDelay={0}
|
||||||
arrow>
|
arrow>
|
||||||
<ToolbarButton type="text" disabled={!isGenerateImageModel(model)} onClick={onEnableGenerateImage}>
|
<ToolbarButton type="text" disabled={!isGenerateImageModel(model)} onClick={onEnableGenerateImage}>
|
||||||
<Image size={18} color={assistant.enableGenerateImage ? 'var(--color-link)' : 'var(--color-icon)'} />
|
<Image size={18} color={assistant.enableGenerateImage ? 'var(--color-primary)' : 'var(--color-icon)'} />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -397,6 +397,7 @@ const InputbarTools = ({
|
|||||||
ToolbarButton={ToolbarButton}
|
ToolbarButton={ToolbarButton}
|
||||||
couldMentionNotVisionModel={couldMentionNotVisionModel}
|
couldMentionNotVisionModel={couldMentionNotVisionModel}
|
||||||
files={files}
|
files={files}
|
||||||
|
setText={setText}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@ -87,7 +87,10 @@ const KnowledgeBaseButton: FC<Props> = ({ ref, selectedBases, onSelect, disabled
|
|||||||
return (
|
return (
|
||||||
<Tooltip placement="top" title={t('chat.input.knowledge_base')} mouseLeaveDelay={0} arrow>
|
<Tooltip placement="top" title={t('chat.input.knowledge_base')} mouseLeaveDelay={0} arrow>
|
||||||
<ToolbarButton type="text" onClick={handleOpenQuickPanel} disabled={disabled}>
|
<ToolbarButton type="text" onClick={handleOpenQuickPanel} disabled={disabled}>
|
||||||
<FileSearch size={18} />
|
<FileSearch
|
||||||
|
size={18}
|
||||||
|
color={selectedBases && selectedBases.length > 0 ? 'var(--color-primary)' : 'var(--color-icon)'}
|
||||||
|
/>
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -27,6 +27,7 @@ interface Props {
|
|||||||
couldMentionNotVisionModel: boolean
|
couldMentionNotVisionModel: boolean
|
||||||
files: FileType[]
|
files: FileType[]
|
||||||
ToolbarButton: any
|
ToolbarButton: any
|
||||||
|
setText: React.Dispatch<React.SetStateAction<string>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const MentionModelsButton: FC<Props> = ({
|
const MentionModelsButton: FC<Props> = ({
|
||||||
@ -35,13 +36,17 @@ const MentionModelsButton: FC<Props> = ({
|
|||||||
onMentionModel,
|
onMentionModel,
|
||||||
couldMentionNotVisionModel,
|
couldMentionNotVisionModel,
|
||||||
files,
|
files,
|
||||||
ToolbarButton
|
ToolbarButton,
|
||||||
|
setText
|
||||||
}) => {
|
}) => {
|
||||||
const { providers } = useProviders()
|
const { providers } = useProviders()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const quickPanel = useQuickPanel()
|
const quickPanel = useQuickPanel()
|
||||||
|
|
||||||
|
// 记录是否有模型被选择的动作发生
|
||||||
|
const hasModelActionRef = useRef<boolean>(false)
|
||||||
|
|
||||||
const pinnedModels = useLiveQuery(
|
const pinnedModels = useLiveQuery(
|
||||||
async () => {
|
async () => {
|
||||||
const setting = await db.settings.get('pinned:models')
|
const setting = await db.settings.get('pinned:models')
|
||||||
@ -74,7 +79,10 @@ const MentionModelsButton: FC<Props> = ({
|
|||||||
</Avatar>
|
</Avatar>
|
||||||
),
|
),
|
||||||
filterText: getFancyProviderName(p) + m.name,
|
filterText: getFancyProviderName(p) + m.name,
|
||||||
action: () => onMentionModel(m),
|
action: () => {
|
||||||
|
hasModelActionRef.current = true // 标记有模型动作发生
|
||||||
|
onMentionModel(m)
|
||||||
|
},
|
||||||
isSelected: mentionedModels.some((selected) => getModelUniqId(selected) === getModelUniqId(m))
|
isSelected: mentionedModels.some((selected) => getModelUniqId(selected) === getModelUniqId(m))
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
@ -107,7 +115,10 @@ const MentionModelsButton: FC<Props> = ({
|
|||||||
</Avatar>
|
</Avatar>
|
||||||
),
|
),
|
||||||
filterText: getFancyProviderName(p) + m.name,
|
filterText: getFancyProviderName(p) + m.name,
|
||||||
action: () => onMentionModel(m),
|
action: () => {
|
||||||
|
hasModelActionRef.current = true // 标记有模型动作发生
|
||||||
|
onMentionModel(m)
|
||||||
|
},
|
||||||
isSelected: mentionedModels.some((selected) => getModelUniqId(selected) === getModelUniqId(m))
|
isSelected: mentionedModels.some((selected) => getModelUniqId(selected) === getModelUniqId(m))
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -127,6 +138,9 @@ const MentionModelsButton: FC<Props> = ({
|
|||||||
}, [pinnedModels, providers, t, couldMentionNotVisionModel, mentionedModels, onMentionModel, navigate])
|
}, [pinnedModels, providers, t, couldMentionNotVisionModel, mentionedModels, onMentionModel, navigate])
|
||||||
|
|
||||||
const openQuickPanel = useCallback(() => {
|
const openQuickPanel = useCallback(() => {
|
||||||
|
// 重置模型动作标记
|
||||||
|
hasModelActionRef.current = false
|
||||||
|
|
||||||
quickPanel.open({
|
quickPanel.open({
|
||||||
title: t('agents.edit.model.select.title'),
|
title: t('agents.edit.model.select.title'),
|
||||||
list: modelItems,
|
list: modelItems,
|
||||||
@ -134,9 +148,25 @@ const MentionModelsButton: FC<Props> = ({
|
|||||||
multiple: true,
|
multiple: true,
|
||||||
afterAction({ item }) {
|
afterAction({ item }) {
|
||||||
item.isSelected = !item.isSelected
|
item.isSelected = !item.isSelected
|
||||||
|
},
|
||||||
|
onClose({ action }) {
|
||||||
|
// ESC或Backspace关闭时的特殊处理
|
||||||
|
if (action === 'esc' || action === 'delete-symbol') {
|
||||||
|
// 如果有模型选择动作发生,删除@字符
|
||||||
|
if (hasModelActionRef.current) {
|
||||||
|
// 使用React的setText来更新状态,而不是直接操作DOM
|
||||||
|
setText((currentText) => {
|
||||||
|
const lastAtIndex = currentText.lastIndexOf('@')
|
||||||
|
if (lastAtIndex !== -1) {
|
||||||
|
return currentText.slice(0, lastAtIndex) + currentText.slice(lastAtIndex + 1)
|
||||||
|
}
|
||||||
|
return currentText
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [modelItems, quickPanel, t])
|
}, [modelItems, quickPanel, t, setText])
|
||||||
|
|
||||||
const handleOpenQuickPanel = useCallback(() => {
|
const handleOpenQuickPanel = useCallback(() => {
|
||||||
if (quickPanel.isVisible && quickPanel.symbol === '@') {
|
if (quickPanel.isVisible && quickPanel.symbol === '@') {
|
||||||
@ -165,7 +195,7 @@ const MentionModelsButton: FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<Tooltip placement="top" title={t('agents.edit.model.select.title')} mouseLeaveDelay={0} arrow>
|
<Tooltip placement="top" title={t('agents.edit.model.select.title')} mouseLeaveDelay={0} arrow>
|
||||||
<ToolbarButton type="text" onClick={handleOpenQuickPanel}>
|
<ToolbarButton type="text" onClick={handleOpenQuickPanel}>
|
||||||
<AtSign size={18} />
|
<AtSign size={18} color={mentionedModels.length > 0 ? 'var(--color-primary)' : 'var(--color-icon)'} />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -33,7 +33,7 @@ const UrlContextButton: FC<Props> = ({ assistant, ToolbarButton }) => {
|
|||||||
<Link
|
<Link
|
||||||
size={18}
|
size={18}
|
||||||
style={{
|
style={{
|
||||||
color: assistant.enableUrlContext ? 'var(--color-link)' : 'var(--color-icon)'
|
color: assistant.enableUrlContext ? 'var(--color-primary)' : 'var(--color-icon)'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import { Pluggable } from 'unified'
|
|||||||
|
|
||||||
import CodeBlock from './CodeBlock'
|
import CodeBlock from './CodeBlock'
|
||||||
import Link from './Link'
|
import Link from './Link'
|
||||||
|
import rehypeHeadingIds from './plugins/rehypeHeadingIds'
|
||||||
import remarkDisableConstructs from './plugins/remarkDisableConstructs'
|
import remarkDisableConstructs from './plugins/remarkDisableConstructs'
|
||||||
import Table from './Table'
|
import Table from './Table'
|
||||||
|
|
||||||
@ -45,7 +46,7 @@ interface Props {
|
|||||||
|
|
||||||
const Markdown: FC<Props> = ({ block, postProcess }) => {
|
const Markdown: FC<Props> = ({ block, postProcess }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { mathEngine } = useSettings()
|
const { mathEngine, mathEnableSingleDollar } = useSettings()
|
||||||
|
|
||||||
const isTrulyDone = 'status' in block && block.status === 'success'
|
const isTrulyDone = 'status' in block && block.status === 'success'
|
||||||
const [displayedContent, setDisplayedContent] = useState(postProcess ? postProcess(block.content) : block.content)
|
const [displayedContent, setDisplayedContent] = useState(postProcess ? postProcess(block.content) : block.content)
|
||||||
@ -97,10 +98,10 @@ const Markdown: FC<Props> = ({ block, postProcess }) => {
|
|||||||
remarkDisableConstructs(['codeIndented'])
|
remarkDisableConstructs(['codeIndented'])
|
||||||
]
|
]
|
||||||
if (mathEngine !== 'none') {
|
if (mathEngine !== 'none') {
|
||||||
plugins.push(remarkMath)
|
plugins.push([remarkMath, { singleDollarTextMath: mathEnableSingleDollar }])
|
||||||
}
|
}
|
||||||
return plugins
|
return plugins
|
||||||
}, [mathEngine])
|
}, [mathEngine, mathEnableSingleDollar])
|
||||||
|
|
||||||
const messageContent = useMemo(() => {
|
const messageContent = useMemo(() => {
|
||||||
if ('status' in block && block.status === 'paused' && isEmpty(block.content)) {
|
if ('status' in block && block.status === 'paused' && isEmpty(block.content)) {
|
||||||
@ -110,17 +111,18 @@ const Markdown: FC<Props> = ({ block, postProcess }) => {
|
|||||||
}, [block, displayedContent, t])
|
}, [block, displayedContent, t])
|
||||||
|
|
||||||
const rehypePlugins = useMemo(() => {
|
const rehypePlugins = useMemo(() => {
|
||||||
const plugins: any[] = []
|
const plugins: Pluggable[] = []
|
||||||
if (ALLOWED_ELEMENTS.test(messageContent)) {
|
if (ALLOWED_ELEMENTS.test(messageContent)) {
|
||||||
plugins.push(rehypeRaw)
|
plugins.push(rehypeRaw)
|
||||||
}
|
}
|
||||||
|
plugins.push([rehypeHeadingIds, { prefix: `heading-${block.id}` }])
|
||||||
if (mathEngine === 'KaTeX') {
|
if (mathEngine === 'KaTeX') {
|
||||||
plugins.push(rehypeKatex as any)
|
plugins.push(rehypeKatex)
|
||||||
} else if (mathEngine === 'MathJax') {
|
} else if (mathEngine === 'MathJax') {
|
||||||
plugins.push(rehypeMathjax as any)
|
plugins.push(rehypeMathjax)
|
||||||
}
|
}
|
||||||
return plugins
|
return plugins
|
||||||
}, [mathEngine, messageContent])
|
}, [mathEngine, messageContent, block.id])
|
||||||
|
|
||||||
const onSaveCodeBlock = useCallback(
|
const onSaveCodeBlock = useCallback(
|
||||||
(id: string, newContent: string) => {
|
(id: string, newContent: string) => {
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { CopyIcon } from '@renderer/components/Icons'
|
import { CopyIcon } from '@renderer/components/Icons'
|
||||||
|
import { useTemporaryValue } from '@renderer/hooks/useTemporaryValue'
|
||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
||||||
import { Tooltip } from 'antd'
|
import { Tooltip } from 'antd'
|
||||||
import { Check } from 'lucide-react'
|
import { Check } from 'lucide-react'
|
||||||
import React, { memo, useCallback, useState } from 'react'
|
import React, { memo, useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -18,7 +19,7 @@ interface Props {
|
|||||||
*/
|
*/
|
||||||
const Table: React.FC<Props> = ({ children, node, blockId }) => {
|
const Table: React.FC<Props> = ({ children, node, blockId }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useTemporaryValue(false, 2000)
|
||||||
|
|
||||||
const handleCopyTable = useCallback(() => {
|
const handleCopyTable = useCallback(() => {
|
||||||
const tableMarkdown = extractTableMarkdown(blockId ?? '', node?.position)
|
const tableMarkdown = extractTableMarkdown(blockId ?? '', node?.position)
|
||||||
@ -28,12 +29,11 @@ const Table: React.FC<Props> = ({ children, node, blockId }) => {
|
|||||||
.writeText(tableMarkdown)
|
.writeText(tableMarkdown)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setCopied(true)
|
setCopied(true)
|
||||||
setTimeout(() => setCopied(false), 2000)
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
window.message?.error({ content: `${t('message.copy.failed')}: ${error}`, key: 'copy-table-error' })
|
window.message?.error({ content: `${t('message.copy.failed')}: ${error}`, key: 'copy-table-error' })
|
||||||
})
|
})
|
||||||
}, [node, blockId, t])
|
}, [blockId, node?.position, setCopied, t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableWrapper className="table-wrapper">
|
<TableWrapper className="table-wrapper">
|
||||||
|
|||||||
@ -144,7 +144,7 @@ describe('Markdown', () => {
|
|||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
|
||||||
// Default settings
|
// Default settings
|
||||||
mockUseSettings.mockReturnValue({ mathEngine: 'KaTeX' })
|
mockUseSettings.mockReturnValue({ mathEngine: 'KaTeX', mathEnableSingleDollar: true })
|
||||||
mockUseTranslation.mockReturnValue({
|
mockUseTranslation.mockReturnValue({
|
||||||
t: (key: string) => (key === 'message.chat.completion.paused' ? 'Paused' : key)
|
t: (key: string) => (key === 'message.chat.completion.paused' ? 'Paused' : key)
|
||||||
})
|
})
|
||||||
@ -270,7 +270,7 @@ describe('Markdown', () => {
|
|||||||
|
|
||||||
describe('math engine configuration', () => {
|
describe('math engine configuration', () => {
|
||||||
it('should configure KaTeX when mathEngine is KaTeX', () => {
|
it('should configure KaTeX when mathEngine is KaTeX', () => {
|
||||||
mockUseSettings.mockReturnValue({ mathEngine: 'KaTeX' })
|
mockUseSettings.mockReturnValue({ mathEngine: 'KaTeX', mathEnableSingleDollar: true })
|
||||||
|
|
||||||
render(<Markdown block={createMainTextBlock()} />)
|
render(<Markdown block={createMainTextBlock()} />)
|
||||||
|
|
||||||
@ -279,7 +279,7 @@ describe('Markdown', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should configure MathJax when mathEngine is MathJax', () => {
|
it('should configure MathJax when mathEngine is MathJax', () => {
|
||||||
mockUseSettings.mockReturnValue({ mathEngine: 'MathJax' })
|
mockUseSettings.mockReturnValue({ mathEngine: 'MathJax', mathEnableSingleDollar: true })
|
||||||
|
|
||||||
render(<Markdown block={createMainTextBlock()} />)
|
render(<Markdown block={createMainTextBlock()} />)
|
||||||
|
|
||||||
@ -288,7 +288,7 @@ describe('Markdown', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should not load math plugins when mathEngine is none', () => {
|
it('should not load math plugins when mathEngine is none', () => {
|
||||||
mockUseSettings.mockReturnValue({ mathEngine: 'none' })
|
mockUseSettings.mockReturnValue({ mathEngine: 'none', mathEnableSingleDollar: true })
|
||||||
|
|
||||||
render(<Markdown block={createMainTextBlock()} />)
|
render(<Markdown block={createMainTextBlock()} />)
|
||||||
|
|
||||||
@ -384,12 +384,12 @@ describe('Markdown', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should re-render when math engine changes', () => {
|
it('should re-render when math engine changes', () => {
|
||||||
mockUseSettings.mockReturnValue({ mathEngine: 'KaTeX' })
|
mockUseSettings.mockReturnValue({ mathEngine: 'KaTeX', mathEnableSingleDollar: true })
|
||||||
const { rerender } = render(<Markdown block={createMainTextBlock()} />)
|
const { rerender } = render(<Markdown block={createMainTextBlock()} />)
|
||||||
|
|
||||||
expect(screen.getByTestId('markdown-content')).toBeInTheDocument()
|
expect(screen.getByTestId('markdown-content')).toBeInTheDocument()
|
||||||
|
|
||||||
mockUseSettings.mockReturnValue({ mathEngine: 'MathJax' })
|
mockUseSettings.mockReturnValue({ mathEngine: 'MathJax', mathEnableSingleDollar: true })
|
||||||
rerender(<Markdown block={createMainTextBlock()} />)
|
rerender(<Markdown block={createMainTextBlock()} />)
|
||||||
|
|
||||||
// Should still render correctly with new math engine
|
// Should still render correctly with new math engine
|
||||||
|
|||||||
@ -0,0 +1,70 @@
|
|||||||
|
import type { Element, Node, Root, Text } from 'hast'
|
||||||
|
import { visit } from 'unist-util-visit'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于 GitHub 风格的标题 slug 生成器(去重逻辑)
|
||||||
|
* - 小写
|
||||||
|
* - 去除前后空白
|
||||||
|
* - 移除部分标点
|
||||||
|
* - 将空白与非字母数字字符合并为单个 '-'
|
||||||
|
* - 多次出现的相同 slug 加上递增后缀(-1, -2...)
|
||||||
|
*/
|
||||||
|
export function createSlugger() {
|
||||||
|
const seen = new Map<string, number>()
|
||||||
|
const normalize = (text: string): string => {
|
||||||
|
const slug = (text || 'section')
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
// 移除常见分隔符和标点
|
||||||
|
.replace(/[\u200B-\u200D\uFEFF]/g, '') // 零宽字符
|
||||||
|
.replace(/["'`(){}[\]:;!?.,]/g, '')
|
||||||
|
// 将空白和非字母数字字符转换为 '-'
|
||||||
|
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
|
||||||
|
// 合并多余的 '-'
|
||||||
|
.replace(/-{2,}/g, '-')
|
||||||
|
// 去除首尾 '-'
|
||||||
|
.replace(/^-|-$/g, '')
|
||||||
|
|
||||||
|
return slug
|
||||||
|
}
|
||||||
|
|
||||||
|
const slug = (text: string): string => {
|
||||||
|
const base = normalize(text)
|
||||||
|
const count = seen.get(base) || 0
|
||||||
|
seen.set(base, count + 1)
|
||||||
|
return `${base}-${count}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return { slug }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractTextFromNode(node: Node | Text | Element | null | undefined): string {
|
||||||
|
if (!node) return ''
|
||||||
|
|
||||||
|
if (typeof (node as Text).value === 'string') {
|
||||||
|
return (node as Text).value
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((node as Element).children?.length) {
|
||||||
|
return (node as Element).children.map(extractTextFromNode).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function rehypeHeadingIds(options?: { prefix?: string }) {
|
||||||
|
return (tree: Root) => {
|
||||||
|
const slugger = createSlugger()
|
||||||
|
const prefix = options?.prefix ? `${options.prefix}--` : ''
|
||||||
|
visit(tree, 'element', (node) => {
|
||||||
|
if (!node || typeof node.tagName !== 'string') return
|
||||||
|
const tag = node.tagName.toLowerCase()
|
||||||
|
if (!/^h[1-6]$/.test(tag)) return
|
||||||
|
|
||||||
|
const text = extractTextFromNode(node)
|
||||||
|
const id = prefix + slugger.slug(text)
|
||||||
|
node.properties = node.properties || {}
|
||||||
|
if (!node.properties.id) node.properties.id = id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,13 +1,14 @@
|
|||||||
import ContextMenu from '@renderer/components/ContextMenu'
|
import ContextMenu from '@renderer/components/ContextMenu'
|
||||||
import Favicon from '@renderer/components/Icons/FallbackFavicon'
|
import Favicon from '@renderer/components/Icons/FallbackFavicon'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
|
import { useTemporaryValue } from '@renderer/hooks/useTemporaryValue'
|
||||||
import { Citation } from '@renderer/types'
|
import { Citation } from '@renderer/types'
|
||||||
import { fetchWebContent } from '@renderer/utils/fetch'
|
import { fetchWebContent } from '@renderer/utils/fetch'
|
||||||
import { cleanMarkdownContent } from '@renderer/utils/formats'
|
import { cleanMarkdownContent } from '@renderer/utils/formats'
|
||||||
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'
|
||||||
import { Button, message, Popover, Skeleton } from 'antd'
|
import { Button, message, Popover, Skeleton } from 'antd'
|
||||||
import { Check, Copy, FileSearch } from 'lucide-react'
|
import { Check, Copy, FileSearch } from 'lucide-react'
|
||||||
import React, { useState } from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -116,7 +117,7 @@ const handleLinkClick = (url: string, event: React.MouseEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CopyButton: React.FC<{ content: string }> = ({ content }) => {
|
const CopyButton: React.FC<{ content: string }> = ({ content }) => {
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useTemporaryValue(false, 2000)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const handleCopy = () => {
|
const handleCopy = () => {
|
||||||
@ -126,7 +127,6 @@ const CopyButton: React.FC<{ content: string }> = ({ content }) => {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
setCopied(true)
|
setCopied(true)
|
||||||
window.message.success(t('common.copied'))
|
window.message.success(t('common.copied'))
|
||||||
setTimeout(() => setCopied(false), 2000)
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
message.error(t('message.copy.failed'))
|
message.error(t('message.copy.failed'))
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import MessageEditor from './MessageEditor'
|
|||||||
import MessageErrorBoundary from './MessageErrorBoundary'
|
import MessageErrorBoundary from './MessageErrorBoundary'
|
||||||
import MessageHeader from './MessageHeader'
|
import MessageHeader from './MessageHeader'
|
||||||
import MessageMenubar from './MessageMenubar'
|
import MessageMenubar from './MessageMenubar'
|
||||||
|
import MessageOutline from './MessageOutline'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: Message
|
message: Message
|
||||||
@ -66,7 +67,7 @@ const MessageItem: FC<Props> = ({
|
|||||||
const { assistant, setModel } = useAssistant(message.assistantId)
|
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||||
const { isMultiSelectMode } = useChatContext(topic)
|
const { isMultiSelectMode } = useChatContext(topic)
|
||||||
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
|
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
|
||||||
const { messageFont, fontSize, messageStyle } = useSettings()
|
const { messageFont, fontSize, messageStyle, showMessageOutline } = useSettings()
|
||||||
const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic)
|
const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic)
|
||||||
const messageContainerRef = useRef<HTMLDivElement>(null)
|
const messageContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const { editingMessageId, stopEditing } = useMessageEditing()
|
const { editingMessageId, stopEditing } = useMessageEditing()
|
||||||
@ -183,6 +184,9 @@ const MessageItem: FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
{!isEditing && (
|
{!isEditing && (
|
||||||
<>
|
<>
|
||||||
|
{!isMultiSelectMode && message.role === 'assistant' && showMessageOutline && (
|
||||||
|
<MessageOutline message={message} />
|
||||||
|
)}
|
||||||
<MessageContentContainer
|
<MessageContentContainer
|
||||||
className="message-content-container"
|
className="message-content-container"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@ -378,7 +378,7 @@ interface MessageWrapperProps {
|
|||||||
|
|
||||||
const MessageWrapper = styled.div<MessageWrapperProps>`
|
const MessageWrapper = styled.div<MessageWrapperProps>`
|
||||||
&.horizontal {
|
&.horizontal {
|
||||||
padding-right: 1px;
|
padding: 1px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
.message {
|
.message {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
180
src/renderer/src/pages/home/Messages/MessageOutline.tsx
Normal file
180
src/renderer/src/pages/home/Messages/MessageOutline.tsx
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
|
import { RootState } from '@renderer/store'
|
||||||
|
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
||||||
|
import { Message, MessageBlockType } from '@renderer/types/newMessage'
|
||||||
|
import React, { FC, useMemo, useRef } from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import remarkParse from 'remark-parse'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { unified } from 'unified'
|
||||||
|
import { visit } from 'unist-util-visit'
|
||||||
|
|
||||||
|
import { createSlugger, extractTextFromNode } from '../Markdown/plugins/rehypeHeadingIds'
|
||||||
|
|
||||||
|
interface MessageOutlineProps {
|
||||||
|
message: Message
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HeadingItem {
|
||||||
|
id: string
|
||||||
|
level: number
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageOutline: FC<MessageOutlineProps> = ({ message }) => {
|
||||||
|
const blockEntities = useSelector((state: RootState) => messageBlocksSelectors.selectEntities(state))
|
||||||
|
|
||||||
|
const headings: HeadingItem[] = useMemo(() => {
|
||||||
|
const mainTextBlocks = message.blocks
|
||||||
|
.map((blockId) => blockEntities[blockId])
|
||||||
|
.filter((b) => b?.type === MessageBlockType.MAIN_TEXT)
|
||||||
|
|
||||||
|
if (!mainTextBlocks?.length) return []
|
||||||
|
|
||||||
|
const result: HeadingItem[] = []
|
||||||
|
mainTextBlocks.forEach((mainTextBlock) => {
|
||||||
|
const tree = unified().use(remarkParse).parse(mainTextBlock?.content)
|
||||||
|
const slugger = createSlugger()
|
||||||
|
visit(tree, ['heading', 'html'], (node) => {
|
||||||
|
if (node.type === 'heading') {
|
||||||
|
const level = node.depth ?? 0
|
||||||
|
if (!level || level < 1 || level > 6) return
|
||||||
|
const text = extractTextFromNode(node)
|
||||||
|
if (!text) return
|
||||||
|
const id = `heading-${mainTextBlock.id}--` + slugger.slug(text || '')
|
||||||
|
result.push({ id, level, text: text })
|
||||||
|
} else if (node.type === 'html') {
|
||||||
|
// 匹配 <h1>...</h1> 到 <h6>...</h6>
|
||||||
|
const match = node.value.match(/<h([1-6])[^>]*>(.*?)<\/h\1>/i)
|
||||||
|
if (match) {
|
||||||
|
const level = parseInt(match[1], 10)
|
||||||
|
const text = match[2].replace(/<[^>]*>/g, '').trim() // 移除内部的HTML标签
|
||||||
|
if (text) {
|
||||||
|
const id = `heading-${mainTextBlock.id}--${slugger.slug(text || '')}`
|
||||||
|
result.push({ id, level, text })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}, [message.blocks, blockEntities])
|
||||||
|
|
||||||
|
const miniLevel = useMemo(() => {
|
||||||
|
return headings.length ? Math.min(...headings.map((heading) => heading.level)) : 1
|
||||||
|
}, [headings])
|
||||||
|
|
||||||
|
const messageOutlineContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const scrollToHeading = (id: string) => {
|
||||||
|
const parent = messageOutlineContainerRef.current?.parentElement
|
||||||
|
const messageContentContainer = parent?.querySelector('.message-content-container')
|
||||||
|
if (messageContentContainer) {
|
||||||
|
const headingElement = messageContentContainer.querySelector(`#${id}`)
|
||||||
|
if (headingElement) {
|
||||||
|
const scrollBlock = ['horizontal', 'grid'].includes(message.multiModelMessageStyle ?? '') ? 'nearest' : 'start'
|
||||||
|
headingElement.scrollIntoView({ behavior: 'smooth', block: scrollBlock })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暂时不支持 grid,因为在锚点滚动时会导致渲染错位
|
||||||
|
if (message.multiModelMessageStyle === 'grid' || !headings.length) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MessageOutlineContainer ref={messageOutlineContainerRef}>
|
||||||
|
<MessageOutlineBody $count={headings.length}>
|
||||||
|
{headings.map((heading, index) => (
|
||||||
|
<MessageOutlineItem key={index} onClick={() => scrollToHeading(heading.id)}>
|
||||||
|
<MessageOutlineItemDot $level={heading.level} />
|
||||||
|
<MessageOutlineItemText $level={heading.level} $miniLevel={miniLevel}>
|
||||||
|
{heading.text}
|
||||||
|
</MessageOutlineItemText>
|
||||||
|
</MessageOutlineItem>
|
||||||
|
))}
|
||||||
|
</MessageOutlineBody>
|
||||||
|
</MessageOutlineContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageOutlineContainer = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
inset: 63px 0 36px 10px;
|
||||||
|
z-index: 999;
|
||||||
|
pointer-events: none;
|
||||||
|
& ~ .message-content-container {
|
||||||
|
padding-left: 46px !important;
|
||||||
|
}
|
||||||
|
& ~ .MessageFooter {
|
||||||
|
margin-left: 46px !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const MessageOutlineItemDot = styled.div<{ $level: number }>`
|
||||||
|
width: ${({ $level }) => 16 - $level * 2}px;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--color-border);
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-right: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
`
|
||||||
|
|
||||||
|
const MessageOutlineItemText = styled.div<{ $level: number; $miniLevel: number }>`
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
opacity: 0;
|
||||||
|
display: none;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
padding: 2px 8px;
|
||||||
|
padding-left: ${({ $level, $miniLevel }) => ($level - $miniLevel) * 8}px;
|
||||||
|
font-size: ${({ $level }) => 16 - $level}px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`
|
||||||
|
|
||||||
|
const MessageOutlineItem = styled.div`
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
&:hover {
|
||||||
|
${MessageOutlineItemText} {
|
||||||
|
color: var(--color-text-2);
|
||||||
|
}
|
||||||
|
${MessageOutlineItemDot} {
|
||||||
|
background: var(--color-text-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const MessageOutlineBody = styled(Scrollbar)<{ $count: number }>`
|
||||||
|
max-width: 50%;
|
||||||
|
max-height: min(100%, 70vh);
|
||||||
|
position: sticky;
|
||||||
|
top: max(calc(50% - ${({ $count }) => ($count * 24) / 2 + 10}px), 20px);
|
||||||
|
bottom: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: hidden;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px 0 10px 10px;
|
||||||
|
gap: 4px;
|
||||||
|
border-radius: 10px;
|
||||||
|
pointer-events: auto;
|
||||||
|
&:hover {
|
||||||
|
padding: 10px 10px 10px 10px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--color-background);
|
||||||
|
box-shadow: 0 0 10px 0 rgba(128, 128, 128, 0.2);
|
||||||
|
${MessageOutlineItemText} {
|
||||||
|
opacity: 1;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default React.memo(MessageOutline)
|
||||||
@ -29,6 +29,7 @@ import {
|
|||||||
setEnableBackspaceDeleteModel,
|
setEnableBackspaceDeleteModel,
|
||||||
setEnableQuickPanelTriggers,
|
setEnableQuickPanelTriggers,
|
||||||
setFontSize,
|
setFontSize,
|
||||||
|
setMathEnableSingleDollar,
|
||||||
setMathEngine,
|
setMathEngine,
|
||||||
setMessageFont,
|
setMessageFont,
|
||||||
setMessageNavigation,
|
setMessageNavigation,
|
||||||
@ -38,6 +39,7 @@ import {
|
|||||||
setPasteLongTextThreshold,
|
setPasteLongTextThreshold,
|
||||||
setRenderInputMessageAsMarkdown,
|
setRenderInputMessageAsMarkdown,
|
||||||
setShowInputEstimatedTokens,
|
setShowInputEstimatedTokens,
|
||||||
|
setShowMessageOutline,
|
||||||
setShowPrompt,
|
setShowPrompt,
|
||||||
setShowTranslateConfirm,
|
setShowTranslateConfirm,
|
||||||
setThoughtAutoCollapse
|
setThoughtAutoCollapse
|
||||||
@ -96,6 +98,7 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
codeImageTools,
|
codeImageTools,
|
||||||
codeExecution,
|
codeExecution,
|
||||||
mathEngine,
|
mathEngine,
|
||||||
|
mathEnableSingleDollar,
|
||||||
autoTranslateWithSpace,
|
autoTranslateWithSpace,
|
||||||
pasteLongTextThreshold,
|
pasteLongTextThreshold,
|
||||||
multiModelMessageStyle,
|
multiModelMessageStyle,
|
||||||
@ -103,7 +106,8 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
messageNavigation,
|
messageNavigation,
|
||||||
enableQuickPanelTriggers,
|
enableQuickPanelTriggers,
|
||||||
enableBackspaceDeleteModel,
|
enableBackspaceDeleteModel,
|
||||||
showTranslateConfirm
|
showTranslateConfirm,
|
||||||
|
showMessageOutline
|
||||||
} = useSettings()
|
} = useSettings()
|
||||||
|
|
||||||
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
|
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
|
||||||
@ -332,6 +336,15 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>{t('settings.messages.show_message_outline')}</SettingRowTitleSmall>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={showMessageOutline}
|
||||||
|
onChange={(checked) => dispatch(setShowMessageOutline(checked))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitleSmall>{t('message.message.style.label')}</SettingRowTitleSmall>
|
<SettingRowTitleSmall>{t('message.message.style.label')}</SettingRowTitleSmall>
|
||||||
<Selector
|
<Selector
|
||||||
@ -371,19 +384,6 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
<SettingRow>
|
|
||||||
<SettingRowTitleSmall>{t('settings.messages.math_engine.label')}</SettingRowTitleSmall>
|
|
||||||
<Selector
|
|
||||||
value={mathEngine}
|
|
||||||
onChange={(value) => dispatch(setMathEngine(value as MathEngine))}
|
|
||||||
options={[
|
|
||||||
{ value: 'KaTeX', label: 'KaTeX' },
|
|
||||||
{ value: 'MathJax', label: 'MathJax' },
|
|
||||||
{ value: 'none', label: t('settings.messages.math_engine.none') }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</SettingRow>
|
|
||||||
<SettingDivider />
|
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitleSmall>{t('settings.font_size.title')}</SettingRowTitleSmall>
|
<SettingRowTitleSmall>{t('settings.font_size.title')}</SettingRowTitleSmall>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
@ -407,6 +407,37 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
</CollapsibleSettingGroup>
|
</CollapsibleSettingGroup>
|
||||||
|
<CollapsibleSettingGroup title={t('settings.math.title')} defaultExpanded={true}>
|
||||||
|
<SettingGroup>
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>{t('settings.math.engine.label')}</SettingRowTitleSmall>
|
||||||
|
<Selector
|
||||||
|
value={mathEngine}
|
||||||
|
onChange={(value) => dispatch(setMathEngine(value as MathEngine))}
|
||||||
|
options={[
|
||||||
|
{ value: 'KaTeX', label: 'KaTeX' },
|
||||||
|
{ value: 'MathJax', label: 'MathJax' },
|
||||||
|
{ value: 'none', label: t('settings.math.engine.none') }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>
|
||||||
|
{t('settings.math.single_dollar.label')}{' '}
|
||||||
|
<Tooltip title={t('settings.math.single_dollar.tip')}>
|
||||||
|
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
|
||||||
|
</Tooltip>
|
||||||
|
</SettingRowTitleSmall>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={mathEnableSingleDollar}
|
||||||
|
onChange={(checked) => dispatch(setMathEnableSingleDollar(checked))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
</SettingGroup>
|
||||||
|
</CollapsibleSettingGroup>
|
||||||
<CollapsibleSettingGroup title={t('chat.settings.code.title')} defaultExpanded={true}>
|
<CollapsibleSettingGroup title={t('chat.settings.code.title')} defaultExpanded={true}>
|
||||||
<SettingGroup>
|
<SettingGroup>
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)} */}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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')}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { CheckOutlined, SendOutlined, SwapOutlined } from '@ant-design/icons'
|
import { SendOutlined, SwapOutlined } from '@ant-design/icons'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||||
|
import { CopyIcon } from '@renderer/components/Icons'
|
||||||
import LanguageSelect from '@renderer/components/LanguageSelect'
|
import LanguageSelect from '@renderer/components/LanguageSelect'
|
||||||
import ModelSelectButton from '@renderer/components/ModelSelectButton'
|
import ModelSelectButton from '@renderer/components/ModelSelectButton'
|
||||||
import { isEmbeddingModel, isRerankModel, isTextToImageModel } from '@renderer/config/models'
|
import { isEmbeddingModel, isRerankModel, isTextToImageModel } from '@renderer/config/models'
|
||||||
@ -25,7 +26,7 @@ import {
|
|||||||
import { Button, Flex, Popover, Tooltip, Typography } from 'antd'
|
import { Button, Flex, Popover, Tooltip, Typography } from 'antd'
|
||||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||||
import { isEmpty, throttle } from 'lodash'
|
import { isEmpty, throttle } from 'lodash'
|
||||||
import { CopyIcon, FolderClock, Settings2 } from 'lucide-react'
|
import { Check, FolderClock, Settings2 } from 'lucide-react'
|
||||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -473,7 +474,7 @@ const TranslatePage: FC = () => {
|
|||||||
className="copy-button"
|
className="copy-button"
|
||||||
onClick={onCopy}
|
onClick={onCopy}
|
||||||
disabled={!translatedContent}
|
disabled={!translatedContent}
|
||||||
icon={copied ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <CopyIcon size={16} />}
|
icon={copied ? <Check size={16} color="var(--color-primary)" /> : <CopyIcon size={16} />}
|
||||||
/>
|
/>
|
||||||
<OutputText ref={outputTextRef} onScroll={handleOutputScroll} className={'selectable'}>
|
<OutputText ref={outputTextRef} onScroll={handleOutputScroll} className={'selectable'}>
|
||||||
{!translatedContent ? (
|
{!translatedContent ? (
|
||||||
|
|||||||
@ -89,15 +89,9 @@ export default class LocalSearchProvider extends BaseWebSearchProvider {
|
|||||||
* @returns 带有语言过滤的查询
|
* @returns 带有语言过滤的查询
|
||||||
*/
|
*/
|
||||||
protected applyLanguageFilter(query: string, language: string): string {
|
protected applyLanguageFilter(query: string, language: string): string {
|
||||||
if (this.provider.id.includes('local-google')) {
|
if (this.provider.id.includes('local-google') || this.provider.id.includes('local-bing')) {
|
||||||
return `${query} lang:${language.split('-')[0]}`
|
return `${query} lang:${language.split('-')[0]}`
|
||||||
}
|
}
|
||||||
if (this.provider.id.includes('local-bing')) {
|
|
||||||
return `${query} language:${language}`
|
|
||||||
}
|
|
||||||
if (this.provider.id.includes('local-baidu')) {
|
|
||||||
return `${query} language:${language.split('-')[0]}`
|
|
||||||
}
|
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -104,9 +104,9 @@ async function fetchExternalTool(
|
|||||||
const showListTools = enabledMCPs && enabledMCPs.length > 0
|
const showListTools = enabledMCPs && enabledMCPs.length > 0
|
||||||
|
|
||||||
// 是否使用工具
|
// 是否使用工具
|
||||||
const hasAnyTool = shouldWebSearch || shouldKnowledgeSearch || shouldSearchMemory || showListTools
|
const hasAnyTool = shouldWebSearch || shouldKnowledgeSearch || showListTools
|
||||||
|
|
||||||
// 在工具链开始时发送进度通知
|
// 在工具链开始时发送进度通知(不包括记忆搜索)
|
||||||
if (hasAnyTool) {
|
if (hasAnyTool) {
|
||||||
onChunkReceived({ type: ChunkType.EXTERNEL_TOOL_IN_PROGRESS })
|
onChunkReceived({ type: ChunkType.EXTERNEL_TOOL_IN_PROGRESS })
|
||||||
}
|
}
|
||||||
@ -456,8 +456,6 @@ export async function fetchChatCompletion({
|
|||||||
const { mcpTools } = await fetchExternalTool(lastUserMessage, assistant, onChunkReceived, lastAnswer)
|
const { mcpTools } = await fetchExternalTool(lastUserMessage, assistant, onChunkReceived, lastAnswer)
|
||||||
const model = assistant.model || getDefaultModel()
|
const model = assistant.model || getDefaultModel()
|
||||||
|
|
||||||
onChunkReceived({ type: ChunkType.LLM_RESPONSE_CREATED })
|
|
||||||
|
|
||||||
const { maxTokens, contextCount } = getAssistantSettings(assistant)
|
const { maxTokens, contextCount } = getAssistantSettings(assistant)
|
||||||
|
|
||||||
const filteredMessages2 = filterUsefulMessages(filteredMessages1)
|
const filteredMessages2 = filterUsefulMessages(filteredMessages1)
|
||||||
@ -488,7 +486,7 @@ export async function fetchChatCompletion({
|
|||||||
isGenerateImageModel(model) && (isSupportedDisableGenerationModel(model) ? assistant.enableGenerateImage : true)
|
isGenerateImageModel(model) && (isSupportedDisableGenerationModel(model) ? assistant.enableGenerateImage : true)
|
||||||
|
|
||||||
// --- Call AI Completions ---
|
// --- Call AI Completions ---
|
||||||
|
onChunkReceived({ type: ChunkType.LLM_RESPONSE_CREATED })
|
||||||
const completionsParams: CompletionsParams = {
|
const completionsParams: CompletionsParams = {
|
||||||
callType: 'chat',
|
callType: 'chat',
|
||||||
messages: _messages,
|
messages: _messages,
|
||||||
|
|||||||
@ -21,6 +21,17 @@ export const getKnowledgeBaseParams = (base: KnowledgeBase): KnowledgeBaseParams
|
|||||||
const aiProvider = new AiProvider(provider)
|
const aiProvider = new AiProvider(provider)
|
||||||
const rerankAiProvider = new AiProvider(rerankProvider)
|
const rerankAiProvider = new AiProvider(rerankProvider)
|
||||||
|
|
||||||
|
// get preprocess provider from store instead of base.preprocessProvider
|
||||||
|
const preprocessProvider = store
|
||||||
|
.getState()
|
||||||
|
.preprocess.providers.find((p) => p.id === base.preprocessProvider?.provider.id)
|
||||||
|
const updatedPreprocessProvider = preprocessProvider
|
||||||
|
? {
|
||||||
|
type: 'preprocess' as const,
|
||||||
|
provider: preprocessProvider
|
||||||
|
}
|
||||||
|
: base.preprocessProvider
|
||||||
|
|
||||||
let host = aiProvider.getBaseURL()
|
let host = aiProvider.getBaseURL()
|
||||||
const rerankHost = rerankAiProvider.getBaseURL()
|
const rerankHost = rerankAiProvider.getBaseURL()
|
||||||
if (provider.type === 'gemini') {
|
if (provider.type === 'gemini') {
|
||||||
@ -57,7 +68,7 @@ export const getKnowledgeBaseParams = (base: KnowledgeBase): KnowledgeBaseParams
|
|||||||
apiKey: rerankAiProvider.getApiKey() || 'secret',
|
apiKey: rerankAiProvider.getApiKey() || 'secret',
|
||||||
baseURL: rerankHost
|
baseURL: rerankHost
|
||||||
},
|
},
|
||||||
preprocessProvider: base.preprocessProvider,
|
preprocessProvider: updatedPreprocessProvider,
|
||||||
documentCount: base.documentCount
|
documentCount: base.documentCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,7 +62,7 @@ const persistedReducer = persistReducer(
|
|||||||
{
|
{
|
||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 130,
|
version: 131,
|
||||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
import { FileMetadata, KnowledgeBase, KnowledgeItem, ProcessingStatus } from '@renderer/types'
|
import { FileMetadata, KnowledgeBase, KnowledgeItem, PreprocessProvider, ProcessingStatus } from '@renderer/types'
|
||||||
|
|
||||||
const logger = loggerService.withContext('Store:Knowledge')
|
const logger = loggerService.withContext('Store:Knowledge')
|
||||||
|
|
||||||
@ -174,6 +174,18 @@ const knowledgeSlice = createSlice({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
syncPreprocessProvider(state, action: PayloadAction<Partial<PreprocessProvider>>) {
|
||||||
|
const updatedProvider = action.payload
|
||||||
|
state.bases.forEach((base) => {
|
||||||
|
if (base.preprocessProvider && base.preprocessProvider.provider.id === updatedProvider.id) {
|
||||||
|
base.preprocessProvider.provider = {
|
||||||
|
...base.preprocessProvider.provider,
|
||||||
|
...updatedProvider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
updateBaseItemUniqueId(
|
updateBaseItemUniqueId(
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ baseId: string; itemId: string; uniqueId: string; uniqueIds: string[] }>
|
action: PayloadAction<{ baseId: string; itemId: string; uniqueId: string; uniqueIds: string[] }>
|
||||||
@ -221,7 +233,8 @@ export const {
|
|||||||
clearCompletedProcessing,
|
clearCompletedProcessing,
|
||||||
clearAllProcessing,
|
clearAllProcessing,
|
||||||
updateBaseItemUniqueId,
|
updateBaseItemUniqueId,
|
||||||
updateBaseItemIsPreprocessed
|
updateBaseItemIsPreprocessed,
|
||||||
|
syncPreprocessProvider
|
||||||
} = knowledgeSlice.actions
|
} = knowledgeSlice.actions
|
||||||
|
|
||||||
export default knowledgeSlice.reducer
|
export default knowledgeSlice.reducer
|
||||||
|
|||||||
@ -2094,6 +2094,15 @@ const migrateConfig = {
|
|||||||
logger.error('migrate 130 error', error as Error)
|
logger.error('migrate 130 error', error as Error)
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'131': (state: RootState) => {
|
||||||
|
try {
|
||||||
|
state.settings.mathEnableSingleDollar = true
|
||||||
|
return state
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('migrate 131 error', error as Error)
|
||||||
|
return state
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -105,6 +105,7 @@ export interface SettingsState {
|
|||||||
codeWrappable: boolean
|
codeWrappable: boolean
|
||||||
codeImageTools: boolean
|
codeImageTools: boolean
|
||||||
mathEngine: MathEngine
|
mathEngine: MathEngine
|
||||||
|
mathEnableSingleDollar: boolean
|
||||||
messageStyle: 'plain' | 'bubble'
|
messageStyle: 'plain' | 'bubble'
|
||||||
foldDisplayMode: 'expanded' | 'compact'
|
foldDisplayMode: 'expanded' | 'compact'
|
||||||
gridColumns: number
|
gridColumns: number
|
||||||
@ -217,6 +218,7 @@ export interface SettingsState {
|
|||||||
navbarPosition: 'left' | 'top'
|
navbarPosition: 'left' | 'top'
|
||||||
// API Server
|
// API Server
|
||||||
apiServer: ApiServerConfig
|
apiServer: ApiServerConfig
|
||||||
|
showMessageOutline?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
|
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
|
||||||
@ -286,6 +288,7 @@ export const initialState: SettingsState = {
|
|||||||
codeWrappable: false,
|
codeWrappable: false,
|
||||||
codeImageTools: false,
|
codeImageTools: false,
|
||||||
mathEngine: 'KaTeX',
|
mathEngine: 'KaTeX',
|
||||||
|
mathEnableSingleDollar: true,
|
||||||
messageStyle: 'plain',
|
messageStyle: 'plain',
|
||||||
foldDisplayMode: 'expanded',
|
foldDisplayMode: 'expanded',
|
||||||
gridColumns: 2,
|
gridColumns: 2,
|
||||||
@ -404,7 +407,8 @@ export const initialState: SettingsState = {
|
|||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 23333,
|
port: 23333,
|
||||||
apiKey: `cs-sk-${uuid()}`
|
apiKey: `cs-sk-${uuid()}`
|
||||||
}
|
},
|
||||||
|
showMessageOutline: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsSlice = createSlice({
|
const settingsSlice = createSlice({
|
||||||
@ -614,6 +618,9 @@ const settingsSlice = createSlice({
|
|||||||
setMathEngine: (state, action: PayloadAction<MathEngine>) => {
|
setMathEngine: (state, action: PayloadAction<MathEngine>) => {
|
||||||
state.mathEngine = action.payload
|
state.mathEngine = action.payload
|
||||||
},
|
},
|
||||||
|
setMathEnableSingleDollar: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.mathEnableSingleDollar = action.payload
|
||||||
|
},
|
||||||
setFoldDisplayMode: (state, action: PayloadAction<'expanded' | 'compact'>) => {
|
setFoldDisplayMode: (state, action: PayloadAction<'expanded' | 'compact'>) => {
|
||||||
state.foldDisplayMode = action.payload
|
state.foldDisplayMode = action.payload
|
||||||
},
|
},
|
||||||
@ -833,6 +840,9 @@ const settingsSlice = createSlice({
|
|||||||
...state.apiServer,
|
...state.apiServer,
|
||||||
apiKey: action.payload
|
apiKey: action.payload
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
setShowMessageOutline: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.showMessageOutline = action.payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -893,6 +903,7 @@ export const {
|
|||||||
setCodeWrappable,
|
setCodeWrappable,
|
||||||
setCodeImageTools,
|
setCodeImageTools,
|
||||||
setMathEngine,
|
setMathEngine,
|
||||||
|
setMathEnableSingleDollar,
|
||||||
setFoldDisplayMode,
|
setFoldDisplayMode,
|
||||||
setGridColumns,
|
setGridColumns,
|
||||||
setGridPopoverTrigger,
|
setGridPopoverTrigger,
|
||||||
@ -958,6 +969,7 @@ export const {
|
|||||||
setS3Partial,
|
setS3Partial,
|
||||||
setEnableDeveloperMode,
|
setEnableDeveloperMode,
|
||||||
setNavbarPosition,
|
setNavbarPosition,
|
||||||
|
setShowMessageOutline,
|
||||||
// API Server actions
|
// API Server actions
|
||||||
setApiServerEnabled,
|
setApiServerEnabled,
|
||||||
setApiServerPort,
|
setApiServerPort,
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -56,6 +56,7 @@ export type ReasoningEffortOption = NonNullable<OpenAI.ReasoningEffort> | 'auto'
|
|||||||
export type ThinkingOption = ReasoningEffortOption | 'off'
|
export type ThinkingOption = ReasoningEffortOption | 'off'
|
||||||
export type ThinkingModelType =
|
export type ThinkingModelType =
|
||||||
| 'default'
|
| 'default'
|
||||||
|
| 'o'
|
||||||
| 'gpt5'
|
| 'gpt5'
|
||||||
| 'grok'
|
| 'grok'
|
||||||
| 'gemini'
|
| 'gemini'
|
||||||
@ -609,8 +610,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 +688,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
|
||||||
|
|||||||
@ -162,6 +162,7 @@ export interface AwsBedrockSdkParams {
|
|||||||
topP?: number
|
topP?: number
|
||||||
stream?: boolean
|
stream?: boolean
|
||||||
tools?: AwsBedrockSdkTool[]
|
tools?: AwsBedrockSdkTool[]
|
||||||
|
[key: string]: any // Allow any additional custom parameters
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AwsBedrockSdkMessageParam {
|
export interface AwsBedrockSdkMessageParam {
|
||||||
@ -206,6 +207,22 @@ export interface AwsBedrockSdkMessageParam {
|
|||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AwsBedrockStreamChunk {
|
||||||
|
type: string
|
||||||
|
delta?: {
|
||||||
|
text?: string
|
||||||
|
toolUse?: { input?: string }
|
||||||
|
type?: string
|
||||||
|
thinking?: string
|
||||||
|
}
|
||||||
|
index?: number
|
||||||
|
content_block?: any
|
||||||
|
usage?: {
|
||||||
|
inputTokens?: number
|
||||||
|
outputTokens?: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface AwsBedrockSdkRawChunk {
|
export interface AwsBedrockSdkRawChunk {
|
||||||
contentBlockStart?: {
|
contentBlockStart?: {
|
||||||
start?: {
|
start?: {
|
||||||
@ -222,6 +239,8 @@ export interface AwsBedrockSdkRawChunk {
|
|||||||
toolUse?: {
|
toolUse?: {
|
||||||
input?: string
|
input?: string
|
||||||
}
|
}
|
||||||
|
type?: string // 支持 'thinking_delta' 等类型
|
||||||
|
thinking?: string // 支持 thinking 内容
|
||||||
}
|
}
|
||||||
contentBlockIndex?: number
|
contentBlockIndex?: number
|
||||||
}
|
}
|
||||||
|
|||||||
44
yarn.lock
44
yarn.lock
@ -4559,12 +4559,12 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@mermaid-js/parser@npm:^0.5.0":
|
"@mermaid-js/parser@npm:^0.6.2":
|
||||||
version: 0.5.0
|
version: 0.6.2
|
||||||
resolution: "@mermaid-js/parser@npm:0.5.0"
|
resolution: "@mermaid-js/parser@npm:0.6.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
langium: "npm:3.3.1"
|
langium: "npm:3.3.1"
|
||||||
checksum: 10c0/af1c1cf6cfe808bf5f7c232a881e5f9d6778c2fc3997d8ea3da93f59097411d0e13f74649e2576488f82227bab58e47a49f4e77cb11cf4196176f3c4135c724d
|
checksum: 10c0/6059341a5dc3fdf56dd75c858843154e18c582e5cc41c3e73e9a076e218116c6bdbdba729d27154cef61430c900d87342423bbb81e37d8a9968c6c2fdd99e87a
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -8771,7 +8771,7 @@ __metadata:
|
|||||||
lucide-react: "npm:^0.525.0"
|
lucide-react: "npm:^0.525.0"
|
||||||
macos-release: "npm:^3.4.0"
|
macos-release: "npm:^3.4.0"
|
||||||
markdown-it: "npm:^14.1.0"
|
markdown-it: "npm:^14.1.0"
|
||||||
mermaid: "npm:^11.7.0"
|
mermaid: "npm:^11.9.0"
|
||||||
mime: "npm:^4.0.4"
|
mime: "npm:^4.0.4"
|
||||||
motion: "npm:^12.10.5"
|
motion: "npm:^12.10.5"
|
||||||
node-stream-zip: "npm:^1.15.0"
|
node-stream-zip: "npm:^1.15.0"
|
||||||
@ -8812,7 +8812,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"
|
||||||
@ -15192,7 +15192,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"katex@npm:^0.16.0, katex@npm:^0.16.9":
|
"katex@npm:^0.16.0, katex@npm:^0.16.22":
|
||||||
version: 0.16.22
|
version: 0.16.22
|
||||||
resolution: "katex@npm:0.16.22"
|
resolution: "katex@npm:0.16.22"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -16044,12 +16044,12 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"marked@npm:^15.0.7":
|
"marked@npm:^16.0.0":
|
||||||
version: 15.0.11
|
version: 16.1.2
|
||||||
resolution: "marked@npm:15.0.11"
|
resolution: "marked@npm:16.1.2"
|
||||||
bin:
|
bin:
|
||||||
marked: bin/marked.js
|
marked: bin/marked.js
|
||||||
checksum: 10c0/d532db4955c1f2ac6efc65a644725e9e12e7944cb6af40c7148baecfd3b3c2f3564229b3daf12d2125635466448fb9b367ce52357be3aea0273e3d152efdbdcf
|
checksum: 10c0/4e5878f1aa89de139bed14835865af20f26527674f41dedf2b33d2f85360298a1a0cc0505c675f072175c86eb30684c7b4e287d18f5958daa26e36bc1308d321
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -16467,13 +16467,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"mermaid@npm:^11.7.0":
|
"mermaid@npm:^11.9.0":
|
||||||
version: 11.7.0
|
version: 11.9.0
|
||||||
resolution: "mermaid@npm:11.7.0"
|
resolution: "mermaid@npm:11.9.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@braintree/sanitize-url": "npm:^7.0.4"
|
"@braintree/sanitize-url": "npm:^7.0.4"
|
||||||
"@iconify/utils": "npm:^2.1.33"
|
"@iconify/utils": "npm:^2.1.33"
|
||||||
"@mermaid-js/parser": "npm:^0.5.0"
|
"@mermaid-js/parser": "npm:^0.6.2"
|
||||||
"@types/d3": "npm:^7.4.3"
|
"@types/d3": "npm:^7.4.3"
|
||||||
cytoscape: "npm:^3.29.3"
|
cytoscape: "npm:^3.29.3"
|
||||||
cytoscape-cose-bilkent: "npm:^4.1.0"
|
cytoscape-cose-bilkent: "npm:^4.1.0"
|
||||||
@ -16483,15 +16483,15 @@ __metadata:
|
|||||||
dagre-d3-es: "npm:7.0.11"
|
dagre-d3-es: "npm:7.0.11"
|
||||||
dayjs: "npm:^1.11.13"
|
dayjs: "npm:^1.11.13"
|
||||||
dompurify: "npm:^3.2.5"
|
dompurify: "npm:^3.2.5"
|
||||||
katex: "npm:^0.16.9"
|
katex: "npm:^0.16.22"
|
||||||
khroma: "npm:^2.1.0"
|
khroma: "npm:^2.1.0"
|
||||||
lodash-es: "npm:^4.17.21"
|
lodash-es: "npm:^4.17.21"
|
||||||
marked: "npm:^15.0.7"
|
marked: "npm:^16.0.0"
|
||||||
roughjs: "npm:^4.6.6"
|
roughjs: "npm:^4.6.6"
|
||||||
stylis: "npm:^4.3.6"
|
stylis: "npm:^4.3.6"
|
||||||
ts-dedent: "npm:^2.2.0"
|
ts-dedent: "npm:^2.2.0"
|
||||||
uuid: "npm:^11.1.0"
|
uuid: "npm:^11.1.0"
|
||||||
checksum: 10c0/ab37f563b54d53c513d792a91aae54c6e2ed20f4d8606cdec993d60b8c50534ac6ab740408d710a655c6190341704cf133f0a7fb47e230c0c94b38cf08e07775
|
checksum: 10c0/f3420d0fd8919b31e36354cbf0ddd26398898c960e0bcb0e52aceae657245fcf1e5fe3e28651bff83c9b1fb8b6d3e07fc8b26d111ef3159fcf780d53ce40a437
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -20446,14 +20446,14 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"selection-hook@npm:^1.0.8":
|
"selection-hook@npm:^1.0.9":
|
||||||
version: 1.0.8
|
version: 1.0.9
|
||||||
resolution: "selection-hook@npm:1.0.8"
|
resolution: "selection-hook@npm:1.0.9"
|
||||||
dependencies:
|
dependencies:
|
||||||
node-addon-api: "npm:^8.4.0"
|
node-addon-api: "npm:^8.4.0"
|
||||||
node-gyp: "npm:latest"
|
node-gyp: "npm:latest"
|
||||||
node-gyp-build: "npm:^4.8.4"
|
node-gyp-build: "npm:^4.8.4"
|
||||||
checksum: 10c0/ed7e230ddf10fcd1974b166c5e73170900260664e40454e4e1fcdf0ba21d2a08cf95824c085fa07069aa99b663e0ee3f2aed74c3fbdba0f4e99abe6956bd51dc
|
checksum: 10c0/5f3114b528d9e1545a5dc4b99927a0ab441570063bb348b52784d757c8f250f0d6a875175d371adf5dc2bfc82bf6bb86f99d3ee66fefe0749040c0b50f3217c3
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user