tabby-ai-assistant 1.0.5 → 1.0.7
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 +8 -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,258 @@
|
|
|
1
|
+
import { Injectable, ComponentFactoryResolver, ApplicationRef, Injector, EmbeddedViewRef, ComponentRef } from '@angular/core';
|
|
2
|
+
import { ConfigService } from 'tabby-core';
|
|
3
|
+
import { AiSidebarComponent } from '../../components/chat/ai-sidebar.component';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AI Sidebar 配置接口
|
|
7
|
+
*/
|
|
8
|
+
export interface AiSidebarConfig {
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
position?: 'left' | 'right';
|
|
11
|
+
showInToolbar?: boolean;
|
|
12
|
+
sidebarVisible?: boolean;
|
|
13
|
+
sidebarCollapsed?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* AI Sidebar 服务 - 管理 AI 聊天侧边栏的生命周期
|
|
18
|
+
*
|
|
19
|
+
* 采用 Flexbox 布局方式,将 sidebar 插入到 app-root 作为第一个子元素,
|
|
20
|
+
* app-root 变为水平 flex 容器,sidebar 在左侧
|
|
21
|
+
*/
|
|
22
|
+
@Injectable({ providedIn: 'root' })
|
|
23
|
+
export class AiSidebarService {
|
|
24
|
+
private sidebarComponentRef: ComponentRef<AiSidebarComponent> | null = null;
|
|
25
|
+
private sidebarElement: HTMLElement | null = null;
|
|
26
|
+
private styleElement: HTMLStyleElement | null = null;
|
|
27
|
+
private _isVisible = false;
|
|
28
|
+
private readonly SIDEBAR_WIDTH = 320;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 侧边栏是否可见
|
|
32
|
+
*/
|
|
33
|
+
get sidebarVisible(): boolean {
|
|
34
|
+
return this._isVisible;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
private componentFactoryResolver: ComponentFactoryResolver,
|
|
39
|
+
private appRef: ApplicationRef,
|
|
40
|
+
private injector: Injector,
|
|
41
|
+
private config: ConfigService,
|
|
42
|
+
) { }
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 显示 sidebar
|
|
46
|
+
*/
|
|
47
|
+
show(): void {
|
|
48
|
+
if (this._isVisible) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.createSidebar();
|
|
53
|
+
|
|
54
|
+
const pluginConfig = this.getPluginConfig();
|
|
55
|
+
pluginConfig.sidebarVisible = true;
|
|
56
|
+
this.savePluginConfig(pluginConfig);
|
|
57
|
+
|
|
58
|
+
this._isVisible = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 隐藏 sidebar
|
|
63
|
+
*/
|
|
64
|
+
hide(): void {
|
|
65
|
+
if (!this._isVisible) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.destroySidebar();
|
|
70
|
+
|
|
71
|
+
const pluginConfig = this.getPluginConfig();
|
|
72
|
+
pluginConfig.sidebarVisible = false;
|
|
73
|
+
this.savePluginConfig(pluginConfig);
|
|
74
|
+
|
|
75
|
+
this._isVisible = false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 切换 sidebar 显示状态
|
|
80
|
+
*/
|
|
81
|
+
toggle(): void {
|
|
82
|
+
if (this._isVisible) {
|
|
83
|
+
this.hide();
|
|
84
|
+
} else {
|
|
85
|
+
this.show();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 获取当前显示状态
|
|
91
|
+
*/
|
|
92
|
+
get visible(): boolean {
|
|
93
|
+
return this._isVisible;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 初始化 - 应用启动时调用
|
|
98
|
+
*/
|
|
99
|
+
initialize(): void {
|
|
100
|
+
const pluginConfig = this.getPluginConfig();
|
|
101
|
+
// 默认显示 sidebar,除非明确设置为隐藏
|
|
102
|
+
if (pluginConfig.sidebarVisible !== false) {
|
|
103
|
+
this.show();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 创建 sidebar 组件
|
|
109
|
+
*
|
|
110
|
+
* 使用固定定位方案:
|
|
111
|
+
* 1. 侧边栏 position: fixed,固定在左侧
|
|
112
|
+
* 2. 主内容区通过 margin-left 推开
|
|
113
|
+
* 这样不改变任何现有元素的 flex 布局
|
|
114
|
+
*/
|
|
115
|
+
private createSidebar(): void {
|
|
116
|
+
// 创建组件
|
|
117
|
+
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(AiSidebarComponent);
|
|
118
|
+
this.sidebarComponentRef = componentFactory.create(this.injector);
|
|
119
|
+
|
|
120
|
+
// 附加到应用
|
|
121
|
+
this.appRef.attachView(this.sidebarComponentRef.hostView);
|
|
122
|
+
|
|
123
|
+
// 获取 DOM 元素
|
|
124
|
+
const domElem = (this.sidebarComponentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
|
|
125
|
+
|
|
126
|
+
// 创建 wrapper 元素 - 使用固定定位
|
|
127
|
+
const wrapper = document.createElement('div');
|
|
128
|
+
wrapper.className = 'ai-sidebar-wrapper';
|
|
129
|
+
wrapper.style.cssText = `
|
|
130
|
+
position: fixed;
|
|
131
|
+
left: 0;
|
|
132
|
+
top: 0;
|
|
133
|
+
width: ${this.SIDEBAR_WIDTH}px;
|
|
134
|
+
height: 100%;
|
|
135
|
+
display: flex;
|
|
136
|
+
flex-direction: column;
|
|
137
|
+
background: var(--bs-body-bg, #1e1e1e);
|
|
138
|
+
border-right: 1px solid var(--bs-border-color, #333);
|
|
139
|
+
box-shadow: 2px 0 10px rgba(0,0,0,0.3);
|
|
140
|
+
z-index: 1000;
|
|
141
|
+
`;
|
|
142
|
+
|
|
143
|
+
wrapper.appendChild(domElem);
|
|
144
|
+
|
|
145
|
+
// 插入到 body
|
|
146
|
+
document.body.appendChild(wrapper);
|
|
147
|
+
|
|
148
|
+
this.sidebarElement = wrapper;
|
|
149
|
+
|
|
150
|
+
// 注入布局 CSS - 只添加 margin-left 把主内容推开
|
|
151
|
+
this.injectLayoutCSS();
|
|
152
|
+
|
|
153
|
+
// 注入服务引用到组件
|
|
154
|
+
if (this.sidebarComponentRef) {
|
|
155
|
+
const component = this.sidebarComponentRef.instance;
|
|
156
|
+
component.sidebarService = this;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 销毁 sidebar 组件
|
|
162
|
+
*/
|
|
163
|
+
private destroySidebar(): void {
|
|
164
|
+
// 移除注入的 CSS
|
|
165
|
+
this.removeLayoutCSS();
|
|
166
|
+
|
|
167
|
+
if (this.sidebarComponentRef) {
|
|
168
|
+
this.appRef.detachView(this.sidebarComponentRef.hostView);
|
|
169
|
+
this.sidebarComponentRef.destroy();
|
|
170
|
+
this.sidebarComponentRef = null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (this.sidebarElement) {
|
|
174
|
+
this.sidebarElement.remove();
|
|
175
|
+
this.sidebarElement = null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 调整 .content 元素样式 - 只处理第二个(更深层的).content
|
|
181
|
+
*/
|
|
182
|
+
private adjustContentStyles(appRoot: Element, apply: boolean): void {
|
|
183
|
+
const contentElements = appRoot.querySelectorAll('.content');
|
|
184
|
+
|
|
185
|
+
if (contentElements.length > 1) {
|
|
186
|
+
// 选择第二个(更深层的).content 元素,这是 Tabby 的主内容区
|
|
187
|
+
const contentElement = contentElements[1] as HTMLElement;
|
|
188
|
+
if (apply) {
|
|
189
|
+
contentElement.style.width = 'auto';
|
|
190
|
+
contentElement.style.flex = '1 1 auto';
|
|
191
|
+
contentElement.style.minWidth = '0';
|
|
192
|
+
} else {
|
|
193
|
+
contentElement.style.removeProperty('width');
|
|
194
|
+
contentElement.style.removeProperty('flex');
|
|
195
|
+
contentElement.style.removeProperty('min-width');
|
|
196
|
+
}
|
|
197
|
+
} else if (contentElements.length === 1) {
|
|
198
|
+
// 如果只有一个 .content,则处理它
|
|
199
|
+
const contentElement = contentElements[0] as HTMLElement;
|
|
200
|
+
if (apply) {
|
|
201
|
+
contentElement.style.width = 'auto';
|
|
202
|
+
contentElement.style.flex = '1 1 auto';
|
|
203
|
+
contentElement.style.minWidth = '0';
|
|
204
|
+
} else {
|
|
205
|
+
contentElement.style.removeProperty('width');
|
|
206
|
+
contentElement.style.removeProperty('flex');
|
|
207
|
+
contentElement.style.removeProperty('min-width');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 注入布局 CSS - 使用 margin-left 把主内容推开
|
|
214
|
+
*
|
|
215
|
+
* 固定定位方案:侧边栏 fixed,主内容区 margin-left
|
|
216
|
+
*/
|
|
217
|
+
private injectLayoutCSS(): void {
|
|
218
|
+
const style = document.createElement('style');
|
|
219
|
+
style.id = 'ai-sidebar-layout-css';
|
|
220
|
+
style.textContent = `
|
|
221
|
+
/* 用 margin-left 把 app-root 推开,为侧边栏腾出空间 */
|
|
222
|
+
app-root {
|
|
223
|
+
margin-left: ${this.SIDEBAR_WIDTH}px !important;
|
|
224
|
+
}
|
|
225
|
+
`;
|
|
226
|
+
|
|
227
|
+
document.head.appendChild(style);
|
|
228
|
+
this.styleElement = style;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 移除布局 CSS
|
|
233
|
+
*/
|
|
234
|
+
private removeLayoutCSS(): void {
|
|
235
|
+
if (this.styleElement) {
|
|
236
|
+
this.styleElement.remove();
|
|
237
|
+
this.styleElement = null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* 获取插件配置
|
|
243
|
+
*/
|
|
244
|
+
private getPluginConfig(): AiSidebarConfig {
|
|
245
|
+
return this.config.store.pluginConfig?.['ai-assistant'] || {};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 保存插件配置
|
|
250
|
+
*/
|
|
251
|
+
private savePluginConfig(pluginConfig: AiSidebarConfig): void {
|
|
252
|
+
if (!this.config.store.pluginConfig) {
|
|
253
|
+
this.config.store.pluginConfig = {};
|
|
254
|
+
}
|
|
255
|
+
this.config.store.pluginConfig['ai-assistant'] = pluginConfig;
|
|
256
|
+
this.config.save();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -10,6 +10,23 @@ export interface SavedSession {
|
|
|
10
10
|
createdAt: Date;
|
|
11
11
|
updatedAt: Date;
|
|
12
12
|
messageCount: number;
|
|
13
|
+
// 上下文工程相关字段
|
|
14
|
+
contextInfo?: {
|
|
15
|
+
tokenUsage?: {
|
|
16
|
+
input: number;
|
|
17
|
+
output: number;
|
|
18
|
+
cacheRead: number;
|
|
19
|
+
cacheWrite: number;
|
|
20
|
+
};
|
|
21
|
+
compactionHistory?: Array<{
|
|
22
|
+
timestamp: Date;
|
|
23
|
+
type: 'prune' | 'compact' | 'truncate';
|
|
24
|
+
tokensSaved: number;
|
|
25
|
+
condenseId?: string;
|
|
26
|
+
}>;
|
|
27
|
+
hasCompression?: boolean;
|
|
28
|
+
lastCompactionAt?: Date;
|
|
29
|
+
};
|
|
13
30
|
}
|
|
14
31
|
|
|
15
32
|
const STORAGE_KEY = 'tabby-ai-assistant-chat-history';
|
|
@@ -236,4 +253,295 @@ export class ChatHistoryService {
|
|
|
236
253
|
// 保留最近的messages
|
|
237
254
|
return messages.slice(-MAX_MESSAGES_PER_SESSION);
|
|
238
255
|
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 更新会话的上下文信息(压缩标记支持)
|
|
259
|
+
*/
|
|
260
|
+
updateContextInfo(sessionId: string, contextInfo: SavedSession['contextInfo']): void {
|
|
261
|
+
const sessions = this.sessionsSubject.value;
|
|
262
|
+
const existingIndex = sessions.findIndex(s => s.sessionId === sessionId);
|
|
263
|
+
|
|
264
|
+
if (existingIndex >= 0) {
|
|
265
|
+
sessions[existingIndex] = {
|
|
266
|
+
...sessions[existingIndex],
|
|
267
|
+
contextInfo
|
|
268
|
+
};
|
|
269
|
+
this.sessionsSubject.next([...sessions]);
|
|
270
|
+
this.saveToStorage(sessions);
|
|
271
|
+
this.logger.info('Context info updated', { sessionId });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 导出会话(包含压缩状态)
|
|
277
|
+
*/
|
|
278
|
+
exportSessionWithContext(sessionId: string): string | undefined {
|
|
279
|
+
const session = this.loadSession(sessionId);
|
|
280
|
+
if (!session) {
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const exportData = {
|
|
285
|
+
exportDate: new Date().toISOString(),
|
|
286
|
+
version: '2.0',
|
|
287
|
+
session: {
|
|
288
|
+
...session,
|
|
289
|
+
contextInfo: session.contextInfo || {}
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
return JSON.stringify(exportData, null, 2);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 导出会话历史(包含所有压缩状态)
|
|
297
|
+
*/
|
|
298
|
+
exportAllHistoryWithContext(): string {
|
|
299
|
+
const sessions = this.sessionsSubject.value;
|
|
300
|
+
const exportData = {
|
|
301
|
+
exportDate: new Date().toISOString(),
|
|
302
|
+
version: '2.0',
|
|
303
|
+
sessions: sessions.map(s => ({
|
|
304
|
+
...s,
|
|
305
|
+
contextInfo: s.contextInfo || {}
|
|
306
|
+
}))
|
|
307
|
+
};
|
|
308
|
+
return JSON.stringify(exportData, null, 2);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 导入会话(包含压缩状态)
|
|
313
|
+
*/
|
|
314
|
+
importSessionWithContext(data: string): void {
|
|
315
|
+
try {
|
|
316
|
+
const importData = JSON.parse(data);
|
|
317
|
+
|
|
318
|
+
if (!importData.session && !importData.sessions) {
|
|
319
|
+
throw new Error('Invalid session format');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const sessions = this.sessionsSubject.value;
|
|
323
|
+
|
|
324
|
+
if (importData.session) {
|
|
325
|
+
// 导入单个会话
|
|
326
|
+
const session = importData.session;
|
|
327
|
+
const existingIndex = sessions.findIndex(s => s.sessionId === session.sessionId);
|
|
328
|
+
|
|
329
|
+
const processedSession = {
|
|
330
|
+
...session,
|
|
331
|
+
createdAt: new Date(session.createdAt),
|
|
332
|
+
updatedAt: new Date(session.updatedAt),
|
|
333
|
+
contextInfo: session.contextInfo || undefined
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
if (existingIndex >= 0) {
|
|
337
|
+
sessions[existingIndex] = processedSession;
|
|
338
|
+
} else {
|
|
339
|
+
sessions.unshift(processedSession);
|
|
340
|
+
}
|
|
341
|
+
} else if (importData.sessions) {
|
|
342
|
+
// 批量导入会话
|
|
343
|
+
const importedSessions = importData.sessions.map((s: any) => ({
|
|
344
|
+
...s,
|
|
345
|
+
createdAt: new Date(s.createdAt),
|
|
346
|
+
updatedAt: new Date(s.updatedAt),
|
|
347
|
+
contextInfo: s.contextInfo || undefined
|
|
348
|
+
}));
|
|
349
|
+
|
|
350
|
+
// 合并现有会话,避免重复
|
|
351
|
+
const mergedSessions = [...sessions];
|
|
352
|
+
importedSessions.forEach(imported => {
|
|
353
|
+
const existingIndex = mergedSessions.findIndex(s => s.sessionId === imported.sessionId);
|
|
354
|
+
if (existingIndex >= 0) {
|
|
355
|
+
mergedSessions[existingIndex] = imported;
|
|
356
|
+
} else {
|
|
357
|
+
mergedSessions.unshift(imported);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// 限制会话数量
|
|
362
|
+
this.sessionsSubject.next(mergedSessions.slice(0, MAX_SESSIONS));
|
|
363
|
+
this.saveToStorage(mergedSessions.slice(0, MAX_SESSIONS));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
this.logger.info('Session(s) with context imported successfully');
|
|
367
|
+
} catch (error) {
|
|
368
|
+
this.logger.error('Failed to import session with context', error);
|
|
369
|
+
throw new Error('Invalid session file format');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* 清理压缩数据
|
|
375
|
+
*/
|
|
376
|
+
cleanupCompressedData(sessionId?: string): void {
|
|
377
|
+
const sessions = this.sessionsSubject.value;
|
|
378
|
+
|
|
379
|
+
if (sessionId) {
|
|
380
|
+
// 清理指定会话的压缩数据
|
|
381
|
+
const sessionIndex = sessions.findIndex(s => s.sessionId === sessionId);
|
|
382
|
+
if (sessionIndex >= 0 && sessions[sessionIndex].contextInfo) {
|
|
383
|
+
sessions[sessionIndex] = {
|
|
384
|
+
...sessions[sessionIndex],
|
|
385
|
+
contextInfo: undefined
|
|
386
|
+
};
|
|
387
|
+
this.sessionsSubject.next([...sessions]);
|
|
388
|
+
this.saveToStorage(sessions);
|
|
389
|
+
this.logger.info('Compressed data cleaned for session', { sessionId });
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
// 清理所有会话的压缩数据
|
|
393
|
+
sessions.forEach(session => {
|
|
394
|
+
if (session.contextInfo) {
|
|
395
|
+
session.contextInfo = undefined;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
this.sessionsSubject.next([...sessions]);
|
|
399
|
+
this.saveToStorage(sessions);
|
|
400
|
+
this.logger.info('All compressed data cleaned');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* 获取压缩统计信息
|
|
406
|
+
*/
|
|
407
|
+
getCompressionStatistics(): {
|
|
408
|
+
totalSessions: number;
|
|
409
|
+
sessionsWithCompression: number;
|
|
410
|
+
totalTokensSaved: number;
|
|
411
|
+
averageTokensSaved: number;
|
|
412
|
+
lastCompactionAt?: Date;
|
|
413
|
+
compactionHistory: Array<{
|
|
414
|
+
sessionId: string;
|
|
415
|
+
type: 'prune' | 'compact' | 'truncate';
|
|
416
|
+
timestamp: Date;
|
|
417
|
+
tokensSaved: number;
|
|
418
|
+
}>;
|
|
419
|
+
} {
|
|
420
|
+
const sessions = this.sessionsSubject.value;
|
|
421
|
+
const sessionsWithCompression = sessions.filter(s => s.contextInfo?.hasCompression);
|
|
422
|
+
let totalTokensSaved = 0;
|
|
423
|
+
const allCompactionEvents: Array<{
|
|
424
|
+
sessionId: string;
|
|
425
|
+
type: 'prune' | 'compact' | 'truncate';
|
|
426
|
+
timestamp: Date;
|
|
427
|
+
tokensSaved: number;
|
|
428
|
+
}> = [];
|
|
429
|
+
let lastCompactionAt: Date | undefined;
|
|
430
|
+
|
|
431
|
+
sessionsWithCompression.forEach(session => {
|
|
432
|
+
if (session.contextInfo?.compactionHistory) {
|
|
433
|
+
session.contextInfo.compactionHistory.forEach(event => {
|
|
434
|
+
totalTokensSaved += event.tokensSaved;
|
|
435
|
+
allCompactionEvents.push({
|
|
436
|
+
sessionId: session.sessionId,
|
|
437
|
+
type: event.type,
|
|
438
|
+
timestamp: event.timestamp,
|
|
439
|
+
tokensSaved: event.tokensSaved
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
if (!lastCompactionAt || event.timestamp > lastCompactionAt) {
|
|
443
|
+
lastCompactionAt = event.timestamp;
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
totalSessions: sessions.length,
|
|
451
|
+
sessionsWithCompression: sessionsWithCompression.length,
|
|
452
|
+
totalTokensSaved,
|
|
453
|
+
averageTokensSaved: sessionsWithCompression.length > 0
|
|
454
|
+
? Math.round(totalTokensSaved / sessionsWithCompression.length)
|
|
455
|
+
: 0,
|
|
456
|
+
lastCompactionAt,
|
|
457
|
+
compactionHistory: allCompactionEvents
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* 记录压缩事件
|
|
463
|
+
*/
|
|
464
|
+
recordCompactionEvent(
|
|
465
|
+
sessionId: string,
|
|
466
|
+
type: 'prune' | 'compact' | 'truncate',
|
|
467
|
+
tokensSaved: number,
|
|
468
|
+
condenseId?: string
|
|
469
|
+
): void {
|
|
470
|
+
const sessions = this.sessionsSubject.value;
|
|
471
|
+
const sessionIndex = sessions.findIndex(s => s.sessionId === sessionId);
|
|
472
|
+
|
|
473
|
+
if (sessionIndex >= 0) {
|
|
474
|
+
const session = sessions[sessionIndex];
|
|
475
|
+
const contextInfo = session.contextInfo || {};
|
|
476
|
+
|
|
477
|
+
const compactionHistory = contextInfo.compactionHistory || [];
|
|
478
|
+
compactionHistory.push({
|
|
479
|
+
timestamp: new Date(),
|
|
480
|
+
type,
|
|
481
|
+
tokensSaved,
|
|
482
|
+
condenseId
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
sessions[sessionIndex] = {
|
|
486
|
+
...session,
|
|
487
|
+
contextInfo: {
|
|
488
|
+
...contextInfo,
|
|
489
|
+
hasCompression: true,
|
|
490
|
+
lastCompactionAt: new Date(),
|
|
491
|
+
compactionHistory
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
this.sessionsSubject.next([...sessions]);
|
|
496
|
+
this.saveToStorage(sessions);
|
|
497
|
+
this.logger.info('Compaction event recorded', { sessionId, type, tokensSaved });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* 检查会话是否有压缩标记
|
|
503
|
+
*/
|
|
504
|
+
hasCompressionMarkers(sessionId: string): boolean {
|
|
505
|
+
const session = this.loadSession(sessionId);
|
|
506
|
+
return session?.contextInfo?.hasCompression || false;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* 获取会话的压缩历史
|
|
511
|
+
*/
|
|
512
|
+
getCompactionHistory(sessionId: string): Array<{
|
|
513
|
+
timestamp: Date;
|
|
514
|
+
type: 'prune' | 'compact' | 'truncate';
|
|
515
|
+
tokensSaved: number;
|
|
516
|
+
condenseId?: string;
|
|
517
|
+
}> {
|
|
518
|
+
const session = this.loadSession(sessionId);
|
|
519
|
+
return session?.contextInfo?.compactionHistory || [];
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* 更新Token使用统计
|
|
524
|
+
*/
|
|
525
|
+
updateTokenUsage(sessionId: string, tokenUsage?: {
|
|
526
|
+
input: number;
|
|
527
|
+
output: number;
|
|
528
|
+
cacheRead: number;
|
|
529
|
+
cacheWrite: number;
|
|
530
|
+
}): void {
|
|
531
|
+
const sessions = this.sessionsSubject.value;
|
|
532
|
+
const sessionIndex = sessions.findIndex(s => s.sessionId === sessionId);
|
|
533
|
+
|
|
534
|
+
if (sessionIndex >= 0) {
|
|
535
|
+
const session = sessions[sessionIndex];
|
|
536
|
+
sessions[sessionIndex] = {
|
|
537
|
+
...session,
|
|
538
|
+
contextInfo: {
|
|
539
|
+
...session.contextInfo,
|
|
540
|
+
tokenUsage
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
this.sessionsSubject.next([...sessions]);
|
|
544
|
+
this.saveToStorage(sessions);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
239
547
|
}
|