Compare commits
4 Commits
main
...
feat/acces
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76d48f9ccb | ||
|
|
115cd80432 | ||
|
|
531101742e | ||
|
|
c3c577dff4 |
@@ -374,5 +374,13 @@ export enum IpcChannel {
|
|||||||
WebSocket_Stop = 'webSocket:stop',
|
WebSocket_Stop = 'webSocket:stop',
|
||||||
WebSocket_Status = 'webSocket:status',
|
WebSocket_Status = 'webSocket:status',
|
||||||
WebSocket_SendFile = 'webSocket:send-file',
|
WebSocket_SendFile = 'webSocket:send-file',
|
||||||
WebSocket_GetAllCandidates = 'webSocket:get-all-candidates'
|
WebSocket_GetAllCandidates = 'webSocket:get-all-candidates',
|
||||||
|
|
||||||
|
// Volcengine
|
||||||
|
Volcengine_SaveCredentials = 'volcengine:save-credentials',
|
||||||
|
Volcengine_HasCredentials = 'volcengine:has-credentials',
|
||||||
|
Volcengine_ClearCredentials = 'volcengine:clear-credentials',
|
||||||
|
Volcengine_ListModels = 'volcengine:list-models',
|
||||||
|
Volcengine_GetAuthHeaders = 'volcengine:get-auth-headers',
|
||||||
|
Volcengine_MakeRequest = 'volcengine:make-request'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ import {
|
|||||||
import storeSyncService from './services/StoreSyncService'
|
import storeSyncService from './services/StoreSyncService'
|
||||||
import { themeService } from './services/ThemeService'
|
import { themeService } from './services/ThemeService'
|
||||||
import VertexAIService from './services/VertexAIService'
|
import VertexAIService from './services/VertexAIService'
|
||||||
|
import VolcengineService from './services/VolcengineService'
|
||||||
import WebSocketService from './services/WebSocketService'
|
import WebSocketService from './services/WebSocketService'
|
||||||
import { setOpenLinkExternal } from './services/WebviewService'
|
import { setOpenLinkExternal } from './services/WebviewService'
|
||||||
import { windowService } from './services/WindowService'
|
import { windowService } from './services/WindowService'
|
||||||
@@ -1077,6 +1078,14 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
ipcMain.handle(IpcChannel.WebSocket_SendFile, WebSocketService.sendFile)
|
ipcMain.handle(IpcChannel.WebSocket_SendFile, WebSocketService.sendFile)
|
||||||
ipcMain.handle(IpcChannel.WebSocket_GetAllCandidates, WebSocketService.getAllCandidates)
|
ipcMain.handle(IpcChannel.WebSocket_GetAllCandidates, WebSocketService.getAllCandidates)
|
||||||
|
|
||||||
|
// Volcengine
|
||||||
|
ipcMain.handle(IpcChannel.Volcengine_SaveCredentials, VolcengineService.saveCredentials)
|
||||||
|
ipcMain.handle(IpcChannel.Volcengine_HasCredentials, VolcengineService.hasCredentials)
|
||||||
|
ipcMain.handle(IpcChannel.Volcengine_ClearCredentials, VolcengineService.clearCredentials)
|
||||||
|
ipcMain.handle(IpcChannel.Volcengine_ListModels, VolcengineService.listModels)
|
||||||
|
ipcMain.handle(IpcChannel.Volcengine_GetAuthHeaders, VolcengineService.getAuthHeaders)
|
||||||
|
ipcMain.handle(IpcChannel.Volcengine_MakeRequest, VolcengineService.makeRequest)
|
||||||
|
|
||||||
ipcMain.handle(IpcChannel.APP_CrashRenderProcess, () => {
|
ipcMain.handle(IpcChannel.APP_CrashRenderProcess, () => {
|
||||||
mainWindow.webContents.forcefullyCrashRenderer()
|
mainWindow.webContents.forcefullyCrashRenderer()
|
||||||
})
|
})
|
||||||
|
|||||||
732
src/main/services/VolcengineService.ts
Normal file
732
src/main/services/VolcengineService.ts
Normal file
@@ -0,0 +1,732 @@
|
|||||||
|
import { loggerService } from '@logger'
|
||||||
|
import crypto from 'crypto'
|
||||||
|
import { app, net, safeStorage } from 'electron'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
|
import { getConfigDir } from '../utils/file'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('VolcengineService')
|
||||||
|
|
||||||
|
// Configuration constants
|
||||||
|
const CONFIG = {
|
||||||
|
ALGORITHM: 'HMAC-SHA256',
|
||||||
|
REQUEST_TYPE: 'request',
|
||||||
|
DEFAULT_REGION: 'cn-beijing',
|
||||||
|
SERVICE_NAME: 'ark',
|
||||||
|
DEFAULT_HEADERS: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
accept: 'application/json'
|
||||||
|
},
|
||||||
|
API_URLS: {
|
||||||
|
ARK_HOST: 'open.volcengineapi.com'
|
||||||
|
},
|
||||||
|
CREDENTIALS_FILE_NAME: '.volcengine_credentials',
|
||||||
|
API_VERSION: '2024-01-01',
|
||||||
|
DEFAULT_PAGE_SIZE: 100
|
||||||
|
} as const
|
||||||
|
|
||||||
|
// Request schemas
|
||||||
|
const ListFoundationModelsRequestSchema = z.object({
|
||||||
|
PageNumber: z.optional(z.number()),
|
||||||
|
PageSize: z.optional(z.number())
|
||||||
|
})
|
||||||
|
|
||||||
|
const ListEndpointsRequestSchema = z.object({
|
||||||
|
ProjectName: z.optional(z.string()),
|
||||||
|
PageNumber: z.optional(z.number()),
|
||||||
|
PageSize: z.optional(z.number())
|
||||||
|
})
|
||||||
|
|
||||||
|
// Response schemas - only keep fields needed for model list
|
||||||
|
const FoundationModelItemSchema = z.object({
|
||||||
|
Name: z.string(),
|
||||||
|
DisplayName: z.optional(z.string()),
|
||||||
|
Description: z.optional(z.string())
|
||||||
|
})
|
||||||
|
|
||||||
|
const EndpointItemSchema = z.object({
|
||||||
|
Id: z.string(),
|
||||||
|
Name: z.optional(z.string()),
|
||||||
|
Description: z.optional(z.string()),
|
||||||
|
ModelReference: z.optional(
|
||||||
|
z.object({
|
||||||
|
FoundationModel: z.optional(
|
||||||
|
z.object({
|
||||||
|
Name: z.optional(z.string()),
|
||||||
|
ModelVersion: z.optional(z.string())
|
||||||
|
})
|
||||||
|
),
|
||||||
|
CustomModelId: z.optional(z.string())
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const ListFoundationModelsResponseSchema = z.object({
|
||||||
|
Result: z.object({
|
||||||
|
TotalCount: z.number(),
|
||||||
|
Items: z.array(FoundationModelItemSchema)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const ListEndpointsResponseSchema = z.object({
|
||||||
|
Result: z.object({
|
||||||
|
TotalCount: z.number(),
|
||||||
|
Items: z.array(EndpointItemSchema)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Infer types from schemas
|
||||||
|
type ListFoundationModelsRequest = z.infer<typeof ListFoundationModelsRequestSchema>
|
||||||
|
type ListEndpointsRequest = z.infer<typeof ListEndpointsRequestSchema>
|
||||||
|
type ListFoundationModelsResponse = z.infer<typeof ListFoundationModelsResponseSchema>
|
||||||
|
type ListEndpointsResponse = z.infer<typeof ListEndpointsResponseSchema>
|
||||||
|
|
||||||
|
// ============= Internal Type Definitions =============
|
||||||
|
|
||||||
|
interface VolcengineCredentials {
|
||||||
|
accessKeyId: string
|
||||||
|
secretAccessKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SignedRequestParams {
|
||||||
|
method: 'GET' | 'POST'
|
||||||
|
host: string
|
||||||
|
path: string
|
||||||
|
query: Record<string, string>
|
||||||
|
headers: Record<string, string>
|
||||||
|
body?: string
|
||||||
|
service: string
|
||||||
|
region: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SignedHeaders {
|
||||||
|
Authorization: string
|
||||||
|
'X-Date': string
|
||||||
|
'X-Content-Sha256': string
|
||||||
|
Host: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModelInfo {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
created?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListModelsResult {
|
||||||
|
models: ModelInfo[]
|
||||||
|
total?: number
|
||||||
|
warnings?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom error class
|
||||||
|
class VolcengineServiceError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public readonly cause?: unknown
|
||||||
|
) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'VolcengineServiceError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Volcengine API Signing Service
|
||||||
|
*
|
||||||
|
* Implements HMAC-SHA256 signing algorithm for Volcengine API authentication.
|
||||||
|
* Securely stores credentials using Electron's safeStorage.
|
||||||
|
*/
|
||||||
|
class VolcengineService {
|
||||||
|
private readonly credentialsFilePath: string
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.credentialsFilePath = this.getCredentialsFilePath()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path for storing encrypted credentials
|
||||||
|
*/
|
||||||
|
private getCredentialsFilePath(): string {
|
||||||
|
const oldPath = path.join(app.getPath('userData'), CONFIG.CREDENTIALS_FILE_NAME)
|
||||||
|
if (fs.existsSync(oldPath)) {
|
||||||
|
return oldPath
|
||||||
|
}
|
||||||
|
return path.join(getConfigDir(), CONFIG.CREDENTIALS_FILE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= Cryptographic Helper Methods =============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate SHA256 hash of data and return hex encoded string
|
||||||
|
*/
|
||||||
|
private sha256Hash(data: string | Buffer): string {
|
||||||
|
return crypto.createHash('sha256').update(data).digest('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate HMAC-SHA256 and return buffer
|
||||||
|
*/
|
||||||
|
private hmacSha256(key: Buffer | string, data: string): Buffer {
|
||||||
|
return crypto.createHmac('sha256', key).update(data, 'utf8').digest()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate HMAC-SHA256 and return hex encoded string
|
||||||
|
*/
|
||||||
|
private hmacSha256Hex(key: Buffer | string, data: string): string {
|
||||||
|
return crypto.createHmac('sha256', key).update(data, 'utf8').digest('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL encode according to RFC3986
|
||||||
|
*/
|
||||||
|
private uriEncode(str: string, encodeSlash: boolean = true): string {
|
||||||
|
if (!str) return ''
|
||||||
|
|
||||||
|
return str
|
||||||
|
.split('')
|
||||||
|
.map((char) => {
|
||||||
|
if (
|
||||||
|
(char >= 'A' && char <= 'Z') ||
|
||||||
|
(char >= 'a' && char <= 'z') ||
|
||||||
|
(char >= '0' && char <= '9') ||
|
||||||
|
char === '_' ||
|
||||||
|
char === '-' ||
|
||||||
|
char === '~' ||
|
||||||
|
char === '.'
|
||||||
|
) {
|
||||||
|
return char
|
||||||
|
}
|
||||||
|
if (char === '/' && !encodeSlash) {
|
||||||
|
return char
|
||||||
|
}
|
||||||
|
return encodeURIComponent(char)
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= Signing Implementation =============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current UTC time in ISO8601 format (YYYYMMDD'T'HHMMSS'Z')
|
||||||
|
*/
|
||||||
|
private getIso8601DateTime(): string {
|
||||||
|
const now = new Date()
|
||||||
|
return now
|
||||||
|
.toISOString()
|
||||||
|
.replace(/[-:]/g, '')
|
||||||
|
.replace(/\.\d{3}/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get date portion from datetime (YYYYMMDD)
|
||||||
|
*/
|
||||||
|
private getDateFromDateTime(dateTime: string): string {
|
||||||
|
return dateTime.substring(0, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build canonical query string from query parameters
|
||||||
|
*/
|
||||||
|
private buildCanonicalQueryString(query: Record<string, string>): string {
|
||||||
|
if (!query || Object.keys(query).length === 0) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(query)
|
||||||
|
.sort()
|
||||||
|
.map((key) => `${this.uriEncode(key)}=${this.uriEncode(query[key])}`)
|
||||||
|
.join('&')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build canonical headers string
|
||||||
|
*/
|
||||||
|
private buildCanonicalHeaders(headers: Record<string, string>): {
|
||||||
|
canonicalHeaders: string
|
||||||
|
signedHeaders: string
|
||||||
|
} {
|
||||||
|
const sortedKeys = Object.keys(headers)
|
||||||
|
.map((k) => k.toLowerCase())
|
||||||
|
.sort()
|
||||||
|
|
||||||
|
const canonicalHeaders = sortedKeys.map((key) => `${key}:${headers[key]?.trim() || ''}`).join('\n') + '\n'
|
||||||
|
|
||||||
|
const signedHeaders = sortedKeys.join(';')
|
||||||
|
|
||||||
|
return { canonicalHeaders, signedHeaders }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the signing key through a series of HMAC operations
|
||||||
|
*
|
||||||
|
* kSecret = SecretAccessKey
|
||||||
|
* kDate = HMAC(kSecret, Date)
|
||||||
|
* kRegion = HMAC(kDate, Region)
|
||||||
|
* kService = HMAC(kRegion, Service)
|
||||||
|
* kSigning = HMAC(kService, "request")
|
||||||
|
*/
|
||||||
|
private deriveSigningKey(secretKey: string, date: string, region: string, service: string): Buffer {
|
||||||
|
const kDate = this.hmacSha256(secretKey, date)
|
||||||
|
const kRegion = this.hmacSha256(kDate, region)
|
||||||
|
const kService = this.hmacSha256(kRegion, service)
|
||||||
|
const kSigning = this.hmacSha256(kService, CONFIG.REQUEST_TYPE)
|
||||||
|
return kSigning
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create canonical request string
|
||||||
|
*
|
||||||
|
* CanonicalRequest =
|
||||||
|
* HTTPRequestMethod + '\n' +
|
||||||
|
* CanonicalURI + '\n' +
|
||||||
|
* CanonicalQueryString + '\n' +
|
||||||
|
* CanonicalHeaders + '\n' +
|
||||||
|
* SignedHeaders + '\n' +
|
||||||
|
* HexEncode(Hash(RequestPayload))
|
||||||
|
*/
|
||||||
|
private createCanonicalRequest(
|
||||||
|
method: string,
|
||||||
|
canonicalUri: string,
|
||||||
|
canonicalQueryString: string,
|
||||||
|
canonicalHeaders: string,
|
||||||
|
signedHeaders: string,
|
||||||
|
payloadHash: string
|
||||||
|
): string {
|
||||||
|
return [method, canonicalUri, canonicalQueryString, canonicalHeaders, signedHeaders, payloadHash].join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create string to sign
|
||||||
|
*
|
||||||
|
* StringToSign =
|
||||||
|
* Algorithm + '\n' +
|
||||||
|
* RequestDateTime + '\n' +
|
||||||
|
* CredentialScope + '\n' +
|
||||||
|
* HexEncode(Hash(CanonicalRequest))
|
||||||
|
*/
|
||||||
|
private createStringToSign(dateTime: string, credentialScope: string, canonicalRequest: string): string {
|
||||||
|
const hashedCanonicalRequest = this.sha256Hash(canonicalRequest)
|
||||||
|
return [CONFIG.ALGORITHM, dateTime, credentialScope, hashedCanonicalRequest].join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate signature for the request
|
||||||
|
*/
|
||||||
|
private generateSignature(params: SignedRequestParams, credentials: VolcengineCredentials): SignedHeaders {
|
||||||
|
const { method, host, path: requestPath, query, body, service, region } = params
|
||||||
|
|
||||||
|
// Step 1: Prepare datetime
|
||||||
|
const dateTime = this.getIso8601DateTime()
|
||||||
|
const date = this.getDateFromDateTime(dateTime)
|
||||||
|
|
||||||
|
// Step 2: Calculate payload hash
|
||||||
|
const payloadHash = this.sha256Hash(body || '')
|
||||||
|
|
||||||
|
// Step 3: Prepare headers for signing
|
||||||
|
const headersToSign: Record<string, string> = {
|
||||||
|
host: host,
|
||||||
|
'x-date': dateTime,
|
||||||
|
'x-content-sha256': payloadHash,
|
||||||
|
'content-type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Build canonical components
|
||||||
|
const canonicalUri = this.uriEncode(requestPath, false) || '/'
|
||||||
|
const canonicalQueryString = this.buildCanonicalQueryString(query)
|
||||||
|
const { canonicalHeaders, signedHeaders } = this.buildCanonicalHeaders(headersToSign)
|
||||||
|
|
||||||
|
// Step 5: Create canonical request
|
||||||
|
const canonicalRequest = this.createCanonicalRequest(
|
||||||
|
method.toUpperCase(),
|
||||||
|
canonicalUri,
|
||||||
|
canonicalQueryString,
|
||||||
|
canonicalHeaders,
|
||||||
|
signedHeaders,
|
||||||
|
payloadHash
|
||||||
|
)
|
||||||
|
|
||||||
|
// Step 6: Create credential scope and string to sign
|
||||||
|
const credentialScope = `${date}/${region}/${service}/${CONFIG.REQUEST_TYPE}`
|
||||||
|
const stringToSign = this.createStringToSign(dateTime, credentialScope, canonicalRequest)
|
||||||
|
|
||||||
|
// Step 7: Calculate signature
|
||||||
|
const signingKey = this.deriveSigningKey(credentials.secretAccessKey, date, region, service)
|
||||||
|
const signature = this.hmacSha256Hex(signingKey, stringToSign)
|
||||||
|
|
||||||
|
// Step 8: Build authorization header
|
||||||
|
const authorization = `${CONFIG.ALGORITHM} Credential=${credentials.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
|
||||||
|
|
||||||
|
return {
|
||||||
|
Authorization: authorization,
|
||||||
|
'X-Date': dateTime,
|
||||||
|
'X-Content-Sha256': payloadHash,
|
||||||
|
Host: host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= Credential Management =============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save credentials securely using Electron's safeStorage
|
||||||
|
*/
|
||||||
|
public saveCredentials = async (
|
||||||
|
_: Electron.IpcMainInvokeEvent,
|
||||||
|
accessKeyId: string,
|
||||||
|
secretAccessKey: string
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if (!accessKeyId || !secretAccessKey) {
|
||||||
|
throw new VolcengineServiceError('Access Key ID and Secret Access Key are required')
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials: VolcengineCredentials = { accessKeyId, secretAccessKey }
|
||||||
|
const credentialsJson = JSON.stringify(credentials)
|
||||||
|
const encryptedData = safeStorage.encryptString(credentialsJson)
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
const dir = path.dirname(this.credentialsFilePath)
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
await fs.promises.mkdir(dir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.promises.writeFile(this.credentialsFilePath, encryptedData)
|
||||||
|
logger.info('Volcengine credentials saved successfully')
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to save Volcengine credentials:', error as Error)
|
||||||
|
throw new VolcengineServiceError('Failed to save credentials', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load credentials from encrypted storage
|
||||||
|
* @throws VolcengineServiceError if credentials file exists but is corrupted
|
||||||
|
*/
|
||||||
|
private async loadCredentials(): Promise<VolcengineCredentials | null> {
|
||||||
|
if (!fs.existsSync(this.credentialsFilePath)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const encryptedData = await fs.promises.readFile(this.credentialsFilePath)
|
||||||
|
const decryptedJson = safeStorage.decryptString(Buffer.from(encryptedData))
|
||||||
|
return JSON.parse(decryptedJson) as VolcengineCredentials
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to load Volcengine credentials:', error as Error)
|
||||||
|
throw new VolcengineServiceError(
|
||||||
|
'Credentials file exists but could not be loaded. Please re-enter your credentials.',
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if credentials exist
|
||||||
|
*/
|
||||||
|
public hasCredentials = async (): Promise<boolean> => {
|
||||||
|
return fs.existsSync(this.credentialsFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear stored credentials
|
||||||
|
*/
|
||||||
|
public clearCredentials = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(this.credentialsFilePath)) {
|
||||||
|
await fs.promises.unlink(this.credentialsFilePath)
|
||||||
|
logger.info('Volcengine credentials cleared')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to clear Volcengine credentials:', error as Error)
|
||||||
|
throw new VolcengineServiceError('Failed to clear credentials', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= API Methods =============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a signed request to Volcengine API
|
||||||
|
*/
|
||||||
|
private async makeSignedRequest<T>(
|
||||||
|
method: 'GET' | 'POST',
|
||||||
|
host: string,
|
||||||
|
path: string,
|
||||||
|
action: string,
|
||||||
|
version: string,
|
||||||
|
query?: Record<string, string>,
|
||||||
|
body?: Record<string, unknown>,
|
||||||
|
service: string = CONFIG.SERVICE_NAME,
|
||||||
|
region: string = CONFIG.DEFAULT_REGION
|
||||||
|
): Promise<T> {
|
||||||
|
const credentials = await this.loadCredentials()
|
||||||
|
if (!credentials) {
|
||||||
|
throw new VolcengineServiceError('No credentials found. Please save credentials first.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullQuery: Record<string, string> = {
|
||||||
|
Action: action,
|
||||||
|
Version: version,
|
||||||
|
...query
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyString = body ? JSON.stringify(body) : ''
|
||||||
|
|
||||||
|
const signedHeaders = this.generateSignature(
|
||||||
|
{
|
||||||
|
method,
|
||||||
|
host,
|
||||||
|
path,
|
||||||
|
query: fullQuery,
|
||||||
|
headers: {},
|
||||||
|
body: bodyString,
|
||||||
|
service,
|
||||||
|
region
|
||||||
|
},
|
||||||
|
credentials
|
||||||
|
)
|
||||||
|
|
||||||
|
// Build URL with query string (use simple encoding for URL, canonical encoding is only for signature)
|
||||||
|
const urlParams = new URLSearchParams(fullQuery)
|
||||||
|
const url = `https://${host}${path}?${urlParams.toString()}`
|
||||||
|
|
||||||
|
const requestHeaders: Record<string, string> = {
|
||||||
|
...CONFIG.DEFAULT_HEADERS,
|
||||||
|
Authorization: signedHeaders.Authorization,
|
||||||
|
'X-Date': signedHeaders['X-Date'],
|
||||||
|
'X-Content-Sha256': signedHeaders['X-Content-Sha256']
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Making Volcengine API request', { url, method, action })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await net.fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: requestHeaders,
|
||||||
|
body: method === 'POST' && bodyString ? bodyString : undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text()
|
||||||
|
logger.error(`Volcengine API error: ${response.status}`, { errorText })
|
||||||
|
throw new VolcengineServiceError(`API request failed: ${response.status} - ${errorText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await response.json()) as T
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof VolcengineServiceError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
logger.error('Volcengine API request failed:', error as Error)
|
||||||
|
throw new VolcengineServiceError('API request failed', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List foundation models from Volcengine ARK
|
||||||
|
*/
|
||||||
|
private async listFoundationModels(region: string = CONFIG.DEFAULT_REGION): Promise<ListFoundationModelsResponse> {
|
||||||
|
const requestBody: ListFoundationModelsRequest = {
|
||||||
|
PageNumber: 1,
|
||||||
|
PageSize: CONFIG.DEFAULT_PAGE_SIZE
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.makeSignedRequest<unknown>(
|
||||||
|
'POST',
|
||||||
|
CONFIG.API_URLS.ARK_HOST,
|
||||||
|
'/',
|
||||||
|
'ListFoundationModels',
|
||||||
|
CONFIG.API_VERSION,
|
||||||
|
{},
|
||||||
|
requestBody,
|
||||||
|
CONFIG.SERVICE_NAME,
|
||||||
|
region
|
||||||
|
)
|
||||||
|
|
||||||
|
return ListFoundationModelsResponseSchema.parse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List user-created endpoints from Volcengine ARK
|
||||||
|
*/
|
||||||
|
private async listEndpoints(
|
||||||
|
projectName?: string,
|
||||||
|
region: string = CONFIG.DEFAULT_REGION
|
||||||
|
): Promise<ListEndpointsResponse> {
|
||||||
|
const requestBody: ListEndpointsRequest = {
|
||||||
|
ProjectName: projectName || 'default',
|
||||||
|
PageNumber: 1,
|
||||||
|
PageSize: CONFIG.DEFAULT_PAGE_SIZE
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.makeSignedRequest<unknown>(
|
||||||
|
'POST',
|
||||||
|
CONFIG.API_URLS.ARK_HOST,
|
||||||
|
'/',
|
||||||
|
'ListEndpoints',
|
||||||
|
CONFIG.API_VERSION,
|
||||||
|
{},
|
||||||
|
requestBody,
|
||||||
|
CONFIG.SERVICE_NAME,
|
||||||
|
region
|
||||||
|
)
|
||||||
|
|
||||||
|
return ListEndpointsResponseSchema.parse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all available models from Volcengine ARK
|
||||||
|
* Combines foundation models and user-created endpoints
|
||||||
|
*/
|
||||||
|
public listModels = async (
|
||||||
|
_?: Electron.IpcMainInvokeEvent,
|
||||||
|
projectName?: string,
|
||||||
|
region?: string
|
||||||
|
): Promise<ListModelsResult> => {
|
||||||
|
try {
|
||||||
|
const effectiveRegion = region || CONFIG.DEFAULT_REGION
|
||||||
|
const [foundationModelsResult, endpointsResult] = await Promise.allSettled([
|
||||||
|
this.listFoundationModels(effectiveRegion),
|
||||||
|
this.listEndpoints(projectName, effectiveRegion)
|
||||||
|
])
|
||||||
|
|
||||||
|
const models: ModelInfo[] = []
|
||||||
|
const warnings: string[] = []
|
||||||
|
|
||||||
|
if (foundationModelsResult.status === 'fulfilled') {
|
||||||
|
const foundationModels = foundationModelsResult.value
|
||||||
|
for (const item of foundationModels.Result.Items) {
|
||||||
|
models.push({
|
||||||
|
id: item.Name,
|
||||||
|
name: item.DisplayName || item.Name,
|
||||||
|
description: item.Description
|
||||||
|
})
|
||||||
|
}
|
||||||
|
logger.info(`Found ${foundationModels.Result.Items.length} foundation models`)
|
||||||
|
} else {
|
||||||
|
const errorMsg = `Failed to fetch foundation models: ${foundationModelsResult.reason}`
|
||||||
|
logger.warn(errorMsg)
|
||||||
|
warnings.push(errorMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process endpoints
|
||||||
|
if (endpointsResult.status === 'fulfilled') {
|
||||||
|
const endpoints = endpointsResult.value
|
||||||
|
for (const item of endpoints.Result.Items) {
|
||||||
|
const modelRef = item.ModelReference
|
||||||
|
const foundationModelName = modelRef?.FoundationModel?.Name
|
||||||
|
const modelVersion = modelRef?.FoundationModel?.ModelVersion
|
||||||
|
const customModelId = modelRef?.CustomModelId
|
||||||
|
|
||||||
|
let displayName = item.Name || item.Id
|
||||||
|
if (foundationModelName) {
|
||||||
|
displayName = modelVersion ? `${foundationModelName} (${modelVersion})` : foundationModelName
|
||||||
|
} else if (customModelId) {
|
||||||
|
displayName = customModelId
|
||||||
|
}
|
||||||
|
|
||||||
|
models.push({
|
||||||
|
id: item.Id,
|
||||||
|
name: displayName,
|
||||||
|
description: item.Description
|
||||||
|
})
|
||||||
|
}
|
||||||
|
logger.info(`Found ${endpoints.Result.Items.length} endpoints`)
|
||||||
|
} else {
|
||||||
|
const errorMsg = `Failed to fetch endpoints: ${endpointsResult.reason}`
|
||||||
|
logger.warn(errorMsg)
|
||||||
|
warnings.push(errorMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If both failed, throw error
|
||||||
|
if (foundationModelsResult.status === 'rejected' && endpointsResult.status === 'rejected') {
|
||||||
|
throw new VolcengineServiceError('Failed to fetch both foundation models and endpoints')
|
||||||
|
}
|
||||||
|
|
||||||
|
const total =
|
||||||
|
(foundationModelsResult.status === 'fulfilled' ? foundationModelsResult.value.Result.TotalCount : 0) +
|
||||||
|
(endpointsResult.status === 'fulfilled' ? endpointsResult.value.Result.TotalCount : 0)
|
||||||
|
|
||||||
|
logger.info(`Total models found: ${models.length}`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
models,
|
||||||
|
total,
|
||||||
|
warnings: warnings.length > 0 ? warnings : undefined
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to list Volcengine models:', error as Error)
|
||||||
|
throw new VolcengineServiceError('Failed to list models', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get authorization headers for external use
|
||||||
|
* This allows the renderer process to make direct API calls with proper authentication
|
||||||
|
*/
|
||||||
|
public getAuthHeaders = async (
|
||||||
|
_: Electron.IpcMainInvokeEvent,
|
||||||
|
params: {
|
||||||
|
method: 'GET' | 'POST'
|
||||||
|
host: string
|
||||||
|
path: string
|
||||||
|
query?: Record<string, string>
|
||||||
|
body?: string
|
||||||
|
service?: string
|
||||||
|
region?: string
|
||||||
|
}
|
||||||
|
): Promise<SignedHeaders> => {
|
||||||
|
const credentials = await this.loadCredentials()
|
||||||
|
if (!credentials) {
|
||||||
|
throw new VolcengineServiceError('No credentials found. Please save credentials first.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.generateSignature(
|
||||||
|
{
|
||||||
|
method: params.method,
|
||||||
|
host: params.host,
|
||||||
|
path: params.path,
|
||||||
|
query: params.query || {},
|
||||||
|
headers: {},
|
||||||
|
body: params.body,
|
||||||
|
service: params.service || CONFIG.SERVICE_NAME,
|
||||||
|
region: params.region || CONFIG.DEFAULT_REGION
|
||||||
|
},
|
||||||
|
credentials
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a generic signed API request
|
||||||
|
* This is a more flexible method that allows custom API calls
|
||||||
|
*/
|
||||||
|
public makeRequest = async (
|
||||||
|
_: Electron.IpcMainInvokeEvent,
|
||||||
|
params: {
|
||||||
|
method: 'GET' | 'POST'
|
||||||
|
host: string
|
||||||
|
path: string
|
||||||
|
action: string
|
||||||
|
version: string
|
||||||
|
query?: Record<string, string>
|
||||||
|
body?: Record<string, unknown>
|
||||||
|
service?: string
|
||||||
|
region?: string
|
||||||
|
}
|
||||||
|
): Promise<unknown> => {
|
||||||
|
return this.makeSignedRequest(
|
||||||
|
params.method,
|
||||||
|
params.host,
|
||||||
|
params.path,
|
||||||
|
params.action,
|
||||||
|
params.version,
|
||||||
|
params.query,
|
||||||
|
params.body,
|
||||||
|
params.service || CONFIG.SERVICE_NAME,
|
||||||
|
params.region || CONFIG.DEFAULT_REGION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new VolcengineService()
|
||||||
@@ -572,6 +572,41 @@ const api = {
|
|||||||
status: () => ipcRenderer.invoke(IpcChannel.WebSocket_Status),
|
status: () => ipcRenderer.invoke(IpcChannel.WebSocket_Status),
|
||||||
sendFile: (filePath: string) => ipcRenderer.invoke(IpcChannel.WebSocket_SendFile, filePath),
|
sendFile: (filePath: string) => ipcRenderer.invoke(IpcChannel.WebSocket_SendFile, filePath),
|
||||||
getAllCandidates: () => ipcRenderer.invoke(IpcChannel.WebSocket_GetAllCandidates)
|
getAllCandidates: () => ipcRenderer.invoke(IpcChannel.WebSocket_GetAllCandidates)
|
||||||
|
},
|
||||||
|
volcengine: {
|
||||||
|
saveCredentials: (accessKeyId: string, secretAccessKey: string): Promise<void> =>
|
||||||
|
ipcRenderer.invoke(IpcChannel.Volcengine_SaveCredentials, accessKeyId, secretAccessKey),
|
||||||
|
hasCredentials: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.Volcengine_HasCredentials),
|
||||||
|
clearCredentials: (): Promise<void> => ipcRenderer.invoke(IpcChannel.Volcengine_ClearCredentials),
|
||||||
|
listModels: (
|
||||||
|
projectName?: string,
|
||||||
|
region?: string
|
||||||
|
): Promise<{
|
||||||
|
models: Array<{ id: string; name: string; description?: string; created?: number }>
|
||||||
|
total?: number
|
||||||
|
warnings?: string[]
|
||||||
|
}> => ipcRenderer.invoke(IpcChannel.Volcengine_ListModels, projectName, region),
|
||||||
|
getAuthHeaders: (params: {
|
||||||
|
method: 'GET' | 'POST'
|
||||||
|
host: string
|
||||||
|
path: string
|
||||||
|
query?: Record<string, string>
|
||||||
|
body?: string
|
||||||
|
service?: string
|
||||||
|
region?: string
|
||||||
|
}): Promise<{ Authorization: string; 'X-Date': string; 'X-Content-Sha256': string; Host: string }> =>
|
||||||
|
ipcRenderer.invoke(IpcChannel.Volcengine_GetAuthHeaders, params),
|
||||||
|
makeRequest: (params: {
|
||||||
|
method: 'GET' | 'POST'
|
||||||
|
host: string
|
||||||
|
path: string
|
||||||
|
action: string
|
||||||
|
version: string
|
||||||
|
query?: Record<string, string>
|
||||||
|
body?: Record<string, unknown>
|
||||||
|
service?: string
|
||||||
|
region?: string
|
||||||
|
}): Promise<unknown> => ipcRenderer.invoke(IpcChannel.Volcengine_MakeRequest, params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { OpenAIAPIClient } from './openai/OpenAIApiClient'
|
|||||||
import { OpenAIResponseAPIClient } from './openai/OpenAIResponseAPIClient'
|
import { OpenAIResponseAPIClient } from './openai/OpenAIResponseAPIClient'
|
||||||
import { OVMSClient } from './ovms/OVMSClient'
|
import { OVMSClient } from './ovms/OVMSClient'
|
||||||
import { PPIOAPIClient } from './ppio/PPIOAPIClient'
|
import { PPIOAPIClient } from './ppio/PPIOAPIClient'
|
||||||
|
import { VolcengineAPIClient } from './volcengine/VolcengineAPIClient'
|
||||||
import { ZhipuAPIClient } from './zhipu/ZhipuAPIClient'
|
import { ZhipuAPIClient } from './zhipu/ZhipuAPIClient'
|
||||||
|
|
||||||
const logger = loggerService.withContext('ApiClientFactory')
|
const logger = loggerService.withContext('ApiClientFactory')
|
||||||
@@ -64,6 +65,12 @@ export class ApiClientFactory {
|
|||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (provider.id === 'doubao') {
|
||||||
|
logger.debug(`Creating VolcengineAPIClient for provider: ${provider.id}`)
|
||||||
|
instance = new VolcengineAPIClient(provider) as BaseApiClient
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
if (provider.id === 'ovms') {
|
if (provider.id === 'ovms') {
|
||||||
logger.debug(`Creating OVMSClient for provider: ${provider.id}`)
|
logger.debug(`Creating OVMSClient for provider: ${provider.id}`)
|
||||||
instance = new OVMSClient(provider) as BaseApiClient
|
instance = new OVMSClient(provider) as BaseApiClient
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import type OpenAI from '@cherrystudio/openai'
|
||||||
|
import { loggerService } from '@logger'
|
||||||
|
import { getVolcengineProjectName, getVolcengineRegion } from '@renderer/hooks/useVolcengine'
|
||||||
|
import type { Provider } from '@renderer/types'
|
||||||
|
|
||||||
|
import { OpenAIAPIClient } from '../openai/OpenAIApiClient'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('VolcengineAPIClient')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Volcengine (Doubao) API Client
|
||||||
|
*
|
||||||
|
* Extends OpenAIAPIClient for standard chat completions (OpenAI-compatible),
|
||||||
|
* but overrides listModels to use Volcengine's signed API via IPC.
|
||||||
|
*/
|
||||||
|
export class VolcengineAPIClient extends OpenAIAPIClient {
|
||||||
|
constructor(provider: Provider) {
|
||||||
|
super(provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List models using Volcengine's signed API
|
||||||
|
* This calls the main process VolcengineService which handles HMAC-SHA256 signing
|
||||||
|
*/
|
||||||
|
override async listModels(): Promise<OpenAI.Models.Model[]> {
|
||||||
|
try {
|
||||||
|
const hasCredentials = await window.api.volcengine.hasCredentials()
|
||||||
|
|
||||||
|
if (!hasCredentials) {
|
||||||
|
logger.info('Volcengine credentials not configured, falling back to OpenAI-compatible list')
|
||||||
|
// Fall back to standard OpenAI-compatible API if no Volcengine credentials
|
||||||
|
return super.listModels()
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Fetching models from Volcengine API using signed request')
|
||||||
|
|
||||||
|
const projectName = getVolcengineProjectName()
|
||||||
|
const region = getVolcengineRegion()
|
||||||
|
const response = await window.api.volcengine.listModels(projectName, region)
|
||||||
|
|
||||||
|
if (!response || !response.models) {
|
||||||
|
logger.warn('Empty response from Volcengine listModels')
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify user of any partial failures
|
||||||
|
if (response.warnings && response.warnings.length > 0) {
|
||||||
|
for (const warning of response.warnings) {
|
||||||
|
logger.warn(warning)
|
||||||
|
}
|
||||||
|
window.toast?.warning('Some Volcengine models could not be fetched. Check logs for details.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const models: OpenAI.Models.Model[] = response.models.map((model) => ({
|
||||||
|
id: model.id,
|
||||||
|
object: 'model' as const,
|
||||||
|
created: model.created || Math.floor(Date.now() / 1000),
|
||||||
|
owned_by: 'volcengine',
|
||||||
|
// @ts-ignore - description is used by UI to display model name
|
||||||
|
name: model.name || model.id
|
||||||
|
}))
|
||||||
|
|
||||||
|
logger.info(`Found ${models.length} models from Volcengine API`)
|
||||||
|
return models
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to list Volcengine models:', error as Error)
|
||||||
|
// Notify user before falling back
|
||||||
|
window.toast?.warning('Failed to fetch Volcengine models. Check credentials if this persists.')
|
||||||
|
// Fall back to standard OpenAI-compatible API on error
|
||||||
|
logger.info('Falling back to OpenAI-compatible model list')
|
||||||
|
return super.listModels()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/renderer/src/hooks/useVolcengine.ts
Normal file
26
src/renderer/src/hooks/useVolcengine.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import store, { useAppSelector } from '@renderer/store'
|
||||||
|
import { setVolcengineProjectName, setVolcengineRegion } from '@renderer/store/llm'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
|
||||||
|
export function useVolcengineSettings() {
|
||||||
|
const settings = useAppSelector((state) => state.llm.settings.volcengine)
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
return {
|
||||||
|
...settings,
|
||||||
|
setRegion: (region: string) => dispatch(setVolcengineRegion(region)),
|
||||||
|
setProjectName: (projectName: string) => dispatch(setVolcengineProjectName(projectName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVolcengineSettings() {
|
||||||
|
return store.getState().llm.settings.volcengine
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVolcengineRegion() {
|
||||||
|
return store.getState().llm.settings.volcengine.region
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVolcengineProjectName() {
|
||||||
|
return store.getState().llm.settings.volcengine.projectName
|
||||||
|
}
|
||||||
@@ -4510,6 +4510,23 @@
|
|||||||
"private_key_placeholder": "Enter Service Account private key",
|
"private_key_placeholder": "Enter Service Account private key",
|
||||||
"title": "Service Account Configuration"
|
"title": "Service Account Configuration"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"volcengine": {
|
||||||
|
"access_key_id": "Access Key ID",
|
||||||
|
"access_key_id_help": "Your Volcengine Access Key ID",
|
||||||
|
"clear_credentials": "Clear Credentials",
|
||||||
|
"credentials_cleared": "Credentials cleared",
|
||||||
|
"credentials_required": "Please fill in Access Key ID and Secret Access Key",
|
||||||
|
"credentials_saved": "Credentials saved",
|
||||||
|
"description": "Volcengine is ByteDance's cloud service platform, providing Doubao and other large language model services. Please use IAM user's Access Key for authentication, do not use the root user credentials.",
|
||||||
|
"project_name": "Project Name",
|
||||||
|
"project_name_help": "Project name for endpoint filtering, default is 'default'",
|
||||||
|
"region": "Region",
|
||||||
|
"region_help": "Service region, e.g., cn-beijing",
|
||||||
|
"save_credentials": "Save Credentials",
|
||||||
|
"secret_access_key": "Secret Access Key",
|
||||||
|
"secret_access_key_help": "Your Volcengine Secret Access Key, please keep it secure",
|
||||||
|
"title": "Volcengine Configuration"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proxy": {
|
"proxy": {
|
||||||
|
|||||||
@@ -4510,6 +4510,23 @@
|
|||||||
"private_key_placeholder": "请输入 Service Account 私钥",
|
"private_key_placeholder": "请输入 Service Account 私钥",
|
||||||
"title": "Service Account 配置"
|
"title": "Service Account 配置"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"volcengine": {
|
||||||
|
"access_key_id": "Access Key ID",
|
||||||
|
"access_key_id_help": "您的火山引擎 Access Key ID",
|
||||||
|
"clear_credentials": "清除凭证",
|
||||||
|
"credentials_cleared": "凭证已清除",
|
||||||
|
"credentials_required": "请填写 Access Key ID 和 Secret Access Key",
|
||||||
|
"credentials_saved": "凭证已保存",
|
||||||
|
"description": "火山引擎是字节跳动旗下的云服务平台,提供豆包等大语言模型服务。请使用 IAM 子用户的 Access Key 进行身份验证,不要使用主账号的根用户密钥。",
|
||||||
|
"project_name": "项目名称",
|
||||||
|
"project_name_help": "用于筛选推理接入点的项目名称,默认为 'default'",
|
||||||
|
"region": "地域",
|
||||||
|
"region_help": "服务地域,例如 cn-beijing",
|
||||||
|
"save_credentials": "保存凭证",
|
||||||
|
"secret_access_key": "Secret Access Key",
|
||||||
|
"secret_access_key_help": "您的火山引擎 Secret Access Key,请妥善保管",
|
||||||
|
"title": "火山引擎配置"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proxy": {
|
"proxy": {
|
||||||
|
|||||||
@@ -4510,6 +4510,23 @@
|
|||||||
"private_key_placeholder": "輸入服務帳戶私密金鑰",
|
"private_key_placeholder": "輸入服務帳戶私密金鑰",
|
||||||
"title": "服務帳戶設定"
|
"title": "服務帳戶設定"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"volcengine": {
|
||||||
|
"access_key_id": "Access Key ID",
|
||||||
|
"access_key_id_help": "您的火山引擎 Access Key ID",
|
||||||
|
"clear_credentials": "清除憑證",
|
||||||
|
"credentials_cleared": "憑證已清除",
|
||||||
|
"credentials_required": "請填寫 Access Key ID 和 Secret Access Key",
|
||||||
|
"credentials_saved": "憑證已儲存",
|
||||||
|
"description": "火山引擎是字節跳動旗下的雲端服務平台,提供豆包等大型語言模型服務。請使用 IAM 子用戶的 Access Key 進行身份驗證,不要使用主帳號的根用戶密鑰。",
|
||||||
|
"project_name": "專案名稱",
|
||||||
|
"project_name_help": "用於篩選推論接入點的專案名稱,預設為 'default'",
|
||||||
|
"region": "地區",
|
||||||
|
"region_help": "服務地區,例如 cn-beijing",
|
||||||
|
"save_credentials": "儲存憑證",
|
||||||
|
"secret_access_key": "Secret Access Key",
|
||||||
|
"secret_access_key_help": "您的火山引擎 Secret Access Key,請妥善保管",
|
||||||
|
"title": "火山引擎設定"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proxy": {
|
"proxy": {
|
||||||
|
|||||||
@@ -4510,6 +4510,23 @@
|
|||||||
"private_key_placeholder": "Service Account-Privat-Schlüssel eingeben",
|
"private_key_placeholder": "Service Account-Privat-Schlüssel eingeben",
|
||||||
"title": "Service Account-Konfiguration"
|
"title": "Service Account-Konfiguration"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"volcengine": {
|
||||||
|
"access_key_id": "[to be translated]:Access Key ID",
|
||||||
|
"access_key_id_help": "[to be translated]:Your Volcengine Access Key ID",
|
||||||
|
"clear_credentials": "[to be translated]:Clear Credentials",
|
||||||
|
"credentials_cleared": "[to be translated]:Credentials cleared",
|
||||||
|
"credentials_required": "[to be translated]:Please fill in Access Key ID and Secret Access Key",
|
||||||
|
"credentials_saved": "[to be translated]:Credentials saved",
|
||||||
|
"description": "[to be translated]:Volcengine is ByteDance's cloud service platform, providing Doubao and other large language model services. Use Access Key for authentication to fetch model list.",
|
||||||
|
"project_name": "[to be translated]:Project Name",
|
||||||
|
"project_name_help": "[to be translated]:Project name for endpoint filtering, default is 'default'",
|
||||||
|
"region": "[to be translated]:Region",
|
||||||
|
"region_help": "[to be translated]:Service region, e.g., cn-beijing",
|
||||||
|
"save_credentials": "[to be translated]:Save Credentials",
|
||||||
|
"secret_access_key": "[to be translated]:Secret Access Key",
|
||||||
|
"secret_access_key_help": "[to be translated]:Your Volcengine Secret Access Key, please keep it secure",
|
||||||
|
"title": "[to be translated]:Volcengine Configuration"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proxy": {
|
"proxy": {
|
||||||
|
|||||||
@@ -4510,6 +4510,23 @@
|
|||||||
"private_key_placeholder": "Παρακαλώ εισάγετε το ιδιωτικό κλειδί του λογαριασμού υπηρεσίας",
|
"private_key_placeholder": "Παρακαλώ εισάγετε το ιδιωτικό κλειδί του λογαριασμού υπηρεσίας",
|
||||||
"title": "Διαμόρφωση λογαριασμού υπηρεσίας"
|
"title": "Διαμόρφωση λογαριασμού υπηρεσίας"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"volcengine": {
|
||||||
|
"access_key_id": "[to be translated]:Access Key ID",
|
||||||
|
"access_key_id_help": "[to be translated]:Your Volcengine Access Key ID",
|
||||||
|
"clear_credentials": "[to be translated]:Clear Credentials",
|
||||||
|
"credentials_cleared": "[to be translated]:Credentials cleared",
|
||||||
|
"credentials_required": "[to be translated]:Please fill in Access Key ID and Secret Access Key",
|
||||||
|
"credentials_saved": "[to be translated]:Credentials saved",
|
||||||
|
"description": "[to be translated]:Volcengine is ByteDance's cloud service platform, providing Doubao and other large language model services. Use Access Key for authentication to fetch model list.",
|
||||||
|
"project_name": "[to be translated]:Project Name",
|
||||||
|
"project_name_help": "[to be translated]:Project name for endpoint filtering, default is 'default'",
|
||||||
|
"region": "[to be translated]:Region",
|
||||||
|
"region_help": "[to be translated]:Service region, e.g., cn-beijing",
|
||||||
|
"save_credentials": "[to be translated]:Save Credentials",
|
||||||
|
"secret_access_key": "[to be translated]:Secret Access Key",
|
||||||
|
"secret_access_key_help": "[to be translated]:Your Volcengine Secret Access Key, please keep it secure",
|
||||||
|
"title": "[to be translated]:Volcengine Configuration"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proxy": {
|
"proxy": {
|
||||||
|
|||||||
@@ -4510,6 +4510,23 @@
|
|||||||
"private_key_placeholder": "Ingrese la clave privada de Service Account",
|
"private_key_placeholder": "Ingrese la clave privada de Service Account",
|
||||||
"title": "Configuración de Service Account"
|
"title": "Configuración de Service Account"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"volcengine": {
|
||||||
|
"access_key_id": "[to be translated]:Access Key ID",
|
||||||
|
"access_key_id_help": "[to be translated]:Your Volcengine Access Key ID",
|
||||||
|
"clear_credentials": "[to be translated]:Clear Credentials",
|
||||||
|
"credentials_cleared": "[to be translated]:Credentials cleared",
|
||||||
|
"credentials_required": "[to be translated]:Please fill in Access Key ID and Secret Access Key",
|
||||||
|
"credentials_saved": "[to be translated]:Credentials saved",
|
||||||
|
"description": "[to be translated]:Volcengine is ByteDance's cloud service platform, providing Doubao and other large language model services. Use Access Key for authentication to fetch model list.",
|
||||||
|
"project_name": "[to be translated]:Project Name",
|
||||||
|
"project_name_help": "[to be translated]:Project name for endpoint filtering, default is 'default'",
|
||||||
|
"region": "[to be translated]:Region",
|
||||||
|
"region_help": "[to be translated]:Service region, e.g., cn-beijing",
|
||||||
|
"save_credentials": "[to be translated]:Save Credentials",
|
||||||
|
"secret_access_key": "[to be translated]:Secret Access Key",
|
||||||
|
"secret_access_key_help": "[to be translated]:Your Volcengine Secret Access Key, please keep it secure",
|
||||||
|
"title": "[to be translated]:Volcengine Configuration"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proxy": {
|
"proxy": {
|
||||||
|
|||||||
@@ -4510,6 +4510,23 @@
|
|||||||
"private_key_placeholder": "Veuillez saisir la clé privée du compte de service",
|
"private_key_placeholder": "Veuillez saisir la clé privée du compte de service",
|
||||||
"title": "Configuration du compte de service"
|
"title": "Configuration du compte de service"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"volcengine": {
|
||||||
|
"access_key_id": "[to be translated]:Access Key ID",
|
||||||
|
"access_key_id_help": "[to be translated]:Your Volcengine Access Key ID",
|
||||||
|
"clear_credentials": "[to be translated]:Clear Credentials",
|
||||||
|
"credentials_cleared": "[to be translated]:Credentials cleared",
|
||||||
|
"credentials_required": "[to be translated]:Please fill in Access Key ID and Secret Access Key",
|
||||||
|
"credentials_saved": "[to be translated]:Credentials saved",
|
||||||
|
"description": "[to be translated]:Volcengine is ByteDance's cloud service platform, providing Doubao and other large language model services. Use Access Key for authentication to fetch model list.",
|
||||||
|
"project_name": "[to be translated]:Project Name",
|
||||||
|
"project_name_help": "[to be translated]:Project name for endpoint filtering, default is 'default'",
|
||||||
|
"region": "[to be translated]:Region",
|
||||||
|
"region_help": "[to be translated]:Service region, e.g., cn-beijing",
|
||||||
|
"save_credentials": "[to be translated]:Save Credentials",
|
||||||
|
"secret_access_key": "[to be translated]:Secret Access Key",
|
||||||
|
"secret_access_key_help": "[to be translated]:Your Volcengine Secret Access Key, please keep it secure",
|
||||||
|
"title": "[to be translated]:Volcengine Configuration"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proxy": {
|
"proxy": {
|
||||||
|
|||||||
@@ -4510,6 +4510,23 @@
|
|||||||
"private_key_placeholder": "サービスアカウントの秘密鍵を入力してください",
|
"private_key_placeholder": "サービスアカウントの秘密鍵を入力してください",
|
||||||
"title": "サービスアカウント設定"
|
"title": "サービスアカウント設定"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"volcengine": {
|
||||||
|
"access_key_id": "[to be translated]:Access Key ID",
|
||||||
|
"access_key_id_help": "[to be translated]:Your Volcengine Access Key ID",
|
||||||
|
"clear_credentials": "[to be translated]:Clear Credentials",
|
||||||
|
"credentials_cleared": "[to be translated]:Credentials cleared",
|
||||||
|
"credentials_required": "[to be translated]:Please fill in Access Key ID and Secret Access Key",
|
||||||
|
"credentials_saved": "[to be translated]:Credentials saved",
|
||||||
|
"description": "[to be translated]:Volcengine is ByteDance's cloud service platform, providing Doubao and other large language model services. Use Access Key for authentication to fetch model list.",
|
||||||
|
"project_name": "[to be translated]:Project Name",
|
||||||
|
"project_name_help": "[to be translated]:Project name for endpoint filtering, default is 'default'",
|
||||||
|
"region": "[to be translated]:Region",
|
||||||
|
"region_help": "[to be translated]:Service region, e.g., cn-beijing",
|
||||||
|
"save_credentials": "[to be translated]:Save Credentials",
|
||||||
|
"secret_access_key": "[to be translated]:Secret Access Key",
|
||||||
|
"secret_access_key_help": "[to be translated]:Your Volcengine Secret Access Key, please keep it secure",
|
||||||
|
"title": "[to be translated]:Volcengine Configuration"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proxy": {
|
"proxy": {
|
||||||
|
|||||||
@@ -4510,6 +4510,23 @@
|
|||||||
"private_key_placeholder": "Por favor, insira a chave privada da Conta de Serviço",
|
"private_key_placeholder": "Por favor, insira a chave privada da Conta de Serviço",
|
||||||
"title": "Configuração da Conta de Serviço"
|
"title": "Configuração da Conta de Serviço"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"volcengine": {
|
||||||
|
"access_key_id": "[to be translated]:Access Key ID",
|
||||||
|
"access_key_id_help": "[to be translated]:Your Volcengine Access Key ID",
|
||||||
|
"clear_credentials": "[to be translated]:Clear Credentials",
|
||||||
|
"credentials_cleared": "[to be translated]:Credentials cleared",
|
||||||
|
"credentials_required": "[to be translated]:Please fill in Access Key ID and Secret Access Key",
|
||||||
|
"credentials_saved": "[to be translated]:Credentials saved",
|
||||||
|
"description": "[to be translated]:Volcengine is ByteDance's cloud service platform, providing Doubao and other large language model services. Use Access Key for authentication to fetch model list.",
|
||||||
|
"project_name": "[to be translated]:Project Name",
|
||||||
|
"project_name_help": "[to be translated]:Project name for endpoint filtering, default is 'default'",
|
||||||
|
"region": "[to be translated]:Region",
|
||||||
|
"region_help": "[to be translated]:Service region, e.g., cn-beijing",
|
||||||
|
"save_credentials": "[to be translated]:Save Credentials",
|
||||||
|
"secret_access_key": "[to be translated]:Secret Access Key",
|
||||||
|
"secret_access_key_help": "[to be translated]:Your Volcengine Secret Access Key, please keep it secure",
|
||||||
|
"title": "[to be translated]:Volcengine Configuration"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proxy": {
|
"proxy": {
|
||||||
|
|||||||
@@ -4510,6 +4510,23 @@
|
|||||||
"private_key_placeholder": "Введите приватный ключ Service Account",
|
"private_key_placeholder": "Введите приватный ключ Service Account",
|
||||||
"title": "Конфигурация Service Account"
|
"title": "Конфигурация Service Account"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"volcengine": {
|
||||||
|
"access_key_id": "[to be translated]:Access Key ID",
|
||||||
|
"access_key_id_help": "[to be translated]:Your Volcengine Access Key ID",
|
||||||
|
"clear_credentials": "[to be translated]:Clear Credentials",
|
||||||
|
"credentials_cleared": "[to be translated]:Credentials cleared",
|
||||||
|
"credentials_required": "[to be translated]:Please fill in Access Key ID and Secret Access Key",
|
||||||
|
"credentials_saved": "[to be translated]:Credentials saved",
|
||||||
|
"description": "[to be translated]:Volcengine is ByteDance's cloud service platform, providing Doubao and other large language model services. Use Access Key for authentication to fetch model list.",
|
||||||
|
"project_name": "[to be translated]:Project Name",
|
||||||
|
"project_name_help": "[to be translated]:Project name for endpoint filtering, default is 'default'",
|
||||||
|
"region": "[to be translated]:Region",
|
||||||
|
"region_help": "[to be translated]:Service region, e.g., cn-beijing",
|
||||||
|
"save_credentials": "[to be translated]:Save Credentials",
|
||||||
|
"secret_access_key": "[to be translated]:Secret Access Key",
|
||||||
|
"secret_access_key_help": "[to be translated]:Your Volcengine Secret Access Key, please keep it secure",
|
||||||
|
"title": "[to be translated]:Volcengine Configuration"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proxy": {
|
"proxy": {
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import OVMSSettings from './OVMSSettings'
|
|||||||
import ProviderOAuth from './ProviderOAuth'
|
import ProviderOAuth from './ProviderOAuth'
|
||||||
import SelectProviderModelPopup from './SelectProviderModelPopup'
|
import SelectProviderModelPopup from './SelectProviderModelPopup'
|
||||||
import VertexAISettings from './VertexAISettings'
|
import VertexAISettings from './VertexAISettings'
|
||||||
|
import VolcengineSettings from './VolcengineSettings'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
providerId: string
|
providerId: string
|
||||||
@@ -593,6 +594,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
|||||||
{provider.id === 'copilot' && <GithubCopilotSettings providerId={provider.id} />}
|
{provider.id === 'copilot' && <GithubCopilotSettings providerId={provider.id} />}
|
||||||
{provider.id === 'aws-bedrock' && <AwsBedrockSettings />}
|
{provider.id === 'aws-bedrock' && <AwsBedrockSettings />}
|
||||||
{provider.id === 'vertexai' && <VertexAISettings />}
|
{provider.id === 'vertexai' && <VertexAISettings />}
|
||||||
|
{provider.id === 'doubao' && <VolcengineSettings />}
|
||||||
<ModelList providerId={provider.id} />
|
<ModelList providerId={provider.id} />
|
||||||
</SettingContainer>
|
</SettingContainer>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import { HStack } from '@renderer/components/Layout'
|
||||||
|
import { PROVIDER_URLS } from '@renderer/config/providers'
|
||||||
|
import { useVolcengineSettings } from '@renderer/hooks/useVolcengine'
|
||||||
|
import { Alert, Button, Input, Space } from 'antd'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle } from '..'
|
||||||
|
|
||||||
|
const VolcengineSettings: FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { region, projectName, setRegion, setProjectName } = useVolcengineSettings()
|
||||||
|
|
||||||
|
const providerConfig = PROVIDER_URLS['doubao']
|
||||||
|
const apiKeyWebsite = providerConfig?.websites?.apiKey
|
||||||
|
|
||||||
|
const [localAccessKeyId, setLocalAccessKeyId] = useState('')
|
||||||
|
const [localSecretAccessKey, setLocalSecretAccessKey] = useState('')
|
||||||
|
const [localRegion, setLocalRegion] = useState(region)
|
||||||
|
const [localProjectName, setLocalProjectName] = useState(projectName)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [hasCredentials, setHasCredentials] = useState(false)
|
||||||
|
|
||||||
|
// Check if credentials exist on mount
|
||||||
|
useEffect(() => {
|
||||||
|
window.api.volcengine.hasCredentials().then(setHasCredentials)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Sync local state with store (only for region and projectName)
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalRegion(region)
|
||||||
|
setLocalProjectName(projectName)
|
||||||
|
}, [region, projectName])
|
||||||
|
|
||||||
|
const handleSaveCredentials = useCallback(async () => {
|
||||||
|
if (!localAccessKeyId || !localSecretAccessKey) {
|
||||||
|
window.toast.error(t('settings.provider.volcengine.credentials_required'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSaving(true)
|
||||||
|
try {
|
||||||
|
// Save credentials to secure storage via IPC first
|
||||||
|
await window.api.volcengine.saveCredentials(localAccessKeyId, localSecretAccessKey)
|
||||||
|
|
||||||
|
// Only update Redux after IPC success (for region and projectName only)
|
||||||
|
setRegion(localRegion)
|
||||||
|
setProjectName(localProjectName)
|
||||||
|
|
||||||
|
setHasCredentials(true)
|
||||||
|
// Clear local credential state after successful save (they're now in secure storage)
|
||||||
|
setLocalAccessKeyId('')
|
||||||
|
setLocalSecretAccessKey('')
|
||||||
|
window.toast.success(t('settings.provider.volcengine.credentials_saved'))
|
||||||
|
} catch (error) {
|
||||||
|
window.toast.error(String(error))
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}, [localAccessKeyId, localSecretAccessKey, localRegion, localProjectName, setRegion, setProjectName, t])
|
||||||
|
|
||||||
|
const handleClearCredentials = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await window.api.volcengine.clearCredentials()
|
||||||
|
setLocalAccessKeyId('')
|
||||||
|
setLocalSecretAccessKey('')
|
||||||
|
setHasCredentials(false)
|
||||||
|
window.toast.success(t('settings.provider.volcengine.credentials_cleared'))
|
||||||
|
} catch (error) {
|
||||||
|
window.toast.error(String(error))
|
||||||
|
}
|
||||||
|
}, [t])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.volcengine.title')}</SettingSubtitle>
|
||||||
|
<Alert type="info" style={{ marginTop: 5 }} message={t('settings.provider.volcengine.description')} showIcon />
|
||||||
|
|
||||||
|
<SettingSubtitle style={{ marginTop: 15 }}>{t('settings.provider.volcengine.access_key_id')}</SettingSubtitle>
|
||||||
|
<Input
|
||||||
|
value={localAccessKeyId}
|
||||||
|
placeholder="Access Key ID"
|
||||||
|
onChange={(e) => setLocalAccessKeyId(e.target.value)}
|
||||||
|
style={{ marginTop: 5 }}
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
<SettingHelpTextRow>
|
||||||
|
<SettingHelpText>{t('settings.provider.volcengine.access_key_id_help')}</SettingHelpText>
|
||||||
|
</SettingHelpTextRow>
|
||||||
|
|
||||||
|
<SettingSubtitle style={{ marginTop: 15 }}>{t('settings.provider.volcengine.secret_access_key')}</SettingSubtitle>
|
||||||
|
<Input.Password
|
||||||
|
value={localSecretAccessKey}
|
||||||
|
placeholder="Secret Access Key"
|
||||||
|
onChange={(e) => setLocalSecretAccessKey(e.target.value)}
|
||||||
|
style={{ marginTop: 5 }}
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||||
|
<HStack>
|
||||||
|
{apiKeyWebsite && (
|
||||||
|
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||||
|
{t('settings.provider.get_api_key')}
|
||||||
|
</SettingHelpLink>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
<SettingHelpText>{t('settings.provider.volcengine.secret_access_key_help')}</SettingHelpText>
|
||||||
|
</SettingHelpTextRow>
|
||||||
|
|
||||||
|
<SettingSubtitle style={{ marginTop: 15 }}>{t('settings.provider.volcengine.region')}</SettingSubtitle>
|
||||||
|
<Input
|
||||||
|
value={localRegion}
|
||||||
|
placeholder="cn-beijing"
|
||||||
|
onChange={(e) => setLocalRegion(e.target.value)}
|
||||||
|
onBlur={() => setRegion(localRegion)}
|
||||||
|
style={{ marginTop: 5 }}
|
||||||
|
/>
|
||||||
|
<SettingHelpTextRow>
|
||||||
|
<SettingHelpText>{t('settings.provider.volcengine.region_help')}</SettingHelpText>
|
||||||
|
</SettingHelpTextRow>
|
||||||
|
|
||||||
|
<SettingSubtitle style={{ marginTop: 15 }}>{t('settings.provider.volcengine.project_name')}</SettingSubtitle>
|
||||||
|
<Input
|
||||||
|
value={localProjectName}
|
||||||
|
placeholder="default"
|
||||||
|
onChange={(e) => setLocalProjectName(e.target.value)}
|
||||||
|
onBlur={() => setProjectName(localProjectName)}
|
||||||
|
style={{ marginTop: 5 }}
|
||||||
|
/>
|
||||||
|
<SettingHelpTextRow>
|
||||||
|
<SettingHelpText>{t('settings.provider.volcengine.project_name_help')}</SettingHelpText>
|
||||||
|
</SettingHelpTextRow>
|
||||||
|
|
||||||
|
<Space style={{ marginTop: 15 }}>
|
||||||
|
<Button type="primary" onClick={handleSaveCredentials} loading={saving}>
|
||||||
|
{t('settings.provider.volcengine.save_credentials')}
|
||||||
|
</Button>
|
||||||
|
{hasCredentials && (
|
||||||
|
<Button danger onClick={handleClearCredentials}>
|
||||||
|
{t('settings.provider.volcengine.clear_credentials')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VolcengineSettings
|
||||||
@@ -242,6 +242,10 @@ vi.mock('@renderer/store/llm.ts', () => {
|
|||||||
secretAccessKey: '',
|
secretAccessKey: '',
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
region: ''
|
region: ''
|
||||||
|
},
|
||||||
|
volcengine: {
|
||||||
|
region: 'cn-beijing',
|
||||||
|
projectName: 'default'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} satisfies LlmState
|
} satisfies LlmState
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
|
|||||||
{
|
{
|
||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 179,
|
version: 180,
|
||||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'],
|
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ type LlmSettings = {
|
|||||||
apiKey: string
|
apiKey: string
|
||||||
region: string
|
region: string
|
||||||
}
|
}
|
||||||
|
volcengine: {
|
||||||
|
region: string
|
||||||
|
projectName: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LlmState {
|
export interface LlmState {
|
||||||
@@ -75,6 +79,10 @@ export const initialState: LlmState = {
|
|||||||
secretAccessKey: '',
|
secretAccessKey: '',
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
region: ''
|
region: ''
|
||||||
|
},
|
||||||
|
volcengine: {
|
||||||
|
region: 'cn-beijing',
|
||||||
|
projectName: 'default'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,6 +224,12 @@ const llmSlice = createSlice({
|
|||||||
setAwsBedrockRegion: (state, action: PayloadAction<string>) => {
|
setAwsBedrockRegion: (state, action: PayloadAction<string>) => {
|
||||||
state.settings.awsBedrock.region = action.payload
|
state.settings.awsBedrock.region = action.payload
|
||||||
},
|
},
|
||||||
|
setVolcengineRegion: (state, action: PayloadAction<string>) => {
|
||||||
|
state.settings.volcengine.region = action.payload
|
||||||
|
},
|
||||||
|
setVolcengineProjectName: (state, action: PayloadAction<string>) => {
|
||||||
|
state.settings.volcengine.projectName = action.payload
|
||||||
|
},
|
||||||
updateModel: (
|
updateModel: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<{
|
||||||
@@ -257,6 +271,8 @@ export const {
|
|||||||
setAwsBedrockSecretAccessKey,
|
setAwsBedrockSecretAccessKey,
|
||||||
setAwsBedrockApiKey,
|
setAwsBedrockApiKey,
|
||||||
setAwsBedrockRegion,
|
setAwsBedrockRegion,
|
||||||
|
setVolcengineRegion,
|
||||||
|
setVolcengineProjectName,
|
||||||
updateModel
|
updateModel
|
||||||
} = llmSlice.actions
|
} = llmSlice.actions
|
||||||
|
|
||||||
|
|||||||
@@ -2906,6 +2906,19 @@ const migrateConfig = {
|
|||||||
logger.error('migrate 179 error', error as Error)
|
logger.error('migrate 179 error', error as Error)
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'180': (state: RootState) => {
|
||||||
|
try {
|
||||||
|
// Initialize volcengine settings
|
||||||
|
if (!state.llm.settings.volcengine) {
|
||||||
|
state.llm.settings.volcengine = llmInitialState.settings.volcengine
|
||||||
|
}
|
||||||
|
logger.info('migrate 180 success')
|
||||||
|
return state
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('migrate 180 error', error as Error)
|
||||||
|
return state
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user