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.
Files changed (116) hide show
  1. package/dist/components/chat/ai-sidebar.component.d.ts +147 -0
  2. package/dist/components/chat/chat-interface.component.d.ts +38 -6
  3. package/dist/components/settings/general-settings.component.d.ts +6 -3
  4. package/dist/components/settings/provider-config.component.d.ts +25 -12
  5. package/dist/components/terminal/command-preview.component.d.ts +38 -0
  6. package/dist/index-full.d.ts +8 -0
  7. package/dist/index-minimal.d.ts +3 -0
  8. package/dist/index.d.ts +7 -3
  9. package/dist/index.js +1 -2
  10. package/dist/providers/tabby/ai-config.provider.d.ts +57 -5
  11. package/dist/providers/tabby/ai-hotkey.provider.d.ts +8 -14
  12. package/dist/providers/tabby/ai-toolbar-button.provider.d.ts +8 -9
  13. package/dist/services/chat/ai-sidebar.service.d.ts +89 -0
  14. package/dist/services/chat/chat-history.service.d.ts +78 -0
  15. package/dist/services/chat/chat-session.service.d.ts +57 -2
  16. package/dist/services/context/compaction.d.ts +90 -0
  17. package/dist/services/context/manager.d.ts +69 -0
  18. package/dist/services/context/memory.d.ts +116 -0
  19. package/dist/services/context/token-budget.d.ts +105 -0
  20. package/dist/services/core/ai-assistant.service.d.ts +40 -1
  21. package/dist/services/core/checkpoint.service.d.ts +130 -0
  22. package/dist/services/platform/escape-sequence.service.d.ts +132 -0
  23. package/dist/services/platform/platform-detection.service.d.ts +146 -0
  24. package/dist/services/providers/anthropic-provider.service.d.ts +5 -0
  25. package/dist/services/providers/base-provider.service.d.ts +6 -1
  26. package/dist/services/providers/glm-provider.service.d.ts +5 -0
  27. package/dist/services/providers/minimax-provider.service.d.ts +10 -1
  28. package/dist/services/providers/ollama-provider.service.d.ts +76 -0
  29. package/dist/services/providers/openai-compatible.service.d.ts +5 -0
  30. package/dist/services/providers/openai-provider.service.d.ts +5 -0
  31. package/dist/services/providers/vllm-provider.service.d.ts +82 -0
  32. package/dist/services/terminal/buffer-analyzer.service.d.ts +128 -0
  33. package/dist/services/terminal/terminal-manager.service.d.ts +185 -0
  34. package/dist/services/terminal/terminal-tools.service.d.ts +79 -0
  35. package/dist/types/ai.types.d.ts +92 -0
  36. package/dist/types/provider.types.d.ts +1 -1
  37. package/package.json +7 -10
  38. package/src/components/chat/ai-sidebar.component.ts +945 -0
  39. package/src/components/chat/chat-input.component.html +9 -24
  40. package/src/components/chat/chat-input.component.scss +3 -2
  41. package/src/components/chat/chat-interface.component.html +77 -69
  42. package/src/components/chat/chat-interface.component.scss +54 -4
  43. package/src/components/chat/chat-interface.component.ts +250 -34
  44. package/src/components/chat/chat-settings.component.scss +4 -4
  45. package/src/components/chat/chat-settings.component.ts +22 -11
  46. package/src/components/common/error-message.component.html +15 -0
  47. package/src/components/common/error-message.component.scss +77 -0
  48. package/src/components/common/error-message.component.ts +2 -96
  49. package/src/components/common/loading-spinner.component.html +4 -0
  50. package/src/components/common/loading-spinner.component.scss +57 -0
  51. package/src/components/common/loading-spinner.component.ts +2 -63
  52. package/src/components/security/consent-dialog.component.html +22 -0
  53. package/src/components/security/consent-dialog.component.scss +34 -0
  54. package/src/components/security/consent-dialog.component.ts +2 -55
  55. package/src/components/security/password-prompt.component.html +19 -0
  56. package/src/components/security/password-prompt.component.scss +30 -0
  57. package/src/components/security/password-prompt.component.ts +2 -54
  58. package/src/components/security/risk-confirm-dialog.component.html +8 -12
  59. package/src/components/security/risk-confirm-dialog.component.scss +8 -5
  60. package/src/components/security/risk-confirm-dialog.component.ts +6 -6
  61. package/src/components/settings/ai-settings-tab.component.html +16 -20
  62. package/src/components/settings/ai-settings-tab.component.scss +8 -5
  63. package/src/components/settings/ai-settings-tab.component.ts +12 -12
  64. package/src/components/settings/general-settings.component.html +8 -17
  65. package/src/components/settings/general-settings.component.scss +6 -3
  66. package/src/components/settings/general-settings.component.ts +62 -22
  67. package/src/components/settings/provider-config.component.html +19 -39
  68. package/src/components/settings/provider-config.component.scss +182 -39
  69. package/src/components/settings/provider-config.component.ts +119 -7
  70. package/src/components/settings/security-settings.component.scss +1 -1
  71. package/src/components/terminal/ai-toolbar-button.component.html +8 -0
  72. package/src/components/terminal/ai-toolbar-button.component.scss +20 -0
  73. package/src/components/terminal/ai-toolbar-button.component.ts +2 -30
  74. package/src/components/terminal/command-preview.component.html +61 -0
  75. package/src/components/terminal/command-preview.component.scss +72 -0
  76. package/src/components/terminal/command-preview.component.ts +127 -140
  77. package/src/components/terminal/command-suggestion.component.html +23 -0
  78. package/src/components/terminal/command-suggestion.component.scss +55 -0
  79. package/src/components/terminal/command-suggestion.component.ts +2 -77
  80. package/src/index-minimal.ts +32 -0
  81. package/src/index.ts +94 -11
  82. package/src/index.ts.backup +165 -0
  83. package/src/providers/tabby/ai-config.provider.ts +60 -51
  84. package/src/providers/tabby/ai-hotkey.provider.ts +23 -39
  85. package/src/providers/tabby/ai-settings-tab.provider.ts +2 -2
  86. package/src/providers/tabby/ai-toolbar-button.provider.ts +29 -24
  87. package/src/services/chat/ai-sidebar.service.ts +258 -0
  88. package/src/services/chat/chat-history.service.ts +308 -0
  89. package/src/services/chat/chat-history.service.ts.backup +239 -0
  90. package/src/services/chat/chat-session.service.ts +276 -3
  91. package/src/services/context/compaction.ts +483 -0
  92. package/src/services/context/manager.ts +442 -0
  93. package/src/services/context/memory.ts +519 -0
  94. package/src/services/context/token-budget.ts +422 -0
  95. package/src/services/core/ai-assistant.service.ts +280 -5
  96. package/src/services/core/ai-provider-manager.service.ts +2 -2
  97. package/src/services/core/checkpoint.service.ts +619 -0
  98. package/src/services/platform/escape-sequence.service.ts +499 -0
  99. package/src/services/platform/platform-detection.service.ts +494 -0
  100. package/src/services/providers/anthropic-provider.service.ts +28 -1
  101. package/src/services/providers/base-provider.service.ts +7 -1
  102. package/src/services/providers/glm-provider.service.ts +28 -1
  103. package/src/services/providers/minimax-provider.service.ts +209 -11
  104. package/src/services/providers/ollama-provider.service.ts +445 -0
  105. package/src/services/providers/openai-compatible.service.ts +9 -0
  106. package/src/services/providers/openai-provider.service.ts +9 -0
  107. package/src/services/providers/vllm-provider.service.ts +463 -0
  108. package/src/services/security/risk-assessment.service.ts +6 -2
  109. package/src/services/terminal/buffer-analyzer.service.ts +594 -0
  110. package/src/services/terminal/terminal-manager.service.ts +748 -0
  111. package/src/services/terminal/terminal-tools.service.ts +441 -0
  112. package/src/styles/ai-assistant.scss +78 -6
  113. package/src/types/ai.types.ts +144 -0
  114. package/src/types/provider.types.ts +1 -1
  115. package/tsconfig.json +9 -9
  116. package/webpack.config.js +28 -6
