Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ electron.vite.config.*.mjs
# Logs
logs/
!src/renderer/src/components/logs/
!frontend/src/components/logs/
*.log
npm-debug.log*
yarn-debug.log*
Expand Down
6 changes: 3 additions & 3 deletions backend/oauth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Defines types and interfaces for provider authentication
*/

import type { ProviderVendor } from '../../shared/types'
import type { ProviderVendor } from '../shared/types'

export type ProviderType = Exclude<ProviderVendor, 'custom'>

Expand All @@ -22,7 +22,7 @@ export type OAuthStatus = 'idle' | 'pending' | 'success' | 'error' | 'cancelled'
/**
* Token type
*/
export type TokenType = 'jwt' | 'refresh' | 'access' | 'cookie'
export type TokenType = 'jwt' | 'refresh' | 'access' | 'cookie' | 'token'

/**
* OAuth login result
Expand Down Expand Up @@ -129,7 +129,7 @@ export interface ManualTokenConfig {
/**
* Manual input config for each provider
*/
export const MANUAL_TOKEN_CONFIGS: Record<ProviderType, ManualTokenConfig[]> = {
export const MANUAL_TOKEN_CONFIGS: Partial<Record<ProviderType, ManualTokenConfig[]>> = {
deepseek: [
{
providerType: 'deepseek',
Expand Down
2 changes: 1 addition & 1 deletion backend/providers/custom.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { storeManager } from '../store/store'
import type { Provider, AuthType } from '../../shared/types'
import type { Provider, AuthType } from '../shared/types'
import type { CredentialField } from '../store/types'

export interface CustomProviderData {
Expand Down
3 changes: 2 additions & 1 deletion backend/proxy/adapters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export { GLMAdapter, GLMStreamHandler, glmAdapter } from './glm'
export { KimiAdapter, KimiStreamHandler, kimiAdapter } from './kimi'
export { MimoAdapter, MimoStreamHandler, mimoAdapter } from './mimo'
export { MiniMaxAdapter, MiniMaxStreamHandler, minimaxAdapter } from './minimax'
export { PerplexityAdapter, PerplexityStreamHandler, perplexityAdapter } from './perplexity'
export { PerplexityAdapter, perplexityAdapter } from './perplexity'
export { PerplexityStreamHandler } from './perplexity-stream'
export { QwenAdapter, QwenStreamHandler, qwenAdapter } from './qwen'
export { QwenAiAdapter, QwenAiStreamHandler, qwenAiAdapter } from './qwen-ai'
export { ZaiAdapter, ZaiStreamHandler, zaiAdapter } from './zai'
6 changes: 4 additions & 2 deletions backend/proxy/adapters/prompt/BasePromptAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export abstract class BasePromptAdapter implements PromptAdapter {
return { messages, tools: undefined, injected: false }
}

const variant = this.getPromptVariant(model, provider)
const variant = this.getPromptVariant(model, provider) ?? undefined
const toolsPrompt = this.toolsToPrompt(tools, variant)

const transformedMessages = this.injectPrompt(messages, toolsPrompt)
Expand Down Expand Up @@ -166,7 +166,9 @@ export abstract class BasePromptAdapter implements PromptAdapter {
if (typeof part === 'string') {
parts.push(part)
} else if (part && typeof part === 'object' && 'text' in part) {
parts.push(part.text)
if (typeof part.text === 'string') {
parts.push(part.text)
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion backend/proxy/adapters/prompt/CherryStudioPromptAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export class CherryStudioPromptAdapter extends BasePromptAdapter {
return { messages, tools: undefined, injected: false }
}

const variant = this.getPromptVariant(model)
const variant = this.getPromptVariant(model) ?? undefined
const toolsPrompt = this.toolsToPrompt(tools, variant)
const transformedMessages = this.injectPrompt(messages, toolsPrompt)

Expand Down
3 changes: 2 additions & 1 deletion backend/proxy/adapters/prompt/DefaultPromptAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { ChatMessage, ChatCompletionTool, ToolCall } from '../../types'
import { BasePromptAdapter, PromptVariant, TransformResult, ParseResult, ToolCallFormat } from './BasePromptAdapter'
import { ClientType } from '../../utils/promptSignatures'
import { parseToolCallsFromText } from '../../utils/toolParser'
import { TOOL_PROMPT_SIGNATURES, hasGeneralToolPromptSignature } from '../../constants/signatures'
import { TOOL_PROMPT_SIGNATURES } from '../../utils/tools'
import { hasGeneralToolPromptSignature } from '../../constants/signatures'
import { DEFAULT_VARIANT, XML_VARIANT } from '../../prompt/variants'

/**
Expand Down
4 changes: 2 additions & 2 deletions backend/proxy/adapters/prompt/KiloCodePromptAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class KiloCodePromptAdapter extends BasePromptAdapter {
if (this.hasPromptInjected(messages)) {
console.log('[KiloCodeAdapter] Kilo Code prompt detected, replacing with standard format')
const cleanedMessages = this.cleanKiloCodePrompt(messages)
const variant = this.getPromptVariant(model, provider)
const variant = this.getPromptVariant(model, provider) ?? undefined
const toolsPrompt = this.toolsToPrompt(tools, variant)
const transformedMessages = this.injectPrompt(cleanedMessages, toolsPrompt)

Expand All @@ -146,7 +146,7 @@ export class KiloCodePromptAdapter extends BasePromptAdapter {
}
}

const variant = this.getPromptVariant(model, provider)
const variant = this.getPromptVariant(model, provider) ?? undefined
const toolsPrompt = this.toolsToPrompt(tools, variant)
const transformedMessages = this.injectPrompt(messages, toolsPrompt)

Expand Down
4 changes: 3 additions & 1 deletion backend/proxy/adapters/prompt/PromptAdapterRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,9 @@ export class PromptAdapterRegistry {
if (typeof part === 'string') {
parts.push(part)
} else if (part && typeof part === 'object' && 'text' in part) {
parts.push(part.text)
if (typeof part.text === 'string') {
parts.push(part.text)
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion backend/proxy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ export { LoadBalancer, loadBalancer } from './loadbalancer'
export { ModelMapper, modelMapper } from './modelMapper'
export { RequestForwarder, requestForwarder } from './forwarder'
export { StreamHandler, streamHandler } from './stream'
export { routes } from './routes'
export { default as routes } from './routes'
40 changes: 36 additions & 4 deletions backend/proxy/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,21 @@ export class ProxyServer {
this.app.use(async (ctx, next) => {
const origin = ctx.get('Origin')

if (ctx.path.startsWith('/v0/management')) {
// The bookmarklet ingest endpoint is intentionally cross-origin:
// operators run the bookmarklet from the provider's own page
// (e.g. https://chatglm.cn) and POST the captured token back to
// this server. The endpoint authenticates with a one-shot ticket
// baked into the bookmarklet, so it does not (and must not) accept
// cookies. We therefore allow any origin unconditionally — anything
// narrower would force every operator to whitelist every provider
// domain in CHAT2API_CORS_ORIGINS, which defeats the whole point.
const isBookmarkletIngest =
ctx.path === '/v0/management/oauth/bookmarklet/ingest'

if (isBookmarkletIngest) {
ctx.set('Access-Control-Allow-Origin', '*')
ctx.set('Vary', 'Origin')
} else if (ctx.path.startsWith('/v0/management')) {
// Management API: only allow explicitly configured origins.
// CHAT2API_CORS_ORIGINS accepts a comma-separated list (e.g. "https://admin.example.com,http://localhost:3000").
// If unset, only same-origin requests (no Origin header) are permitted.
Expand Down Expand Up @@ -138,6 +152,19 @@ export class ProxyServer {
return
}

// The API key gate is only meant to protect the OpenAI-compatible
// proxy endpoints. Don't gate the SPA's own assets / client-side
// routes, otherwise turning on "Enable API Key" makes the web UI
// itself unreachable (browser fetches /assets/*.js -> 401 -> blank
// page). Anything that isn't an explicit API path is allowed
// through; the static fallback (or 404) will handle it.
const isProxyApi =
ctx.path.startsWith('/v1/') || ctx.path === '/v1'
if (!isProxyApi) {
await next()
return
}

const config = storeManager.getConfig()

if (config.enableApiKey && config.apiKeys && config.apiKeys.length > 0) {
Expand Down Expand Up @@ -225,10 +252,15 @@ export class ProxyServer {
this.router.use(route.allowedMethods())
}

this.router.get('/', async (ctx) => {
// When the SPA is mounted, let the static fallback render index.html.
this.router.get('/', async (ctx, next) => {
// When the SPA is mounted, hand off to the static fallback so it
// can serve index.html (and let client-side routing take over).
// We MUST call next(): koa-router will not run any later middleware
// if the handler returns without invoking it, so the previous
// `ctx.status = 404; return` short-circuited the SPA fallback and
// browsers got a bare 404 at the site root.
if (this.staticFrontendDir) {
ctx.status = 404
await next()
return
}
ctx.body = {
Expand Down
5 changes: 1 addition & 4 deletions backend/proxy/utils/accountUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,10 @@ export function createAccount(
return {
providerId,
credentials,
name: accountInfo?.name,
name: accountInfo?.name || '',
email: accountInfo?.email,
userId: accountInfo?.userId,
status: 'active',
lastUsed: undefined,
usageCount: 0,
metadata: {},
}
}

Expand Down
3 changes: 2 additions & 1 deletion backend/proxy/utils/clientDetector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,9 @@ export function removeToolPromptSection(content: string, clientType: ClientType)
export function cleanToolPrompts(messages: ChatMessage[]): ChatMessage[] {
const allContent = extractAllContent(messages)
const clientResult = detectClientFromContent(allContent)
const config = CLIENT_SIGNATURES[clientResult.clientType]

if (!clientResult.promptSectionMarkers) {
if (!config?.promptSectionMarkers) {
return messages
}

Expand Down
7 changes: 6 additions & 1 deletion backend/proxy/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ export * from './tools'
// 新的统一工具解析模块
export * from './toolParser/index'
// 保留旧的 streamToolHandler 以保持向后兼容
export * from './streamToolHandler'
// 仅导出 streamToolHandler 中独有的成员,避免与 toolParser/index 重复导出冲突
export {
ToolCallState,
createToolCallState,
processStreamContent,
} from './streamToolHandler'
8 changes: 7 additions & 1 deletion backend/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,16 @@ export type LoadBalanceStrategy = 'round-robin' | 'fill-first' | 'failover'

export type Theme = 'light' | 'dark' | 'system'

export type {
import type {
LegacyToolPromptConfig,
ToolCallingConfig,
} from './toolCalling'

export type {
LegacyToolPromptConfig,
ToolCallingConfig,
}

export interface Account {
id: string
providerId: string
Expand Down Expand Up @@ -68,6 +73,7 @@ export interface Provider {
modelMappings?: Record<string, string>
status?: ProviderStatus
lastStatusCheck?: number
credentialFields?: CredentialField[]
}

export interface ModelMapping {
Expand Down
1 change: 1 addition & 0 deletions backend/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
export * from './types'

// Core storage
import { storeManager } from './store'
export { storeManager, StoreManager } from './store'

// Account management API
Expand Down
2 changes: 1 addition & 1 deletion backend/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ type StoreType = JsonStore<StoreSchema>
* Storage Manager Class
* Responsible for data persistence and encryption
*/
class StoreManager {
export class StoreManager {
private store: StoreType | null = null
private isInitialized: boolean = false
private initializationError: Error | null = null
Expand Down
2 changes: 2 additions & 0 deletions backend/store/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ export interface Provider {
status?: ProviderStatus
/** Last status check time */
lastStatusCheck?: number
/** Credential field configuration (optional, mainly used by built-in providers) */
credentialFields?: CredentialField[]
}

/**
Expand Down
Loading