tabby-ai-assistant 1.0.5 → 1.0.6
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/dist/components/chat/ai-sidebar.component.d.ts +147 -0
- package/dist/components/chat/chat-interface.component.d.ts +38 -6
- package/dist/components/settings/general-settings.component.d.ts +6 -3
- package/dist/components/settings/provider-config.component.d.ts +25 -12
- package/dist/components/terminal/command-preview.component.d.ts +38 -0
- package/dist/index-full.d.ts +8 -0
- package/dist/index-minimal.d.ts +3 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +1 -2
- package/dist/providers/tabby/ai-config.provider.d.ts +57 -5
- package/dist/providers/tabby/ai-hotkey.provider.d.ts +8 -14
- package/dist/providers/tabby/ai-toolbar-button.provider.d.ts +8 -9
- package/dist/services/chat/ai-sidebar.service.d.ts +89 -0
- package/dist/services/chat/chat-history.service.d.ts +78 -0
- package/dist/services/chat/chat-session.service.d.ts +57 -2
- package/dist/services/context/compaction.d.ts +90 -0
- package/dist/services/context/manager.d.ts +69 -0
- package/dist/services/context/memory.d.ts +116 -0
- package/dist/services/context/token-budget.d.ts +105 -0
- package/dist/services/core/ai-assistant.service.d.ts +40 -1
- package/dist/services/core/checkpoint.service.d.ts +130 -0
- package/dist/services/platform/escape-sequence.service.d.ts +132 -0
- package/dist/services/platform/platform-detection.service.d.ts +146 -0
- package/dist/services/providers/anthropic-provider.service.d.ts +5 -0
- package/dist/services/providers/base-provider.service.d.ts +6 -1
- package/dist/services/providers/glm-provider.service.d.ts +5 -0
- package/dist/services/providers/minimax-provider.service.d.ts +10 -1
- package/dist/services/providers/ollama-provider.service.d.ts +76 -0
- package/dist/services/providers/openai-compatible.service.d.ts +5 -0
- package/dist/services/providers/openai-provider.service.d.ts +5 -0
- package/dist/services/providers/vllm-provider.service.d.ts +82 -0
- package/dist/services/terminal/buffer-analyzer.service.d.ts +128 -0
- package/dist/services/terminal/terminal-manager.service.d.ts +185 -0
- package/dist/services/terminal/terminal-tools.service.d.ts +79 -0
- package/dist/types/ai.types.d.ts +92 -0
- package/dist/types/provider.types.d.ts +1 -1
- package/package.json +7 -10
- package/src/components/chat/ai-sidebar.component.ts +945 -0
- package/src/components/chat/chat-input.component.html +9 -24
- package/src/components/chat/chat-input.component.scss +3 -2
- package/src/components/chat/chat-interface.component.html +77 -69
- package/src/components/chat/chat-interface.component.scss +54 -4
- package/src/components/chat/chat-interface.component.ts +250 -34
- package/src/components/chat/chat-settings.component.scss +4 -4
- package/src/components/chat/chat-settings.component.ts +22 -11
- package/src/components/common/error-message.component.html +15 -0
- package/src/components/common/error-message.component.scss +77 -0
- package/src/components/common/error-message.component.ts +2 -96
- package/src/components/common/loading-spinner.component.html +4 -0
- package/src/components/common/loading-spinner.component.scss +57 -0
- package/src/components/common/loading-spinner.component.ts +2 -63
- package/src/components/security/consent-dialog.component.html +22 -0
- package/src/components/security/consent-dialog.component.scss +34 -0
- package/src/components/security/consent-dialog.component.ts +2 -55
- package/src/components/security/password-prompt.component.html +19 -0
- package/src/components/security/password-prompt.component.scss +30 -0
- package/src/components/security/password-prompt.component.ts +2 -54
- package/src/components/security/risk-confirm-dialog.component.html +8 -12
- package/src/components/security/risk-confirm-dialog.component.scss +8 -5
- package/src/components/security/risk-confirm-dialog.component.ts +6 -6
- package/src/components/settings/ai-settings-tab.component.html +16 -20
- package/src/components/settings/ai-settings-tab.component.scss +8 -5
- package/src/components/settings/ai-settings-tab.component.ts +12 -12
- package/src/components/settings/general-settings.component.html +8 -17
- package/src/components/settings/general-settings.component.scss +6 -3
- package/src/components/settings/general-settings.component.ts +62 -22
- package/src/components/settings/provider-config.component.html +19 -39
- package/src/components/settings/provider-config.component.scss +182 -39
- package/src/components/settings/provider-config.component.ts +119 -7
- package/src/components/settings/security-settings.component.scss +1 -1
- package/src/components/terminal/ai-toolbar-button.component.html +8 -0
- package/src/components/terminal/ai-toolbar-button.component.scss +20 -0
- package/src/components/terminal/ai-toolbar-button.component.ts +2 -30
- package/src/components/terminal/command-preview.component.html +61 -0
- package/src/components/terminal/command-preview.component.scss +72 -0
- package/src/components/terminal/command-preview.component.ts +127 -140
- package/src/components/terminal/command-suggestion.component.html +23 -0
- package/src/components/terminal/command-suggestion.component.scss +55 -0
- package/src/components/terminal/command-suggestion.component.ts +2 -77
- package/src/index-minimal.ts +32 -0
- package/src/index.ts +94 -11
- package/src/index.ts.backup +165 -0
- package/src/providers/tabby/ai-config.provider.ts +60 -51
- package/src/providers/tabby/ai-hotkey.provider.ts +23 -39
- package/src/providers/tabby/ai-settings-tab.provider.ts +2 -2
- package/src/providers/tabby/ai-toolbar-button.provider.ts +29 -24
- package/src/services/chat/ai-sidebar.service.ts +258 -0
- package/src/services/chat/chat-history.service.ts +308 -0
- package/src/services/chat/chat-history.service.ts.backup +239 -0
- package/src/services/chat/chat-session.service.ts +276 -3
- package/src/services/context/compaction.ts +483 -0
- package/src/services/context/manager.ts +442 -0
- package/src/services/context/memory.ts +519 -0
- package/src/services/context/token-budget.ts +422 -0
- package/src/services/core/ai-assistant.service.ts +280 -5
- package/src/services/core/ai-provider-manager.service.ts +2 -2
- package/src/services/core/checkpoint.service.ts +619 -0
- package/src/services/platform/escape-sequence.service.ts +499 -0
- package/src/services/platform/platform-detection.service.ts +494 -0
- package/src/services/providers/anthropic-provider.service.ts +28 -1
- package/src/services/providers/base-provider.service.ts +7 -1
- package/src/services/providers/glm-provider.service.ts +28 -1
- package/src/services/providers/minimax-provider.service.ts +209 -11
- package/src/services/providers/ollama-provider.service.ts +445 -0
- package/src/services/providers/openai-compatible.service.ts +9 -0
- package/src/services/providers/openai-provider.service.ts +9 -0
- package/src/services/providers/vllm-provider.service.ts +463 -0
- package/src/services/security/risk-assessment.service.ts +6 -2
- package/src/services/terminal/buffer-analyzer.service.ts +594 -0
- package/src/services/terminal/terminal-manager.service.ts +748 -0
- package/src/services/terminal/terminal-tools.service.ts +441 -0
- package/src/styles/ai-assistant.scss +78 -6
- package/src/types/ai.types.ts +144 -0
- package/src/types/provider.types.ts +1 -1
- package/tsconfig.json +9 -9
- package/webpack.config.js +28 -6
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { LoggerService } from '../core/logger.service';
|
|
3
|
+
import { ChatHistoryService, SavedSession } from '../chat/chat-history.service';
|
|
4
|
+
import {
|
|
5
|
+
ApiMessage,
|
|
6
|
+
ContextConfig,
|
|
7
|
+
DEFAULT_CONTEXT_CONFIG,
|
|
8
|
+
TokenUsage,
|
|
9
|
+
CompactionResult,
|
|
10
|
+
PruneResult,
|
|
11
|
+
TruncationResult
|
|
12
|
+
} from '../../types/ai.types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 上下文管理器 - 上下文工程的核心组件
|
|
16
|
+
* 负责Token预算管理、压缩触发决策、历史过滤等核心功能
|
|
17
|
+
*/
|
|
18
|
+
@Injectable({
|
|
19
|
+
providedIn: 'root'
|
|
20
|
+
})
|
|
21
|
+
export class ContextManager {
|
|
22
|
+
private config: ContextConfig;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
private logger: LoggerService,
|
|
26
|
+
private chatHistoryService: ChatHistoryService
|
|
27
|
+
) {
|
|
28
|
+
this.config = { ...DEFAULT_CONTEXT_CONFIG };
|
|
29
|
+
this.logger.info('ContextManager initialized', { config: this.config });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 计算Token使用量
|
|
34
|
+
*/
|
|
35
|
+
calculateTokenUsage(messages: ApiMessage[]): TokenUsage {
|
|
36
|
+
let inputTokens = 0;
|
|
37
|
+
let outputTokens = 0;
|
|
38
|
+
|
|
39
|
+
for (const message of messages) {
|
|
40
|
+
const content = typeof message.content === 'string'
|
|
41
|
+
? message.content
|
|
42
|
+
: JSON.stringify(message.content);
|
|
43
|
+
|
|
44
|
+
const messageTokens = this.estimateTokens(content);
|
|
45
|
+
|
|
46
|
+
if (message.role === 'user' || message.role === 'system') {
|
|
47
|
+
inputTokens += messageTokens;
|
|
48
|
+
} else if (message.role === 'assistant') {
|
|
49
|
+
outputTokens += messageTokens;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
input: inputTokens,
|
|
55
|
+
output: outputTokens,
|
|
56
|
+
cacheRead: 0,
|
|
57
|
+
cacheWrite: 0
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 计算使用率
|
|
63
|
+
*/
|
|
64
|
+
calculateUsageRate(tokenUsage: TokenUsage): number {
|
|
65
|
+
const totalUsed = tokenUsage.input + tokenUsage.output;
|
|
66
|
+
const availableTokens = this.config.maxContextTokens - this.config.reservedOutputTokens;
|
|
67
|
+
return totalUsed / availableTokens;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 判断是否需要管理上下文
|
|
72
|
+
*/
|
|
73
|
+
shouldManageContext(sessionId: string): boolean {
|
|
74
|
+
const session = this.chatHistoryService.loadSession(sessionId);
|
|
75
|
+
if (!session || !session.contextInfo?.tokenUsage) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const usageRate = this.calculateUsageRate(session.contextInfo.tokenUsage);
|
|
80
|
+
|
|
81
|
+
// 触发条件:使用率超过压缩阈值或裁剪阈值
|
|
82
|
+
return usageRate >= this.config.compactThreshold ||
|
|
83
|
+
usageRate >= this.config.pruneThreshold;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 统一管理上下文入口
|
|
88
|
+
*/
|
|
89
|
+
async manageContext(sessionId: string): Promise<{
|
|
90
|
+
compactionResult?: CompactionResult;
|
|
91
|
+
pruneResult?: PruneResult;
|
|
92
|
+
truncationResult?: TruncationResult;
|
|
93
|
+
}> {
|
|
94
|
+
this.logger.info('Starting context management', { sessionId });
|
|
95
|
+
|
|
96
|
+
const session = this.chatHistoryService.loadSession(sessionId);
|
|
97
|
+
if (!session) {
|
|
98
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const messages = session.messages as unknown as ApiMessage[];
|
|
102
|
+
const tokenUsage = this.calculateTokenUsage(messages);
|
|
103
|
+
const usageRate = this.calculateUsageRate(tokenUsage);
|
|
104
|
+
|
|
105
|
+
const results: any = {};
|
|
106
|
+
|
|
107
|
+
// 更新Token使用统计
|
|
108
|
+
this.chatHistoryService.updateTokenUsage(sessionId, tokenUsage);
|
|
109
|
+
|
|
110
|
+
// 1. 首先尝试裁剪(Prune)- 移除工具输出中的冗余信息
|
|
111
|
+
if (usageRate >= this.config.pruneThreshold) {
|
|
112
|
+
this.logger.info('Prune threshold exceeded, applying prune', {
|
|
113
|
+
sessionId,
|
|
114
|
+
usageRate,
|
|
115
|
+
threshold: this.config.pruneThreshold
|
|
116
|
+
});
|
|
117
|
+
results.pruneResult = await this.prune(sessionId, messages);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 2. 如果裁剪后仍超过压缩阈值,进行压缩(Compact)
|
|
121
|
+
const currentUsageRate = this.calculateUsageRate(
|
|
122
|
+
this.calculateTokenUsage(results.pruneResult?.messages || messages)
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
if (currentUsageRate >= this.config.compactThreshold) {
|
|
126
|
+
this.logger.info('Compact threshold exceeded, applying compact', {
|
|
127
|
+
sessionId,
|
|
128
|
+
usageRate: currentUsageRate,
|
|
129
|
+
threshold: this.config.compactThreshold
|
|
130
|
+
});
|
|
131
|
+
results.compactionResult = await this.compact(sessionId, results.pruneResult?.messages || messages);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 3. 如果压缩后仍超过阈值,使用截断(Truncate)
|
|
135
|
+
const finalUsageRate = this.calculateUsageRate(
|
|
136
|
+
this.calculateTokenUsage(results.compactionResult?.messages || results.pruneResult?.messages || messages)
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (finalUsageRate >= 0.95) {
|
|
140
|
+
this.logger.info('Severe threshold exceeded, applying truncate', {
|
|
141
|
+
sessionId,
|
|
142
|
+
usageRate: finalUsageRate
|
|
143
|
+
});
|
|
144
|
+
results.truncationResult = await this.truncate(sessionId, results.compactionResult?.messages || results.pruneResult?.messages || messages);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.logger.info('Context management completed', {
|
|
148
|
+
sessionId,
|
|
149
|
+
results: Object.keys(results)
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return results;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 获取有效历史(过滤被压缩/截断的消息)
|
|
157
|
+
*/
|
|
158
|
+
getEffectiveHistory(sessionId: string): ApiMessage[] {
|
|
159
|
+
const session = this.chatHistoryService.loadSession(sessionId);
|
|
160
|
+
if (!session) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const messages = session.messages as unknown as ApiMessage[];
|
|
165
|
+
|
|
166
|
+
// 过滤掉被压缩的消息
|
|
167
|
+
const effectiveMessages = messages.filter(msg => {
|
|
168
|
+
// 保留非压缩消息
|
|
169
|
+
if (!msg.condenseParent && !msg.truncationParent) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 保留摘要消息
|
|
174
|
+
if (msg.isSummary) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 保留截断标记
|
|
179
|
+
if (msg.isTruncationMarker) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return false;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// 保留最近的N条消息作为锚点
|
|
187
|
+
const messagesToKeep = Math.min(this.config.messagesToKeep, effectiveMessages.length);
|
|
188
|
+
const recentMessages = effectiveMessages.slice(-messagesToKeep);
|
|
189
|
+
const olderMessages = effectiveMessages.slice(0, -messagesToKeep);
|
|
190
|
+
|
|
191
|
+
return [...olderMessages, ...recentMessages];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 滑动窗口截断
|
|
196
|
+
*/
|
|
197
|
+
async truncate(sessionId: string, messages: ApiMessage[]): Promise<TruncationResult> {
|
|
198
|
+
this.logger.info('Applying truncate', { sessionId, messageCount: messages.length });
|
|
199
|
+
|
|
200
|
+
const truncationId = `truncate_${Date.now()}`;
|
|
201
|
+
const messagesToKeep = Math.min(this.config.messagesToKeep, messages.length);
|
|
202
|
+
|
|
203
|
+
// 保留最近的N条消息
|
|
204
|
+
const keptMessages = messages.slice(-messagesToKeep);
|
|
205
|
+
const removedMessages = messages.slice(0, -messagesToKeep);
|
|
206
|
+
|
|
207
|
+
// 添加截断标记
|
|
208
|
+
const truncationMarker: ApiMessage = {
|
|
209
|
+
role: 'system',
|
|
210
|
+
content: `[${removedMessages.length}条消息已被截断以节省Token成本]`,
|
|
211
|
+
ts: Date.now(),
|
|
212
|
+
isTruncationMarker: true,
|
|
213
|
+
truncationId,
|
|
214
|
+
truncationParent: undefined
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const resultMessages = [...keptMessages, truncationMarker];
|
|
218
|
+
|
|
219
|
+
// 记录截断事件
|
|
220
|
+
this.chatHistoryService.recordCompactionEvent(
|
|
221
|
+
sessionId,
|
|
222
|
+
'truncate',
|
|
223
|
+
removedMessages.length * 100 // 估算节省的token数
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
messages: resultMessages,
|
|
228
|
+
truncationId,
|
|
229
|
+
messagesRemoved: removedMessages.length
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 清理孤儿标记
|
|
235
|
+
*/
|
|
236
|
+
cleanupOrphanedTags(sessionId: string): number {
|
|
237
|
+
this.logger.info('Cleaning up orphaned tags', { sessionId });
|
|
238
|
+
|
|
239
|
+
const session = this.chatHistoryService.loadSession(sessionId);
|
|
240
|
+
if (!session) {
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const messages = session.messages as unknown as ApiMessage[];
|
|
245
|
+
let cleanedCount = 0;
|
|
246
|
+
|
|
247
|
+
// 查找所有condenseId和truncationId
|
|
248
|
+
const condenseIds = new Set(
|
|
249
|
+
messages
|
|
250
|
+
.filter(m => m.condenseId)
|
|
251
|
+
.map(m => m.condenseId)
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const truncationIds = new Set(
|
|
255
|
+
messages
|
|
256
|
+
.filter(m => m.truncationId)
|
|
257
|
+
.map(m => m.truncationId)
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// 检查并清理孤儿的摘要消息和截断标记
|
|
261
|
+
messages.forEach(msg => {
|
|
262
|
+
// 检查摘要消息是否有对应的condenseId引用
|
|
263
|
+
if (msg.isSummary && msg.condenseId && !condenseIds.has(msg.condenseId)) {
|
|
264
|
+
msg.isSummary = false;
|
|
265
|
+
msg.condenseId = undefined;
|
|
266
|
+
cleanedCount++;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 检查截断标记是否有对应的truncationId引用
|
|
270
|
+
if (msg.isTruncationMarker && msg.truncationId && !truncationIds.has(msg.truncationId)) {
|
|
271
|
+
msg.isTruncationMarker = false;
|
|
272
|
+
msg.truncationId = undefined;
|
|
273
|
+
cleanedCount++;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (cleanedCount > 0) {
|
|
278
|
+
this.logger.info('Cleaned orphaned tags', { sessionId, count: cleanedCount });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return cleanedCount;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 更新配置
|
|
286
|
+
*/
|
|
287
|
+
updateConfig(newConfig: Partial<ContextConfig>): void {
|
|
288
|
+
this.config = { ...this.config, ...newConfig };
|
|
289
|
+
this.logger.info('Context config updated', { config: this.config });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 获取当前配置
|
|
294
|
+
*/
|
|
295
|
+
getConfig(): ContextConfig {
|
|
296
|
+
return { ...this.config };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 压缩(Compact)- 使用AI生成摘要
|
|
301
|
+
*/
|
|
302
|
+
private async compact(sessionId: string, messages: ApiMessage[]): Promise<CompactionResult> {
|
|
303
|
+
this.logger.info('Applying compact', { sessionId, messageCount: messages.length });
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const condenseId = `compact_${Date.now()}`;
|
|
307
|
+
const messagesToKeep = Math.min(this.config.messagesToKeep, messages.length);
|
|
308
|
+
|
|
309
|
+
// 保留最近的N条消息
|
|
310
|
+
const keptMessages = messages.slice(-messagesToKeep);
|
|
311
|
+
const messagesToSummarize = messages.slice(0, -messagesToKeep);
|
|
312
|
+
|
|
313
|
+
if (messagesToSummarize.length === 0) {
|
|
314
|
+
return {
|
|
315
|
+
success: true,
|
|
316
|
+
messages,
|
|
317
|
+
tokensSaved: 0,
|
|
318
|
+
cost: 0
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// 格式化消息用于摘要
|
|
323
|
+
const summaryInput = this.formatMessagesForSummary(messagesToSummarize);
|
|
324
|
+
|
|
325
|
+
// TODO: 调用AI API生成摘要
|
|
326
|
+
// 这里先使用占位符实现
|
|
327
|
+
const summary = `[摘要: ${messagesToSummarize.length}条消息已压缩为摘要,节省约${messagesToSummarize.length * 50}个Token]`;
|
|
328
|
+
|
|
329
|
+
// 创建摘要消息
|
|
330
|
+
const summaryMessage: ApiMessage = {
|
|
331
|
+
role: 'system',
|
|
332
|
+
content: summary,
|
|
333
|
+
ts: Date.now(),
|
|
334
|
+
isSummary: true,
|
|
335
|
+
condenseId,
|
|
336
|
+
condenseParent: undefined
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// 标记被压缩的消息
|
|
340
|
+
messagesToSummarize.forEach(msg => {
|
|
341
|
+
msg.condenseParent = condenseId;
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const resultMessages = [...keptMessages, summaryMessage];
|
|
345
|
+
|
|
346
|
+
// 估算节省的Token数
|
|
347
|
+
const originalTokens = this.calculateTokenUsage(messagesToSummarize);
|
|
348
|
+
const summaryTokens = this.estimateTokens(summary);
|
|
349
|
+
const tokensSaved = (originalTokens.input + originalTokens.output) - summaryTokens;
|
|
350
|
+
|
|
351
|
+
// 记录压缩事件
|
|
352
|
+
this.chatHistoryService.recordCompactionEvent(
|
|
353
|
+
sessionId,
|
|
354
|
+
'compact',
|
|
355
|
+
tokensSaved,
|
|
356
|
+
condenseId
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
success: true,
|
|
361
|
+
messages: resultMessages,
|
|
362
|
+
summary,
|
|
363
|
+
condenseId,
|
|
364
|
+
tokensSaved,
|
|
365
|
+
cost: 0 // TODO: 计算实际API成本
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
} catch (error) {
|
|
369
|
+
this.logger.error('Compact failed', { sessionId, error });
|
|
370
|
+
return {
|
|
371
|
+
success: false,
|
|
372
|
+
messages,
|
|
373
|
+
tokensSaved: 0,
|
|
374
|
+
cost: 0,
|
|
375
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* 裁剪(Prune)- 移除工具输出中的冗余信息
|
|
382
|
+
*/
|
|
383
|
+
private async prune(sessionId: string, messages: ApiMessage[]): Promise<PruneResult> {
|
|
384
|
+
this.logger.info('Applying prune', { sessionId, messageCount: messages.length });
|
|
385
|
+
|
|
386
|
+
let tokensSaved = 0;
|
|
387
|
+
let partsCompacted = 0;
|
|
388
|
+
|
|
389
|
+
// 简化版实现:移除过长的工具输出
|
|
390
|
+
const prunedMessages = messages.map(msg => {
|
|
391
|
+
if (typeof msg.content === 'string' && msg.content.length > 1000) {
|
|
392
|
+
partsCompacted++;
|
|
393
|
+
const originalLength = msg.content.length;
|
|
394
|
+
const prunedContent = msg.content.substring(0, 500) + '\n[...输出已裁剪以节省Token...]';
|
|
395
|
+
|
|
396
|
+
// 估算节省的Token数
|
|
397
|
+
tokensSaved += Math.floor((originalLength - prunedContent.length) / 4);
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
...msg,
|
|
401
|
+
content: prunedContent
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return msg;
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
if (tokensSaved > 0) {
|
|
408
|
+
// 记录裁剪事件
|
|
409
|
+
this.chatHistoryService.recordCompactionEvent(
|
|
410
|
+
sessionId,
|
|
411
|
+
'prune',
|
|
412
|
+
tokensSaved
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
pruned: tokensSaved > 0,
|
|
418
|
+
tokensSaved,
|
|
419
|
+
partsCompacted
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* 格式化消息用于摘要
|
|
425
|
+
*/
|
|
426
|
+
private formatMessagesForSummary(messages: ApiMessage[]): string {
|
|
427
|
+
return messages.map(msg => {
|
|
428
|
+
const content = typeof msg.content === 'string'
|
|
429
|
+
? msg.content
|
|
430
|
+
: '[复杂内容]';
|
|
431
|
+
return `${msg.role}: ${content}`;
|
|
432
|
+
}).join('\n');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* 估算Token数量(简化版)
|
|
437
|
+
*/
|
|
438
|
+
private estimateTokens(text: string): number {
|
|
439
|
+
// 粗略估算:1个Token约等于4个字符
|
|
440
|
+
return Math.ceil(text.length / 4);
|
|
441
|
+
}
|
|
442
|
+
}
|