tabby-ai-assistant 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +18 -0
- package/dist/index.js +1 -1
- package/package.json +6 -4
- package/src/components/chat/ai-sidebar.component.scss +220 -9
- package/src/components/chat/ai-sidebar.component.ts +364 -29
- package/src/components/chat/chat-input.component.ts +36 -4
- package/src/components/chat/chat-interface.component.ts +225 -5
- package/src/components/chat/chat-message.component.ts +6 -1
- package/src/components/settings/context-settings.component.ts +91 -91
- package/src/components/terminal/ai-toolbar-button.component.ts +4 -2
- package/src/components/terminal/command-suggestion.component.ts +148 -6
- package/src/index.ts +0 -6
- package/src/providers/tabby/ai-toolbar-button.provider.ts +7 -3
- package/src/services/chat/ai-sidebar.service.ts +414 -410
- package/src/services/chat/chat-session.service.ts +36 -12
- package/src/services/context/compaction.ts +110 -134
- package/src/services/context/manager.ts +27 -7
- package/src/services/context/memory.ts +17 -33
- package/src/services/context/summary.service.ts +136 -0
- package/src/services/core/ai-assistant.service.ts +1060 -37
- package/src/services/core/ai-provider-manager.service.ts +154 -25
- package/src/services/core/checkpoint.service.ts +218 -18
- package/src/services/core/toast.service.ts +106 -106
- package/src/services/providers/anthropic-provider.service.ts +126 -30
- package/src/services/providers/base-provider.service.ts +90 -7
- package/src/services/providers/glm-provider.service.ts +151 -38
- package/src/services/providers/minimax-provider.service.ts +55 -40
- package/src/services/providers/ollama-provider.service.ts +117 -28
- package/src/services/providers/openai-compatible.service.ts +164 -34
- package/src/services/providers/openai-provider.service.ts +169 -34
- package/src/services/providers/vllm-provider.service.ts +116 -28
- package/src/services/terminal/terminal-context.service.ts +265 -5
- package/src/services/terminal/terminal-manager.service.ts +748 -748
- package/src/services/terminal/terminal-tools.service.ts +612 -441
- package/src/types/ai.types.ts +156 -3
- package/src/utils/cost.utils.ts +249 -0
- package/src/utils/validation.utils.ts +306 -2
- package/dist/index.js.LICENSE.txt +0 -18
- package/src/services/terminal/command-analyzer.service.ts +0 -43
- package/src/services/terminal/context-menu.service.ts +0 -45
- package/src/services/terminal/hotkey.service.ts +0 -53
|
@@ -6,6 +6,7 @@ import { LoggerService } from '../core/logger.service';
|
|
|
6
6
|
import { ConfigProviderService } from '../core/config-provider.service';
|
|
7
7
|
import { ChatHistoryService } from './chat-history.service';
|
|
8
8
|
import { ContextManager } from '../context/manager';
|
|
9
|
+
import { CheckpointManager } from '../core/checkpoint.service';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* 聊天会话服务
|
|
@@ -31,7 +32,8 @@ export class ChatSessionService {
|
|
|
31
32
|
private logger: LoggerService,
|
|
32
33
|
private chatHistoryService: ChatHistoryService,
|
|
33
34
|
private contextManager: ContextManager,
|
|
34
|
-
private configService: ConfigProviderService
|
|
35
|
+
private configService: ConfigProviderService,
|
|
36
|
+
private checkpointManager: CheckpointManager
|
|
35
37
|
) { }
|
|
36
38
|
|
|
37
39
|
/**
|
|
@@ -46,10 +48,34 @@ export class ChatSessionService {
|
|
|
46
48
|
|
|
47
49
|
/**
|
|
48
50
|
* 切换到指定会话
|
|
51
|
+
* 从存储中加载会话历史
|
|
49
52
|
*/
|
|
50
53
|
switchToSession(sessionId: string): void {
|
|
51
54
|
this.currentSessionId = sessionId;
|
|
52
|
-
|
|
55
|
+
|
|
56
|
+
// 从 ChatHistoryService 加载会话历史
|
|
57
|
+
const sessionData = this.chatHistoryService.loadSession(sessionId);
|
|
58
|
+
|
|
59
|
+
if (sessionData && sessionData.messages && sessionData.messages.length > 0) {
|
|
60
|
+
// 恢复消息历史
|
|
61
|
+
const chatMessages: ChatMessage[] = sessionData.messages.map((msg, index) => ({
|
|
62
|
+
id: msg.id || `msg_${sessionId}_${index}`,
|
|
63
|
+
role: msg.role,
|
|
64
|
+
content: msg.content,
|
|
65
|
+
timestamp: msg.timestamp instanceof Date ? msg.timestamp : new Date(msg.timestamp || Date.now())
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
this.messagesSubject.next(chatMessages);
|
|
69
|
+
this.logger.info('Loaded session history', {
|
|
70
|
+
sessionId,
|
|
71
|
+
messageCount: chatMessages.length
|
|
72
|
+
});
|
|
73
|
+
} else {
|
|
74
|
+
// 新会话,清空当前消息
|
|
75
|
+
this.messagesSubject.next([]);
|
|
76
|
+
this.logger.info('Started new session', { sessionId });
|
|
77
|
+
}
|
|
78
|
+
|
|
53
79
|
this.logger.info('Switched to session', { sessionId });
|
|
54
80
|
}
|
|
55
81
|
|
|
@@ -325,17 +351,15 @@ export class ChatSessionService {
|
|
|
325
351
|
/**
|
|
326
352
|
* 压缩存储检查点
|
|
327
353
|
*/
|
|
328
|
-
compressCheckpoint(checkpointId: string):
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
354
|
+
async compressCheckpoint(checkpointId: string): Promise<void> {
|
|
355
|
+
try {
|
|
356
|
+
// 使用 CheckpointManager 的压缩功能
|
|
357
|
+
await this.checkpointManager.compressForCheckpoint(checkpointId);
|
|
358
|
+
this.logger.info('Checkpoint compressed', { checkpointId });
|
|
359
|
+
} catch (error) {
|
|
360
|
+
this.logger.error('Failed to compress checkpoint', { checkpointId, error });
|
|
361
|
+
throw error;
|
|
332
362
|
}
|
|
333
|
-
|
|
334
|
-
// TODO: 实现检查点压缩逻辑
|
|
335
|
-
// 这里先返回原检查点,实际实现中可以使用 ContextManager 进行压缩
|
|
336
|
-
this.logger.info('Checkpoint compressed', { checkpointId });
|
|
337
|
-
|
|
338
|
-
return checkpoint;
|
|
339
363
|
}
|
|
340
364
|
|
|
341
365
|
/**
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
PruneResult,
|
|
9
9
|
TruncationResult
|
|
10
10
|
} from '../../types/ai.types';
|
|
11
|
+
import { SummaryService } from './summary.service';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* 压缩算法实现类
|
|
@@ -19,7 +20,10 @@ import {
|
|
|
19
20
|
export class Compaction {
|
|
20
21
|
private config: ContextConfig;
|
|
21
22
|
|
|
22
|
-
constructor(
|
|
23
|
+
constructor(
|
|
24
|
+
private logger: LoggerService,
|
|
25
|
+
private summaryService: SummaryService
|
|
26
|
+
) {
|
|
23
27
|
this.config = { ...DEFAULT_CONTEXT_CONFIG };
|
|
24
28
|
}
|
|
25
29
|
|
|
@@ -67,20 +71,26 @@ export class Compaction {
|
|
|
67
71
|
const condenseId = `compact_${Date.now()}`;
|
|
68
72
|
|
|
69
73
|
try {
|
|
70
|
-
//
|
|
71
|
-
const
|
|
74
|
+
// 使用SummaryService生成AI摘要
|
|
75
|
+
const summaryResult = await this.summaryService.generateSummary(messages);
|
|
76
|
+
const summary = summaryResult.summary;
|
|
72
77
|
|
|
73
|
-
//
|
|
74
|
-
const summary = await this.generateSummary(summaryInput, messages);
|
|
75
|
-
|
|
76
|
-
// 3. 创建摘要消息
|
|
78
|
+
// 创建摘要消息,包含元数据
|
|
77
79
|
const summaryMessage: ApiMessage = {
|
|
78
80
|
role: 'system',
|
|
79
81
|
content: summary,
|
|
80
82
|
ts: Date.now(),
|
|
81
83
|
isSummary: true,
|
|
82
84
|
condenseId,
|
|
83
|
-
condenseParent: undefined
|
|
85
|
+
condenseParent: undefined,
|
|
86
|
+
summaryMeta: {
|
|
87
|
+
originalMessageCount: summaryResult.originalMessageCount,
|
|
88
|
+
tokensCost: summaryResult.tokensCost,
|
|
89
|
+
compressionRatio: this.summaryService.calculateCompressionRatio(
|
|
90
|
+
summaryResult.originalMessageCount,
|
|
91
|
+
summary.length
|
|
92
|
+
)
|
|
93
|
+
}
|
|
84
94
|
};
|
|
85
95
|
|
|
86
96
|
// 4. 标记被压缩的消息
|
|
@@ -97,7 +107,8 @@ export class Compaction {
|
|
|
97
107
|
condenseId,
|
|
98
108
|
originalTokens,
|
|
99
109
|
summaryTokens,
|
|
100
|
-
tokensSaved
|
|
110
|
+
tokensSaved,
|
|
111
|
+
compressionRatio: summaryMessage.summaryMeta?.compressionRatio
|
|
101
112
|
});
|
|
102
113
|
|
|
103
114
|
return {
|
|
@@ -106,7 +117,7 @@ export class Compaction {
|
|
|
106
117
|
summary,
|
|
107
118
|
condenseId,
|
|
108
119
|
tokensSaved,
|
|
109
|
-
cost:
|
|
120
|
+
cost: summaryResult.tokensCost
|
|
110
121
|
};
|
|
111
122
|
|
|
112
123
|
} catch (error) {
|
|
@@ -301,162 +312,127 @@ export class Compaction {
|
|
|
301
312
|
|
|
302
313
|
/**
|
|
303
314
|
* 裁剪复杂内容(工具调用结果等)
|
|
315
|
+
* 智能裁剪长文本、JSON、代码块
|
|
304
316
|
*/
|
|
305
317
|
private pruneComplexContent(message: ApiMessage, content: any[]): ApiMessage & { tokensSaved?: number; partsCompacted?: number } {
|
|
306
|
-
|
|
307
|
-
|
|
318
|
+
let tokensSaved = 0;
|
|
319
|
+
let partsCompacted = 0;
|
|
320
|
+
const originalLength = JSON.stringify(content).length;
|
|
321
|
+
|
|
322
|
+
// 处理每个内容块
|
|
323
|
+
const prunedContent = content.map(block => {
|
|
324
|
+
// 如果是文本块,尝试智能裁剪
|
|
325
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
326
|
+
const prunedText = this.pruneText(block.text, 500);
|
|
327
|
+
if (prunedText.length < block.text.length) {
|
|
328
|
+
tokensSaved += this.estimateTokens(block.text) - this.estimateTokens(prunedText);
|
|
329
|
+
partsCompacted++;
|
|
330
|
+
return { ...block, text: prunedText };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// 如果是工具调用结果,尝试精简
|
|
334
|
+
if (block.type === 'tool_use') {
|
|
335
|
+
const prunedTool = this.pruneToolResult(block);
|
|
336
|
+
if (prunedTool !== block) {
|
|
337
|
+
partsCompacted++;
|
|
338
|
+
return prunedTool;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return block;
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// 如果内容类型不对,只返回原内容
|
|
345
|
+
if (typeof message.content === 'string') {
|
|
346
|
+
return {
|
|
347
|
+
...message,
|
|
348
|
+
tokensSaved: 0,
|
|
349
|
+
partsCompacted: 0
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
308
353
|
return {
|
|
309
354
|
...message,
|
|
310
|
-
content,
|
|
311
|
-
tokensSaved
|
|
312
|
-
partsCompacted
|
|
355
|
+
content: prunedContent,
|
|
356
|
+
tokensSaved,
|
|
357
|
+
partsCompacted
|
|
313
358
|
};
|
|
314
359
|
}
|
|
315
360
|
|
|
316
361
|
/**
|
|
317
|
-
*
|
|
362
|
+
* 智能裁剪文本内容
|
|
318
363
|
*/
|
|
319
|
-
private
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
return messages;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* 格式化消息用于摘要
|
|
327
|
-
*/
|
|
328
|
-
private formatMessagesForSummary(messages: ApiMessage[]): string {
|
|
329
|
-
const formattedMessages = messages.map(msg => {
|
|
330
|
-
const content = this.extractTextContent(msg.content);
|
|
331
|
-
return `[${msg.role}] ${content}`;
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
return formattedMessages.join('\n---\n');
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* 提取文本内容
|
|
339
|
-
*/
|
|
340
|
-
private extractTextContent(content: string | any[]): string {
|
|
341
|
-
if (typeof content === 'string') {
|
|
364
|
+
private pruneText(content: string, maxLength: number = 500): string {
|
|
365
|
+
// 如果内容不超过限制,直接返回
|
|
366
|
+
if (content.length <= maxLength) {
|
|
342
367
|
return content;
|
|
343
368
|
}
|
|
344
369
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* 生成摘要(集成AI API)
|
|
357
|
-
*/
|
|
358
|
-
private async generateSummary(input: string, messages: ApiMessage[]): Promise<string> {
|
|
359
|
-
try {
|
|
360
|
-
// TODO: 实现AI摘要生成
|
|
361
|
-
// 实际实现中应该调用AI API
|
|
362
|
-
|
|
363
|
-
// 临时实现:基于规则的智能摘要
|
|
364
|
-
const messageCount = messages.length;
|
|
365
|
-
const estimatedTokens = this.calculateTotalTokens(messages);
|
|
366
|
-
|
|
367
|
-
// 如果消息数量较少,使用简单摘要
|
|
368
|
-
if (messageCount < 5) {
|
|
369
|
-
return this.createSimpleSummary(messages);
|
|
370
|
+
// JSON 内容:保留结构概要
|
|
371
|
+
if (content.trim().startsWith('{') || content.trim().startsWith('[')) {
|
|
372
|
+
try {
|
|
373
|
+
const parsed = JSON.parse(content);
|
|
374
|
+
const keys = Object.keys(parsed);
|
|
375
|
+
return `[JSON对象,包含${keys.length}个字段: ${keys.slice(0, 5).join(', ')}${keys.length > 5 ? '...' : ''}]`;
|
|
376
|
+
} catch {
|
|
377
|
+
// 非有效 JSON,继续处理
|
|
370
378
|
}
|
|
379
|
+
}
|
|
371
380
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
${
|
|
378
|
-
|
|
379
|
-
--- 上下文压缩完成 ---`;
|
|
380
|
-
|
|
381
|
-
} catch (error) {
|
|
382
|
-
this.logger.error('Failed to generate AI summary', { error });
|
|
383
|
-
// 降级到本地摘要
|
|
384
|
-
return this.createFallbackSummary(messages);
|
|
381
|
+
// 代码块:保留头尾
|
|
382
|
+
if (content.includes('\n') && content.split('\n').length > 10) {
|
|
383
|
+
const lines = content.split('\n');
|
|
384
|
+
const head = lines.slice(0, 5).join('\n');
|
|
385
|
+
const tail = lines.slice(-3).join('\n');
|
|
386
|
+
return `${head}\n[...省略${lines.length - 8}行...]\n${tail}`;
|
|
385
387
|
}
|
|
388
|
+
|
|
389
|
+
// 普通长文本:截断并添加省略标记
|
|
390
|
+
return content.substring(0, maxLength) + '\n[...内容已裁剪...]';
|
|
386
391
|
}
|
|
387
392
|
|
|
388
393
|
/**
|
|
389
|
-
*
|
|
394
|
+
* 精简工具调用结果
|
|
390
395
|
*/
|
|
391
|
-
private
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
396
|
+
private pruneToolResult(block: any): any {
|
|
397
|
+
// 如果是工具输出块
|
|
398
|
+
if (block.type === 'tool_result' && block.content) {
|
|
399
|
+
let content = block.content;
|
|
400
|
+
if (typeof content === 'string') {
|
|
401
|
+
// 对长文本结果进行裁剪
|
|
402
|
+
if (content.length > 1000) {
|
|
403
|
+
content = content.substring(0, 500) + '\n[...输出已截断...]';
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return { ...block, content };
|
|
397
407
|
}
|
|
398
|
-
|
|
399
|
-
const firstMessage = this.extractTextContent(userMessages[0].content);
|
|
400
|
-
const lastMessage = this.extractTextContent(assistantMessages[assistantMessages.length - 1]?.content || '');
|
|
401
|
-
|
|
402
|
-
return `【摘要】简短对话:
|
|
403
|
-
用户问题:${firstMessage.substring(0, 80)}${firstMessage.length > 80 ? '...' : ''}
|
|
404
|
-
助手回复:${lastMessage.substring(0, 80)}${lastMessage.length > 80 ? '...' : ''}
|
|
405
|
-
消息总数:${messages.length}条`;
|
|
408
|
+
return block;
|
|
406
409
|
}
|
|
407
410
|
|
|
408
411
|
/**
|
|
409
|
-
*
|
|
412
|
+
* 应用Prune结果到消息
|
|
410
413
|
*/
|
|
411
|
-
private
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const totalTokens = this.calculateTotalTokens(messages);
|
|
416
|
-
|
|
417
|
-
const summary = [
|
|
418
|
-
`【压缩摘要】`,
|
|
419
|
-
`消息统计:${messageCount}条消息(用户${userCount}条,助手${assistantCount}条)`,
|
|
420
|
-
`Token使用:约${totalTokens}个Token`,
|
|
421
|
-
``,
|
|
422
|
-
`关键内容提取:`
|
|
423
|
-
];
|
|
424
|
-
|
|
425
|
-
// 提取关键消息
|
|
426
|
-
const keyMessages = messages
|
|
427
|
-
.filter(m => m.role === 'user' && this.extractTextContent(m.content).length > 10)
|
|
428
|
-
.slice(0, 3)
|
|
429
|
-
.map((m, i) => ` ${i + 1}. ${this.extractTextContent(m.content).substring(0, 60)}...`);
|
|
430
|
-
|
|
431
|
-
summary.push(...keyMessages);
|
|
432
|
-
|
|
433
|
-
summary.push('', '--- 压缩完成 ---');
|
|
434
|
-
|
|
435
|
-
return summary.join('\n');
|
|
414
|
+
private async applyPruneResult(messages: ApiMessage[], pruneResult: PruneResult): Promise<ApiMessage[]> {
|
|
415
|
+
// 简化实现:假设prune已经处理了消息
|
|
416
|
+
// 实际实现中需要更复杂的逻辑来处理prune结果
|
|
417
|
+
return messages;
|
|
436
418
|
}
|
|
437
419
|
|
|
438
420
|
/**
|
|
439
|
-
*
|
|
421
|
+
* 提取文本内容
|
|
440
422
|
*/
|
|
441
|
-
private
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
let summary = '';
|
|
446
|
-
|
|
447
|
-
if (userMessages.length > 0) {
|
|
448
|
-
summary += `用户发送了${userMessages.length}条消息\n`;
|
|
449
|
-
summary += `主要问题:${this.extractTextContent(userMessages[0].content).substring(0, 100)}...\n`;
|
|
423
|
+
private extractTextContent(content: string | any[]): string {
|
|
424
|
+
if (typeof content === 'string') {
|
|
425
|
+
return content;
|
|
450
426
|
}
|
|
451
427
|
|
|
452
|
-
if (
|
|
453
|
-
|
|
428
|
+
if (Array.isArray(content)) {
|
|
429
|
+
return content
|
|
430
|
+
.filter(block => block.type === 'text' && block.text)
|
|
431
|
+
.map(block => block.text)
|
|
432
|
+
.join('\n');
|
|
454
433
|
}
|
|
455
434
|
|
|
456
|
-
|
|
457
|
-
summary += `关键决策:基于历史消息提取的关键操作\n`;
|
|
458
|
-
|
|
459
|
-
return summary;
|
|
435
|
+
return '[复杂内容]';
|
|
460
436
|
}
|
|
461
437
|
|
|
462
438
|
/**
|
|
@@ -2,6 +2,8 @@ import { Injectable } from '@angular/core';
|
|
|
2
2
|
import { LoggerService } from '../core/logger.service';
|
|
3
3
|
import { ConfigProviderService } from '../core/config-provider.service';
|
|
4
4
|
import { ChatHistoryService, SavedSession } from '../chat/chat-history.service';
|
|
5
|
+
import { SummaryService } from './summary.service';
|
|
6
|
+
import { calculateCost, formatCost } from '../../utils/cost.utils';
|
|
5
7
|
import {
|
|
6
8
|
ApiMessage,
|
|
7
9
|
ContextConfig,
|
|
@@ -9,7 +11,8 @@ import {
|
|
|
9
11
|
TokenUsage,
|
|
10
12
|
CompactionResult,
|
|
11
13
|
PruneResult,
|
|
12
|
-
TruncationResult
|
|
14
|
+
TruncationResult,
|
|
15
|
+
ChatMessage
|
|
13
16
|
} from '../../types/ai.types';
|
|
14
17
|
|
|
15
18
|
/**
|
|
@@ -25,7 +28,8 @@ export class ContextManager {
|
|
|
25
28
|
constructor(
|
|
26
29
|
private logger: LoggerService,
|
|
27
30
|
private chatHistoryService: ChatHistoryService,
|
|
28
|
-
private configService: ConfigProviderService
|
|
31
|
+
private configService: ConfigProviderService,
|
|
32
|
+
private summaryService: SummaryService
|
|
29
33
|
) {
|
|
30
34
|
this.config = { ...DEFAULT_CONTEXT_CONFIG };
|
|
31
35
|
// 动态获取当前供应商的上下文限制
|
|
@@ -353,9 +357,9 @@ export class ContextManager {
|
|
|
353
357
|
// 格式化消息用于摘要
|
|
354
358
|
const summaryInput = this.formatMessagesForSummary(messagesToSummarize);
|
|
355
359
|
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
const summary =
|
|
360
|
+
// 调用AI API生成摘要
|
|
361
|
+
const summaryResult = await this.summaryService.generateSummary(messagesToSummarize);
|
|
362
|
+
const summary = summaryResult.summary;
|
|
359
363
|
|
|
360
364
|
// 创建摘要消息
|
|
361
365
|
const summaryMessage: ApiMessage = {
|
|
@@ -364,7 +368,13 @@ export class ContextManager {
|
|
|
364
368
|
ts: Date.now(),
|
|
365
369
|
isSummary: true,
|
|
366
370
|
condenseId,
|
|
367
|
-
condenseParent: undefined
|
|
371
|
+
condenseParent: undefined,
|
|
372
|
+
// 记录摘要元数据
|
|
373
|
+
summaryMeta: {
|
|
374
|
+
originalMessageCount: summaryResult.originalMessageCount,
|
|
375
|
+
tokensCost: summaryResult.tokensCost,
|
|
376
|
+
compressionRatio: this.summaryService.calculateCompressionRatio(summaryResult.originalMessageCount, summary.length)
|
|
377
|
+
}
|
|
368
378
|
};
|
|
369
379
|
|
|
370
380
|
// 标记被压缩的消息
|
|
@@ -387,13 +397,23 @@ export class ContextManager {
|
|
|
387
397
|
condenseId
|
|
388
398
|
);
|
|
389
399
|
|
|
400
|
+
// 计算API成本
|
|
401
|
+
const provider = this.configService.getDefaultProvider();
|
|
402
|
+
const providerConfig = this.configService.getProviderConfig(provider);
|
|
403
|
+
const model = providerConfig?.model || 'gpt-4o';
|
|
404
|
+
const costResult = calculateCost(
|
|
405
|
+
provider as any,
|
|
406
|
+
model,
|
|
407
|
+
{ inputTokens: summaryResult.tokensCost, outputTokens: Math.floor(summaryResult.tokensCost * 0.05) }
|
|
408
|
+
);
|
|
409
|
+
|
|
390
410
|
return {
|
|
391
411
|
success: true,
|
|
392
412
|
messages: resultMessages,
|
|
393
413
|
summary,
|
|
394
414
|
condenseId,
|
|
395
415
|
tokensSaved,
|
|
396
|
-
cost:
|
|
416
|
+
cost: costResult.totalCost
|
|
397
417
|
};
|
|
398
418
|
|
|
399
419
|
} catch (error) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Injectable } from '@angular/core';
|
|
2
2
|
import { LoggerService } from '../core/logger.service';
|
|
3
3
|
import { ApiMessage } from '../../types/ai.types';
|
|
4
|
+
import { SummaryService } from './summary.service';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* 记忆层类型
|
|
@@ -75,7 +76,10 @@ export class Memory {
|
|
|
75
76
|
// 重要性阈值
|
|
76
77
|
private readonly IMPORTANCE_THRESHOLD = 0.7;
|
|
77
78
|
|
|
78
|
-
constructor(
|
|
79
|
+
constructor(
|
|
80
|
+
private logger: LoggerService,
|
|
81
|
+
private summaryService: SummaryService
|
|
82
|
+
) {
|
|
79
83
|
this.logger.info('Memory system initialized');
|
|
80
84
|
this.loadFromStorage();
|
|
81
85
|
}
|
|
@@ -253,8 +257,16 @@ export class Memory {
|
|
|
253
257
|
return '无相关记忆';
|
|
254
258
|
}
|
|
255
259
|
|
|
256
|
-
//
|
|
257
|
-
const
|
|
260
|
+
// 将记忆转换为ApiMessage格式供AI摘要生成
|
|
261
|
+
const messages: ApiMessage[] = sessionMemories.map(m => ({
|
|
262
|
+
role: m.content.includes('assistant:') ? 'assistant' as const : 'user' as const,
|
|
263
|
+
content: m.content,
|
|
264
|
+
ts: m.metadata.timestamp
|
|
265
|
+
}));
|
|
266
|
+
|
|
267
|
+
// 使用AI生成摘要
|
|
268
|
+
const summaryResult = await this.summaryService.generateSummary(messages);
|
|
269
|
+
const summary = summaryResult.summary;
|
|
258
270
|
|
|
259
271
|
// 保存为中期记忆
|
|
260
272
|
const midTermId = this.store(summary, MemoryLayer.MID_TERM, {
|
|
@@ -266,6 +278,8 @@ export class Memory {
|
|
|
266
278
|
this.logger.info('Created mid-term summary', {
|
|
267
279
|
sessionId,
|
|
268
280
|
summaryLength: summary.length,
|
|
281
|
+
originalMessageCount: summaryResult.originalMessageCount,
|
|
282
|
+
tokensCost: summaryResult.tokensCost,
|
|
269
283
|
memoryId: midTermId
|
|
270
284
|
});
|
|
271
285
|
|
|
@@ -442,36 +456,6 @@ export class Memory {
|
|
|
442
456
|
toDelete.forEach(([id]) => memories.delete(id));
|
|
443
457
|
}
|
|
444
458
|
|
|
445
|
-
private generateIntelligentSummary(memories: MemoryItem[]): string {
|
|
446
|
-
// 简化版摘要生成
|
|
447
|
-
// 实际实现中应该使用AI
|
|
448
|
-
|
|
449
|
-
const userMessages = memories.filter(m => m.content.includes('user:'));
|
|
450
|
-
const assistantMessages = memories.filter(m => m.content.includes('assistant:'));
|
|
451
|
-
|
|
452
|
-
let summary = `会话摘要(共${memories.length}条记忆):\n\n`;
|
|
453
|
-
|
|
454
|
-
if (userMessages.length > 0) {
|
|
455
|
-
summary += `用户主要问题:\n`;
|
|
456
|
-
summary += `${userMessages[0].content.substring(0, 100)}...\n\n`;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (assistantMessages.length > 0) {
|
|
460
|
-
summary += `助手主要回复:\n`;
|
|
461
|
-
summary += `${assistantMessages[0].content.substring(0, 100)}...\n\n`;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
summary += `关键要点:\n`;
|
|
465
|
-
memories
|
|
466
|
-
.filter(m => m.metadata.importance > this.IMPORTANCE_THRESHOLD)
|
|
467
|
-
.slice(0, 3)
|
|
468
|
-
.forEach((m, i) => {
|
|
469
|
-
summary += `${i + 1}. ${m.content.substring(0, 80)}...\n`;
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
return summary;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
459
|
private calculateSimilarity(text1: string, text2: string): number {
|
|
476
460
|
// 简化的文本相似性计算
|
|
477
461
|
const words1 = new Set(text1.toLowerCase().split(/\s+/));
|