feat(context): 添加上下文压缩 API 和 UI 组件
Server API: - 扩展 Agent Adapter 接口添加压缩相关方法 - 新增 context.ts 路由 (GET /sessions/:id/context, POST /sessions/:id/compress) - 扩展 config.ts 添加摘要模型配置接口 (GET/PUT /config/summary) UI 组件: - 新增 ContextUsage 组件显示上下文使用情况 - 扩展 ConfigPanel 添加摘要模型配置区域 - 添加 API 客户端方法和类型定义 Web 集成: - 在 Chat 页面头部集成 ContextUsage 紧凑模式显示
This commit is contained in:
@@ -1,7 +1,19 @@
|
|||||||
export { Agent } from './core/agent.js';
|
export { Agent } from './core/agent.js';
|
||||||
export { toolRegistry, todoManager, initTaskContext, updateTaskDescription, updateSkillDescription } from './tools/index.js';
|
export { toolRegistry, todoManager, initTaskContext, updateTaskDescription, updateSkillDescription } from './tools/index.js';
|
||||||
export { loadConfig, saveConfig, getConfig, loadVisionConfig } from './utils/config.js';
|
export { loadConfig, saveConfig, getConfig, loadVisionConfig, loadSummaryConfig } from './utils/config.js';
|
||||||
export type { VisionConfig } from './utils/config.js';
|
export type { VisionConfig, SummaryConfig } from './utils/config.js';
|
||||||
|
|
||||||
|
// Context compression
|
||||||
|
export {
|
||||||
|
CompressionManager,
|
||||||
|
CompressionStatus,
|
||||||
|
DEFAULT_COMPRESSION_CONFIG,
|
||||||
|
} from './context/index.js';
|
||||||
|
export type {
|
||||||
|
TokenUsage,
|
||||||
|
CompressionConfig,
|
||||||
|
DetailedCompressionResult,
|
||||||
|
} from './context/index.js';
|
||||||
export { SessionStorage } from './session/storage.js';
|
export { SessionStorage } from './session/storage.js';
|
||||||
export { SessionManager } from './session/index.js';
|
export { SessionManager } from './session/index.js';
|
||||||
export type { SessionData, SessionSummary } from './session/types.js';
|
export type { SessionData, SessionSummary } from './session/types.js';
|
||||||
|
|||||||
@@ -17,6 +17,28 @@ import { createServerPermissionCallback } from '../permission/handler.js';
|
|||||||
// Core 模块接口定义(避免直接依赖 @ai-assistant/core 类型)
|
// Core 模块接口定义(避免直接依赖 @ai-assistant/core 类型)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token 使用情况接口
|
||||||
|
*/
|
||||||
|
export interface TokenUsage {
|
||||||
|
input: number;
|
||||||
|
contextLimit: number;
|
||||||
|
available: number;
|
||||||
|
usagePercent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 压缩结果接口
|
||||||
|
*/
|
||||||
|
export interface CompressionResult {
|
||||||
|
type: 'prune' | 'compaction' | 'both' | 'none';
|
||||||
|
status: 'success' | 'noop' | 'failed_empty_summary' | 'failed_token_inflated' | 'failed_error';
|
||||||
|
freedTokens: number;
|
||||||
|
error?: string;
|
||||||
|
originalTokens?: number;
|
||||||
|
summaryTokens?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Agent 实例接口
|
* Agent 实例接口
|
||||||
*/
|
*/
|
||||||
@@ -25,6 +47,11 @@ interface AgentInstance {
|
|||||||
chat(message: string, onStream?: (chunk: string) => void): Promise<string>;
|
chat(message: string, onStream?: (chunk: string) => void): Promise<string>;
|
||||||
getToolCount(): { core: number; discovered: number; total: number };
|
getToolCount(): { core: number; discovered: number; total: number };
|
||||||
getContextUsageFormatted(): string;
|
getContextUsageFormatted(): string;
|
||||||
|
getContextUsage(): TokenUsage;
|
||||||
|
compactHistory(): Promise<{ freedTokens: number; type: string }>;
|
||||||
|
getCompressionManager(): {
|
||||||
|
shouldCompress(messages: unknown[]): boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,6 +76,16 @@ interface PermissionManager {
|
|||||||
setAskCallback(callback: (ctx: unknown) => Promise<{ allow: boolean; remember?: boolean }>): void;
|
setAskCallback(callback: (ctx: unknown) => Promise<{ allow: boolean; remember?: boolean }>): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Summary 配置接口
|
||||||
|
*/
|
||||||
|
export interface SummaryConfig {
|
||||||
|
provider: string;
|
||||||
|
apiKey: string;
|
||||||
|
model: string;
|
||||||
|
baseUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core 模块接口
|
* Core 模块接口
|
||||||
*/
|
*/
|
||||||
@@ -56,6 +93,8 @@ interface CoreModule {
|
|||||||
Agent: AgentConstructor;
|
Agent: AgentConstructor;
|
||||||
toolRegistry: ToolRegistry;
|
toolRegistry: ToolRegistry;
|
||||||
loadConfig: () => unknown;
|
loadConfig: () => unknown;
|
||||||
|
saveConfig: (config: Record<string, unknown>) => void;
|
||||||
|
loadSummaryConfig: () => SummaryConfig | null;
|
||||||
getPermissionManager: (projectRoot?: string) => PermissionManager;
|
getPermissionManager: (projectRoot?: string) => PermissionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,3 +372,142 @@ async function generateSessionTitle(
|
|||||||
console.log(`[Agent] Session title generated: "${title}"`);
|
console.log(`[Agent] Session title generated: "${title}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 上下文压缩 API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上下文使用情况(带额外字段)
|
||||||
|
*/
|
||||||
|
export interface ContextUsageInfo extends TokenUsage {
|
||||||
|
formatted: string;
|
||||||
|
shouldCompress: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话的上下文使用情况
|
||||||
|
*/
|
||||||
|
export function getContextUsage(sessionId: string): ContextUsageInfo | null {
|
||||||
|
if (!coreModule) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const agent = agentCache.get(sessionId);
|
||||||
|
if (!agent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const usage = agent.getContextUsage();
|
||||||
|
const formatted = agent.getContextUsageFormatted();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...usage,
|
||||||
|
formatted,
|
||||||
|
shouldCompress: usage.usagePercent >= 80, // 80% 阈值建议压缩
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行上下文压缩
|
||||||
|
*/
|
||||||
|
export async function compressContext(
|
||||||
|
sessionId: string,
|
||||||
|
force: boolean = false
|
||||||
|
): Promise<CompressionResult | null> {
|
||||||
|
if (!coreModule) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const agent = agentCache.get(sessionId);
|
||||||
|
if (!agent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用强制压缩或普通压缩
|
||||||
|
const result = await agent.compactHistory();
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: result.type as CompressionResult['type'],
|
||||||
|
status: result.freedTokens > 0 ? 'success' : 'noop',
|
||||||
|
freedTokens: result.freedTokens,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
type: 'none',
|
||||||
|
status: 'failed_error',
|
||||||
|
freedTokens: 0,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 摘要配置 API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 摘要配置(不含 API Key 明文)
|
||||||
|
*/
|
||||||
|
export interface SummaryConfigInfo {
|
||||||
|
provider?: string;
|
||||||
|
model?: string;
|
||||||
|
hasApiKey: boolean;
|
||||||
|
baseUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取摘要配置
|
||||||
|
*/
|
||||||
|
export function getSummaryConfig(): SummaryConfigInfo | null {
|
||||||
|
if (!coreModule) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = coreModule.loadSummaryConfig();
|
||||||
|
if (!config) {
|
||||||
|
return {
|
||||||
|
hasApiKey: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
provider: config.provider,
|
||||||
|
model: config.model,
|
||||||
|
hasApiKey: !!config.apiKey,
|
||||||
|
baseUrl: config.baseUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新摘要配置
|
||||||
|
*/
|
||||||
|
export function updateSummaryConfig(config: {
|
||||||
|
provider?: string;
|
||||||
|
model?: string;
|
||||||
|
apiKey?: string;
|
||||||
|
baseUrl?: string;
|
||||||
|
}): boolean {
|
||||||
|
if (!coreModule) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建要保存的配置
|
||||||
|
const saveData: Record<string, unknown> = {};
|
||||||
|
if (config.provider !== undefined) {
|
||||||
|
saveData.summaryProvider = config.provider;
|
||||||
|
}
|
||||||
|
if (config.model !== undefined) {
|
||||||
|
saveData.summaryModel = config.model;
|
||||||
|
}
|
||||||
|
if (config.apiKey !== undefined) {
|
||||||
|
saveData.summaryApiKey = config.apiKey;
|
||||||
|
}
|
||||||
|
if (config.baseUrl !== undefined) {
|
||||||
|
saveData.summaryBaseUrl = config.baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
coreModule.saveConfig(saveData);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,4 +12,15 @@ export {
|
|||||||
processMessage,
|
processMessage,
|
||||||
cancelProcessing,
|
cancelProcessing,
|
||||||
getAgentStats,
|
getAgentStats,
|
||||||
|
// 上下文压缩相关
|
||||||
|
getContextUsage,
|
||||||
|
compressContext,
|
||||||
|
getSummaryConfig,
|
||||||
|
updateSummaryConfig,
|
||||||
|
// 类型导出
|
||||||
|
type TokenUsage,
|
||||||
|
type CompressionResult,
|
||||||
|
type ContextUsageInfo,
|
||||||
|
type SummaryConfigInfo,
|
||||||
|
type SummaryConfig,
|
||||||
} from './adapter.js';
|
} from './adapter.js';
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { cors } from 'hono/cors';
|
|||||||
import { logger } from 'hono/logger';
|
import { logger } from 'hono/logger';
|
||||||
import { createBunWebSocket } from 'hono/bun';
|
import { createBunWebSocket } from 'hono/bun';
|
||||||
|
|
||||||
import { sessionsRouter, toolsRouter, configRouter, filesRouter, commandsRouter, mcpRouter, hooksRouter, agentsRouter, checkpointsRouter, providersRouter } from './routes/index.js';
|
import { sessionsRouter, toolsRouter, configRouter, filesRouter, commandsRouter, mcpRouter, hooksRouter, agentsRouter, checkpointsRouter, providersRouter, contextRouter } from './routes/index.js';
|
||||||
import {
|
import {
|
||||||
handleWebSocket,
|
handleWebSocket,
|
||||||
handleWebSocketMessage,
|
handleWebSocketMessage,
|
||||||
@@ -89,6 +89,9 @@ api.route('/agents', agentsRouter);
|
|||||||
api.route('/checkpoints', checkpointsRouter);
|
api.route('/checkpoints', checkpointsRouter);
|
||||||
api.route('/providers', providersRouter);
|
api.route('/providers', providersRouter);
|
||||||
|
|
||||||
|
// 上下文压缩相关(挂载到根路径,内部路由包含 /sessions/:id/context)
|
||||||
|
api.route('/', contextRouter);
|
||||||
|
|
||||||
// SSE 事件流
|
// SSE 事件流
|
||||||
api.get('/sessions/:id/events', handleSSE);
|
api.get('/sessions/:id/events', handleSSE);
|
||||||
|
|
||||||
@@ -253,6 +256,14 @@ export {
|
|||||||
getAgentStats,
|
getAgentStats,
|
||||||
processMessage,
|
processMessage,
|
||||||
cancelProcessing,
|
cancelProcessing,
|
||||||
|
getContextUsage,
|
||||||
|
compressContext,
|
||||||
|
getSummaryConfig,
|
||||||
|
updateSummaryConfig,
|
||||||
|
type TokenUsage,
|
||||||
|
type CompressionResult,
|
||||||
|
type ContextUsageInfo,
|
||||||
|
type SummaryConfigInfo,
|
||||||
} from './agent/index.js';
|
} from './agent/index.js';
|
||||||
export {
|
export {
|
||||||
initAuth,
|
initAuth,
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
|
import {
|
||||||
|
getSummaryConfig,
|
||||||
|
updateSummaryConfig,
|
||||||
|
type SummaryConfigInfo,
|
||||||
|
} from '../agent/adapter.js';
|
||||||
|
|
||||||
export const configRouter = new Hono();
|
export const configRouter = new Hono();
|
||||||
|
|
||||||
@@ -107,3 +112,70 @@ export function getConfig(): ServerConfig {
|
|||||||
export function setConfig(config: Partial<ServerConfig>): void {
|
export function setConfig(config: Partial<ServerConfig>): void {
|
||||||
serverConfig = { ...serverConfig, ...config };
|
serverConfig = { ...serverConfig, ...config };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 摘要配置 API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /config/summary - 获取摘要模型配置
|
||||||
|
*/
|
||||||
|
configRouter.get('/summary', (c) => {
|
||||||
|
const config = getSummaryConfig();
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
// Core 模块不可用
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
hasApiKey: false,
|
||||||
|
} as SummaryConfigInfo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: config,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /config/summary - 更新摘要模型配置
|
||||||
|
*/
|
||||||
|
configRouter.put('/summary', async (c) => {
|
||||||
|
try {
|
||||||
|
const body = await c.req.json();
|
||||||
|
|
||||||
|
const success = updateSummaryConfig({
|
||||||
|
provider: body.provider,
|
||||||
|
model: body.model,
|
||||||
|
apiKey: body.apiKey,
|
||||||
|
baseUrl: body.baseUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Core module not available',
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回更新后的配置(不含 API Key)
|
||||||
|
const config = getSummaryConfig();
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: config,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Invalid input',
|
||||||
|
},
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* Context API Routes
|
||||||
|
*
|
||||||
|
* 上下文压缩相关的 REST API
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
import { getSessionManager } from '../session/manager.js';
|
||||||
|
import {
|
||||||
|
getContextUsage,
|
||||||
|
compressContext,
|
||||||
|
type ContextUsageInfo,
|
||||||
|
type CompressionResult,
|
||||||
|
} from '../agent/adapter.js';
|
||||||
|
|
||||||
|
export const contextRouter = new Hono();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /sessions/:id/context - 获取会话上下文使用情况
|
||||||
|
*/
|
||||||
|
contextRouter.get('/sessions/:id/context', (c) => {
|
||||||
|
const sessionId = c.req.param('id');
|
||||||
|
const sessionManager = getSessionManager();
|
||||||
|
|
||||||
|
// 验证会话存在
|
||||||
|
if (!sessionManager.exists(sessionId)) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Session not found',
|
||||||
|
},
|
||||||
|
404
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const usage = getContextUsage(sessionId);
|
||||||
|
|
||||||
|
if (!usage) {
|
||||||
|
// Agent 未初始化,返回默认值
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
input: 0,
|
||||||
|
contextLimit: 200000,
|
||||||
|
available: 180000,
|
||||||
|
usagePercent: 0,
|
||||||
|
formatted: '0/180K (0%)',
|
||||||
|
shouldCompress: false,
|
||||||
|
} as ContextUsageInfo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: usage,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /sessions/:id/compress - 触发手动压缩
|
||||||
|
*/
|
||||||
|
contextRouter.post('/sessions/:id/compress', async (c) => {
|
||||||
|
const sessionId = c.req.param('id');
|
||||||
|
const sessionManager = getSessionManager();
|
||||||
|
|
||||||
|
// 验证会话存在
|
||||||
|
if (!sessionManager.exists(sessionId)) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Session not found',
|
||||||
|
},
|
||||||
|
404
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await c.req.json().catch(() => ({}));
|
||||||
|
const force = body.force === true;
|
||||||
|
|
||||||
|
const result = await compressContext(sessionId, force);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Agent not initialized for this session',
|
||||||
|
},
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Compression failed',
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -14,3 +14,4 @@ export { hooksRouter } from './hooks.js';
|
|||||||
export { agentsRouter } from './agents.js';
|
export { agentsRouter } from './agents.js';
|
||||||
export { checkpointsRouter } from './checkpoints.js';
|
export { checkpointsRouter } from './checkpoints.js';
|
||||||
export { providersRouter } from './providers.js';
|
export { providersRouter } from './providers.js';
|
||||||
|
export { contextRouter } from './context.js';
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ import type {
|
|||||||
CustomProviderDefinition,
|
CustomProviderDefinition,
|
||||||
ProviderConfig,
|
ProviderConfig,
|
||||||
ConnectionTestResult,
|
ConnectionTestResult,
|
||||||
|
// Context & Summary types
|
||||||
|
ContextUsageInfo,
|
||||||
|
CompressionResult,
|
||||||
|
SummaryConfigInfo,
|
||||||
|
SummaryConfigInput,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
// Re-export types
|
// Re-export types
|
||||||
@@ -113,6 +118,14 @@ export type {
|
|||||||
CustomProviderDefinition,
|
CustomProviderDefinition,
|
||||||
ProviderConfig,
|
ProviderConfig,
|
||||||
ConnectionTestResult,
|
ConnectionTestResult,
|
||||||
|
// Context compression types
|
||||||
|
TokenUsage,
|
||||||
|
ContextUsageInfo,
|
||||||
|
CompressionStatus,
|
||||||
|
CompressionType,
|
||||||
|
CompressionResult,
|
||||||
|
SummaryConfigInfo,
|
||||||
|
SummaryConfigInput,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
// API Configuration
|
// API Configuration
|
||||||
@@ -936,3 +949,54 @@ export async function deleteProviderModel(
|
|||||||
}> {
|
}> {
|
||||||
return request('DELETE', `/providers/${encodeURIComponent(providerId)}/models/${encodeURIComponent(modelId)}`);
|
return request('DELETE', `/providers/${encodeURIComponent(providerId)}/models/${encodeURIComponent(modelId)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ Context Compression API ============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话上下文使用情况
|
||||||
|
*/
|
||||||
|
export async function getContextUsage(sessionId: string): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
data?: ContextUsageInfo;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
return request('GET', `/sessions/${encodeURIComponent(sessionId)}/context`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发上下文压缩
|
||||||
|
*/
|
||||||
|
export async function compressContext(
|
||||||
|
sessionId: string,
|
||||||
|
options?: { force?: boolean }
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
data?: CompressionResult;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
return request('POST', `/sessions/${encodeURIComponent(sessionId)}/compress`, options || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取摘要模型配置
|
||||||
|
*/
|
||||||
|
export async function getSummaryConfig(): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
data?: SummaryConfigInfo;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
return request('GET', '/config/summary');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新摘要模型配置
|
||||||
|
*/
|
||||||
|
export async function updateSummaryConfig(
|
||||||
|
config: SummaryConfigInput
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
data?: SummaryConfigInfo;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
return request('PUT', '/config/summary', config);
|
||||||
|
}
|
||||||
|
|||||||
@@ -711,3 +711,76 @@ export interface ConnectionTestResult {
|
|||||||
/** 错误信息 */
|
/** 错误信息 */
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ 上下文压缩相关 ============
|
||||||
|
|
||||||
|
/** Token 使用情况 */
|
||||||
|
export interface TokenUsage {
|
||||||
|
/** 当前输入 tokens */
|
||||||
|
input: number;
|
||||||
|
/** 上下文限制 */
|
||||||
|
contextLimit: number;
|
||||||
|
/** 可用空间 */
|
||||||
|
available: number;
|
||||||
|
/** 使用百分比 (0-100) */
|
||||||
|
usagePercent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 上下文使用信息(包含格式化字符串) */
|
||||||
|
export interface ContextUsageInfo extends TokenUsage {
|
||||||
|
/** 格式化字符串,如 "12.5K/100K (12%)" */
|
||||||
|
formatted: string;
|
||||||
|
/** 是否建议压缩 */
|
||||||
|
shouldCompress: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 压缩状态 */
|
||||||
|
export type CompressionStatus =
|
||||||
|
| 'success'
|
||||||
|
| 'noop'
|
||||||
|
| 'failed_empty_summary'
|
||||||
|
| 'failed_token_inflated'
|
||||||
|
| 'failed_error';
|
||||||
|
|
||||||
|
/** 压缩类型 */
|
||||||
|
export type CompressionType = 'prune' | 'compaction' | 'both' | 'none';
|
||||||
|
|
||||||
|
/** 压缩结果 */
|
||||||
|
export interface CompressionResult {
|
||||||
|
/** 压缩类型 */
|
||||||
|
type: CompressionType;
|
||||||
|
/** 压缩状态 */
|
||||||
|
status: CompressionStatus;
|
||||||
|
/** 释放的 tokens */
|
||||||
|
freedTokens: number;
|
||||||
|
/** 错误信息 */
|
||||||
|
error?: string;
|
||||||
|
/** 原始 tokens(压缩前) */
|
||||||
|
originalTokens?: number;
|
||||||
|
/** 摘要 tokens(压缩后) */
|
||||||
|
summaryTokens?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 摘要模型配置信息(不含 API Key 明文) */
|
||||||
|
export interface SummaryConfigInfo {
|
||||||
|
/** 提供商类型 */
|
||||||
|
provider?: string;
|
||||||
|
/** 模型名称 */
|
||||||
|
model?: string;
|
||||||
|
/** 是否已配置 API Key */
|
||||||
|
hasApiKey: boolean;
|
||||||
|
/** 服务地址 */
|
||||||
|
baseUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 摘要模型配置输入 */
|
||||||
|
export interface SummaryConfigInput {
|
||||||
|
/** 提供商类型 */
|
||||||
|
provider?: string;
|
||||||
|
/** 模型名称 */
|
||||||
|
model?: string;
|
||||||
|
/** API Key */
|
||||||
|
apiKey?: string;
|
||||||
|
/** 服务地址 */
|
||||||
|
baseUrl?: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,14 @@ import { Input } from '../primitives/Input';
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../primitives/Select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../primitives/Select';
|
||||||
import { Slider } from '../primitives/Slider';
|
import { Slider } from '../primitives/Slider';
|
||||||
import { Skeleton } from './Skeleton';
|
import { Skeleton } from './Skeleton';
|
||||||
import { getConfig, updateConfig, type ServerConfig } from '../api/client.js';
|
import {
|
||||||
|
getConfig,
|
||||||
|
updateConfig,
|
||||||
|
getSummaryConfig,
|
||||||
|
updateSummaryConfig,
|
||||||
|
type ServerConfig,
|
||||||
|
type SummaryConfigInfo,
|
||||||
|
} from '../api/client.js';
|
||||||
|
|
||||||
interface ConfigPanelProps {
|
interface ConfigPanelProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -31,6 +38,14 @@ const AVAILABLE_MODELS = [
|
|||||||
{ id: 'claude-3-haiku-20240307', name: 'Claude 3 Haiku' },
|
{ id: 'claude-3-haiku-20240307', name: 'Claude 3 Haiku' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 摘要模型提供商列表
|
||||||
|
const SUMMARY_PROVIDERS = [
|
||||||
|
{ id: 'openai', name: 'OpenAI' },
|
||||||
|
{ id: 'deepseek', name: 'DeepSeek' },
|
||||||
|
{ id: 'anthropic', name: 'Anthropic' },
|
||||||
|
{ id: 'openai-compatible', name: 'OpenAI Compatible' },
|
||||||
|
];
|
||||||
|
|
||||||
// Temperature 语义标签
|
// Temperature 语义标签
|
||||||
function getTemperatureLabel(value: number): string {
|
function getTemperatureLabel(value: number): string {
|
||||||
if (value <= 0.3) return 'Precise';
|
if (value <= 0.3) return 'Precise';
|
||||||
@@ -40,8 +55,10 @@ function getTemperatureLabel(value: number): string {
|
|||||||
|
|
||||||
export function ConfigPanel({ onClose, responsive = false }: ConfigPanelProps) {
|
export function ConfigPanel({ onClose, responsive = false }: ConfigPanelProps) {
|
||||||
const [config, setConfig] = useState<ServerConfig | null>(null);
|
const [config, setConfig] = useState<ServerConfig | null>(null);
|
||||||
|
const [summaryConfig, setSummaryConfig] = useState<SummaryConfigInfo | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [savingSummary, setSavingSummary] = useState(false);
|
||||||
|
|
||||||
// 表单状态
|
// 表单状态
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
@@ -51,18 +68,41 @@ export function ConfigPanel({ onClose, responsive = false }: ConfigPanelProps) {
|
|||||||
workdir: '',
|
workdir: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 摘要模型表单状态
|
||||||
|
const [summaryFormData, setSummaryFormData] = useState({
|
||||||
|
provider: '',
|
||||||
|
model: '',
|
||||||
|
apiKey: '',
|
||||||
|
baseUrl: '',
|
||||||
|
});
|
||||||
|
|
||||||
// 加载配置
|
// 加载配置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
try {
|
try {
|
||||||
const response = await getConfig();
|
// 并行加载主配置和摘要配置
|
||||||
setConfig(response.data);
|
const [configResponse, summaryResponse] = await Promise.all([
|
||||||
|
getConfig(),
|
||||||
|
getSummaryConfig(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setConfig(configResponse.data);
|
||||||
setFormData({
|
setFormData({
|
||||||
model: response.data.model,
|
model: configResponse.data.model,
|
||||||
maxTokens: response.data.maxTokens,
|
maxTokens: configResponse.data.maxTokens,
|
||||||
temperature: response.data.temperature,
|
temperature: configResponse.data.temperature,
|
||||||
workdir: response.data.workdir,
|
workdir: configResponse.data.workdir,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (summaryResponse.success && summaryResponse.data) {
|
||||||
|
setSummaryConfig(summaryResponse.data);
|
||||||
|
setSummaryFormData({
|
||||||
|
provider: summaryResponse.data.provider || '',
|
||||||
|
model: summaryResponse.data.model || '',
|
||||||
|
apiKey: '', // 不显示已保存的 API Key
|
||||||
|
baseUrl: summaryResponse.data.baseUrl || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err instanceof Error ? err.message : 'Failed to load config');
|
toast.error(err instanceof Error ? err.message : 'Failed to load config');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -87,6 +127,33 @@ export function ConfigPanel({ onClose, responsive = false }: ConfigPanelProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 保存摘要配置
|
||||||
|
const handleSaveSummary = async () => {
|
||||||
|
setSavingSummary(true);
|
||||||
|
try {
|
||||||
|
// 只发送有变化的字段
|
||||||
|
const updateData: { provider?: string; model?: string; apiKey?: string; baseUrl?: string } = {};
|
||||||
|
if (summaryFormData.provider) updateData.provider = summaryFormData.provider;
|
||||||
|
if (summaryFormData.model) updateData.model = summaryFormData.model;
|
||||||
|
if (summaryFormData.apiKey) updateData.apiKey = summaryFormData.apiKey;
|
||||||
|
if (summaryFormData.baseUrl) updateData.baseUrl = summaryFormData.baseUrl;
|
||||||
|
|
||||||
|
const response = await updateSummaryConfig(updateData);
|
||||||
|
if (response.success && response.data) {
|
||||||
|
setSummaryConfig(response.data);
|
||||||
|
// 清空 API Key 输入框
|
||||||
|
setSummaryFormData((prev) => ({ ...prev, apiKey: '' }));
|
||||||
|
toast.success('Summary model config saved');
|
||||||
|
} else {
|
||||||
|
toast.error(response.error || 'Failed to save summary config');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err instanceof Error ? err.message : 'Failed to save summary config');
|
||||||
|
} finally {
|
||||||
|
setSavingSummary(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 重置为默认值
|
// 重置为默认值
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
if (config) {
|
if (config) {
|
||||||
@@ -262,6 +329,95 @@ export function ConfigPanel({ onClose, responsive = false }: ConfigPanelProps) {
|
|||||||
<p className="text-xs text-gray-500">Root directory for file operations</p>
|
<p className="text-xs text-gray-500">Root directory for file operations</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Summary Model Config */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.15 }}
|
||||||
|
className="pt-4 border-t border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-sm font-medium text-gray-400">Summary Model</h3>
|
||||||
|
{summaryConfig?.hasApiKey && (
|
||||||
|
<span className="text-xs text-green-400">✓ Configured</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mb-4">
|
||||||
|
Configure a separate model for context compression. Uses a smaller, faster model to summarize conversation history.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Provider */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-xs font-medium text-gray-400">Provider</label>
|
||||||
|
<Select
|
||||||
|
value={summaryFormData.provider}
|
||||||
|
onValueChange={(value) => setSummaryFormData({ ...summaryFormData, provider: value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9">
|
||||||
|
<SelectValue placeholder="Select provider" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{SUMMARY_PROVIDERS.map((provider) => (
|
||||||
|
<SelectItem key={provider.id} value={provider.id}>
|
||||||
|
{provider.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Model Name */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-xs font-medium text-gray-400">Model</label>
|
||||||
|
<Input
|
||||||
|
value={summaryFormData.model}
|
||||||
|
onChange={(e) => setSummaryFormData({ ...summaryFormData, model: e.target.value })}
|
||||||
|
className="h-9"
|
||||||
|
placeholder="e.g., deepseek-chat, gpt-4o-mini"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* API Key */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-xs font-medium text-gray-400">
|
||||||
|
API Key {summaryConfig?.hasApiKey && <span className="text-gray-500">(saved)</span>}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={summaryFormData.apiKey}
|
||||||
|
onChange={(e) => setSummaryFormData({ ...summaryFormData, apiKey: e.target.value })}
|
||||||
|
className="h-9 font-mono"
|
||||||
|
placeholder={summaryConfig?.hasApiKey ? '••••••••' : 'sk-...'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Base URL (optional) */}
|
||||||
|
{(summaryFormData.provider === 'openai-compatible' || summaryFormData.baseUrl) && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-xs font-medium text-gray-400">Base URL (optional)</label>
|
||||||
|
<Input
|
||||||
|
value={summaryFormData.baseUrl}
|
||||||
|
onChange={(e) => setSummaryFormData({ ...summaryFormData, baseUrl: e.target.value })}
|
||||||
|
className="h-9 font-mono"
|
||||||
|
placeholder="https://api.example.com/v1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSaveSummary}
|
||||||
|
disabled={savingSummary || (!summaryFormData.provider && !summaryFormData.model && !summaryFormData.apiKey)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{savingSummary ? 'Saving...' : 'Save Summary Config'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{/* Server Info */}
|
{/* Server Info */}
|
||||||
{config && (
|
{config && (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
/**
|
||||||
|
* ContextUsage Component
|
||||||
|
*
|
||||||
|
* 显示会话上下文使用情况和压缩控制
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { Zap, AlertTriangle, RefreshCw } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { cn } from '../utils/cn';
|
||||||
|
import { Button } from '../primitives/Button';
|
||||||
|
import {
|
||||||
|
getContextUsage,
|
||||||
|
compressContext,
|
||||||
|
type ContextUsageInfo,
|
||||||
|
} from '../api/client.js';
|
||||||
|
|
||||||
|
interface ContextUsageProps {
|
||||||
|
/** 会话 ID */
|
||||||
|
sessionId: string;
|
||||||
|
/** 是否显示压缩按钮 */
|
||||||
|
showCompressButton?: boolean;
|
||||||
|
/** 刷新间隔(毫秒),0 表示不自动刷新 */
|
||||||
|
refreshInterval?: number;
|
||||||
|
/** 紧凑模式(只显示进度条和数值) */
|
||||||
|
compact?: boolean;
|
||||||
|
/** 自定义类名 */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取使用百分比对应的颜色
|
||||||
|
*/
|
||||||
|
function getUsageColor(percent: number): string {
|
||||||
|
if (percent >= 90) return 'bg-red-500';
|
||||||
|
if (percent >= 80) return 'bg-amber-500';
|
||||||
|
if (percent >= 60) return 'bg-yellow-500';
|
||||||
|
return 'bg-primary-500';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取使用百分比对应的文本颜色
|
||||||
|
*/
|
||||||
|
function getTextColor(percent: number): string {
|
||||||
|
if (percent >= 90) return 'text-red-400';
|
||||||
|
if (percent >= 80) return 'text-amber-400';
|
||||||
|
return 'text-gray-400';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContextUsage({
|
||||||
|
sessionId,
|
||||||
|
showCompressButton = true,
|
||||||
|
refreshInterval = 0,
|
||||||
|
compact = false,
|
||||||
|
className,
|
||||||
|
}: ContextUsageProps) {
|
||||||
|
const [usage, setUsage] = useState<ContextUsageInfo | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [compressing, setCompressing] = useState(false);
|
||||||
|
|
||||||
|
// 获取上下文使用情况
|
||||||
|
const fetchUsage = useCallback(async () => {
|
||||||
|
if (!sessionId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await getContextUsage(sessionId);
|
||||||
|
if (response.success && response.data) {
|
||||||
|
setUsage(response.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 静默失败,不影响用户体验
|
||||||
|
console.error('Failed to fetch context usage:', error);
|
||||||
|
}
|
||||||
|
}, [sessionId]);
|
||||||
|
|
||||||
|
// 初始加载和定时刷新
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUsage();
|
||||||
|
|
||||||
|
if (refreshInterval > 0) {
|
||||||
|
const interval = setInterval(fetchUsage, refreshInterval);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, [fetchUsage, refreshInterval]);
|
||||||
|
|
||||||
|
// 手动刷新
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
await fetchUsage();
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 触发压缩
|
||||||
|
const handleCompress = async () => {
|
||||||
|
if (!sessionId) return;
|
||||||
|
|
||||||
|
setCompressing(true);
|
||||||
|
try {
|
||||||
|
const response = await compressContext(sessionId, { force: true });
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
const { status, freedTokens, type } = response.data;
|
||||||
|
|
||||||
|
if (status === 'success') {
|
||||||
|
const freedK = (freedTokens / 1000).toFixed(1);
|
||||||
|
toast.success(`已释放 ${freedK}K tokens (${type})`);
|
||||||
|
// 刷新使用情况
|
||||||
|
await fetchUsage();
|
||||||
|
} else if (status === 'noop') {
|
||||||
|
toast.info('当前无需压缩');
|
||||||
|
} else if (status === 'failed_empty_summary') {
|
||||||
|
toast.error('压缩失败:生成的摘要为空');
|
||||||
|
} else if (status === 'failed_token_inflated') {
|
||||||
|
toast.error('压缩失败:摘要比原文更长');
|
||||||
|
} else {
|
||||||
|
toast.error(`压缩失败:${response.data.error || status}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error(response.error || '压缩失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : '压缩失败');
|
||||||
|
} finally {
|
||||||
|
setCompressing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 无数据时显示占位
|
||||||
|
if (!usage) {
|
||||||
|
return (
|
||||||
|
<div className={cn('flex items-center gap-2 text-sm text-gray-500', className)}>
|
||||||
|
<div className="h-2 w-24 bg-gray-700 rounded-full animate-pulse" />
|
||||||
|
<span>--</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { usagePercent, formatted, shouldCompress } = usage;
|
||||||
|
const barColor = getUsageColor(usagePercent);
|
||||||
|
const textColor = getTextColor(usagePercent);
|
||||||
|
|
||||||
|
// 紧凑模式
|
||||||
|
if (compact) {
|
||||||
|
return (
|
||||||
|
<div className={cn('flex items-center gap-2', className)}>
|
||||||
|
{/* 进度条 */}
|
||||||
|
<div className="relative h-1.5 w-16 bg-gray-700 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={cn('absolute h-full rounded-full transition-all', barColor)}
|
||||||
|
style={{ width: `${Math.min(usagePercent, 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* 数值 */}
|
||||||
|
<span className={cn('text-xs', textColor)}>{formatted}</span>
|
||||||
|
{/* 警告图标 */}
|
||||||
|
{shouldCompress && (
|
||||||
|
<AlertTriangle size={12} className="text-amber-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完整模式
|
||||||
|
return (
|
||||||
|
<div className={cn('space-y-2', className)}>
|
||||||
|
{/* 标题行 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Zap size={14} className="text-gray-400" />
|
||||||
|
<span className="text-xs font-medium text-gray-300">Context Usage</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{/* 刷新按钮 */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<RefreshCw size={12} className={cn(loading && 'animate-spin')} />
|
||||||
|
</Button>
|
||||||
|
{/* 数值 */}
|
||||||
|
<span className={cn('text-xs font-mono', textColor)}>{formatted}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 进度条 */}
|
||||||
|
<div className="relative h-2 w-full bg-gray-700 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={cn('absolute h-full rounded-full transition-all duration-300', barColor)}
|
||||||
|
style={{ width: `${Math.min(usagePercent, 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 压缩按钮 */}
|
||||||
|
{showCompressButton && shouldCompress && (
|
||||||
|
<div className="flex items-center justify-between pt-1">
|
||||||
|
<div className="flex items-center gap-1.5 text-amber-500">
|
||||||
|
<AlertTriangle size={12} />
|
||||||
|
<span className="text-xs">建议压缩上下文</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 text-xs"
|
||||||
|
onClick={handleCompress}
|
||||||
|
disabled={compressing}
|
||||||
|
>
|
||||||
|
{compressing ? '压缩中...' : '压缩'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -90,6 +90,11 @@ export {
|
|||||||
deleteProvider,
|
deleteProvider,
|
||||||
addProviderModel,
|
addProviderModel,
|
||||||
deleteProviderModel,
|
deleteProviderModel,
|
||||||
|
// Context & Summary API
|
||||||
|
getContextUsage,
|
||||||
|
compressContext,
|
||||||
|
getSummaryConfig,
|
||||||
|
updateSummaryConfig,
|
||||||
} from './api/client.js';
|
} from './api/client.js';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
@@ -161,6 +166,13 @@ export type {
|
|||||||
CustomProviderDefinition,
|
CustomProviderDefinition,
|
||||||
ProviderConfig,
|
ProviderConfig,
|
||||||
ConnectionTestResult,
|
ConnectionTestResult,
|
||||||
|
// Context & Summary types
|
||||||
|
TokenUsage,
|
||||||
|
ContextUsageInfo,
|
||||||
|
CompressionStatus,
|
||||||
|
CompressionResult,
|
||||||
|
SummaryConfigInfo,
|
||||||
|
SummaryConfigInput,
|
||||||
} from './api/client.js';
|
} from './api/client.js';
|
||||||
|
|
||||||
// Primitives (shadcn/ui style)
|
// Primitives (shadcn/ui style)
|
||||||
@@ -192,6 +204,7 @@ export type { PermissionRequest, PermissionType, PermissionRequestContext, DiffI
|
|||||||
export { Sidebar } from './components/Sidebar.js';
|
export { Sidebar } from './components/Sidebar.js';
|
||||||
export { FileBrowser } from './components/FileBrowser.js';
|
export { FileBrowser } from './components/FileBrowser.js';
|
||||||
export { ConfigPanel } from './components/ConfigPanel.js';
|
export { ConfigPanel } from './components/ConfigPanel.js';
|
||||||
|
export { ContextUsage } from './components/ContextUsage.js';
|
||||||
export { Toaster } from './components/Toaster.js';
|
export { Toaster } from './components/Toaster.js';
|
||||||
export { Skeleton, MessageSkeleton, SessionSkeleton, FileSkeleton } from './components/Skeleton.js';
|
export { Skeleton, MessageSkeleton, SessionSkeleton, FileSkeleton } from './components/Skeleton.js';
|
||||||
export { Markdown } from './components/Markdown.js';
|
export { Markdown } from './components/Markdown.js';
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
TypingIndicator,
|
TypingIndicator,
|
||||||
ChatInput,
|
ChatInput,
|
||||||
PermissionDialog,
|
PermissionDialog,
|
||||||
|
ContextUsage,
|
||||||
} from '@ai-assistant/ui';
|
} from '@ai-assistant/ui';
|
||||||
|
|
||||||
interface ChatPageProps {
|
interface ChatPageProps {
|
||||||
@@ -138,6 +139,16 @@ export function ChatPage({
|
|||||||
<div className="flex items-center justify-between px-4 md:px-6 py-3 border-b border-gray-700 bg-gray-800">
|
<div className="flex items-center justify-between px-4 md:px-6 py-3 border-b border-gray-700 bg-gray-800">
|
||||||
<h1 className="text-lg font-medium">Chat</h1>
|
<h1 className="text-lg font-medium">Chat</h1>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
{/* 上下文使用情况 - 紧凑模式 */}
|
||||||
|
{sessionId && (
|
||||||
|
<ContextUsage
|
||||||
|
sessionId={sessionId}
|
||||||
|
compact
|
||||||
|
showCompressButton={false}
|
||||||
|
refreshInterval={30000}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 连接状态 */}
|
{/* 连接状态 */}
|
||||||
<ConnectionStatus />
|
<ConnectionStatus />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user