mirror of
https://github.com/ILoveBingLu/CipherTalk.git
synced 2026-05-25 05:50:14 +08:00
78dd50de17
兼容最新版Deepseek
718 lines
22 KiB
TypeScript
718 lines
22 KiB
TypeScript
import { ConfigService } from '../config'
|
|
import { aiDatabase, type SaveAnalysisArtifactsInput } from './aiDatabase'
|
|
import { ZhipuProvider, ZhipuMetadata } from './providers/zhipu'
|
|
import { DeepSeekProvider, DeepSeekMetadata } from './providers/deepseek'
|
|
import { QwenProvider, QwenMetadata } from './providers/qwen'
|
|
import { DoubaoProvider, DoubaoMetadata } from './providers/doubao'
|
|
import { KimiProvider, KimiMetadata } from './providers/kimi'
|
|
import { SiliconFlowProvider, SiliconFlowMetadata } from './providers/siliconflow'
|
|
import { XiaomiProvider, XiaomiMetadata } from './providers/xiaomi'
|
|
import { TencentProvider, TencentMetadata } from './providers/tencent'
|
|
import { XAIProvider, XAIMetadata } from './providers/xai'
|
|
import { OpenAIProvider, OpenAIMetadata } from './providers/openai'
|
|
import { MiniMaxProvider, MiniMaxMetadata } from './providers/minimax'
|
|
import { GeminiProvider, GeminiMetadata } from './providers/gemini'
|
|
import { OllamaProvider, OllamaMetadata } from './providers/ollama'
|
|
import { CustomProvider, CustomMetadata } from './providers/custom'
|
|
import { AIProvider } from './providers/base'
|
|
import type { Message, Contact } from '../chatService'
|
|
import {
|
|
fallbackStructuredAnalysisWithoutEvidence,
|
|
resolveStructuredAnalysisEvidence
|
|
} from '../ai-agent/analyzer/evidenceResolver'
|
|
import { extractFactsFromBlocks } from '../ai-agent/analyzer/factExtractor'
|
|
import {
|
|
renderStandardizedMessages,
|
|
sliceAnalysisBlocks,
|
|
standardizeMessagesForAnalysis
|
|
} from '../ai-agent/analyzer/blockSlicer'
|
|
import { mergeStructuredAnalysisBlocks } from '../ai-agent/analyzer/resultMerger'
|
|
import {
|
|
buildLegacySummaryUserPrompt,
|
|
buildStructuredSummaryUserPrompt
|
|
} from '../ai-agent/analyzer/summaryRenderer'
|
|
import type {
|
|
AnalysisBlock,
|
|
ExtractedStructuredAnalysis,
|
|
StructuredAnalysis
|
|
} from '../ai-agent/types/analysis'
|
|
import {
|
|
answerSessionQuestionWithAgent,
|
|
type SessionQAHistoryMessage,
|
|
type SessionQAProgressEvent,
|
|
type SessionQAToolCall
|
|
} from '../ai-agent/qa/sessionQaAgent'
|
|
|
|
/**
|
|
* 摘要选项
|
|
*/
|
|
export interface SummaryOptions {
|
|
sessionId: string
|
|
timeRangeDays: number // 0 表示全部消息
|
|
timeRangeStart?: number
|
|
timeRangeEnd?: number
|
|
provider?: string
|
|
apiKey?: string
|
|
model?: string
|
|
language?: 'zh' | 'en'
|
|
detail?: 'simple' | 'normal' | 'detailed'
|
|
systemPromptPreset?: 'default' | 'decision-focus' | 'action-focus' | 'risk-focus' | 'custom'
|
|
customSystemPrompt?: string
|
|
customRequirement?: string // 用户自定义要求
|
|
sessionName?: string // 会话名称
|
|
enableThinking?: boolean // 是否启用思考模式(推理模式)
|
|
inputMessageScopeNote?: string
|
|
}
|
|
|
|
/**
|
|
* 摘要结果
|
|
*/
|
|
export interface SummaryResult {
|
|
id?: number
|
|
sessionId: string
|
|
timeRangeStart: number
|
|
timeRangeEnd: number
|
|
timeRangeDays: number
|
|
messageCount: number
|
|
summaryText: string
|
|
tokensUsed: number
|
|
cost: number
|
|
provider: string
|
|
model: string
|
|
createdAt: number
|
|
customName?: string
|
|
structuredAnalysis?: StructuredAnalysis
|
|
}
|
|
|
|
export interface SessionQAOptions {
|
|
sessionId: string
|
|
sessionName?: string
|
|
question: string
|
|
summaryText?: string
|
|
structuredAnalysis?: StructuredAnalysis
|
|
history?: SessionQAHistoryMessage[]
|
|
provider?: string
|
|
apiKey?: string
|
|
model?: string
|
|
enableThinking?: boolean
|
|
}
|
|
|
|
export interface SessionQAResult {
|
|
sessionId: string
|
|
question: string
|
|
answerText: string
|
|
evidenceRefs: Array<{
|
|
sessionId: string
|
|
localId: number
|
|
createTime: number
|
|
sortSeq: number
|
|
senderUsername?: string
|
|
senderDisplayName?: string
|
|
previewText: string
|
|
}>
|
|
toolCalls: SessionQAToolCall[]
|
|
tokensUsed: number
|
|
cost: number
|
|
provider: string
|
|
model: string
|
|
createdAt: number
|
|
}
|
|
|
|
interface StructuredAnalysisAttempt {
|
|
blocks: AnalysisBlock[]
|
|
blockAnalyses: ExtractedStructuredAnalysis[]
|
|
mergedAnalysis: ExtractedStructuredAnalysis
|
|
finalAnalysis: StructuredAnalysis
|
|
blockCount: number
|
|
effectiveMessageCount: number
|
|
evidenceResolved: boolean
|
|
}
|
|
|
|
/**
|
|
* AI 服务主类
|
|
*/
|
|
class AIService {
|
|
private configService: ConfigService
|
|
private initialized = false
|
|
|
|
constructor() {
|
|
this.configService = new ConfigService()
|
|
}
|
|
|
|
/**
|
|
* 初始化服务
|
|
*/
|
|
init(): void {
|
|
if (this.initialized) return
|
|
|
|
const cachePath = this.configService.get('cachePath')
|
|
const wxid = this.configService.get('myWxid')
|
|
|
|
if (!cachePath || !wxid) {
|
|
throw new Error('配置未完成,无法初始化AI服务')
|
|
}
|
|
|
|
// 初始化数据库
|
|
aiDatabase.init(cachePath, wxid)
|
|
|
|
this.initialized = true
|
|
}
|
|
|
|
/**
|
|
* 获取所有提供商元数据
|
|
*/
|
|
getAllProviders() {
|
|
return [
|
|
OpenAIMetadata,
|
|
MiniMaxMetadata,
|
|
GeminiMetadata,
|
|
XAIMetadata,
|
|
DeepSeekMetadata,
|
|
ZhipuMetadata,
|
|
QwenMetadata,
|
|
DoubaoMetadata,
|
|
KimiMetadata,
|
|
SiliconFlowMetadata,
|
|
XiaomiMetadata,
|
|
TencentMetadata,
|
|
OllamaMetadata,
|
|
CustomMetadata
|
|
]
|
|
}
|
|
|
|
/**
|
|
* 获取提供商实例
|
|
*/
|
|
private getProvider(providerName?: string, apiKey?: string): AIProvider {
|
|
const name = providerName || this.configService.getAICurrentProvider() || 'zhipu'
|
|
|
|
// 如果没有传入 apiKey,从配置中获取当前提供商的配置
|
|
let key = apiKey
|
|
if (!key) {
|
|
const providerConfig = this.configService.getAIProviderConfig(name)
|
|
key = providerConfig?.apiKey
|
|
}
|
|
|
|
// Ollama 本地服务不需要 API 密钥
|
|
if (!key && name !== 'ollama') {
|
|
throw new Error('未配置API密钥')
|
|
}
|
|
|
|
switch (name) {
|
|
case 'custom':
|
|
// 自定义服务必须提供 baseURL
|
|
const customConfig = this.configService.getAIProviderConfig('custom')
|
|
const customBaseURL = customConfig?.baseURL
|
|
if (!customBaseURL) {
|
|
throw new Error('自定义服务需要配置服务地址')
|
|
}
|
|
return new CustomProvider(key || '', customBaseURL)
|
|
case 'ollama':
|
|
// Ollama 支持自定义 baseURL
|
|
const ollamaConfig = this.configService.getAIProviderConfig('ollama')
|
|
const baseURL = ollamaConfig?.baseURL || 'http://localhost:11434/v1'
|
|
return new OllamaProvider(key || 'ollama', baseURL)
|
|
case 'openai':
|
|
return new OpenAIProvider(key!)
|
|
case 'minimax':
|
|
return new MiniMaxProvider(key!)
|
|
case 'gemini':
|
|
return new GeminiProvider(key!)
|
|
case 'zhipu':
|
|
return new ZhipuProvider(key!)
|
|
case 'deepseek':
|
|
return new DeepSeekProvider(key!)
|
|
case 'qwen':
|
|
return new QwenProvider(key!)
|
|
case 'doubao':
|
|
return new DoubaoProvider(key!)
|
|
case 'kimi':
|
|
return new KimiProvider(key!)
|
|
case 'siliconflow':
|
|
return new SiliconFlowProvider(key!)
|
|
case 'xiaomi':
|
|
return new XiaomiProvider(key!)
|
|
case 'tencent':
|
|
return new TencentProvider(key!)
|
|
case 'xai':
|
|
return new XAIProvider(key!)
|
|
default:
|
|
throw new Error(`不支持的提供商: ${name}`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取系统提示词
|
|
*/
|
|
private getSystemPrompt(
|
|
language: string = 'zh',
|
|
detail: string = 'normal',
|
|
preset: 'default' | 'decision-focus' | 'action-focus' | 'risk-focus' | 'custom' = 'default',
|
|
customSystemPrompt?: string
|
|
): string {
|
|
const detailInstructions = {
|
|
simple: '生成极简摘要,字数控制在 100 字以内。只保留最核心的事件和结论,忽略寒暄和琐碎细节。',
|
|
normal: '生成内容适中的摘要。涵盖对话主要话题、关键信息点及明确的约定事项。',
|
|
detailed: '生成详尽的深度分析。除了核心信息外,还需捕捉对话背景、各方态度倾向、潜在风险、具体细节以及所有隐含的待办事项。'
|
|
}
|
|
|
|
const detailName = {
|
|
simple: '极致精简',
|
|
normal: '标准平衡',
|
|
detailed: '深度详尽'
|
|
}
|
|
|
|
const basePrompt = `### 角色定义
|
|
你是一位拥有 10 年经验的高级情报分析师和沟通专家,擅长从琐碎、碎片化的聊天记录中精准提取高价值信息。
|
|
|
|
### 任务描述
|
|
分析用户提供的微信聊天记录(包含时间、发送者及内容),并生成一份**${detailName[detail as keyof typeof detailName] || '标准'}**级别的分析摘要。
|
|
|
|
### 详细度要求
|
|
${detailInstructions[detail as keyof typeof detailInstructions] || detailInstructions.normal}
|
|
|
|
### 核心规范
|
|
1. **真实性**:严格基于提供的聊天文字,不得臆造事实或推测未提及的信息。
|
|
2. **客观性**:保持专业、中立的第三方视角。
|
|
3. **结构化**:使用清晰的 Markdown 标题和列表。
|
|
4. **去噪**:忽略表情包、拍一拍、撤回提示等无意义的干扰信息,专注于实质性内容。
|
|
5. **语言**:始终使用中文输出。
|
|
|
|
### 输出格式模板
|
|
## 📝 对话概览
|
|
[一句话总结本次对话的核心主题和氛围]
|
|
|
|
## 💡 核心要点
|
|
- [关键点A]:简述事情经过或核心论点。
|
|
- [关键点B]:相关的背景或补充说明。
|
|
|
|
## 🤝 达成共识/决策
|
|
- [决策1]:各方最终确认的具体事项。
|
|
- [决策2]:已达成的阶段性结论。
|
|
|
|
## 📅 待办与后续进展
|
|
- [ ] **待办事项**:具体负责人、截止日期(如有)及待执行动作。
|
|
- [ ] **跟进事项**:需要进一步明确或调研的问题。
|
|
|
|
---
|
|
*注:若对应部分无相关内容,请直接忽略该标题。*`
|
|
|
|
const presetInstructionMap: Record<string, string> = {
|
|
'default': '保持通用摘要风格,兼顾信息完整性与可读性。',
|
|
'decision-focus': '重点提取所有决策、结论、拍板事项。若有意见分歧,请明确分歧点和最终取舍。',
|
|
'action-focus': '重点提取可执行事项:负责人、截止时间、前置依赖、下一步动作。尽量转写为清单。',
|
|
'risk-focus': '重点提取风险、阻塞、争议、潜在误解及其影响范围,并给出可执行的缓解建议。'
|
|
}
|
|
|
|
if (preset === 'custom') {
|
|
const custom = (customSystemPrompt || '').trim()
|
|
if (custom) {
|
|
return `${basePrompt}\n\n### 用户自定义系统提示词\n${custom}`
|
|
}
|
|
return `${basePrompt}\n\n### 提示\n当前选择了自定义系统提示词,但内容为空。请按默认规则输出。`
|
|
}
|
|
|
|
const presetInstruction = presetInstructionMap[preset] || presetInstructionMap.default
|
|
return `${basePrompt}\n\n### 风格偏好\n${presetInstruction}`
|
|
}
|
|
|
|
/**
|
|
* 估算 tokens
|
|
*/
|
|
estimateTokens(text: string): number {
|
|
// 简单估算:中文约1.5字符=1token,英文约4字符=1token
|
|
const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length
|
|
const otherChars = text.length - chineseChars
|
|
return Math.ceil(chineseChars / 1.5 + otherChars / 4)
|
|
}
|
|
|
|
/**
|
|
* 估算成本
|
|
*/
|
|
estimateCost(tokenCount: number, providerName: string): number {
|
|
const provider = this.getProvider(providerName)
|
|
return (tokenCount / 1000) * provider.pricing.input
|
|
}
|
|
|
|
/**
|
|
* 生成缓存键
|
|
*/
|
|
private getCacheKey(sessionId: string, timeRangeDays: number, endTime: number): string {
|
|
// 按天对齐,避免时间差异导致缓存失效
|
|
const dayAlignedEnd = Math.floor(endTime / 86400) * 86400
|
|
return `${sessionId}_${timeRangeDays}d_${dayAlignedEnd}`
|
|
}
|
|
|
|
private buildTimeRangeLabel(timeRangeDays: number): string {
|
|
return timeRangeDays > 0 ? `最近${timeRangeDays}天` : '全部消息'
|
|
}
|
|
|
|
private async tryGenerateStructuredAnalysis(
|
|
provider: AIProvider,
|
|
model: string,
|
|
blocks: AnalysisBlock[],
|
|
standardizedMessages: ReturnType<typeof standardizeMessagesForAnalysis>,
|
|
options: SummaryOptions
|
|
): Promise<StructuredAnalysisAttempt | null> {
|
|
if (standardizedMessages.length === 0 || blocks.length === 0) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
const blockAnalyses = await extractFactsFromBlocks(blocks, provider, {
|
|
model,
|
|
sessionName: options.sessionName || options.sessionId,
|
|
timeRangeLabel: this.buildTimeRangeLabel(options.timeRangeDays)
|
|
})
|
|
|
|
const mergedAnalysis = mergeStructuredAnalysisBlocks(blockAnalyses)
|
|
let finalAnalysis: StructuredAnalysis
|
|
let evidenceResolved = false
|
|
|
|
try {
|
|
finalAnalysis = resolveStructuredAnalysisEvidence(
|
|
mergedAnalysis,
|
|
standardizedMessages,
|
|
options.sessionId
|
|
)
|
|
evidenceResolved = true
|
|
} catch (error) {
|
|
console.warn('[AIService] 证据解析失败,回退到无证据结构化摘要:', error)
|
|
finalAnalysis = fallbackStructuredAnalysisWithoutEvidence(mergedAnalysis)
|
|
}
|
|
|
|
return {
|
|
blocks,
|
|
blockAnalyses,
|
|
mergedAnalysis,
|
|
finalAnalysis,
|
|
blockCount: blocks.length,
|
|
effectiveMessageCount: standardizedMessages.length,
|
|
evidenceResolved
|
|
}
|
|
} catch (error) {
|
|
console.warn('[AIService] 结构化抽取失败,回退到原始摘要链路:', error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 生成摘要(流式)
|
|
*/
|
|
async generateSummary(
|
|
messages: Message[],
|
|
contacts: Map<string, Contact>,
|
|
options: SummaryOptions,
|
|
onChunk: (chunk: string) => void
|
|
): Promise<SummaryResult> {
|
|
if (!this.initialized) {
|
|
this.init()
|
|
}
|
|
|
|
const endTime = Number.isFinite(options.timeRangeEnd) && Number(options.timeRangeEnd) > 0
|
|
? Math.floor(Number(options.timeRangeEnd))
|
|
: Math.floor(Date.now() / 1000)
|
|
const startTime = Number.isFinite(options.timeRangeStart) && Number(options.timeRangeStart) >= 0
|
|
? Math.floor(Number(options.timeRangeStart))
|
|
: (options.timeRangeDays > 0
|
|
? endTime - (options.timeRangeDays * 24 * 60 * 60)
|
|
: (messages[0]?.createTime || endTime))
|
|
|
|
// 获取提供商
|
|
const provider = this.getProvider(options.provider, options.apiKey)
|
|
const model = options.model || provider.models[0]
|
|
|
|
// 统一标准化消息,供结构化抽取和 legacy 回退共用。
|
|
const standardizedMessages = standardizeMessagesForAnalysis(messages, contacts, options.sessionId)
|
|
const analysisBlocks = sliceAnalysisBlocks(standardizedMessages)
|
|
const formattedMessages = renderStandardizedMessages(standardizedMessages)
|
|
|
|
// 构建提示词
|
|
const presetFromConfig = (this.configService.get('aiSystemPromptPreset') as any) || 'default'
|
|
const customSystemPromptFromConfig = (this.configService.get('aiCustomSystemPrompt') as string) || ''
|
|
const systemPrompt = this.getSystemPrompt(
|
|
options.language,
|
|
options.detail,
|
|
options.systemPromptPreset || presetFromConfig,
|
|
options.customSystemPrompt ?? customSystemPromptFromConfig
|
|
)
|
|
|
|
// 使用会话名称优化提示词
|
|
const targetName = options.sessionName || options.sessionId
|
|
const timeRangeLabel = this.buildTimeRangeLabel(options.timeRangeDays)
|
|
const structuredAnalysisResult = await this.tryGenerateStructuredAnalysis(
|
|
provider,
|
|
model,
|
|
analysisBlocks,
|
|
standardizedMessages,
|
|
options
|
|
)
|
|
|
|
const userPrompt = structuredAnalysisResult
|
|
? buildStructuredSummaryUserPrompt({
|
|
targetName,
|
|
timeRangeLabel,
|
|
messageCount: messages.length,
|
|
blockCount: structuredAnalysisResult.blockCount,
|
|
analysis: structuredAnalysisResult.finalAnalysis,
|
|
inputMessageScopeNote: options.inputMessageScopeNote,
|
|
customRequirement: options.customRequirement
|
|
})
|
|
: buildLegacySummaryUserPrompt({
|
|
targetName,
|
|
timeRangeLabel,
|
|
messageCount: messages.length,
|
|
formattedMessages,
|
|
inputMessageScopeNote: options.inputMessageScopeNote,
|
|
customRequirement: options.customRequirement
|
|
})
|
|
|
|
if (structuredAnalysisResult) {
|
|
console.log('[AIService] 结构化抽取完成:', {
|
|
sessionId: options.sessionId,
|
|
blockCount: structuredAnalysisResult.blockCount,
|
|
messageCount: messages.length,
|
|
effectiveMessageCount: standardizedMessages.length,
|
|
evidenceResolved: structuredAnalysisResult.evidenceResolved
|
|
})
|
|
}
|
|
|
|
// 流式生成
|
|
let summaryText = ''
|
|
|
|
await provider.streamChat(
|
|
[
|
|
{ role: 'system', content: systemPrompt },
|
|
{ role: 'user', content: userPrompt }
|
|
],
|
|
{
|
|
model,
|
|
enableThinking: options.enableThinking !== false // 默认启用,除非明确设置为 false
|
|
},
|
|
(chunk) => {
|
|
summaryText += chunk
|
|
onChunk(chunk)
|
|
}
|
|
)
|
|
|
|
// 估算 tokens 和成本
|
|
const totalText = systemPrompt + userPrompt + summaryText
|
|
const tokensUsed = this.estimateTokens(totalText)
|
|
const cost = (tokensUsed / 1000) * provider.pricing.input
|
|
const createdAt = Date.now()
|
|
|
|
// 保存到数据库
|
|
const summaryId = aiDatabase.saveSummary({
|
|
sessionId: options.sessionId,
|
|
timeRangeStart: startTime,
|
|
timeRangeEnd: endTime,
|
|
timeRangeDays: options.timeRangeDays,
|
|
messageCount: messages.length,
|
|
summaryText: summaryText,
|
|
tokensUsed: tokensUsed,
|
|
cost: cost,
|
|
provider: provider.name,
|
|
model: model,
|
|
promptText: userPrompt,
|
|
structuredResultJson: structuredAnalysisResult
|
|
? JSON.stringify(structuredAnalysisResult.finalAnalysis)
|
|
: undefined,
|
|
createdAt
|
|
})
|
|
|
|
console.log('[AIService] 摘要已保存到数据库,ID:', summaryId)
|
|
|
|
const analysisArtifactsPayload: SaveAnalysisArtifactsInput = {
|
|
summaryId,
|
|
sessionId: options.sessionId,
|
|
timeRangeStart: startTime,
|
|
timeRangeEnd: endTime,
|
|
timeRangeDays: options.timeRangeDays,
|
|
rawMessageCount: messages.length,
|
|
effectiveMessageCount: standardizedMessages.length || undefined,
|
|
blockCount: structuredAnalysisResult?.blockCount ?? analysisBlocks.length,
|
|
provider: provider.name,
|
|
model,
|
|
status: structuredAnalysisResult ? 'completed' : 'fallback_legacy',
|
|
sourceKind: structuredAnalysisResult ? 'generate_summary' : 'generate_summary_legacy',
|
|
evidenceResolved: structuredAnalysisResult?.evidenceResolved ?? false,
|
|
blocksAvailable: Boolean(structuredAnalysisResult?.blocks.length),
|
|
blocks: structuredAnalysisResult?.blocks,
|
|
blockAnalyses: structuredAnalysisResult?.blockAnalyses,
|
|
finalAnalysis: structuredAnalysisResult?.finalAnalysis,
|
|
createdAt,
|
|
updatedAt: createdAt
|
|
}
|
|
|
|
try {
|
|
aiDatabase.saveAnalysisArtifacts(analysisArtifactsPayload)
|
|
} catch (error) {
|
|
console.warn('[AIService] 分析产物写入失败,将由后续 catch-up 补录:', error)
|
|
}
|
|
|
|
// 更新使用统计
|
|
aiDatabase.updateUsageStats(provider.name, model, tokensUsed, cost)
|
|
|
|
return {
|
|
id: summaryId,
|
|
sessionId: options.sessionId,
|
|
timeRangeStart: startTime,
|
|
timeRangeEnd: endTime,
|
|
timeRangeDays: options.timeRangeDays,
|
|
messageCount: messages.length,
|
|
summaryText: summaryText,
|
|
tokensUsed: tokensUsed,
|
|
cost: cost,
|
|
provider: provider.name,
|
|
model: model,
|
|
createdAt,
|
|
structuredAnalysis: structuredAnalysisResult?.finalAnalysis
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 测试连接
|
|
*/
|
|
async testConnection(providerName: string, apiKey: string): Promise<{ success: boolean; error?: string; needsProxy?: boolean }> {
|
|
try {
|
|
const provider = this.getProvider(providerName, apiKey)
|
|
const result = await provider.testConnection()
|
|
|
|
return result
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: `连接失败: ${String(error)}`,
|
|
needsProxy: true
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取使用统计
|
|
*/
|
|
getUsageStats(startDate?: string, endDate?: string): any {
|
|
if (!this.initialized) {
|
|
this.init()
|
|
}
|
|
|
|
const rawStats = aiDatabase.getUsageStats(startDate, endDate)
|
|
|
|
// 聚合统计数据
|
|
let totalCount = 0
|
|
let totalTokens = 0
|
|
let totalCost = 0
|
|
|
|
for (const stat of rawStats) {
|
|
totalCount += stat.request_count || 0
|
|
totalTokens += stat.total_tokens || 0
|
|
totalCost += stat.total_cost || 0
|
|
}
|
|
|
|
return {
|
|
totalCount,
|
|
totalTokens,
|
|
totalCost,
|
|
details: rawStats
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 单会话 AI 问答(流式)
|
|
*/
|
|
async answerSessionQuestion(
|
|
options: SessionQAOptions,
|
|
onChunk: (chunk: string) => void,
|
|
onProgress?: (event: SessionQAProgressEvent) => void
|
|
): Promise<SessionQAResult> {
|
|
if (!this.initialized) {
|
|
this.init()
|
|
}
|
|
|
|
const question = String(options.question || '').trim()
|
|
if (!question) {
|
|
throw new Error('问题不能为空')
|
|
}
|
|
|
|
const provider = this.getProvider(options.provider, options.apiKey)
|
|
const model = options.model || provider.models[0]
|
|
|
|
const result = await answerSessionQuestionWithAgent({
|
|
sessionId: options.sessionId,
|
|
sessionName: options.sessionName,
|
|
question,
|
|
summaryText: options.summaryText,
|
|
structuredAnalysis: options.structuredAnalysis,
|
|
history: options.history,
|
|
provider,
|
|
model,
|
|
enableThinking: options.enableThinking,
|
|
onChunk,
|
|
onProgress
|
|
})
|
|
|
|
const totalText = result.promptText + result.answerText
|
|
const tokensUsed = this.estimateTokens(totalText)
|
|
const cost = (tokensUsed / 1000) * provider.pricing.input
|
|
const createdAt = Date.now()
|
|
|
|
aiDatabase.updateUsageStats(provider.name, model, tokensUsed, cost)
|
|
|
|
return {
|
|
sessionId: options.sessionId,
|
|
question,
|
|
answerText: result.answerText,
|
|
evidenceRefs: result.evidenceRefs,
|
|
toolCalls: result.toolCalls,
|
|
tokensUsed,
|
|
cost,
|
|
provider: provider.name,
|
|
model,
|
|
createdAt
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取摘要历史
|
|
*/
|
|
getSummaryHistory(sessionId: string, limit: number = 10): SummaryResult[] {
|
|
if (!this.initialized) {
|
|
this.init()
|
|
}
|
|
return aiDatabase.getSummaryHistory(sessionId, limit)
|
|
}
|
|
|
|
/**
|
|
* 删除摘要
|
|
*/
|
|
deleteSummary(id: number): boolean {
|
|
if (!this.initialized) {
|
|
this.init()
|
|
}
|
|
return aiDatabase.deleteSummary(id)
|
|
}
|
|
|
|
/**
|
|
* 重命名摘要
|
|
*/
|
|
renameSummary(id: number, customName: string): boolean {
|
|
if (!this.initialized) {
|
|
this.init()
|
|
}
|
|
return aiDatabase.renameSummary(id, customName)
|
|
}
|
|
|
|
/**
|
|
* 清理过期缓存
|
|
*/
|
|
cleanExpiredCache(): void {
|
|
if (!this.initialized) {
|
|
this.init()
|
|
}
|
|
aiDatabase.cleanExpiredCache()
|
|
}
|
|
}
|
|
|
|
// 导出单例
|
|
export const aiService = new AIService()
|