mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-23 23:20:55 +08:00
feat: 上下文压缩行为优化
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
* 编排 PiAgentCore 的对话流程(工具调用、流式输出、中止控制)
|
||||
*/
|
||||
|
||||
import { getActiveConfig, buildPiModel } from '../llm'
|
||||
import { getDefaultAssistantConfig, buildPiModel } from '../llm'
|
||||
import { getAllTools, createActivateSkillTool } from '../tools'
|
||||
import type { ToolContext } from '../tools/types'
|
||||
import { getHistoryForAgent } from '../conversations'
|
||||
@@ -355,7 +355,7 @@ export async function runAgent(
|
||||
assistantConfig?: AssistantConfig,
|
||||
skillCtx?: SkillContext
|
||||
): Promise<AgentResult> {
|
||||
const activeConfig = getActiveConfig()
|
||||
const activeConfig = getDefaultAssistantConfig()
|
||||
if (!activeConfig) throw new Error('LLM service not configured')
|
||||
const piModel = buildPiModel(activeConfig)
|
||||
const agent = new Agent(context, piModel, activeConfig.apiKey, config, chatType, locale, assistantConfig, skillCtx)
|
||||
@@ -375,7 +375,7 @@ export async function runAgentStream(
|
||||
assistantConfig?: AssistantConfig,
|
||||
skillCtx?: SkillContext
|
||||
): Promise<AgentResult> {
|
||||
const activeConfig = getActiveConfig()
|
||||
const activeConfig = getDefaultAssistantConfig()
|
||||
if (!activeConfig) throw new Error('LLM service not configured')
|
||||
const piModel = buildPiModel(activeConfig)
|
||||
const agent = new Agent(context, piModel, activeConfig.apiKey, config, chatType, locale, assistantConfig, skillCtx)
|
||||
|
||||
@@ -17,11 +17,13 @@ import {
|
||||
getAllUserAssistantMessages,
|
||||
addSummaryMessage,
|
||||
getMessageCountAfterSummary,
|
||||
type ContentBlock,
|
||||
type AIMessageRole,
|
||||
} from '../conversations'
|
||||
import { buildPiModel, getActiveConfig, findModelDefinition } from '../llm'
|
||||
import { buildPiModel, findModelDefinition } from '../llm'
|
||||
import type { AIServiceConfig } from '../llm/types'
|
||||
import { completeSimple, type TextContent as PiTextContent } from '@mariozechner/pi-ai'
|
||||
import { aiLogger } from '../logger'
|
||||
import { aiLogger, isDebugMode } from '../logger'
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
@@ -31,8 +33,6 @@ export interface CompressionConfig {
|
||||
tokenThresholdPercent: number
|
||||
/** 保留最近消息的缓冲区大小(相对于 context window 的百分比),默认 20 */
|
||||
bufferSizePercent: number
|
||||
/** 独立压缩模型配置(为空则使用当前对话模型) */
|
||||
compressionModelConfigId?: string
|
||||
/** 单次工具返回的最大上下文占比(相对于 context window 的百分比),默认 35 */
|
||||
maxToolResultPercent?: number
|
||||
}
|
||||
@@ -53,19 +53,41 @@ export interface CompressionResult {
|
||||
error?: string
|
||||
}
|
||||
|
||||
const DEFAULT_COMPRESSION_PROMPT = `You are a context compression assistant. Compress the conversation below into a structured summary.
|
||||
const INITIAL_COMPRESSION_PROMPT = `You are a context compression assistant. Compress the conversation below into a structured summary.
|
||||
|
||||
STRICT RULES:
|
||||
- Output ONLY the summary content. No greetings, no preamble, no meta-commentary, no word/token counts.
|
||||
- Use the same language as the conversation.
|
||||
- Maximum output length: {maxTokens} tokens. Be concise.
|
||||
- Organize by topic/thread when possible.
|
||||
- Preserve: key facts, decisions, user preferences, data, names, timestamps, action items.
|
||||
- Omit: pleasantries, filler, redundant back-and-forth.
|
||||
- NEVER reproduce any single message verbatim. Always paraphrase and compress.
|
||||
- Cover ALL topics discussed — no single topic should exceed 30% of the summary.
|
||||
- Organize by topic/thread, using brief headers (e.g. "## Topic").
|
||||
- Preserve: key facts, conclusions, user preferences, data points, names, important timestamps, action items.
|
||||
- Omit: pleasantries, filler, redundant back-and-forth, detailed tables (summarize their conclusions instead).
|
||||
|
||||
CONVERSATION:
|
||||
{messages}`
|
||||
|
||||
const PROGRESSIVE_COMPRESSION_PROMPT = `You are a context compression assistant performing an INCREMENTAL summary update.
|
||||
|
||||
You will receive:
|
||||
1. A [PREVIOUS SUMMARY] — this represents the compressed history of earlier conversation. Its content MUST be preserved in your output.
|
||||
2. [NEW MESSAGES] — recent messages that need to be merged into the summary.
|
||||
|
||||
STRICT RULES:
|
||||
- Output ONLY the updated summary. No greetings, no preamble, no meta-commentary.
|
||||
- Use the same language as the conversation.
|
||||
- Maximum output length: {maxTokens} tokens. Be concise.
|
||||
- CRITICAL: You MUST retain ALL key points from the previous summary. Do not discard prior context.
|
||||
- NEVER reproduce any single message verbatim. Always paraphrase and compress.
|
||||
- Merge new information into appropriate existing topic sections, or add new sections.
|
||||
- Cover ALL topics — no single topic should exceed 30% of the summary.
|
||||
- Organize by topic/thread, using brief headers (e.g. "## Topic").
|
||||
- Preserve: key facts, conclusions, user preferences, data points, names, important timestamps, action items.
|
||||
- Omit: pleasantries, filler, redundant back-and-forth, detailed tables (summarize their conclusions instead).
|
||||
|
||||
{messages}`
|
||||
|
||||
const DEFAULT_CONTEXT_WINDOW = 128000
|
||||
|
||||
// ==================== 核心压缩逻辑 ====================
|
||||
@@ -89,9 +111,18 @@ export async function checkAndCompress(
|
||||
|
||||
// 收集当前上下文消息
|
||||
const summary = getLatestSummary(conversationId)
|
||||
const messages = summary
|
||||
? getMessagesAfterSummary(conversationId, summary.timestamp)
|
||||
: getAllUserAssistantMessages(conversationId)
|
||||
|
||||
let messages: Array<{ role: AIMessageRole; content: string; timestamp: number }>
|
||||
if (summary) {
|
||||
// 从 summary_meta 获取 buffer 边界,取 >= boundary 的消息(buffer + 新消息)
|
||||
const metaBlock = summary.contentBlocks?.find(
|
||||
(b): b is Extract<ContentBlock, { type: 'summary_meta' }> => b.type === 'summary_meta'
|
||||
)
|
||||
const boundary = metaBlock?.bufferBoundaryTimestamp ?? summary.timestamp
|
||||
messages = getMessagesAfterSummary(conversationId, boundary - 1)
|
||||
} else {
|
||||
messages = getAllUserAssistantMessages(conversationId)
|
||||
}
|
||||
|
||||
// 构建 token 计算的消息列表
|
||||
const historyForTokenCount: Array<{ role: string; content: string }> = []
|
||||
@@ -116,43 +147,107 @@ export async function checkAndCompress(
|
||||
|
||||
// 确定缓冲区(保留最近 N% 的消息)
|
||||
const bufferTokenBudget = Math.floor(contextWindow * (config.bufferSizePercent / 100))
|
||||
const { bufferMessages, messagesToCompress } = splitMessagesForCompression(messages, summary, bufferTokenBudget)
|
||||
const { bufferMessages, messagesToCompress } = splitMessagesForCompression(messages, bufferTokenBudget)
|
||||
|
||||
if (messagesToCompress.length === 0) {
|
||||
aiLogger.info('Compression', `Split result`, {
|
||||
totalMessages: messages.length,
|
||||
messagesToCompress: messagesToCompress.length,
|
||||
bufferMessages: bufferMessages.length,
|
||||
bufferTokenBudget,
|
||||
hasPreviousSummary: !!summary,
|
||||
})
|
||||
|
||||
// 待压缩消息数过少时跳过,避免生成低质量摘要(仅保留 1-2 条消息时 LLM 倾向于原文照搬)
|
||||
const MIN_MESSAGES_TO_COMPRESS = 3
|
||||
if (messagesToCompress.length < MIN_MESSAGES_TO_COMPRESS) {
|
||||
aiLogger.info(
|
||||
'Compression',
|
||||
`Skipping: only ${messagesToCompress.length} messages to compress (min: ${MIN_MESSAGES_TO_COMPRESS})`
|
||||
)
|
||||
return { compressed: false, reason: 'skipped_below_threshold', tokensBefore: currentTokens }
|
||||
}
|
||||
|
||||
// 构建压缩输入文本
|
||||
const isProgressive = !!summary
|
||||
const compressInput = buildCompressionInput(messagesToCompress, summary)
|
||||
const targetTokens = Math.min(Math.floor(contextWindow * 0.1), 16384)
|
||||
|
||||
// 三级降级:独立模型 → 当前模型 → 强制截断
|
||||
aiLogger.info('Compression', `Compression params`, {
|
||||
mode: isProgressive ? 'progressive' : 'initial',
|
||||
targetTokens,
|
||||
inputLength: compressInput.length,
|
||||
inputTokensEstimate: countTokens(compressInput),
|
||||
})
|
||||
|
||||
// DEBUG 模式下输出原始消息列表和完整压缩输入
|
||||
if (isDebugMode()) {
|
||||
aiLogger.debug('Compression', 'Messages to compress (raw)', {
|
||||
messages: messagesToCompress.map((m, i) => ({
|
||||
index: i,
|
||||
role: m.role,
|
||||
contentLength: m.content.length,
|
||||
contentPreview: m.content.slice(0, 200),
|
||||
})),
|
||||
})
|
||||
aiLogger.debug('Compression', 'Buffer messages (kept as-is)', {
|
||||
messages: bufferMessages.map((m, i) => ({
|
||||
index: i,
|
||||
role: m.role,
|
||||
contentLength: m.content.length,
|
||||
contentPreview: m.content.slice(0, 200),
|
||||
})),
|
||||
})
|
||||
aiLogger.debug('Compression', 'Full compression input sent to LLM', compressInput)
|
||||
if (summary) {
|
||||
aiLogger.debug('Compression', 'Previous summary content', summary.content)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用默认助手模型压缩,失败则强制截断
|
||||
let summaryText: string | null = null
|
||||
|
||||
// 尝试用配置的压缩模型
|
||||
if (config.compressionModelConfigId) {
|
||||
summaryText = await tryCompress(config.compressionModelConfigId, compressInput, targetTokens)
|
||||
}
|
||||
summaryText = await tryCompressWithConfig(activeAIConfig, compressInput, targetTokens, isProgressive)
|
||||
|
||||
// 降级到当前模型
|
||||
if (!summaryText) {
|
||||
summaryText = await tryCompressWithConfig(activeAIConfig, compressInput, targetTokens)
|
||||
}
|
||||
|
||||
// 最终降级:强制截断
|
||||
if (!summaryText) {
|
||||
aiLogger.warn('Compression', 'LLM compression failed, falling back to truncation')
|
||||
summaryText = forceTruncate(compressInput, targetTokens)
|
||||
aiLogger.info('Compression', 'Truncation fallback applied', {
|
||||
outputLength: summaryText.length,
|
||||
})
|
||||
} else {
|
||||
aiLogger.info('Compression', 'LLM compression succeeded', {
|
||||
outputLength: summaryText.length,
|
||||
outputTokensEstimate: countTokens(summaryText),
|
||||
})
|
||||
if (isDebugMode()) {
|
||||
aiLogger.debug('Compression', 'Generated summary content', summaryText)
|
||||
}
|
||||
}
|
||||
|
||||
// 写入 summary
|
||||
addSummaryMessage(conversationId, summaryText)
|
||||
// 写入 summary:时间戳 = NOW(UI 中显示在触发压缩的位置)
|
||||
// buffer 边界 + 压缩消息数存入 content_blocks 的 summary_meta block
|
||||
const bufferBoundary =
|
||||
bufferMessages.length > 0
|
||||
? bufferMessages[0].timestamp
|
||||
: messagesToCompress[messagesToCompress.length - 1]!.timestamp + 1
|
||||
|
||||
// Thrashing 检查:压缩后重新计算 token
|
||||
const afterMessages = getMessagesAfterSummary(conversationId, Date.now() / 1000 - 1)
|
||||
const summaryMeta = {
|
||||
bufferBoundaryTimestamp: bufferBoundary,
|
||||
compressedMessageCount: messagesToCompress.length,
|
||||
}
|
||||
|
||||
aiLogger.info('Compression', 'Writing summary', {
|
||||
bufferBoundary,
|
||||
compressedCount: messagesToCompress.length,
|
||||
bufferCount: bufferMessages.length,
|
||||
})
|
||||
|
||||
addSummaryMessage(conversationId, summaryText, summaryMeta)
|
||||
|
||||
// Thrashing 检查:压缩后重新计算 token(summary + buffer 消息)
|
||||
const afterTokenCount: Array<{ role: string; content: string }> = [
|
||||
{ role: 'assistant', content: summaryText },
|
||||
...afterMessages.map((m) => ({ role: m.role, content: m.content })),
|
||||
...bufferMessages.map((m) => ({ role: m.role, content: m.content })),
|
||||
]
|
||||
const tokensAfter = countMessagesTokens(afterTokenCount, systemPrompt)
|
||||
|
||||
@@ -217,7 +312,6 @@ interface SplitResult {
|
||||
|
||||
function splitMessagesForCompression(
|
||||
messages: Array<{ role: string; content: string; timestamp: number }>,
|
||||
summary: { content: string } | null,
|
||||
bufferTokenBudget: number
|
||||
): SplitResult {
|
||||
let bufferTokens = 0
|
||||
@@ -249,7 +343,8 @@ function buildCompressionInput(
|
||||
const parts: string[] = []
|
||||
|
||||
if (existingSummary) {
|
||||
parts.push(`[Previous Summary]\n${existingSummary.content}\n`)
|
||||
parts.push(`[PREVIOUS SUMMARY — MUST PRESERVE]\n${existingSummary.content}\n`)
|
||||
parts.push(`[NEW MESSAGES — SUMMARIZE AND MERGE]`)
|
||||
}
|
||||
|
||||
for (const msg of messagesToCompress) {
|
||||
@@ -260,28 +355,16 @@ function buildCompressionInput(
|
||||
return parts.join('\n\n')
|
||||
}
|
||||
|
||||
async function tryCompress(configId: string, input: string, targetTokens: number): Promise<string | null> {
|
||||
try {
|
||||
const { getAllConfigs } = await import('../llm')
|
||||
const allConfigs = getAllConfigs()
|
||||
const config = allConfigs.find((c) => c.id === configId)
|
||||
if (!config) return null
|
||||
|
||||
return await tryCompressWithConfig(config, input, targetTokens)
|
||||
} catch (error) {
|
||||
aiLogger.warn('Compression', `Compression with config ${configId} failed`, { error: String(error) })
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function tryCompressWithConfig(
|
||||
aiConfig: AIServiceConfig,
|
||||
input: string,
|
||||
targetTokens: number
|
||||
targetTokens: number,
|
||||
isProgressive: boolean
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const piModel = buildPiModel(aiConfig)
|
||||
const prompt = DEFAULT_COMPRESSION_PROMPT.replace('{maxTokens}', String(targetTokens)).replace('{messages}', input)
|
||||
const template = isProgressive ? PROGRESSIVE_COMPRESSION_PROMPT : INITIAL_COMPRESSION_PROMPT
|
||||
const prompt = template.replace('{maxTokens}', String(targetTokens)).replace('{messages}', input)
|
||||
|
||||
const result = await completeSimple(
|
||||
piModel,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import Database from 'better-sqlite3'
|
||||
import * as path from 'path'
|
||||
import { getAiDataDir, ensureDir } from '../paths'
|
||||
import { aiLogger } from './logger'
|
||||
|
||||
const DEFAULT_GENERAL_ID = 'general_cn'
|
||||
|
||||
@@ -210,7 +211,7 @@ export interface AIConversation {
|
||||
*/
|
||||
export type ContentBlock =
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'think'; tag: string; text: string; durationMs?: number } // 思考内容块
|
||||
| { type: 'think'; tag: string; text: string; durationMs?: number }
|
||||
| {
|
||||
type: 'tool'
|
||||
tool: {
|
||||
@@ -220,6 +221,11 @@ export type ContentBlock =
|
||||
params?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'summary_meta'
|
||||
bufferBoundaryTimestamp: number
|
||||
compressedMessageCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 消息类型
|
||||
@@ -505,11 +511,11 @@ export function getConversationTokenUsage(conversationId: string): TokenUsageDat
|
||||
* 为 Agent 提供对话历史
|
||||
*
|
||||
* 返回简化的 {role, content} 格式,按时间升序排列。
|
||||
* 当存在 summary 消息时,返回最新 summary + summary 之后的 user/assistant 消息,
|
||||
* 以避免重复加载已被压缩的旧消息。
|
||||
* 当存在 summary 消息时,返回:[summary] + [buffer 消息] + [新消息]。
|
||||
* Buffer 边界从 summary 的 content_blocks 中的 summary_meta block 获取。
|
||||
*
|
||||
* @param conversationId 对话 ID
|
||||
* @param maxMessages 最大返回条数(取最近 N 条,仅对 system 摘要之后的消息生效)
|
||||
* @param maxMessages 最大返回条数(取最近 N 条,仅对 buffer+新消息生效)
|
||||
*/
|
||||
export function getHistoryForAgent(
|
||||
conversationId: string,
|
||||
@@ -520,19 +526,40 @@ export function getHistoryForAgent(
|
||||
(m) => (m.role === 'user' || m.role === 'assistant' || m.role === 'system') && m.content?.trim()
|
||||
)
|
||||
|
||||
// 查找最新的 system 消息位置(压缩摘要)
|
||||
let systemIndex = -1
|
||||
// 查找最新的 system (summary) 消息
|
||||
let systemMsg: AIMessage | undefined
|
||||
for (let i = validMessages.length - 1; i >= 0; i--) {
|
||||
if (validMessages[i].role === 'system') {
|
||||
systemIndex = i
|
||||
systemMsg = validMessages[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let result: Array<{ role: 'user' | 'assistant' | 'system'; content: string }>
|
||||
|
||||
if (systemIndex >= 0) {
|
||||
result = validMessages.slice(systemIndex).map((m) => ({ role: m.role, content: m.content }))
|
||||
if (systemMsg) {
|
||||
// 从 content_blocks 中解析 summary_meta 获取 buffer 边界
|
||||
const metaBlock = systemMsg.contentBlocks?.find(
|
||||
(b): b is Extract<ContentBlock, { type: 'summary_meta' }> => b.type === 'summary_meta'
|
||||
)
|
||||
const bufferBoundary = metaBlock?.bufferBoundaryTimestamp
|
||||
|
||||
if (!metaBlock) {
|
||||
aiLogger.warn('Conversations', 'system message missing summary_meta; agent context will be summary-only', {
|
||||
conversationId,
|
||||
messageId: systemMsg.id,
|
||||
})
|
||||
}
|
||||
|
||||
// 取 timestamp >= boundary 的 user/assistant 消息(buffer + 新消息)
|
||||
const contextMessages = bufferBoundary
|
||||
? validMessages.filter((m) => m.role !== 'system' && m.timestamp >= bufferBoundary)
|
||||
: []
|
||||
|
||||
result = [
|
||||
{ role: 'system' as const, content: systemMsg.content },
|
||||
...contextMessages.map((m) => ({ role: m.role, content: m.content })),
|
||||
]
|
||||
} else {
|
||||
result = validMessages.map((m) => ({ role: m.role, content: m.content }))
|
||||
}
|
||||
@@ -552,13 +579,28 @@ export function getHistoryForAgent(
|
||||
|
||||
/**
|
||||
* 添加 system 消息并替换旧的 system(每个对话只保留一条最新压缩摘要)
|
||||
*
|
||||
* Summary 时间戳 = NOW(UI 中显示在触发压缩的位置)。
|
||||
* Buffer 边界信息存入 content_blocks 的 summary_meta block 中,供 getHistoryForAgent 使用。
|
||||
*/
|
||||
export function addSummaryMessage(conversationId: string, content: string): AIMessage {
|
||||
export function addSummaryMessage(
|
||||
conversationId: string,
|
||||
content: string,
|
||||
meta: { bufferBoundaryTimestamp: number; compressedMessageCount: number }
|
||||
): AIMessage {
|
||||
const db = getAiDb()
|
||||
|
||||
db.prepare("DELETE FROM ai_message WHERE conversation_id = ? AND role = 'system'").run(conversationId)
|
||||
|
||||
return addMessage(conversationId, 'system', content)
|
||||
const contentBlocks: ContentBlock[] = [
|
||||
{
|
||||
type: 'summary_meta',
|
||||
bufferBoundaryTimestamp: meta.bufferBoundaryTimestamp,
|
||||
compressedMessageCount: meta.compressedMessageCount,
|
||||
},
|
||||
]
|
||||
|
||||
return addMessage(conversationId, 'system', content, undefined, undefined, contentBlocks)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -666,11 +708,18 @@ export function getMessageCountAfterSummary(conversationId: string): number {
|
||||
.get(conversationId) as { count: number }
|
||||
return row.count
|
||||
}
|
||||
|
||||
// 从 summary_meta 获取 buffer 边界,统计 >= boundary 的消息数
|
||||
const metaBlock = summary.contentBlocks?.find(
|
||||
(b): b is Extract<ContentBlock, { type: 'summary_meta' }> => b.type === 'summary_meta'
|
||||
)
|
||||
const boundary = metaBlock?.bufferBoundaryTimestamp ?? summary.timestamp
|
||||
|
||||
const db = getAiDb()
|
||||
const row = db
|
||||
.prepare(
|
||||
"SELECT COUNT(*) as count FROM ai_message WHERE conversation_id = ? AND timestamp > ? AND role IN ('user', 'assistant')"
|
||||
"SELECT COUNT(*) as count FROM ai_message WHERE conversation_id = ? AND timestamp >= ? AND role IN ('user', 'assistant')"
|
||||
)
|
||||
.get(conversationId, summary.timestamp) as { count: number }
|
||||
.get(conversationId, boundary) as { count: number }
|
||||
return row.count
|
||||
}
|
||||
|
||||
+103
-28
@@ -81,10 +81,6 @@ function providerDefinitionToInfo(def: ProviderDefinition): ProviderInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有 provider 信息(兼容旧格式)
|
||||
* @deprecated 使用 BUILTIN_PROVIDERS 代替
|
||||
*/
|
||||
export const PROVIDERS: ProviderInfo[] = BUILTIN_PROVIDERS.map(providerDefinitionToInfo)
|
||||
|
||||
// 配置文件路径
|
||||
@@ -127,7 +123,8 @@ function migrateLegacyConfig(legacy: LegacyStoredConfig): AIConfigStore {
|
||||
|
||||
return {
|
||||
configs: [newConfig],
|
||||
activeConfigId: newConfig.id,
|
||||
defaultAssistant: { configId: newConfig.id, modelId: newConfig.model || '' },
|
||||
fastModel: null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +134,7 @@ import { addCustomProvider as _addCustomProviderDirect } from './custom-provider
|
||||
import { addCustomModel as _addCustomModelDirect } from './custom-model-store'
|
||||
import { getBuiltinModelById } from './model-catalog'
|
||||
|
||||
const CURRENT_SCHEMA_VERSION = 2
|
||||
const CURRENT_SCHEMA_VERSION = 3
|
||||
|
||||
/**
|
||||
* MiniMax 等旧 provider 的兼容映射表
|
||||
@@ -208,6 +205,34 @@ function migrateToSchemaV2(store: AIConfigStore): AIConfigStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema v2 → v3:activeConfigId → defaultAssistant { configId, modelId }
|
||||
*/
|
||||
function migrateToSchemaV3(store: AIConfigStore & { activeConfigId?: string | null }): AIConfigStore {
|
||||
aiLogger.info('LLM', 'Migrating config store to schema v3 (dual-slot model selection)')
|
||||
const legacyActiveId = store.activeConfigId ?? null
|
||||
const resolvedConfig =
|
||||
legacyActiveId && store.configs.find((c) => c.id === legacyActiveId)
|
||||
? store.configs.find((c) => c.id === legacyActiveId)!
|
||||
: (store.configs[0] ?? null)
|
||||
|
||||
return {
|
||||
configs: store.configs,
|
||||
defaultAssistant: resolvedConfig ? { configId: resolvedConfig.id, modelId: resolvedConfig.model || '' } : null,
|
||||
fastModel: null,
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析 ModelSlot:如果 configId 无效,回退到 configs[0] */
|
||||
function resolveSlot(
|
||||
slot: import('./model-types').ModelSlot | null | undefined,
|
||||
configs: AIServiceConfig[]
|
||||
): import('./model-types').ModelSlot | null {
|
||||
if (slot && configs.some((c) => c.id === slot.configId)) return slot
|
||||
const fallback = configs[0]
|
||||
return fallback ? { configId: fallback.id, modelId: fallback.model || '' } : null
|
||||
}
|
||||
|
||||
// ==================== 多配置管理 ====================
|
||||
|
||||
/**
|
||||
@@ -217,7 +242,7 @@ export function loadConfigStore(): AIConfigStore {
|
||||
const configPath = getConfigPath()
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
return { configs: [], activeConfigId: null }
|
||||
return { configs: [], defaultAssistant: null, fastModel: null }
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -231,12 +256,19 @@ export function loadConfigStore(): AIConfigStore {
|
||||
return loadConfigStore()
|
||||
}
|
||||
|
||||
let store = data as AIConfigStore & { schemaVersion?: number }
|
||||
let store = data as AIConfigStore & { schemaVersion?: number; activeConfigId?: string | null }
|
||||
let needsSchemaSave = false
|
||||
|
||||
// Schema v1 → v2 迁移
|
||||
if (!store.schemaVersion || store.schemaVersion < CURRENT_SCHEMA_VERSION) {
|
||||
store = { ...migrateToSchemaV2(store), schemaVersion: CURRENT_SCHEMA_VERSION } as typeof store
|
||||
// 先解密再保存(migrateToSchemaV2 不改 apiKey 格式)
|
||||
if (!store.schemaVersion || store.schemaVersion < 2) {
|
||||
store = { ...migrateToSchemaV2(store), schemaVersion: 2 } as typeof store
|
||||
needsSchemaSave = true
|
||||
}
|
||||
|
||||
// Schema v2 → v3 迁移
|
||||
if (store.schemaVersion < 3) {
|
||||
store = { ...migrateToSchemaV3(store), schemaVersion: CURRENT_SCHEMA_VERSION } as typeof store
|
||||
needsSchemaSave = true
|
||||
}
|
||||
|
||||
let needsEncryptionMigration = false
|
||||
@@ -251,7 +283,7 @@ export function loadConfigStore(): AIConfigStore {
|
||||
}
|
||||
})
|
||||
|
||||
if (needsEncryptionMigration || (!data.schemaVersion && store.schemaVersion)) {
|
||||
if (needsEncryptionMigration || needsSchemaSave) {
|
||||
aiLogger.info('LLM', 'Saving migrated config store')
|
||||
saveConfigStoreRaw({
|
||||
...store,
|
||||
@@ -268,7 +300,7 @@ export function loadConfigStore(): AIConfigStore {
|
||||
}
|
||||
} catch (error) {
|
||||
aiLogger.error('LLM', 'Failed to load configs', error)
|
||||
return { configs: [], activeConfigId: null }
|
||||
return { configs: [], defaultAssistant: null, fastModel: null }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,10 +333,37 @@ export function getAllConfigs(): AIServiceConfig[] {
|
||||
return loadConfigStore().configs
|
||||
}
|
||||
|
||||
export function getActiveConfig(): AIServiceConfig | null {
|
||||
/** 获取默认助手 slot(含 configId + modelId) */
|
||||
export function getDefaultAssistantSlot(): import('./model-types').ModelSlot | null {
|
||||
const store = loadConfigStore()
|
||||
if (!store.activeConfigId) return null
|
||||
return store.configs.find((c) => c.id === store.activeConfigId) || null
|
||||
return resolveSlot(store.defaultAssistant, store.configs)
|
||||
}
|
||||
|
||||
/** 获取默认助手模型配置(AI 对话、工具调用、SQL 助手、上下文压缩)。自动覆盖 config.model 为 slot.modelId */
|
||||
export function getDefaultAssistantConfig(): AIServiceConfig | null {
|
||||
const store = loadConfigStore()
|
||||
const slot = resolveSlot(store.defaultAssistant, store.configs)
|
||||
if (!slot) return null
|
||||
const config = store.configs.find((c) => c.id === slot.configId)
|
||||
if (!config) return null
|
||||
return { ...config, model: slot.modelId || config.model }
|
||||
}
|
||||
|
||||
/** 获取快速模型 slot */
|
||||
export function getFastModelSlot(): import('./model-types').ModelSlot | null {
|
||||
const store = loadConfigStore()
|
||||
return resolveSlot(store.fastModel, store.configs)
|
||||
}
|
||||
|
||||
/** 获取快速模型配置(会话摘要),未配置时回退到默认助手 */
|
||||
export function getFastModelConfig(): AIServiceConfig | null {
|
||||
const store = loadConfigStore()
|
||||
const slot = resolveSlot(store.fastModel, store.configs)
|
||||
if (slot) {
|
||||
const config = store.configs.find((c) => c.id === slot.configId)
|
||||
if (config) return { ...config, model: slot.modelId || config.model }
|
||||
}
|
||||
return getDefaultAssistantConfig()
|
||||
}
|
||||
|
||||
export function getConfigById(id: string): AIServiceConfig | null {
|
||||
@@ -334,7 +393,7 @@ export function addConfig(config: Omit<AIServiceConfig, 'id' | 'createdAt' | 'up
|
||||
store.configs.push(newConfig)
|
||||
|
||||
if (store.configs.length === 1) {
|
||||
store.activeConfigId = newConfig.id
|
||||
store.defaultAssistant = { configId: newConfig.id, modelId: newConfig.model || '' }
|
||||
}
|
||||
|
||||
saveConfigStore(store)
|
||||
@@ -372,30 +431,50 @@ export function deleteConfig(id: string): { success: boolean; error?: string } {
|
||||
|
||||
store.configs.splice(index, 1)
|
||||
|
||||
if (store.activeConfigId === id) {
|
||||
store.activeConfigId = store.configs.length > 0 ? store.configs[0].id : null
|
||||
const fallback = store.configs[0]
|
||||
if (store.defaultAssistant?.configId === id) {
|
||||
store.defaultAssistant = fallback ? { configId: fallback.id, modelId: fallback.model || '' } : null
|
||||
}
|
||||
if (store.fastModel?.configId === id) {
|
||||
store.fastModel = fallback ? { configId: fallback.id, modelId: fallback.model || '' } : null
|
||||
}
|
||||
|
||||
saveConfigStore(store)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export function setActiveConfig(id: string): { success: boolean; error?: string } {
|
||||
/** 设置默认助手模型(configId + modelId) */
|
||||
export function setDefaultAssistantModel(configId: string, modelId: string): { success: boolean; error?: string } {
|
||||
const store = loadConfigStore()
|
||||
const config = store.configs.find((c) => c.id === id)
|
||||
const config = store.configs.find((c) => c.id === configId)
|
||||
|
||||
if (!config) {
|
||||
return { success: false, error: t('llm.configNotFound') }
|
||||
}
|
||||
|
||||
store.activeConfigId = id
|
||||
store.defaultAssistant = { configId, modelId }
|
||||
saveConfigStore(store)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
/** 设置快速模型(configId + modelId),传 null 表示跟随默认助手 */
|
||||
export function setFastModel(slot: import('./model-types').ModelSlot | null): { success: boolean; error?: string } {
|
||||
const store = loadConfigStore()
|
||||
|
||||
if (slot !== null) {
|
||||
const config = store.configs.find((c) => c.id === slot.configId)
|
||||
if (!config) {
|
||||
return { success: false, error: t('llm.configNotFound') }
|
||||
}
|
||||
}
|
||||
|
||||
store.fastModel = slot
|
||||
saveConfigStore(store)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export function hasActiveConfig(): boolean {
|
||||
const config = getActiveConfig()
|
||||
return config !== null
|
||||
return getDefaultAssistantConfig() !== null
|
||||
}
|
||||
|
||||
function validateProviderBaseUrl(provider: LLMProvider, baseUrl?: string): void {
|
||||
@@ -425,10 +504,6 @@ function validateProviderBaseUrl(provider: LLMProvider, baseUrl?: string): void
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提供商信息(兼容旧调用)
|
||||
* @deprecated 使用 getBuiltinProviderById 代替
|
||||
*/
|
||||
export function getProviderInfo(provider: LLMProvider): ProviderInfo | null {
|
||||
return PROVIDERS.find((p) => p.id === provider) || null
|
||||
}
|
||||
|
||||
@@ -83,8 +83,14 @@ export interface ModelCatalogStore {
|
||||
models: ModelDefinition[]
|
||||
}
|
||||
|
||||
export interface ModelSlot {
|
||||
configId: string
|
||||
modelId: string
|
||||
}
|
||||
|
||||
export interface LLMConnectionStore {
|
||||
configs: LLMConnectionConfigCompat[]
|
||||
activeConfigId: string | null
|
||||
defaultAssistant: ModelSlot | null
|
||||
fastModel: ModelSlot | null
|
||||
schemaVersion: number
|
||||
}
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
/**
|
||||
* LLM 服务类型定义
|
||||
* 新类型系统基于 model-types.ts,此文件保留兼容别名
|
||||
*/
|
||||
|
||||
// 重新导出新类型系统
|
||||
export * from './model-types'
|
||||
|
||||
// ==================== 兼容别名 ====================
|
||||
|
||||
/**
|
||||
* @deprecated 使用 ProviderDefinition.id (string) 代替
|
||||
*/
|
||||
export type LLMProvider = string
|
||||
|
||||
/**
|
||||
* @deprecated 使用 ProviderDefinition 代替
|
||||
*/
|
||||
export interface ProviderInfo {
|
||||
id: string
|
||||
name: string
|
||||
@@ -27,11 +17,6 @@ export interface ProviderInfo {
|
||||
}>
|
||||
}
|
||||
|
||||
// ==================== 旧配置类型(兼容期保留) ====================
|
||||
|
||||
/**
|
||||
* @deprecated 使用 LLMConnectionConfigCompat 代替
|
||||
*/
|
||||
export interface AIServiceConfig {
|
||||
id: string
|
||||
name: string
|
||||
@@ -48,12 +33,10 @@ export interface AIServiceConfig {
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 使用 LLMConnectionStore 代替
|
||||
*/
|
||||
export interface AIConfigStore {
|
||||
configs: AIServiceConfig[]
|
||||
activeConfigId: string | null
|
||||
defaultAssistant: import('./model-types').ModelSlot | null
|
||||
fastModel: import('./model-types').ModelSlot | null
|
||||
}
|
||||
|
||||
export const MAX_CONFIG_COUNT = 99
|
||||
|
||||
@@ -69,7 +69,7 @@ function resolveApiConfig(config: EmbeddingServiceConfig): {
|
||||
} {
|
||||
if (config.apiSource === 'reuse_llm') {
|
||||
// 复用当前 LLM 配置
|
||||
const llmConfig = llm.getActiveConfig()
|
||||
const llmConfig = llm.getDefaultAssistantConfig()
|
||||
|
||||
if (!llmConfig) {
|
||||
throw new Error('未找到激活的 LLM 配置,请先在「模型配置」中添加 AI 服务')
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getVectorStore } from '../store'
|
||||
import { getSessionChunks } from '../chunking'
|
||||
import { loadRAGConfig } from '../config'
|
||||
import { completeSimple, type TextContent as PiTextContent } from '@mariozechner/pi-ai'
|
||||
import { getActiveConfig, buildPiModel } from '../../llm'
|
||||
import { getDefaultAssistantConfig, buildPiModel } from '../../llm'
|
||||
import { aiLogger as logger } from '../../logger'
|
||||
|
||||
/**
|
||||
@@ -34,7 +34,7 @@ const QUERY_REWRITE_PROMPT = `你是一个查询优化专家。请将用户的
|
||||
*/
|
||||
async function rewriteQuery(query: string, abortSignal?: AbortSignal): Promise<string> {
|
||||
try {
|
||||
const activeConfig = getActiveConfig()
|
||||
const activeConfig = getDefaultAssistantConfig()
|
||||
if (!activeConfig) return query
|
||||
|
||||
const piModel = buildPiModel(activeConfig)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
import Database from 'better-sqlite3'
|
||||
import { completeSimple, type TextContent as PiTextContent } from '@mariozechner/pi-ai'
|
||||
import { getActiveConfig, buildPiModel } from '../llm'
|
||||
import { getFastModelConfig, buildPiModel } from '../llm'
|
||||
import { getDbPath, openDatabase } from '../../database/core'
|
||||
import { aiLogger } from '../logger'
|
||||
import { t } from '../../i18n'
|
||||
@@ -20,12 +20,12 @@ async function llmComplete(
|
||||
userPrompt: string,
|
||||
options?: { temperature?: number; maxTokens?: number }
|
||||
): Promise<string> {
|
||||
const activeConfig = getActiveConfig()
|
||||
if (!activeConfig) {
|
||||
const fastConfig = getFastModelConfig()
|
||||
if (!fastConfig) {
|
||||
throw new Error(t('llm.notConfigured'))
|
||||
}
|
||||
|
||||
const piModel = buildPiModel(activeConfig)
|
||||
const piModel = buildPiModel(fastConfig)
|
||||
const now = Date.now()
|
||||
|
||||
const result = await completeSimple(
|
||||
@@ -35,7 +35,7 @@ async function llmComplete(
|
||||
messages: [{ role: 'user', content: userPrompt, timestamp: now }],
|
||||
},
|
||||
{
|
||||
apiKey: activeConfig.apiKey,
|
||||
apiKey: fastConfig.apiKey,
|
||||
temperature: options?.temperature,
|
||||
maxTokens: options?.maxTokens,
|
||||
}
|
||||
|
||||
+34
-17
@@ -10,7 +10,7 @@ import { serializeError } from '../ai/serialize-error'
|
||||
import { getLogsDir } from '../paths'
|
||||
import { Agent, type AgentStreamChunk, type SkillContext } from '../ai/agent'
|
||||
import { getDefaultGeneralAssistantId } from '../ai/assistant/defaultGeneral'
|
||||
import { getActiveConfig, buildPiModel } from '../ai/llm'
|
||||
import { getDefaultAssistantConfig, buildPiModel } from '../ai/llm'
|
||||
import { checkAndCompress, manualCompress, type CompressionConfig } from '../ai/compression'
|
||||
import { countMessagesTokens } from '../ai/tokenizer'
|
||||
import * as assistantManager from '../ai/assistant'
|
||||
@@ -406,11 +406,17 @@ export function registerAIHandlers({ win }: IpcContext): void {
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取当前激活的配置 ID
|
||||
* 获取默认助手 slot
|
||||
*/
|
||||
ipcMain.handle('llm:getActiveConfigId', async () => {
|
||||
const config = llm.getActiveConfig()
|
||||
return config?.id || null
|
||||
ipcMain.handle('llm:getDefaultAssistantSlot', async () => {
|
||||
return llm.getDefaultAssistantSlot()
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取快速模型 slot
|
||||
*/
|
||||
ipcMain.handle('llm:getFastModelSlot', async () => {
|
||||
return llm.getFastModelSlot()
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -489,11 +495,10 @@ export function registerAIHandlers({ win }: IpcContext): void {
|
||||
*/
|
||||
ipcMain.handle('llm:deleteConfig', async (_, id?: string) => {
|
||||
try {
|
||||
// 如果没有传 id,删除当前激活的配置
|
||||
if (!id) {
|
||||
const activeConfig = llm.getActiveConfig()
|
||||
if (activeConfig) {
|
||||
return llm.deleteConfig(activeConfig.id)
|
||||
const defaultConfig = llm.getDefaultAssistantConfig()
|
||||
if (defaultConfig) {
|
||||
return llm.deleteConfig(defaultConfig.id)
|
||||
}
|
||||
return { success: false, error: t('llm.noActiveConfig') }
|
||||
}
|
||||
@@ -505,13 +510,25 @@ export function registerAIHandlers({ win }: IpcContext): void {
|
||||
})
|
||||
|
||||
/**
|
||||
* 设置激活的配置
|
||||
* 设置默认助手模型(configId + modelId)
|
||||
*/
|
||||
ipcMain.handle('llm:setActiveConfig', async (_, id: string) => {
|
||||
ipcMain.handle('llm:setDefaultAssistantModel', async (_, configId: string, modelId: string) => {
|
||||
try {
|
||||
return llm.setActiveConfig(id)
|
||||
return llm.setDefaultAssistantModel(configId, modelId)
|
||||
} catch (error) {
|
||||
console.error('Failed to set active config:', error)
|
||||
console.error('Failed to set default assistant model:', error)
|
||||
return { success: false, error: String(error) }
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 设置快速模型
|
||||
*/
|
||||
ipcMain.handle('llm:setFastModel', async (_, slot: { configId: string; modelId: string } | null) => {
|
||||
try {
|
||||
return llm.setFastModel(slot)
|
||||
} catch (error) {
|
||||
console.error('Failed to set fast model:', error)
|
||||
return { success: false, error: String(error) }
|
||||
}
|
||||
})
|
||||
@@ -637,7 +654,7 @@ export function registerAIHandlers({ win }: IpcContext): void {
|
||||
options?: { temperature?: number; maxTokens?: number }
|
||||
) => {
|
||||
try {
|
||||
const activeConfig = getActiveConfig()
|
||||
const activeConfig = getDefaultAssistantConfig()
|
||||
if (!activeConfig) {
|
||||
return { success: false, error: t('llm.notConfigured') }
|
||||
}
|
||||
@@ -684,7 +701,7 @@ export function registerAIHandlers({ win }: IpcContext): void {
|
||||
options?: { temperature?: number; maxTokens?: number }
|
||||
) => {
|
||||
try {
|
||||
const activeConfig = getActiveConfig()
|
||||
const activeConfig = getDefaultAssistantConfig()
|
||||
if (!activeConfig) {
|
||||
return { success: false, error: t('llm.notConfigured') }
|
||||
}
|
||||
@@ -1079,7 +1096,7 @@ export function registerAIHandlers({ win }: IpcContext): void {
|
||||
const abortController = new AbortController()
|
||||
activeAgentRequests.set(requestId, abortController)
|
||||
|
||||
const activeAIConfig = getActiveConfig()
|
||||
const activeAIConfig = getDefaultAssistantConfig()
|
||||
if (!activeAIConfig) {
|
||||
return { success: false, error: t('llm.notConfigured') }
|
||||
}
|
||||
@@ -1323,7 +1340,7 @@ export function registerAIHandlers({ win }: IpcContext): void {
|
||||
'ai:compressContext',
|
||||
async (_, conversationId: string, compressionConfig: CompressionConfig, systemPrompt: string) => {
|
||||
try {
|
||||
const activeAIConfig = getActiveConfig()
|
||||
const activeAIConfig = getDefaultAssistantConfig()
|
||||
if (!activeAIConfig) {
|
||||
return { success: false, error: t('llm.notConfigured') }
|
||||
}
|
||||
|
||||
@@ -578,7 +578,6 @@ export const aiApi = {
|
||||
enabled: boolean
|
||||
tokenThresholdPercent: number
|
||||
bufferSizePercent: number
|
||||
compressionModelConfigId?: string
|
||||
maxToolResultPercent?: number
|
||||
},
|
||||
systemPrompt: string
|
||||
@@ -667,10 +666,17 @@ export const llmApi = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取当前激活的配置 ID
|
||||
* 获取默认助手 slot(configId + modelId)
|
||||
*/
|
||||
getActiveConfigId: (): Promise<string | null> => {
|
||||
return ipcRenderer.invoke('llm:getActiveConfigId')
|
||||
getDefaultAssistantSlot: (): Promise<{ configId: string; modelId: string } | null> => {
|
||||
return ipcRenderer.invoke('llm:getDefaultAssistantSlot')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取快速模型 slot
|
||||
*/
|
||||
getFastModelSlot: (): Promise<{ configId: string; modelId: string } | null> => {
|
||||
return ipcRenderer.invoke('llm:getFastModelSlot')
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -720,10 +726,17 @@ export const llmApi = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置激活的配置
|
||||
* 设置默认助手模型(configId + modelId)
|
||||
*/
|
||||
setActiveConfig: (id: string): Promise<{ success: boolean; error?: string }> => {
|
||||
return ipcRenderer.invoke('llm:setActiveConfig', id)
|
||||
setDefaultAssistantModel: (configId: string, modelId: string): Promise<{ success: boolean; error?: string }> => {
|
||||
return ipcRenderer.invoke('llm:setDefaultAssistantModel', configId, modelId)
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置快速模型
|
||||
*/
|
||||
setFastModel: (slot: { configId: string; modelId: string } | null): Promise<{ success: boolean; error?: string }> => {
|
||||
return ipcRenderer.invoke('llm:setFastModel', slot)
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1000,7 +1013,6 @@ export const agentApi = {
|
||||
enabled: boolean
|
||||
tokenThresholdPercent: number
|
||||
bufferSizePercent: number
|
||||
compressionModelConfigId?: string
|
||||
maxToolResultPercent?: number
|
||||
}
|
||||
): {
|
||||
|
||||
Vendored
+9
-5
@@ -304,6 +304,11 @@ type AIContentBlock =
|
||||
}
|
||||
}
|
||||
| { type: 'skill'; skillId: string; skillName: string }
|
||||
| {
|
||||
type: 'summary_meta'
|
||||
bufferBoundaryTimestamp: number
|
||||
compressedMessageCount: number
|
||||
}
|
||||
|
||||
type AIMessageRole = 'user' | 'assistant' | 'system'
|
||||
|
||||
@@ -410,7 +415,6 @@ interface AiApi {
|
||||
enabled: boolean
|
||||
tokenThresholdPercent: number
|
||||
bufferSizePercent: number
|
||||
compressionModelConfigId?: string
|
||||
maxToolResultPercent?: number
|
||||
},
|
||||
systemPrompt: string
|
||||
@@ -565,12 +569,12 @@ interface LlmApi {
|
||||
) => Promise<{ success: boolean; error?: string }>
|
||||
deleteCustomModel: (providerId: string, modelId: string) => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
/** @deprecated 使用 getProviderRegistry 代替 */
|
||||
getProviders: () => Promise<LLMProviderInfo[]>
|
||||
|
||||
// 多配置管理 API
|
||||
getAllConfigs: () => Promise<AIServiceConfigDisplay[]>
|
||||
getActiveConfigId: () => Promise<string | null>
|
||||
getDefaultAssistantSlot: () => Promise<{ configId: string; modelId: string } | null>
|
||||
getFastModelSlot: () => Promise<{ configId: string; modelId: string } | null>
|
||||
addConfig: (config: {
|
||||
name: string
|
||||
provider: string
|
||||
@@ -599,7 +603,8 @@ interface LlmApi {
|
||||
}
|
||||
) => Promise<{ success: boolean; error?: string }>
|
||||
deleteConfig: (id?: string) => Promise<{ success: boolean; error?: string }>
|
||||
setActiveConfig: (id: string) => Promise<{ success: boolean; error?: string }>
|
||||
setDefaultAssistantModel: (configId: string, modelId: string) => Promise<{ success: boolean; error?: string }>
|
||||
setFastModel: (slot: { configId: string; modelId: string } | null) => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// 验证和检查
|
||||
validateApiKey: (
|
||||
@@ -834,7 +839,6 @@ interface AgentApi {
|
||||
enabled: boolean
|
||||
tokenThresholdPercent: number
|
||||
bufferSizePercent: number
|
||||
compressionModelConfigId?: string
|
||||
maxToolResultPercent?: number
|
||||
}
|
||||
) => { requestId: string; promise: Promise<{ success: boolean; result?: AgentResult; error?: SerializedErrorInfo }> }
|
||||
|
||||
@@ -25,7 +25,7 @@ const props = defineProps<{
|
||||
const promptStore = usePromptStore()
|
||||
const llmStore = useLLMStore()
|
||||
const { aiGlobalSettings } = storeToRefs(promptStore)
|
||||
const { configs, activeConfig, isLoading: isLoadingLLM } = storeToRefs(llmStore)
|
||||
const { configs, defaultAssistantConfig, isLoading: isLoadingLLM } = storeToRefs(llmStore)
|
||||
|
||||
// 下拉菜单状态
|
||||
const isModelPopoverOpen = ref(false)
|
||||
@@ -86,10 +86,10 @@ const contextTokens = computed(() => {
|
||||
})
|
||||
|
||||
const modelContextWindow = computed(() => {
|
||||
if (!activeConfig.value) return 128000
|
||||
if (!defaultAssistantConfig.value) return 128000
|
||||
const model =
|
||||
llmStore.getModelById(activeConfig.value.provider, activeConfig.value.model) ||
|
||||
llmStore.findModelAcrossProviders(activeConfig.value.model)
|
||||
llmStore.getModelById(defaultAssistantConfig.value.provider, defaultAssistantConfig.value.model) ||
|
||||
llmStore.findModelAcrossProviders(defaultAssistantConfig.value.model)
|
||||
return model?.contextWindow ?? 128000
|
||||
})
|
||||
|
||||
@@ -129,7 +129,8 @@ function openChatSettings() {
|
||||
|
||||
// 切换 AI 模型配置
|
||||
async function switchModelConfig(configId: string) {
|
||||
const success = await llmStore.setActiveConfig(configId)
|
||||
const config = llmStore.configs.find((c) => c.id === configId)
|
||||
const success = await llmStore.setDefaultAssistantModel(configId, config?.model || '')
|
||||
if (success) {
|
||||
isModelPopoverOpen.value = false
|
||||
} else {
|
||||
@@ -233,7 +234,7 @@ async function openAiLogFile() {
|
||||
>
|
||||
<UIcon name="i-heroicons-cpu-chip" class="h-3.5 w-3.5" />
|
||||
<span class="max-w-[120px] truncate">
|
||||
{{ activeConfig?.name || t('ai.chat.statusBar.model.notConfigured') }}
|
||||
{{ defaultAssistantConfig?.name || t('ai.chat.statusBar.model.notConfigured') }}
|
||||
</span>
|
||||
<UIcon name="i-heroicons-chevron-down" class="h-3 w-3" />
|
||||
</button>
|
||||
@@ -250,16 +251,18 @@ async function openAiLogFile() {
|
||||
:key="config.id"
|
||||
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
:class="[
|
||||
config.id === activeConfig?.id
|
||||
config.id === defaultAssistantConfig?.id
|
||||
? 'text-pink-600 dark:text-pink-400'
|
||||
: 'text-gray-700 dark:text-gray-300',
|
||||
]"
|
||||
@click="switchModelConfig(config.id)"
|
||||
>
|
||||
<UIcon
|
||||
:name="config.id === activeConfig?.id ? 'i-heroicons-check-circle-solid' : 'i-heroicons-cpu-chip'"
|
||||
:name="
|
||||
config.id === defaultAssistantConfig?.id ? 'i-heroicons-check-circle-solid' : 'i-heroicons-cpu-chip'
|
||||
"
|
||||
class="h-4 w-4 shrink-0"
|
||||
:class="[config.id === activeConfig?.id ? 'text-pink-500' : 'text-gray-400']"
|
||||
:class="[config.id === defaultAssistantConfig?.id ? 'text-pink-500' : 'text-gray-400']"
|
||||
/>
|
||||
<div class="flex flex-col truncate">
|
||||
<span class="truncate">{{ config.name }}</span>
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useLLMStore } from '@/stores/llm'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'config-changed': []
|
||||
}>()
|
||||
|
||||
const llmStore = useLLMStore()
|
||||
const { configs, defaultAssistant, fastModel, modelCatalog } = storeToRefs(llmStore)
|
||||
|
||||
function getModelsForConfig(configId: string) {
|
||||
const config = configs.value.find((c) => c.id === configId)
|
||||
if (!config) return []
|
||||
|
||||
if (config.customModels && config.customModels.length > 0) {
|
||||
return config.customModels.map((m) => ({ label: m.name || m.id, value: m.id }))
|
||||
}
|
||||
|
||||
const models = modelCatalog.value.filter(
|
||||
(m) =>
|
||||
m.providerId === config.provider && !m.capabilities.includes('embedding') && !m.capabilities.includes('ranking')
|
||||
)
|
||||
const options = models.map((m) => ({ label: m.name || m.id, value: m.id }))
|
||||
|
||||
if (config.model && !options.some((o) => o.value === config.model)) {
|
||||
options.unshift({ label: config.model, value: config.model })
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
const configOptions = computed(() =>
|
||||
configs.value.map((c) => ({
|
||||
label: c.name,
|
||||
value: c.id,
|
||||
}))
|
||||
)
|
||||
|
||||
// ========== 助手模型 ==========
|
||||
|
||||
const assistantModelOptions = computed(() => {
|
||||
const id = defaultAssistant.value?.configId
|
||||
return id ? getModelsForConfig(id) : []
|
||||
})
|
||||
|
||||
const internalAssistantConfigId = computed({
|
||||
get: () => defaultAssistant.value?.configId ?? '',
|
||||
set: (val: string) => {
|
||||
if (!val) return
|
||||
const models = getModelsForConfig(val)
|
||||
const modelId = models[0]?.value ?? ''
|
||||
llmStore.setDefaultAssistantModel(val, modelId)
|
||||
emit('config-changed')
|
||||
},
|
||||
})
|
||||
|
||||
const internalAssistantModelId = computed({
|
||||
get: () => defaultAssistant.value?.modelId ?? '',
|
||||
set: (val: string) => {
|
||||
const configId = defaultAssistant.value?.configId
|
||||
if (configId && val) {
|
||||
llmStore.setDefaultAssistantModel(configId, val)
|
||||
emit('config-changed')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// ========== 快速模型 ==========
|
||||
|
||||
const FOLLOW_VALUE = '__follow__'
|
||||
|
||||
const fastConfigSelectOptions = computed(() => [
|
||||
{ label: t('settings.defaultModel.fastModel.followAssistant'), value: FOLLOW_VALUE },
|
||||
...configOptions.value,
|
||||
])
|
||||
|
||||
const fastModelOptions = computed(() => {
|
||||
const id = fastModel.value?.configId
|
||||
return id ? getModelsForConfig(id) : []
|
||||
})
|
||||
|
||||
const showFastModelSelector = computed(() => fastModel.value !== null)
|
||||
|
||||
const internalFastConfigId = computed({
|
||||
get: () => fastModel.value?.configId ?? FOLLOW_VALUE,
|
||||
set: (val: string) => {
|
||||
if (val === FOLLOW_VALUE) {
|
||||
llmStore.setFastModel(null)
|
||||
} else {
|
||||
const models = getModelsForConfig(val)
|
||||
const modelId = models[0]?.value ?? ''
|
||||
llmStore.setFastModel({ configId: val, modelId })
|
||||
}
|
||||
emit('config-changed')
|
||||
},
|
||||
})
|
||||
|
||||
const internalFastModelId = computed({
|
||||
get: () => fastModel.value?.modelId ?? '',
|
||||
set: (val: string) => {
|
||||
const configId = fastModel.value?.configId
|
||||
if (configId && val) {
|
||||
llmStore.setFastModel({ configId, modelId: val })
|
||||
emit('config-changed')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!llmStore.isInitialized) {
|
||||
llmStore.init()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div
|
||||
v-if="configs.length === 0"
|
||||
class="rounded-lg border-2 border-dashed border-gray-200 py-8 text-center dark:border-gray-700"
|
||||
>
|
||||
<UIcon name="i-heroicons-cpu-chip" class="mx-auto mb-2 h-8 w-8 text-gray-300 dark:text-gray-600" />
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ t('settings.defaultModel.noConfigs') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<h4 class="mb-3 flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
<UIcon name="i-heroicons-cpu-chip" class="h-4 w-4 text-violet-500" />
|
||||
{{ t('settings.defaultModel.title') }}
|
||||
</h4>
|
||||
<div class="space-y-5 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<!-- 默认助手模型 -->
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.defaultModel.assistantModel.label') }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.defaultModel.assistantModel.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-1">
|
||||
<span class="mb-1 block text-xs text-gray-400 dark:text-gray-500">
|
||||
{{ t('settings.defaultModel.selectProvider') }}
|
||||
</span>
|
||||
<USelect
|
||||
v-model="internalAssistantConfigId"
|
||||
:items="configOptions"
|
||||
:ui="{ content: 'z-[200]' }"
|
||||
class="w-full"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="mb-1 block text-xs text-gray-400 dark:text-gray-500">
|
||||
{{ t('settings.defaultModel.selectModel') }}
|
||||
</span>
|
||||
<USelect
|
||||
v-if="assistantModelOptions.length > 0"
|
||||
v-model="internalAssistantModelId"
|
||||
:items="assistantModelOptions"
|
||||
:ui="{ content: 'z-[200]' }"
|
||||
class="w-full"
|
||||
size="sm"
|
||||
/>
|
||||
<USelect v-else :items="[]" disabled class="w-full" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="border-t border-gray-200 dark:border-gray-700" />
|
||||
|
||||
<!-- 快速模型 -->
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.defaultModel.fastModel.label') }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.defaultModel.fastModel.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-1">
|
||||
<span class="mb-1 block text-xs text-gray-400 dark:text-gray-500">
|
||||
{{ t('settings.defaultModel.selectProvider') }}
|
||||
</span>
|
||||
<USelect
|
||||
v-model="internalFastConfigId"
|
||||
:items="fastConfigSelectOptions"
|
||||
:ui="{ content: 'z-[200]' }"
|
||||
class="w-full"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="mb-1 block text-xs text-gray-400 dark:text-gray-500">
|
||||
{{ t('settings.defaultModel.selectModel') }}
|
||||
</span>
|
||||
<USelect
|
||||
v-if="showFastModelSelector && fastModelOptions.length > 0"
|
||||
v-model="internalFastModelId"
|
||||
:items="fastModelOptions"
|
||||
:ui="{ content: 'z-[200]' }"
|
||||
class="w-full"
|
||||
size="sm"
|
||||
/>
|
||||
<USelect v-else :items="[]" disabled class="w-full" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -24,7 +24,7 @@ const aiTips = computed(() => {
|
||||
// ============ Store ============
|
||||
|
||||
const llmStore = useLLMStore()
|
||||
const { configs, providers, providerRegistry, activeConfigId, isLoading, isMaxConfigs } = storeToRefs(llmStore)
|
||||
const { configs, providers, providerRegistry, isLoading, isMaxConfigs } = storeToRefs(llmStore)
|
||||
|
||||
// 弹窗状态
|
||||
const showEditModal = ref(false)
|
||||
@@ -64,13 +64,6 @@ async function deleteConfig(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function setActive(id: string) {
|
||||
const success = await llmStore.setActiveConfig(id)
|
||||
if (success) {
|
||||
emit('config-changed')
|
||||
}
|
||||
}
|
||||
|
||||
function getProviderName(providerId: string): string {
|
||||
const key = `providers.${providerId}.name`
|
||||
const translated = t(key)
|
||||
@@ -119,86 +112,78 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- 配置列表视图 -->
|
||||
<div v-else class="space-y-4">
|
||||
<!-- 标题 -->
|
||||
<h4 class="flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
<UIcon name="i-heroicons-sparkles" class="h-4 w-4 text-violet-500" />
|
||||
{{ t('settings.aiConfig.title') }}
|
||||
</h4>
|
||||
<AlertTips v-if="configs.length === 0 && aiTips.configTab?.show" :content="aiTips.configTab?.content" />
|
||||
<!-- 配置列表 -->
|
||||
<div v-if="configs.length > 0" class="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700 divide-y divide-gray-100 dark:divide-gray-800">
|
||||
<div
|
||||
v-for="config in configs"
|
||||
:key="config.id"
|
||||
class="group flex cursor-pointer items-center justify-between px-3 py-2 transition-colors"
|
||||
:class="[
|
||||
config.id === activeConfigId
|
||||
? 'bg-primary-50/60 dark:bg-primary-900/20'
|
||||
: 'bg-card-bg hover:bg-gray-50 dark:bg-card-dark dark:hover:bg-gray-800/50',
|
||||
]"
|
||||
@click="setActive(config.id)"
|
||||
>
|
||||
<!-- 配置信息 -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div v-else class="space-y-6">
|
||||
<div>
|
||||
<h4 class="mb-3 flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
<UIcon name="i-heroicons-sparkles" class="h-4 w-4 text-violet-500" />
|
||||
{{ t('settings.aiConfig.title') }}
|
||||
</h4>
|
||||
<AlertTips v-if="configs.length === 0 && aiTips.configTab?.show" :content="aiTips.configTab?.content" />
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<!-- 配置列表 -->
|
||||
<div v-if="configs.length > 0" class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div
|
||||
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full"
|
||||
:class="[
|
||||
config.id === activeConfigId
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-gray-200 text-gray-500 dark:bg-gray-700 dark:text-gray-400',
|
||||
]"
|
||||
v-for="config in configs"
|
||||
:key="config.id"
|
||||
class="group flex items-center justify-between px-4 py-2.5 transition-colors hover:bg-gray-100/50 dark:hover:bg-gray-700/30"
|
||||
>
|
||||
<UIcon
|
||||
:name="config.id === activeConfigId ? 'i-heroicons-check' : 'i-heroicons-sparkles'"
|
||||
class="h-3.5 w-3.5"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<span class="w-28 shrink-0 truncate font-medium text-sm text-gray-900 dark:text-white">{{ config.name }}</span>
|
||||
<span class="h-3.5 w-px shrink-0 bg-gray-200 dark:bg-gray-700" />
|
||||
<span class="flex min-w-0 items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span class="truncate">{{ getProviderName(config.provider) }}</span>
|
||||
<UBadge v-if="getProviderKindLabel(config.provider)" color="neutral" variant="subtle" size="xs">
|
||||
{{ getProviderKindLabel(config.provider) }}
|
||||
</UBadge>
|
||||
<span class="shrink-0">·</span>
|
||||
<span class="truncate">{{ getModelDisplayName(config.provider, config.model) }}</span>
|
||||
</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-gray-200 text-gray-500 dark:bg-gray-700 dark:text-gray-400"
|
||||
>
|
||||
<UIcon name="i-heroicons-sparkles" class="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<span class="w-28 shrink-0 truncate text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ config.name }}
|
||||
</span>
|
||||
<span class="h-3.5 w-px shrink-0 bg-gray-200 dark:bg-gray-700" />
|
||||
<span class="flex min-w-0 items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span class="truncate">{{ getProviderName(config.provider) }}</span>
|
||||
<UBadge v-if="getProviderKindLabel(config.provider)" color="neutral" variant="subtle" size="xs">
|
||||
{{ getProviderKindLabel(config.provider) }}
|
||||
</UBadge>
|
||||
<span class="shrink-0">·</span>
|
||||
<span class="truncate">{{ getModelDisplayName(config.provider, config.model) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<UButton
|
||||
size="xs"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
icon="i-heroicons-pencil-square"
|
||||
@click="openEditModal(config)"
|
||||
/>
|
||||
<UButton
|
||||
size="xs"
|
||||
color="error"
|
||||
variant="ghost"
|
||||
icon="i-heroicons-trash"
|
||||
@click="deleteConfig(config.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100" @click.stop>
|
||||
<UButton
|
||||
size="xs"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
icon="i-heroicons-pencil-square"
|
||||
@click="openEditModal(config)"
|
||||
/>
|
||||
<UButton size="xs" color="error" variant="ghost" icon="i-heroicons-trash" @click="deleteConfig(config.id)" />
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="flex flex-col items-center justify-center py-8">
|
||||
<UIcon name="i-heroicons-sparkles" class="h-8 w-8 text-gray-300 dark:text-gray-600" />
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ t('settings.aiConfig.empty.title') }}</p>
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500">{{ t('settings.aiConfig.empty.description') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 添加按钮 -->
|
||||
<div class="border-t border-gray-200 px-4 py-3 dark:border-gray-700">
|
||||
<UButton variant="soft" :disabled="isMaxConfigs" size="sm" @click="openAddModal">
|
||||
<UIcon name="i-heroicons-plus" class="mr-1.5 h-3.5 w-3.5" />
|
||||
{{ isMaxConfigs ? t('settings.aiConfig.maxConfigs') : t('settings.aiConfig.addConfig') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-200 py-8 dark:border-gray-700"
|
||||
>
|
||||
<UIcon name="i-heroicons-sparkles" class="h-10 w-10 text-gray-300 dark:text-gray-600" />
|
||||
<p class="mt-3 text-sm text-gray-500 dark:text-gray-400">{{ t('settings.aiConfig.empty.title') }}</p>
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500">{{ t('settings.aiConfig.empty.description') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 添加按钮 -->
|
||||
<div class="flex justify-center">
|
||||
<UButton variant="soft" :disabled="isMaxConfigs" size="sm" @click="openAddModal">
|
||||
<UIcon name="i-heroicons-plus" class="mr-2 h-4 w-4" />
|
||||
{{ isMaxConfigs ? t('settings.aiConfig.maxConfigs') : t('settings.aiConfig.addConfig') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑/添加弹窗 -->
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import AIModelConfigTab from './AI/AIModelConfigTab.vue'
|
||||
import AIDefaultModelTab from './AI/AIDefaultModelTab.vue'
|
||||
import AIPromptConfigTab from './AI/AIPromptConfigTab.vue'
|
||||
import AIPreprocessTab from './AI/AIPreprocessTab.vue'
|
||||
// TODO: 向量模型暂时隐藏,待功能完善后恢复
|
||||
@@ -19,6 +20,7 @@ const emit = defineEmits<{
|
||||
// 导航配置
|
||||
const navItems = computed(() => [
|
||||
{ id: 'model', label: t('settings.tabs.aiConfig') },
|
||||
{ id: 'defaultModel', label: t('settings.tabs.aiDefaultModel') },
|
||||
// TODO: 向量模型暂时隐藏,待功能完善后恢复
|
||||
// { id: 'rag', label: t('settings.tabs.aiRAG') },
|
||||
{ id: 'chat', label: t('settings.tabs.aiPrompt') },
|
||||
@@ -66,6 +68,14 @@ void aiModelConfigRef.value
|
||||
<AIModelConfigTab ref="aiModelConfigRef" @config-changed="handleAIConfigChanged" />
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="border-t border-gray-200 dark:border-gray-700" />
|
||||
|
||||
<!-- 默认模型 -->
|
||||
<div :ref="(el) => setSectionRef('defaultModel', el as HTMLElement)">
|
||||
<AIDefaultModelTab @config-changed="handleAIConfigChanged" />
|
||||
</div>
|
||||
|
||||
<!-- TODO: 向量模型暂时隐藏,待功能完善后恢复 -->
|
||||
<!--
|
||||
<div class="border-t border-gray-200 dark:border-gray-700" />
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"basic": "General",
|
||||
"ai": "AI Settings",
|
||||
"aiConfig": "Model Service",
|
||||
"aiDefaultModel": "Default Model",
|
||||
"aiRAG": "Vector Model",
|
||||
"aiPrompt": "Chat Config",
|
||||
"aiPreprocess": "Preprocess",
|
||||
@@ -79,6 +80,21 @@
|
||||
"saveFailed": "Save failed"
|
||||
}
|
||||
},
|
||||
"defaultModel": {
|
||||
"title": "Default Model",
|
||||
"noConfigs": "Please add a model configuration in \"Model Service\" first",
|
||||
"selectProvider": "Select provider",
|
||||
"selectModel": "Select model",
|
||||
"assistantModel": {
|
||||
"label": "Default Assistant Model",
|
||||
"description": "Used for AI chat, tool calls, SQL assistant, and context compression"
|
||||
},
|
||||
"fastModel": {
|
||||
"label": "Fast Model",
|
||||
"description": "Used for session summary generation. Falls back to the default assistant model if not set",
|
||||
"followAssistant": "Follow default assistant model"
|
||||
}
|
||||
},
|
||||
"aiConfig": {
|
||||
"title": "Model Services",
|
||||
"providerTitle": "Service Providers",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"basic": "基本設定",
|
||||
"ai": "AI 設定",
|
||||
"aiConfig": "モデルサービス",
|
||||
"aiDefaultModel": "デフォルトモデル",
|
||||
"aiRAG": "埋め込みモデル",
|
||||
"aiPrompt": "チャット設定",
|
||||
"aiPreprocess": "前処理",
|
||||
@@ -79,6 +80,21 @@
|
||||
"saveFailed": "保存に失敗しました"
|
||||
}
|
||||
},
|
||||
"defaultModel": {
|
||||
"title": "デフォルトモデル",
|
||||
"noConfigs": "先に「モデルサービス」でモデル設定を追加してください",
|
||||
"selectProvider": "プロバイダーを選択",
|
||||
"selectModel": "モデルを選択",
|
||||
"assistantModel": {
|
||||
"label": "デフォルトアシスタントモデル",
|
||||
"description": "AIチャット、ツール呼び出し、SQLアシスタント、コンテキスト圧縮に使用"
|
||||
},
|
||||
"fastModel": {
|
||||
"label": "高速モデル",
|
||||
"description": "セッション要約生成に使用。未設定の場合はデフォルトアシスタントモデルを使用",
|
||||
"followAssistant": "デフォルトアシスタントモデルに従う"
|
||||
}
|
||||
},
|
||||
"aiConfig": {
|
||||
"title": "モデルサービス",
|
||||
"providerTitle": "サービスプロバイダー",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"basic": "基础设置",
|
||||
"ai": "AI 设置",
|
||||
"aiConfig": "模型服务",
|
||||
"aiDefaultModel": "默认模型",
|
||||
"aiRAG": "向量模型",
|
||||
"aiPrompt": "对话配置",
|
||||
"aiPreprocess": "预处理",
|
||||
@@ -79,6 +80,21 @@
|
||||
"saveFailed": "保存失败"
|
||||
}
|
||||
},
|
||||
"defaultModel": {
|
||||
"title": "默认模型",
|
||||
"noConfigs": "请先在「模型服务」中添加模型配置",
|
||||
"selectProvider": "选择提供商",
|
||||
"selectModel": "选择模型",
|
||||
"assistantModel": {
|
||||
"label": "默认助手模型",
|
||||
"description": "用于 AI 对话、工具调用、SQL 助手、上下文压缩"
|
||||
},
|
||||
"fastModel": {
|
||||
"label": "快速模型",
|
||||
"description": "用于会话摘要生成,未配置时使用默认助手模型",
|
||||
"followAssistant": "跟随默认助手模型"
|
||||
}
|
||||
},
|
||||
"aiConfig": {
|
||||
"title": "模型服务",
|
||||
"providerTitle": "服务提供商",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"basic": "基本設定",
|
||||
"ai": "AI 設定",
|
||||
"aiConfig": "模型服務",
|
||||
"aiDefaultModel": "預設模型",
|
||||
"aiRAG": "向量模型",
|
||||
"aiPrompt": "聊天設定",
|
||||
"aiPreprocess": "前處理",
|
||||
@@ -79,6 +80,21 @@
|
||||
"saveFailed": "儲存失敗"
|
||||
}
|
||||
},
|
||||
"defaultModel": {
|
||||
"title": "預設模型",
|
||||
"noConfigs": "請先在「模型服務」中新增模型設定",
|
||||
"selectProvider": "選擇提供商",
|
||||
"selectModel": "選擇模型",
|
||||
"assistantModel": {
|
||||
"label": "預設助手模型",
|
||||
"description": "用於 AI 對話、工具呼叫、SQL 助手、上下文壓縮"
|
||||
},
|
||||
"fastModel": {
|
||||
"label": "快速模型",
|
||||
"description": "用於會話摘要生成,未設定時使用預設助手模型",
|
||||
"followAssistant": "跟隨預設助手模型"
|
||||
}
|
||||
},
|
||||
"aiConfig": {
|
||||
"title": "模型服務",
|
||||
"providerTitle": "服務提供商",
|
||||
|
||||
@@ -50,6 +50,11 @@ export type ContentBlock =
|
||||
}
|
||||
| { type: 'skill'; skillId: string; skillName: string }
|
||||
| { type: 'error'; error: SerializedErrorInfo }
|
||||
| {
|
||||
type: 'summary_meta'
|
||||
bufferBoundaryTimestamp: number
|
||||
compressedMessageCount: number
|
||||
}
|
||||
|
||||
// 消息类型
|
||||
export interface ChatMessage {
|
||||
@@ -920,7 +925,6 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
|
||||
enabled: aiGlobalSettings.value.contextCompression?.enabled ?? false,
|
||||
tokenThresholdPercent: aiGlobalSettings.value.contextCompression?.tokenThresholdPercent ?? 75,
|
||||
bufferSizePercent: aiGlobalSettings.value.contextCompression?.bufferSizePercent ?? 20,
|
||||
compressionModelConfigId: aiGlobalSettings.value.contextCompression?.compressionModelConfigId,
|
||||
maxToolResultPercent: aiGlobalSettings.value.contextCompression?.maxToolResultPercent ?? 50,
|
||||
}
|
||||
)
|
||||
|
||||
+39
-27
@@ -12,13 +12,11 @@ export interface AIServiceConfigDisplay {
|
||||
apiKeySet: boolean
|
||||
model?: string
|
||||
baseUrl?: string
|
||||
customModels?: Array<{ id: string; name: string }>
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 使用 ProviderDefinition 代替
|
||||
*/
|
||||
export interface LLMProvider {
|
||||
id: string
|
||||
name: string
|
||||
@@ -26,35 +24,29 @@ export interface LLMProvider {
|
||||
models: Array<{ id: string; name: string; description?: string }>
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM 配置状态管理
|
||||
*/
|
||||
export const useLLMStore = defineStore('llm', () => {
|
||||
// ============ 状态 ============
|
||||
|
||||
const configs = ref<AIServiceConfigDisplay[]>([])
|
||||
|
||||
/** @deprecated 使用 providerRegistry 代替 */
|
||||
const providers = ref<LLMProvider[]>([])
|
||||
|
||||
const activeConfigId = ref<string | null>(null)
|
||||
const defaultAssistant = ref<{ configId: string; modelId: string } | null>(null)
|
||||
const fastModel = ref<{ configId: string; modelId: string } | null>(null)
|
||||
const isLoading = ref(false)
|
||||
const isInitialized = ref(false)
|
||||
|
||||
/** 新模型系统:Provider Registry(内置 + 自定义) */
|
||||
const providerRegistry = ref<ProviderDefinition[]>([])
|
||||
|
||||
/** 新模型系统:Model Catalog(内置 + 自定义) */
|
||||
const modelCatalog = ref<ModelDefinition[]>([])
|
||||
|
||||
// ============ 计算属性 ============
|
||||
|
||||
const activeConfig = computed(() => configs.value.find((c) => c.id === activeConfigId.value) || null)
|
||||
const hasConfig = computed(() => !!activeConfigId.value)
|
||||
const defaultAssistantConfig = computed(
|
||||
() => configs.value.find((c) => c.id === defaultAssistant.value?.configId) || null
|
||||
)
|
||||
const fastModelConfig = computed(() => configs.value.find((c) => c.id === fastModel.value?.configId) || null)
|
||||
const hasConfig = computed(() => !!defaultAssistant.value)
|
||||
const isMaxConfigs = computed(() => configs.value.length >= 99)
|
||||
|
||||
// ============ 新模型系统 computed ============
|
||||
|
||||
function getProviderById(id: string): ProviderDefinition | undefined {
|
||||
return providerRegistry.value.find((p) => p.id === id)
|
||||
}
|
||||
@@ -82,18 +74,20 @@ export const useLLMStore = defineStore('llm', () => {
|
||||
async function loadConfigs() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const [providersData, registryData, catalogData, configsData, activeId] = await Promise.all([
|
||||
const [providersData, registryData, catalogData, configsData, assistantSlot, fastSlot] = await Promise.all([
|
||||
window.llmApi.getProviders(),
|
||||
window.llmApi.getProviderRegistry(),
|
||||
window.llmApi.getModelCatalog(),
|
||||
window.llmApi.getAllConfigs(),
|
||||
window.llmApi.getActiveConfigId(),
|
||||
window.llmApi.getDefaultAssistantSlot(),
|
||||
window.llmApi.getFastModelSlot(),
|
||||
])
|
||||
providers.value = providersData
|
||||
providerRegistry.value = registryData
|
||||
modelCatalog.value = catalogData
|
||||
configs.value = configsData
|
||||
activeConfigId.value = activeId
|
||||
defaultAssistant.value = assistantSlot
|
||||
fastModel.value = fastSlot
|
||||
} catch (error) {
|
||||
console.error('[LLM Store] 加载配置失败:', error)
|
||||
} finally {
|
||||
@@ -101,17 +95,32 @@ export const useLLMStore = defineStore('llm', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function setActiveConfig(id: string): Promise<boolean> {
|
||||
async function setDefaultAssistantModel(configId: string, modelId: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await window.llmApi.setActiveConfig(id)
|
||||
const result = await window.llmApi.setDefaultAssistantModel(configId, modelId)
|
||||
if (result.success) {
|
||||
activeConfigId.value = id
|
||||
defaultAssistant.value = { configId, modelId }
|
||||
return true
|
||||
}
|
||||
console.error('[LLM Store] 设置激活配置失败:', result.error)
|
||||
console.error('[LLM Store] 设置默认助手模型失败:', result.error)
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('[LLM Store] 设置激活配置失败:', error)
|
||||
console.error('[LLM Store] 设置默认助手模型失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function setFastModel(slot: { configId: string; modelId: string } | null): Promise<boolean> {
|
||||
try {
|
||||
const result = await window.llmApi.setFastModel(slot)
|
||||
if (result.success) {
|
||||
fastModel.value = slot
|
||||
return true
|
||||
}
|
||||
console.error('[LLM Store] 设置快速模型失败:', result.error)
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('[LLM Store] 设置快速模型失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -132,17 +141,20 @@ export const useLLMStore = defineStore('llm', () => {
|
||||
providers,
|
||||
providerRegistry,
|
||||
modelCatalog,
|
||||
activeConfigId,
|
||||
defaultAssistant,
|
||||
fastModel,
|
||||
isLoading,
|
||||
isInitialized,
|
||||
// 计算属性
|
||||
activeConfig,
|
||||
defaultAssistantConfig,
|
||||
fastModelConfig,
|
||||
hasConfig,
|
||||
isMaxConfigs,
|
||||
// 方法
|
||||
init,
|
||||
loadConfigs,
|
||||
setActiveConfig,
|
||||
setDefaultAssistantModel,
|
||||
setFastModel,
|
||||
refreshConfigs,
|
||||
getProviderName,
|
||||
getProviderById,
|
||||
|
||||
@@ -20,7 +20,6 @@ export const usePromptStore = defineStore(
|
||||
enabled: true,
|
||||
tokenThresholdPercent: 75,
|
||||
bufferSizePercent: 20,
|
||||
compressionModelConfigId: undefined as string | undefined,
|
||||
maxToolResultPercent: 50,
|
||||
},
|
||||
})
|
||||
@@ -49,7 +48,6 @@ export const usePromptStore = defineStore(
|
||||
enabled: boolean
|
||||
tokenThresholdPercent: number
|
||||
bufferSizePercent: number
|
||||
compressionModelConfigId?: string
|
||||
maxToolResultPercent?: number
|
||||
}
|
||||
}>
|
||||
|
||||
Reference in New Issue
Block a user