@@ -0,0 +1,239 @@
1
+ import { Injectable } from '@angular/core';
2
+ import { BehaviorSubject, Observable } from 'rxjs';
3
+ import { ChatMessage } from '../../types/ai.types';
4
+ import { LoggerService } from '../core/logger.service';
5
+
6
+ export interface SavedSession {
7
+ sessionId: string;
8
+ title: string;
9
+ messages: ChatMessage[];
10
+ createdAt: Date;
11
+ updatedAt: Date;
12
+ messageCount: number;
13
+ }
14
+
15
+ const STORAGE_KEY = 'tabby-ai-assistant-chat-history';
16
+ const MAX_SESSIONS = 50;
17
+ const MAX_MESSAGES_PER_SESSION = 1000;
18
+
19
+ /**
20
+ * 聊天历史服务
21
+ * 持久化存储和管理聊天会话历史
22
+ */
23
+ @Injectable({
24
+ providedIn: 'root'
25
+ })
26
+ export class ChatHistoryService {
27
+ private sessionsSubject = new BehaviorSubject<SavedSession[]>([]);
28
+ public sessions$ = this.sessionsSubject.asObservable();
29
+
30
+ constructor(private logger: LoggerService) {
31
+ this.loadSessions();
32
+ }
33
+
34
+ /**
35
+ * 保存会话
36
+ */
37
+ saveSession(sessionId: string, messages: ChatMessage[], title?: string): void {
38
+ try {
39
+ const sessions = this.sessionsSubject.value;
40
+ const existingIndex = sessions.findIndex(s => s.sessionId === sessionId);
41
+
42
+ const sessionTitle = title || this.generateSessionTitle(messages);
43
+ const now = new Date();
44
+
45
+ const session: SavedSession = {
46
+ sessionId,
47
+ title: sessionTitle,
48
+ messages: this.trimMessages(messages),
49
+ createdAt: existingIndex >= 0 ? sessions[existingIndex].createdAt : now,
50
+ updatedAt: now,
51
+ messageCount: messages.length
52
+ };
53
+
54
+ if (existingIndex >= 0) {
55
+ sessions[existingIndex] = session;
56
+ } else {
57
+ sessions.unshift(session);
58
+ }
59
+
60
+ // 限制会话数量
61
+ const trimmedSessions = sessions.slice(0, MAX_SESSIONS);
62
+ this.sessionsSubject.next(trimmedSessions);
63
+ this.saveToStorage(trimmedSessions);
64
+
65
+ this.logger.info('Session saved', { sessionId, title: sessionTitle });
66
+
67
+ } catch (error) {
68
+ this.logger.error('Failed to save session', { error, sessionId });
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * 加载会话
75
+ */
76
+ loadSession(sessionId: string): SavedSession | undefined {
77
+ const sessions = this.sessionsSubject.value;
78
+ return sessions.find(s => s.sessionId === sessionId);
79
+ }
80
+
81
+ /**
82
+ * 删除会话
83
+ */
84
+ deleteSession(sessionId: string): void {
85
+ const sessions = this.sessionsSubject.value;
86
+ const filteredSessions = sessions.filter(s => s.sessionId !== sessionId);
87
+ this.sessionsSubject.next(filteredSessions);
88
+ this.saveToStorage(filteredSessions);
89
+ this.logger.info('Session deleted', { sessionId });
90
+ }
91
+
92
+ /**
93
+ * 清空所有历史
94
+ */
95
+ clearAllHistory(): void {
96
+ this.sessionsSubject.next([]);
97
+ this.saveToStorage([]);
98
+ this.logger.info('All chat history cleared');
99
+ }
100
+
101
+ /**
102
+ * 搜索会话
103
+ */
104
+ searchSessions(query: string): SavedSession[] {
105
+ const sessions = this.sessionsSubject.value;
106
+ const lowercaseQuery = query.toLowerCase();
107
+
108
+ return sessions.filter(session =>
109
+ session.title.toLowerCase().includes(lowercaseQuery) ||
110
+ session.messages.some(msg =>
111
+ msg.content.toLowerCase().includes(lowercaseQuery)
112
+ )
113
+ );
114
+ }
115
+
116
+ /**
117
+ * 获取最近的会话
118
+ */
119
+ getRecentSessions(count: number = 10): SavedSession[] {
120
+ const sessions = this.sessionsSubject.value;
121
+ return sessions.slice(0, count);
122
+ }
123
+
124
+ /**
125
+ * 获取会话统计
126
+ */
127
+ getStatistics(): {
128
+ totalSessions: number;
129
+ totalMessages: number;
130
+ averageMessagesPerSession: number;
131
+ oldestSession?: Date;
132
+ newestSession?: Date;
133
+ } {
134
+ const sessions = this.sessionsSubject.value;
135
+ const totalSessions = sessions.length;
136
+ const totalMessages = sessions.reduce((sum, s) => sum + s.messageCount, 0);
137
+ const averageMessagesPerSession = totalSessions > 0 ? totalMessages / totalSessions : 0;
138
+
139
+ const dates = sessions.map(s => s.createdAt.getTime());
140
+ const oldestSession = dates.length > 0 ? new Date(Math.min(...dates)) : undefined;
141
+ const newestSession = dates.length > 0 ? new Date(Math.max(...dates)) : undefined;
142
+
143
+ return {
144
+ totalSessions,
145
+ totalMessages,
146
+ averageMessagesPerSession: Math.round(averageMessagesPerSession * 100) / 100,
147
+ oldestSession,
148
+ newestSession
149
+ };
150
+ }
151
+
152
+ /**
153
+ * 导出所有历史
154
+ */
155
+ exportAllHistory(): string {
156
+ const sessions = this.sessionsSubject.value;
157
+ const exportData = {
158
+ exportDate: new Date().toISOString(),
159
+ version: '1.0',
160
+ sessions
161
+ };
162
+ return JSON.stringify(exportData, null, 2);
163
+ }
164
+
165
+ /**
166
+ * 导入历史
167
+ */
168
+ importHistory(data: string): void {
169
+ try {
170
+ const importData = JSON.parse(data);
171
+
172
+ if (!importData.sessions || !Array.isArray(importData.sessions)) {
173
+ throw new Error('Invalid history format');
174
+ }
175
+
176
+ const sessions = importData.sessions.map((s: any) => ({
177
+ ...s,
178
+ createdAt: new Date(s.createdAt),
179
+ updatedAt: new Date(s.updatedAt)
180
+ }));
181
+
182
+ this.sessionsSubject.next(sessions);
183
+ this.saveToStorage(sessions);
184
+
185
+ this.logger.info('History imported', {
186
+ sessionCount: sessions.length
187
+ });
188
+
189
+ } catch (error) {
190
+ this.logger.error('Failed to import history', error);
191
+ throw new Error('Invalid history file format');
192
+ }
193
+ }
194
+
195
+ private loadSessions(): void {
196
+ try {
197
+ const stored = localStorage.getItem(STORAGE_KEY);
198
+ if (stored) {
199
+ const sessions = JSON.parse(stored).map((s: any) => ({
200
+ ...s,
201
+ createdAt: new Date(s.createdAt),
202
+ updatedAt: new Date(s.updatedAt)
203
+ }));
204
+ this.sessionsSubject.next(sessions);
205
+ this.logger.info('Loaded sessions from storage', { count: sessions.length });
206
+ }
207
+ } catch (error) {
208
+ this.logger.error('Failed to load sessions from storage', error);
209
+ this.sessionsSubject.next([]);
210
+ }
211
+ }
212
+
213
+ private saveToStorage(sessions: SavedSession[]): void {
214
+ try {
215
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(sessions));
216
+ } catch (error) {
217
+ this.logger.error('Failed to save sessions to storage', error);
218
+ }
219
+ }
220
+
221
+ private generateSessionTitle(messages: ChatMessage[]): string {
222
+ const firstUserMessage = messages.find(m => m.role === 'user');
223
+ if (!firstUserMessage) {
224
+ return `会话 ${new Date().toLocaleString()}`;
225
+ }
226
+
227
+ const content = firstUserMessage.content;
228
+ return content.length > 50 ? content.substring(0, 50) + '...' : content;
229
+ }
230
+
231
+ private trimMessages(messages: ChatMessage[]): ChatMessage[] {
232
+ if (messages.length <= MAX_MESSAGES_PER_SESSION) {
233
+ return messages;
234
+ }
235
+
236
+ // 保留最近的messages
237
+ return messages.slice(-MAX_MESSAGES_PER_SESSION);
238
+ }
239
+ }
@@ -1,8 +1,9 @@
1
1
  import { Injectable } from '@angular/core';
2
2
  import { BehaviorSubject, Observable, Subject } from 'rxjs';
3
- import { ChatMessage, ChatRequest, ChatResponse, MessageRole } from '../../types/ai.types';
3
+ import { ChatMessage, ChatRequest, ChatResponse, MessageRole, Checkpoint, ApiMessage } from '../../types/ai.types';
4
4
  import { AiAssistantService } from '../core/ai-assistant.service';
5
5
  import { LoggerService } from '../core/logger.service';
6
+ import { ChatHistoryService } from './chat-history.service';
6
7
 
7
8
  /**
8
9
  * 聊天会话服务
@@ -16,15 +17,18 @@ export class ChatSessionService {
16
17
  private messagesSubject = new BehaviorSubject<ChatMessage[]>([]);
17
18
  private isTypingSubject = new BehaviorSubject<boolean>(false);
18
19
  private errorSubject = new Subject<string>();
20
+ private checkpointsSubject = new BehaviorSubject<Checkpoint[]>([]);
19
21
 
20
22
  public messages$ = this.messagesSubject.asObservable();
21
23
  public isTyping$ = this.isTypingSubject.asObservable();
22
24
  public error$ = this.errorSubject.asObservable();
25
+ public checkpoints$ = this.checkpointsSubject.asObservable();
23
26
 
24
27
  constructor(
25
28
  private aiService: AiAssistantService,
26
- private logger: LoggerService
27
- ) {}
29
+ private logger: LoggerService,
30
+ private chatHistoryService: ChatHistoryService
31
+ ) { }
28
32
 
29
33
  /**
30
34
  * 创建新会话
@@ -177,4 +181,273 @@ export class ChatSessionService {
177
181
  private generateMessageId(): string {
178
182
  return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
179
183
  }
184
+
185
+ // ==================== 检查点管理功能 ====================
186
+
187
+ /**
188
+ * 创建检查点
189
+ */
190
+ createCheckpoint(label?: string): Checkpoint {
191
+ if (!this.currentSessionId) {
192
+ throw new Error('No active session');
193
+ }
194
+
195
+ const checkpointId = this.generateCheckpointId();
196
+ const currentMessages = this.messagesSubject.value;
197
+ const apiMessages: ApiMessage[] = currentMessages.map(msg => ({
198
+ role: msg.role,
199
+ content: msg.content,
200
+ ts: msg.timestamp.getTime()
201
+ }));
202
+
203
+ // 计算Token使用量
204
+ const tokenUsage = {
205
+ input: this.calculateInputTokens(apiMessages),
206
+ output: this.calculateOutputTokens(apiMessages),
207
+ cacheRead: 0,
208
+ cacheWrite: 0
209
+ };
210
+
211
+ // 生成摘要
212
+ const summary = label || this.generateCheckpointSummary(currentMessages);
213
+
214
+ const checkpoint: Checkpoint = {
215
+ id: checkpointId,
216
+ sessionId: this.currentSessionId,
217
+ messages: apiMessages,
218
+ summary,
219
+ createdAt: Date.now(),
220
+ tokenUsage
221
+ };
222
+
223
+ // 保存到会话历史(保留现有contextInfo)
224
+ const existingContextInfo = this.chatHistoryService.loadSession(this.currentSessionId)?.contextInfo;
225
+ this.chatHistoryService.updateContextInfo(this.currentSessionId, {
226
+ ...existingContextInfo
227
+ });
228
+
229
+ // 更新本地检查点列表
230
+ const currentCheckpoints = this.checkpointsSubject.value;
231
+ this.checkpointsSubject.next([...currentCheckpoints, checkpoint]);
232
+
233
+ this.logger.info('Checkpoint created', {
234
+ checkpointId,
235
+ sessionId: this.currentSessionId,
236
+ messageCount: currentMessages.length
237
+ });
238
+
239
+ return checkpoint;
240
+ }
241
+
242
+ /**
243
+ * 恢复检查点
244
+ */
245
+ restoreCheckpoint(checkpointId: string): void {
246
+ const checkpoint = this.getCheckpoint(checkpointId);
247
+ if (!checkpoint) {
248
+ throw new Error(`Checkpoint not found: ${checkpointId}`);
249
+ }
250
+
251
+ if (checkpoint.sessionId !== this.currentSessionId) {
252
+ throw new Error('Checkpoint belongs to different session');
253
+ }
254
+
255
+ // 恢复消息
256
+ const restoredMessages: ChatMessage[] = checkpoint.messages.map(msg => ({
257
+ id: this.generateMessageId(),
258
+ role: msg.role as any,
259
+ content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
260
+ timestamp: new Date(msg.ts)
261
+ }));
262
+
263
+ this.messagesSubject.next(restoredMessages);
264
+
265
+ this.logger.info('Checkpoint restored', {
266
+ checkpointId,
267
+ sessionId: this.currentSessionId,
268
+ messageCount: restoredMessages.length
269
+ });
270
+ }
271
+
272
+ /**
273
+ * 删除检查点
274
+ */
275
+ deleteCheckpoint(checkpointId: string): void {
276
+ const currentCheckpoints = this.checkpointsSubject.value;
277
+ const filteredCheckpoints = currentCheckpoints.filter(cp => cp.id !== checkpointId);
278
+
279
+ this.checkpointsSubject.next(filteredCheckpoints);
280
+
281
+ this.logger.info('Checkpoint deleted', { checkpointId, sessionId: this.currentSessionId });
282
+ }
283
+
284
+ /**
285
+ * 获取指定检查点
286
+ */
287
+ getCheckpoint(checkpointId: string): Checkpoint | undefined {
288
+ return this.checkpointsSubject.value.find(cp => cp.id === checkpointId);
289
+ }
290
+
291
+ /**
292
+ * 列出所有检查点
293
+ */
294
+ listCheckpoints(sessionId?: string): Checkpoint[] {
295
+ const allCheckpoints = this.checkpointsSubject.value;
296
+ if (sessionId) {
297
+ return allCheckpoints.filter(cp => cp.sessionId === sessionId);
298
+ }
299
+ return [...allCheckpoints];
300
+ }
301
+
302
+ /**
303
+ * 清空检查点
304
+ */
305
+ clearCheckpoints(sessionId?: string): void {
306
+ if (sessionId) {
307
+ // 清空指定会话的检查点
308
+ const currentCheckpoints = this.checkpointsSubject.value;
309
+ const filteredCheckpoints = currentCheckpoints.filter(cp => cp.sessionId !== sessionId);
310
+ this.checkpointsSubject.next(filteredCheckpoints);
311
+ } else {
312
+ // 清空所有检查点
313
+ this.checkpointsSubject.next([]);
314
+ }
315
+
316
+ this.logger.info('Checkpoints cleared', { sessionId: sessionId || 'all' });
317
+ }
318
+
319
+ /**
320
+ * 压缩存储检查点
321
+ */
322
+ compressCheckpoint(checkpointId: string): Checkpoint {
323
+ const checkpoint = this.getCheckpoint(checkpointId);
324
+ if (!checkpoint) {
325
+ throw new Error(`Checkpoint not found: ${checkpointId}`);
326
+ }
327
+
328
+ // TODO: 实现检查点压缩逻辑
329
+ // 这里先返回原检查点,实际实现中可以使用 ContextManager 进行压缩
330
+ this.logger.info('Checkpoint compressed', { checkpointId });
331
+
332
+ return checkpoint;
333
+ }
334
+
335
+ /**
336
+ * 导出会话(包含检查点)
337
+ */
338
+ exportSessionWithCheckpoints(): string {
339
+ const sessionData = {
340
+ sessionId: this.currentSessionId,
341
+ messages: this.messagesSubject.value,
342
+ checkpoints: this.checkpointsSubject.value,
343
+ timestamp: new Date().toISOString(),
344
+ version: '2.0'
345
+ };
346
+ return JSON.stringify(sessionData, null, 2);
347
+ }
348
+
349
+ /**
350
+ * 导入会话(包含检查点)
351
+ */
352
+ importSessionWithCheckpoints(sessionData: string): void {
353
+ try {
354
+ const data = JSON.parse(sessionData);
355
+ this.currentSessionId = data.sessionId;
356
+ this.messagesSubject.next(data.messages || []);
357
+
358
+ // 恢复检查点
359
+ if (data.checkpoints) {
360
+ const restoredCheckpoints: Checkpoint[] = data.checkpoints.map((cp: any) => ({
361
+ ...cp,
362
+ createdAt: typeof cp.createdAt === 'number' ? cp.createdAt : new Date(cp.createdAt).getTime(),
363
+ messages: cp.messages.map((msg: any) => ({
364
+ ...msg,
365
+ ts: typeof msg.ts === 'number' ? msg.ts : new Date(msg.ts).getTime()
366
+ }))
367
+ }));
368
+ this.checkpointsSubject.next(restoredCheckpoints);
369
+ }
370
+
371
+ this.logger.info('Session with checkpoints imported', {
372
+ sessionId: this.currentSessionId,
373
+ checkpointCount: data.checkpoints?.length || 0
374
+ });
375
+ } catch (error) {
376
+ this.logger.error('Failed to import session with checkpoints', error);
377
+ throw new Error('Invalid session data format');
378
+ }
379
+ }
380
+
381
+ /**
382
+ * 获取检查点统计信息
383
+ */
384
+ getCheckpointStatistics(): {
385
+ totalCheckpoints: number;
386
+ averageMessagesPerCheckpoint: number;
387
+ totalTokenUsage: number;
388
+ oldestCheckpoint?: Date;
389
+ newestCheckpoint?: Date;
390
+ } {
391
+ const checkpoints = this.checkpointsSubject.value;
392
+ const totalCheckpoints = checkpoints.length;
393
+
394
+ const totalMessages = checkpoints.reduce((sum, cp) => sum + cp.messages.length, 0);
395
+ const averageMessagesPerCheckpoint = totalCheckpoints > 0 ? totalMessages / totalCheckpoints : 0;
396
+
397
+ const totalTokenUsage = checkpoints.reduce((sum, cp) => {
398
+ return sum + cp.tokenUsage.input + cp.tokenUsage.output;
399
+ }, 0);
400
+
401
+ const timestamps = checkpoints.map(cp => cp.createdAt);
402
+ const oldestCheckpoint = timestamps.length > 0 ? new Date(Math.min(...timestamps)) : undefined;
403
+ const newestCheckpoint = timestamps.length > 0 ? new Date(Math.max(...timestamps)) : undefined;
404
+
405
+ return {
406
+ totalCheckpoints,
407
+ averageMessagesPerCheckpoint: Math.round(averageMessagesPerCheckpoint * 100) / 100,
408
+ totalTokenUsage,
409
+ oldestCheckpoint,
410
+ newestCheckpoint
411
+ };
412
+ }
413
+
414
+ // ==================== 私有辅助方法 ====================
415
+
416
+ private generateCheckpointId(): string {
417
+ return `checkpoint_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
418
+ }
419
+
420
+ private generateCheckpointSummary(messages: ChatMessage[]): string {
421
+ if (messages.length === 0) {
422
+ return '空检查点';
423
+ }
424
+
425
+ const firstMessage = messages[0];
426
+ const lastMessage = messages[messages.length - 1];
427
+ const messageCount = messages.length;
428
+
429
+ return `检查点:${messageCount}条消息 | 从 "${this.truncateText(firstMessage.content, 30)}" 到 "${this.truncateText(lastMessage.content, 30)}"`;
430
+ }
431
+
432
+ private truncateText(text: string, maxLength: number): string {
433
+ return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
434
+ }
435
+
436
+ private calculateInputTokens(messages: ApiMessage[]): number {
437
+ return messages
438
+ .filter(msg => msg.role === 'user' || msg.role === 'system')
439
+ .reduce((sum, msg) => {
440
+ const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
441
+ return sum + Math.ceil(content.length / 4);
442
+ }, 0);
443
+ }
444
+
445
+ private calculateOutputTokens(messages: ApiMessage[]): number {
446
+ return messages
447
+ .filter(msg => msg.role === 'assistant')
448
+ .reduce((sum, msg) => {
449
+ const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
450
+ return sum + Math.ceil(content.length / 4);
451
+ }, 0);
452
+ }
180
453
  }