refactor(agent): 将 Summary Model 改造为内置 Sub Agent
- 扩展 AgentMode 类型添加 'internal' 模式 - 新增 summary agent preset (claude-3-5-haiku) - AgentRegistry 添加 getInternal/listInternalAgents 方法 - CompressionManager 添加 setSummaryModelFromAgentConfig - Agent 构造函数改用 Registry 配置初始化 Summary 模型 - 清理旧的 SummaryConfig 配置系统 - UI AgentsPanel 分离显示 System/Preset/Custom agents - UI AgentEditor 为 internal agent 显示简化编辑界面
This commit is contained in:
@@ -5,6 +5,7 @@ import { codeReviewerAgent } from './code-reviewer.js';
|
|||||||
import { buildAgent } from './build.js';
|
import { buildAgent } from './build.js';
|
||||||
import { planAgent } from './plan.js';
|
import { planAgent } from './plan.js';
|
||||||
import { visionAgent } from './vision.js';
|
import { visionAgent } from './vision.js';
|
||||||
|
import { summaryAgent } from './summary.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 预设 Agent 集合
|
* 预设 Agent 集合
|
||||||
@@ -16,6 +17,7 @@ export const presetAgents: Record<string, Omit<AgentInfo, 'name'>> = {
|
|||||||
build: buildAgent,
|
build: buildAgent,
|
||||||
plan: planAgent,
|
plan: planAgent,
|
||||||
vision: visionAgent,
|
vision: visionAgent,
|
||||||
|
summary: summaryAgent,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,4 +34,4 @@ export function isPresetAgent(name: string): boolean {
|
|||||||
return name in presetAgents;
|
return name in presetAgents;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { generalAgent, exploreAgent, codeReviewerAgent, buildAgent, planAgent, visionAgent };
|
export { generalAgent, exploreAgent, codeReviewerAgent, buildAgent, planAgent, visionAgent, summaryAgent };
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { AgentInfo } from '../types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Summary Agent
|
||||||
|
* 内部 Agent,用于对话压缩时生成摘要
|
||||||
|
* 推荐使用成本较低的模型
|
||||||
|
*/
|
||||||
|
export const summaryAgent: Omit<AgentInfo, 'name'> = {
|
||||||
|
description: '对话压缩摘要生成(内部使用)',
|
||||||
|
mode: 'internal',
|
||||||
|
model: {
|
||||||
|
provider: 'anthropic',
|
||||||
|
model: 'claude-3-5-haiku-20241022',
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
enabled: [], // 无工具,纯文本生成
|
||||||
|
noTask: true,
|
||||||
|
},
|
||||||
|
maxSteps: 1,
|
||||||
|
};
|
||||||
@@ -61,16 +61,36 @@ export class AgentRegistry {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 列出可作为子 Agent 的 Agent(供 Task 工具使用)
|
* 列出可作为子 Agent 的 Agent(供 Task 工具使用)
|
||||||
|
* 排除 primary 和 internal 模式
|
||||||
*/
|
*/
|
||||||
listSubagents(): AgentInfo[] {
|
listSubagents(): AgentInfo[] {
|
||||||
return this.list().filter((a) => a.mode !== 'primary');
|
return this.list().filter((a) => a.mode !== 'primary' && a.mode !== 'internal');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 列出可作为主交互 Agent 的 Agent(供 /agent 命令使用)
|
* 列出可作为主交互 Agent 的 Agent(供 /agent 命令使用)
|
||||||
|
* 排除 internal 模式
|
||||||
*/
|
*/
|
||||||
listPrimaryAgents(): AgentInfo[] {
|
listPrimaryAgents(): AgentInfo[] {
|
||||||
return this.list().filter((a) => a.mode !== 'subagent');
|
return this.list().filter((a) => a.mode !== 'subagent' && a.mode !== 'internal');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取内部 Agent(internal 模式)
|
||||||
|
*/
|
||||||
|
getInternal(name: string): AgentInfo | undefined {
|
||||||
|
const agent = this.get(name);
|
||||||
|
if (agent?.mode === 'internal') {
|
||||||
|
return agent;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出所有内部 Agent
|
||||||
|
*/
|
||||||
|
listInternalAgents(): AgentInfo[] {
|
||||||
|
return this.list().filter((a) => a.mode === 'internal');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ export type { PermissionAction, PermissionRule };
|
|||||||
* - primary: 主 Agent,用户直接使用
|
* - primary: 主 Agent,用户直接使用
|
||||||
* - subagent: 子 Agent,由 Task 工具调用
|
* - subagent: 子 Agent,由 Task 工具调用
|
||||||
* - all: 两种方式都可以
|
* - all: 两种方式都可以
|
||||||
|
* - internal: 内部使用,不对外暴露(如 Summary Agent)
|
||||||
*/
|
*/
|
||||||
export type AgentMode = 'primary' | 'subagent' | 'all';
|
export type AgentMode = 'primary' | 'subagent' | 'all' | 'internal';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Agent Bash 权限配置
|
* Agent Bash 权限配置
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
CompressionStatus,
|
CompressionStatus,
|
||||||
DEFAULT_COMPRESSION_CONFIG,
|
DEFAULT_COMPRESSION_CONFIG,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
import type { AgentModelConfig } from '../agent/types.js';
|
||||||
|
import { getProviderRegistry } from '../provider/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 压缩管理器
|
* 压缩管理器
|
||||||
@@ -41,6 +43,32 @@ export class CompressionManager {
|
|||||||
this.summaryModel = model;
|
this.summaryModel = model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Agent 配置设置摘要模型
|
||||||
|
* @param config Agent 的模型配置
|
||||||
|
* @param apiKey API Key
|
||||||
|
* @param baseUrl 可选的 Base URL
|
||||||
|
*/
|
||||||
|
setSummaryModelFromAgentConfig(
|
||||||
|
config: AgentModelConfig,
|
||||||
|
apiKey: string,
|
||||||
|
baseUrl?: string
|
||||||
|
): void {
|
||||||
|
const registry = getProviderRegistry();
|
||||||
|
const provider = config.provider || 'anthropic';
|
||||||
|
const modelName = config.model || 'claude-3-5-haiku-20241022';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const getModel = registry.getModelFactory(provider, {
|
||||||
|
apiKey,
|
||||||
|
baseUrl,
|
||||||
|
});
|
||||||
|
this.summaryModel = getModel(modelName);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[CompressionManager] Failed to create summary model:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用于摘要生成的模型
|
* 获取用于摘要生成的模型
|
||||||
* 优先使用专用摘要模型,无则使用主模型
|
* 优先使用专用摘要模型,无则使用主模型
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import {
|
|||||||
} from '../context/index.js';
|
} from '../context/index.js';
|
||||||
import type { AgentInfo, ImageData } from '../agent/types.js';
|
import type { AgentInfo, ImageData } from '../agent/types.js';
|
||||||
import { agentRegistry, AgentExecutor } from '../agent/index.js';
|
import { agentRegistry, AgentExecutor } from '../agent/index.js';
|
||||||
import { loadVisionConfig, loadSummaryConfig } from '../utils/config.js';
|
import { loadVisionConfig } from '../utils/config.js';
|
||||||
import { getProviderRegistry } from '../provider/index.js';
|
import { getProviderRegistry, resolveApiKey } from '../provider/index.js';
|
||||||
import { getHookManager } from '../hooks/index.js';
|
import { getHookManager } from '../hooks/index.js';
|
||||||
import { getGitManager } from '../git/index.js';
|
import { getGitManager } from '../git/index.js';
|
||||||
|
|
||||||
@@ -62,15 +62,38 @@ export class Agent {
|
|||||||
// 设置主模型(作为摘要模型的后备)
|
// 设置主模型(作为摘要模型的后备)
|
||||||
this.compressionManager.setModel(this.getModel(config.model));
|
this.compressionManager.setModel(this.getModel(config.model));
|
||||||
|
|
||||||
// 加载摘要模型配置(可选,用于降低压缩成本)
|
// 从 Agent Registry 加载 Summary Agent 配置
|
||||||
const summaryConfig = loadSummaryConfig();
|
this.initSummaryModel(config, providerRegistry);
|
||||||
if (summaryConfig) {
|
}
|
||||||
const summaryModelFactory = providerRegistry.getModelFactory(summaryConfig.provider, {
|
|
||||||
apiKey: summaryConfig.apiKey,
|
/**
|
||||||
baseUrl: summaryConfig.baseUrl,
|
* 从 Agent Registry 初始化 Summary 模型
|
||||||
});
|
*/
|
||||||
this.compressionManager.setSummaryModel(summaryModelFactory(summaryConfig.model));
|
private initSummaryModel(
|
||||||
|
config: AgentConfig,
|
||||||
|
providerRegistry: ReturnType<typeof getProviderRegistry>
|
||||||
|
): void {
|
||||||
|
// 获取 Summary Agent(internal 模式)
|
||||||
|
const summaryAgentInfo = agentRegistry.getInternal('summary');
|
||||||
|
if (!summaryAgentInfo?.model) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const modelConfig = summaryAgentInfo.model;
|
||||||
|
// 确定 provider(默认使用主配置的 provider)
|
||||||
|
const provider = modelConfig.provider || config.provider;
|
||||||
|
|
||||||
|
// 从 ProviderRegistry 获取 API Key
|
||||||
|
const providerConfig = providerRegistry.getConfig(provider);
|
||||||
|
const apiKey = resolveApiKey(providerConfig) || config.apiKey;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置 Summary 模型
|
||||||
|
const baseUrl = providerConfig?.baseUrl;
|
||||||
|
this.compressionManager.setSummaryModelFromAgentConfig(modelConfig, apiKey, baseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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, loadSummaryConfig } from './utils/config.js';
|
export { loadConfig, saveConfig, getConfig, loadVisionConfig } from './utils/config.js';
|
||||||
export type { VisionConfig, SummaryConfig } from './utils/config.js';
|
export type { VisionConfig } from './utils/config.js';
|
||||||
|
|
||||||
// Context compression
|
// Context compression
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -19,11 +19,6 @@ interface StoredConfig {
|
|||||||
visionModel?: string;
|
visionModel?: string;
|
||||||
/** Vision 专用的 Base URL(用于 OpenAI 兼容的 Vision 服务) */
|
/** Vision 专用的 Base URL(用于 OpenAI 兼容的 Vision 服务) */
|
||||||
visionBaseUrl?: string;
|
visionBaseUrl?: string;
|
||||||
// Summary 配置(用于对话压缩摘要生成)
|
|
||||||
summaryProvider?: ProviderType;
|
|
||||||
summaryModel?: string;
|
|
||||||
/** Summary 专用的 Base URL(用于 OpenAI 兼容的 Summary 服务) */
|
|
||||||
summaryBaseUrl?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vision 配置接口
|
// Vision 配置接口
|
||||||
@@ -35,15 +30,6 @@ export interface VisionConfig {
|
|||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summary 配置接口(用于对话压缩摘要生成)
|
|
||||||
export interface SummaryConfig {
|
|
||||||
provider: ProviderType;
|
|
||||||
apiKey: string;
|
|
||||||
model: string;
|
|
||||||
/** 自定义 Base URL(用于 OpenAI 兼容的 Summary 服务) */
|
|
||||||
baseUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认模型配置
|
// 默认模型配置
|
||||||
const DEFAULT_MODELS: Record<ProviderType, string> = {
|
const DEFAULT_MODELS: Record<ProviderType, string> = {
|
||||||
anthropic: 'claude-sonnet-4-20250514',
|
anthropic: 'claude-sonnet-4-20250514',
|
||||||
@@ -58,13 +44,6 @@ const DEFAULT_VISION_MODELS: Record<ProviderType, string> = {
|
|||||||
openai: 'gpt-4o',
|
openai: 'gpt-4o',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 默认 Summary 模型(推荐使用成本较低的模型)
|
|
||||||
const DEFAULT_SUMMARY_MODELS: Record<ProviderType, string> = {
|
|
||||||
anthropic: 'claude-3-5-haiku-20241022',
|
|
||||||
deepseek: 'deepseek-chat',
|
|
||||||
openai: 'gpt-4o-mini',
|
|
||||||
};
|
|
||||||
|
|
||||||
// 默认系统提示词
|
// 默认系统提示词
|
||||||
const DEFAULT_SYSTEM_PROMPT = `你是一个运行在终端中的 AI 编程助手。你可以帮助用户:
|
const DEFAULT_SYSTEM_PROMPT = `你是一个运行在终端中的 AI 编程助手。你可以帮助用户:
|
||||||
- 读取和写入文件
|
- 读取和写入文件
|
||||||
@@ -167,49 +146,6 @@ export function loadVisionConfig(): VisionConfig | null {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载 Summary 配置
|
|
||||||
* Summary 用于对话压缩时生成摘要,推荐使用成本较低的小模型
|
|
||||||
* 通过 ProviderRegistry 获取 API Key
|
|
||||||
*/
|
|
||||||
export function loadSummaryConfig(): SummaryConfig | null {
|
|
||||||
// 从配置文件读取
|
|
||||||
const storedConfig = getConfig();
|
|
||||||
|
|
||||||
// 如果没有任何 summary 相关配置,返回 null(使用主模型)
|
|
||||||
const hasSummaryConfig = storedConfig.summaryProvider || storedConfig.summaryModel;
|
|
||||||
|
|
||||||
if (!hasSummaryConfig) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确定 summary provider(默认使用主配置的 provider)
|
|
||||||
const mainProvider = storedConfig.provider || 'anthropic';
|
|
||||||
const finalProvider = storedConfig.summaryProvider || mainProvider;
|
|
||||||
|
|
||||||
// 通过 ProviderRegistry 获取 API Key
|
|
||||||
const providerConfig = providerRegistry.getConfig(finalProvider);
|
|
||||||
const finalApiKey = resolveApiKey(providerConfig);
|
|
||||||
|
|
||||||
// 如果没有 API Key,返回 null
|
|
||||||
if (!finalApiKey) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确定模型
|
|
||||||
const finalModel = storedConfig.summaryModel || DEFAULT_SUMMARY_MODELS[finalProvider];
|
|
||||||
|
|
||||||
// 确定 baseUrl
|
|
||||||
const finalBaseUrl = storedConfig.summaryBaseUrl || providerConfig?.baseUrl;
|
|
||||||
|
|
||||||
return {
|
|
||||||
provider: finalProvider,
|
|
||||||
apiKey: finalApiKey,
|
|
||||||
model: finalModel,
|
|
||||||
baseUrl: finalBaseUrl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存配置
|
// 保存配置
|
||||||
export function saveConfig(config: Partial<StoredConfig>): void {
|
export function saveConfig(config: Partial<StoredConfig>): void {
|
||||||
// 确保目录存在
|
// 确保目录存在
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
describe('Agent Presets - 预设 Agent', () => {
|
describe('Agent Presets - 预设 Agent', () => {
|
||||||
describe('presetAgents 集合', () => {
|
describe('presetAgents 集合', () => {
|
||||||
it('包含所有预设 Agent', () => {
|
it('包含所有预设 Agent', () => {
|
||||||
expect(Object.keys(presetAgents)).toHaveLength(6);
|
expect(Object.keys(presetAgents)).toHaveLength(7); // includes summary agent
|
||||||
});
|
});
|
||||||
|
|
||||||
it('包含 general Agent', () => {
|
it('包含 general Agent', () => {
|
||||||
@@ -67,7 +67,7 @@ describe('Agent Presets - 预设 Agent', () => {
|
|||||||
|
|
||||||
it('返回正确数量', () => {
|
it('返回正确数量', () => {
|
||||||
const names = getPresetAgentNames();
|
const names = getPresetAgentNames();
|
||||||
expect(names).toHaveLength(6);
|
expect(names).toHaveLength(7); // general, explore, code-review, frontend, backend, vision, summary
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -192,9 +192,9 @@ describe('Agent Presets - 预设 Agent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('mode 值是 subagent 或 primary', () => {
|
it('mode 值是有效的 AgentMode', () => {
|
||||||
Object.entries(presetAgents).forEach(([name, agent]) => {
|
Object.entries(presetAgents).forEach(([name, agent]) => {
|
||||||
expect(['subagent', 'primary'], `${name} mode 无效`).toContain(agent.mode);
|
expect(['subagent', 'primary', 'all', 'internal'], `${name} mode 无效`).toContain(agent.mode);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ vi.mock('ai', () => ({
|
|||||||
vi.mock('../../../src/agent/index.js', () => ({
|
vi.mock('../../../src/agent/index.js', () => ({
|
||||||
agentRegistry: {
|
agentRegistry: {
|
||||||
get: vi.fn(),
|
get: vi.fn(),
|
||||||
|
getInternal: vi.fn(() => null), // Returns null by default - no summary agent configured
|
||||||
},
|
},
|
||||||
AgentExecutor: vi.fn().mockImplementation(() => ({
|
AgentExecutor: vi.fn().mockImplementation(() => ({
|
||||||
execute: vi.fn().mockResolvedValue({
|
execute: vi.fn().mockResolvedValue({
|
||||||
|
|||||||
@@ -76,16 +76,6 @@ 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 模块接口
|
||||||
*/
|
*/
|
||||||
@@ -94,7 +84,6 @@ interface CoreModule {
|
|||||||
toolRegistry: ToolRegistry;
|
toolRegistry: ToolRegistry;
|
||||||
loadConfig: () => unknown;
|
loadConfig: () => unknown;
|
||||||
saveConfig: (config: Record<string, unknown>) => void;
|
saveConfig: (config: Record<string, unknown>) => void;
|
||||||
loadSummaryConfig: () => SummaryConfig | null;
|
|
||||||
getPermissionManager: (projectRoot?: string) => PermissionManager;
|
getPermissionManager: (projectRoot?: string) => PermissionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,71 +432,3 @@ export async function compressContext(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 摘要配置 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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,12 +15,8 @@ export {
|
|||||||
// 上下文压缩相关
|
// 上下文压缩相关
|
||||||
getContextUsage,
|
getContextUsage,
|
||||||
compressContext,
|
compressContext,
|
||||||
getSummaryConfig,
|
|
||||||
updateSummaryConfig,
|
|
||||||
// 类型导出
|
// 类型导出
|
||||||
type TokenUsage,
|
type TokenUsage,
|
||||||
type CompressionResult,
|
type CompressionResult,
|
||||||
type ContextUsageInfo,
|
type ContextUsageInfo,
|
||||||
type SummaryConfigInfo,
|
|
||||||
type SummaryConfig,
|
|
||||||
} from './adapter.js';
|
} from './adapter.js';
|
||||||
|
|||||||
@@ -258,12 +258,9 @@ export {
|
|||||||
cancelProcessing,
|
cancelProcessing,
|
||||||
getContextUsage,
|
getContextUsage,
|
||||||
compressContext,
|
compressContext,
|
||||||
getSummaryConfig,
|
|
||||||
updateSummaryConfig,
|
|
||||||
type TokenUsage,
|
type TokenUsage,
|
||||||
type CompressionResult,
|
type CompressionResult,
|
||||||
type ContextUsageInfo,
|
type ContextUsageInfo,
|
||||||
type SummaryConfigInfo,
|
|
||||||
} from './agent/index.js';
|
} from './agent/index.js';
|
||||||
export {
|
export {
|
||||||
initAuth,
|
initAuth,
|
||||||
|
|||||||
@@ -5,11 +5,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
@@ -102,70 +97,3 @@ 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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -45,11 +45,9 @@ import type {
|
|||||||
CustomProviderDefinition,
|
CustomProviderDefinition,
|
||||||
ProviderConfig,
|
ProviderConfig,
|
||||||
ConnectionTestResult,
|
ConnectionTestResult,
|
||||||
// Context & Summary types
|
// Context types
|
||||||
ContextUsageInfo,
|
ContextUsageInfo,
|
||||||
CompressionResult,
|
CompressionResult,
|
||||||
SummaryConfigInfo,
|
|
||||||
SummaryConfigInput,
|
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
// Re-export types
|
// Re-export types
|
||||||
@@ -124,8 +122,6 @@ export type {
|
|||||||
CompressionStatus,
|
CompressionStatus,
|
||||||
CompressionType,
|
CompressionType,
|
||||||
CompressionResult,
|
CompressionResult,
|
||||||
SummaryConfigInfo,
|
|
||||||
SummaryConfigInput,
|
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
// API Configuration
|
// API Configuration
|
||||||
@@ -976,27 +972,3 @@ export async function compressContext(
|
|||||||
}> {
|
}> {
|
||||||
return request('POST', `/sessions/${encodeURIComponent(sessionId)}/compress`, options || {});
|
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);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ export interface HookTestResult {
|
|||||||
// ============ Agent 相关 ============
|
// ============ Agent 相关 ============
|
||||||
|
|
||||||
/** Agent 运行模式 */
|
/** Agent 运行模式 */
|
||||||
export type AgentMode = 'primary' | 'subagent' | 'all';
|
export type AgentMode = 'primary' | 'subagent' | 'all' | 'internal';
|
||||||
|
|
||||||
/** Agent 模型配置 */
|
/** Agent 模型配置 */
|
||||||
export interface AgentModelConfig {
|
export interface AgentModelConfig {
|
||||||
@@ -756,26 +756,3 @@ export interface CompressionResult {
|
|||||||
summaryTokens?: number;
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
|
Lock,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -93,6 +94,9 @@ export function AgentEditor({
|
|||||||
const [name, setName] = useState(defaultName);
|
const [name, setName] = useState(defaultName);
|
||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
const [mode, setMode] = useState<AgentMode>('primary');
|
const [mode, setMode] = useState<AgentMode>('primary');
|
||||||
|
|
||||||
|
// 是否为内部 Agent(Internal Agent 只能编辑模型配置)
|
||||||
|
const isInternalAgent = mode === 'internal';
|
||||||
const [prompt, setPrompt] = useState('');
|
const [prompt, setPrompt] = useState('');
|
||||||
const [maxSteps, setMaxSteps] = useState<number | undefined>(undefined);
|
const [maxSteps, setMaxSteps] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
@@ -298,12 +302,26 @@ export function AgentEditor({
|
|||||||
)}
|
)}
|
||||||
<div className={cn(responsive && 'mt-2 md:mt-0')}>
|
<div className={cn(responsive && 'mt-2 md:mt-0')}>
|
||||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||||
<Bot size={20} className="text-primary-400" />
|
{isInternalAgent ? (
|
||||||
|
<Lock size={20} className="text-slate-400" />
|
||||||
|
) : (
|
||||||
|
<Bot size={20} className="text-primary-400" />
|
||||||
|
)}
|
||||||
{isNewAgent ? 'Create Agent' : `Edit: ${agentName}`}
|
{isNewAgent ? 'Create Agent' : `Edit: ${agentName}`}
|
||||||
|
{isInternalAgent && (
|
||||||
|
<span className="text-xs px-2 py-0.5 bg-slate-500/20 text-slate-400 rounded">
|
||||||
|
System
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</h2>
|
</h2>
|
||||||
{copyFrom && (
|
{copyFrom && (
|
||||||
<p className="text-xs text-gray-500">Copying from: {copyFrom}</p>
|
<p className="text-xs text-gray-500">Copying from: {copyFrom}</p>
|
||||||
)}
|
)}
|
||||||
|
{isInternalAgent && (
|
||||||
|
<p className="text-xs text-slate-400 mt-1">
|
||||||
|
System agents can only modify model configuration
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -354,12 +372,12 @@ export function AgentEditor({
|
|||||||
type="text"
|
type="text"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
disabled={!isNewAgent}
|
disabled={!isNewAgent || isInternalAgent}
|
||||||
placeholder="my-agent"
|
placeholder="my-agent"
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm',
|
'w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm',
|
||||||
'focus:outline-none focus:border-primary-500',
|
'focus:outline-none focus:border-primary-500',
|
||||||
!isNewAgent && 'opacity-50 cursor-not-allowed'
|
(!isNewAgent || isInternalAgent) && 'opacity-50 cursor-not-allowed'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{!isNewAgent && (
|
{!isNewAgent && (
|
||||||
@@ -374,59 +392,81 @@ export function AgentEditor({
|
|||||||
type="text"
|
type="text"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
disabled={isInternalAgent}
|
||||||
placeholder="A helpful coding assistant"
|
placeholder="A helpful coding assistant"
|
||||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500"
|
className={cn(
|
||||||
|
'w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500',
|
||||||
|
isInternalAgent && 'opacity-50 cursor-not-allowed'
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mode */}
|
{/* Mode - 不为内部 Agent 显示 */}
|
||||||
<div>
|
{!isInternalAgent && (
|
||||||
<label className="block text-xs text-gray-400 mb-1">Mode *</label>
|
<div>
|
||||||
<div className="flex gap-2">
|
<label className="block text-xs text-gray-400 mb-1">Mode *</label>
|
||||||
{(['primary', 'subagent', 'all'] as const).map((m) => (
|
<div className="flex gap-2">
|
||||||
<button
|
{(['primary', 'subagent', 'all'] as const).map((m) => (
|
||||||
key={m}
|
<button
|
||||||
type="button"
|
key={m}
|
||||||
onClick={() => setMode(m)}
|
type="button"
|
||||||
className={cn(
|
onClick={() => setMode(m)}
|
||||||
'px-3 py-2 rounded-lg text-sm transition-colors',
|
className={cn(
|
||||||
mode === m
|
'px-3 py-2 rounded-lg text-sm transition-colors',
|
||||||
? 'bg-primary-500 text-white'
|
mode === m
|
||||||
: 'bg-gray-900 text-gray-400 hover:bg-gray-800'
|
? 'bg-primary-500 text-white'
|
||||||
)}
|
: 'bg-gray-900 text-gray-400 hover:bg-gray-800'
|
||||||
>
|
)}
|
||||||
{m === 'primary' ? 'Primary' : m === 'subagent' ? 'Subagent' : 'Both'}
|
>
|
||||||
</button>
|
{m === 'primary' ? 'Primary' : m === 'subagent' ? 'Subagent' : 'Both'}
|
||||||
))}
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
{mode === 'primary'
|
||||||
|
? 'Can be used as the main agent'
|
||||||
|
: mode === 'subagent'
|
||||||
|
? 'Can only be spawned by other agents'
|
||||||
|
: 'Can be used in both modes'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
)}
|
||||||
{mode === 'primary'
|
|
||||||
? 'Can be used as the main agent'
|
{/* Internal Mode 显示只读标签 */}
|
||||||
: mode === 'subagent'
|
{isInternalAgent && (
|
||||||
? 'Can only be spawned by other agents'
|
<div>
|
||||||
: 'Can be used in both modes'}
|
<label className="block text-xs text-gray-400 mb-1">Mode</label>
|
||||||
</p>
|
<div className="px-3 py-2 bg-slate-500/10 border border-slate-600/30 rounded-lg text-sm text-slate-400 inline-flex items-center gap-2">
|
||||||
</div>
|
<Lock size={14} />
|
||||||
|
Internal (System Agent)
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
System agents are used internally and cannot be called directly
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* System Prompt */}
|
{/* System Prompt - 不为内部 Agent 显示 */}
|
||||||
<CollapsibleSection title="System Prompt" defaultOpen={!!prompt}>
|
{!isInternalAgent && (
|
||||||
<div>
|
<CollapsibleSection title="System Prompt" defaultOpen={!!prompt}>
|
||||||
<textarea
|
<div>
|
||||||
value={prompt}
|
<textarea
|
||||||
onChange={(e) => setPrompt(e.target.value)}
|
value={prompt}
|
||||||
placeholder="You are a helpful assistant..."
|
onChange={(e) => setPrompt(e.target.value)}
|
||||||
rows={8}
|
placeholder="You are a helpful assistant..."
|
||||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm font-mono focus:outline-none focus:border-primary-500 resize-y"
|
rows={8}
|
||||||
/>
|
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm font-mono focus:outline-none focus:border-primary-500 resize-y"
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
/>
|
||||||
Custom system prompt for this agent. Leave empty to use defaults.
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
</p>
|
Custom system prompt for this agent. Leave empty to use defaults.
|
||||||
</div>
|
</p>
|
||||||
</CollapsibleSection>
|
</div>
|
||||||
|
</CollapsibleSection>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Model Configuration */}
|
{/* Model Configuration - 内部 Agent 默认展开 */}
|
||||||
<CollapsibleSection title="Model Configuration" defaultOpen={!!modelProvider || !!modelName}>
|
<CollapsibleSection title="Model Configuration" defaultOpen={isInternalAgent || !!modelProvider || !!modelName}>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{/* Provider */}
|
{/* Provider */}
|
||||||
<div>
|
<div>
|
||||||
@@ -489,83 +529,87 @@ export function AgentEditor({
|
|||||||
</div>
|
</div>
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* Tool Configuration */}
|
{/* Tool Configuration - 不为内部 Agent 显示 */}
|
||||||
<CollapsibleSection title="Tool Configuration" defaultOpen={toolMode !== 'all' || noTask}>
|
{!isInternalAgent && (
|
||||||
<div className="space-y-4">
|
<CollapsibleSection title="Tool Configuration" defaultOpen={toolMode !== 'all' || noTask}>
|
||||||
{/* Tool Mode */}
|
<div className="space-y-4">
|
||||||
<div>
|
{/* Tool Mode */}
|
||||||
<label className="block text-xs text-gray-400 mb-1">Tool Access</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{(['all', 'enabled', 'disabled'] as const).map((m) => (
|
|
||||||
<button
|
|
||||||
key={m}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setToolMode(m)}
|
|
||||||
className={cn(
|
|
||||||
'px-3 py-2 rounded-lg text-sm transition-colors',
|
|
||||||
toolMode === m
|
|
||||||
? 'bg-primary-500 text-white'
|
|
||||||
: 'bg-gray-900 text-gray-400 hover:bg-gray-800'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{m === 'all' ? 'All Tools' : m === 'enabled' ? 'Only These' : 'Except These'}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tool List */}
|
|
||||||
{toolMode !== 'all' && (
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-gray-400 mb-1">
|
<label className="block text-xs text-gray-400 mb-1">Tool Access</label>
|
||||||
{toolMode === 'enabled' ? 'Enabled Tools' : 'Disabled Tools'}
|
<div className="flex gap-2">
|
||||||
</label>
|
{(['all', 'enabled', 'disabled'] as const).map((m) => (
|
||||||
<input
|
<button
|
||||||
type="text"
|
key={m}
|
||||||
value={toolList}
|
type="button"
|
||||||
onChange={(e) => setToolList(e.target.value)}
|
onClick={() => setToolMode(m)}
|
||||||
placeholder="bash, read_file, write_file"
|
className={cn(
|
||||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500"
|
'px-3 py-2 rounded-lg text-sm transition-colors',
|
||||||
/>
|
toolMode === m
|
||||||
<p className="text-xs text-gray-500 mt-1">Comma-separated tool names</p>
|
? 'bg-primary-500 text-white'
|
||||||
|
: 'bg-gray-900 text-gray-400 hover:bg-gray-800'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{m === 'all' ? 'All Tools' : m === 'enabled' ? 'Only These' : 'Except These'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* No Task */}
|
{/* Tool List */}
|
||||||
<div className="flex items-center gap-2">
|
{toolMode !== 'all' && (
|
||||||
<input
|
<div>
|
||||||
type="checkbox"
|
<label className="block text-xs text-gray-400 mb-1">
|
||||||
id="noTask"
|
{toolMode === 'enabled' ? 'Enabled Tools' : 'Disabled Tools'}
|
||||||
checked={noTask}
|
</label>
|
||||||
onChange={(e) => setNoTask(e.target.checked)}
|
<input
|
||||||
className="w-4 h-4 rounded border-gray-600 bg-gray-900 text-primary-500 focus:ring-primary-500"
|
type="text"
|
||||||
/>
|
value={toolList}
|
||||||
<label htmlFor="noTask" className="text-sm text-gray-300">
|
onChange={(e) => setToolList(e.target.value)}
|
||||||
Disable nested Task calls
|
placeholder="bash, read_file, write_file"
|
||||||
</label>
|
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">Comma-separated tool names</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* No Task */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="noTask"
|
||||||
|
checked={noTask}
|
||||||
|
onChange={(e) => setNoTask(e.target.checked)}
|
||||||
|
className="w-4 h-4 rounded border-gray-600 bg-gray-900 text-primary-500 focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
<label htmlFor="noTask" className="text-sm text-gray-300">
|
||||||
|
Disable nested Task calls
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CollapsibleSection>
|
||||||
</CollapsibleSection>
|
)}
|
||||||
|
|
||||||
{/* Execution Limits */}
|
{/* Execution Limits - 不为内部 Agent 显示 */}
|
||||||
<CollapsibleSection title="Execution Limits" defaultOpen={maxSteps !== undefined}>
|
{!isInternalAgent && (
|
||||||
<div>
|
<CollapsibleSection title="Execution Limits" defaultOpen={maxSteps !== undefined}>
|
||||||
<label className="block text-xs text-gray-400 mb-1">Max Steps</label>
|
<div>
|
||||||
<input
|
<label className="block text-xs text-gray-400 mb-1">Max Steps</label>
|
||||||
type="number"
|
<input
|
||||||
value={maxSteps ?? ''}
|
type="number"
|
||||||
onChange={(e) =>
|
value={maxSteps ?? ''}
|
||||||
setMaxSteps(e.target.value ? parseInt(e.target.value) : undefined)
|
onChange={(e) =>
|
||||||
}
|
setMaxSteps(e.target.value ? parseInt(e.target.value) : undefined)
|
||||||
placeholder="15"
|
}
|
||||||
min="1"
|
placeholder="15"
|
||||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500"
|
min="1"
|
||||||
/>
|
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500"
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
/>
|
||||||
Maximum number of tool call steps. Leave empty for default.
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
</p>
|
Maximum number of tool call steps. Leave empty for default.
|
||||||
</div>
|
</p>
|
||||||
</CollapsibleSection>
|
</div>
|
||||||
|
</CollapsibleSection>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
Sparkles,
|
Sparkles,
|
||||||
Cpu,
|
Cpu,
|
||||||
Layers,
|
Layers,
|
||||||
|
Lock,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -52,6 +53,8 @@ function getModeColor(mode: AgentListItem['mode']) {
|
|||||||
return 'bg-purple-500/20 text-purple-400';
|
return 'bg-purple-500/20 text-purple-400';
|
||||||
case 'all':
|
case 'all':
|
||||||
return 'bg-green-500/20 text-green-400';
|
return 'bg-green-500/20 text-green-400';
|
||||||
|
case 'internal':
|
||||||
|
return 'bg-slate-500/20 text-slate-400';
|
||||||
default:
|
default:
|
||||||
return 'bg-gray-500/20 text-gray-400';
|
return 'bg-gray-500/20 text-gray-400';
|
||||||
}
|
}
|
||||||
@@ -66,6 +69,8 @@ function getModeText(mode: AgentListItem['mode']) {
|
|||||||
return 'Subagent';
|
return 'Subagent';
|
||||||
case 'all':
|
case 'all':
|
||||||
return 'Both';
|
return 'Both';
|
||||||
|
case 'internal':
|
||||||
|
return 'Internal';
|
||||||
default:
|
default:
|
||||||
return mode;
|
return mode;
|
||||||
}
|
}
|
||||||
@@ -189,9 +194,10 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
setAgentDetails({});
|
setAgentDetails({});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 统计
|
// 统计 - 按分类过滤
|
||||||
const presetAgents = agents.filter((a) => a.isPreset);
|
const internalAgents = agents.filter((a) => a.mode === 'internal');
|
||||||
const customAgents = agents.filter((a) => !a.isPreset);
|
const presetAgents = agents.filter((a) => a.isPreset && a.mode !== 'internal');
|
||||||
|
const customAgents = agents.filter((a) => !a.isPreset && a.mode !== 'internal');
|
||||||
|
|
||||||
// Loading 骨架屏
|
// Loading 骨架屏
|
||||||
const LoadingSkeleton = () => (
|
const LoadingSkeleton = () => (
|
||||||
@@ -214,6 +220,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
const isExpanded = expandedAgents.has(agent.name);
|
const isExpanded = expandedAgents.has(agent.name);
|
||||||
const isLoading = actionLoading === agent.name;
|
const isLoading = actionLoading === agent.name;
|
||||||
const detail = agentDetails[agent.name];
|
const detail = agentDetails[agent.name];
|
||||||
|
const isInternal = agent.mode === 'internal';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -236,7 +243,9 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Icon */}
|
{/* Icon */}
|
||||||
{agent.isPreset ? (
|
{isInternal ? (
|
||||||
|
<Lock size={16} className="text-slate-400" />
|
||||||
|
) : agent.isPreset ? (
|
||||||
<Sparkles size={16} className="text-yellow-400" />
|
<Sparkles size={16} className="text-yellow-400" />
|
||||||
) : (
|
) : (
|
||||||
<Bot size={16} className="text-primary-400" />
|
<Bot size={16} className="text-primary-400" />
|
||||||
@@ -264,8 +273,18 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
<div className="animate-spin rounded-full h-5 w-5 border-t-2 border-b-2 border-primary-500" />
|
<div className="animate-spin rounded-full h-5 w-5 border-t-2 border-b-2 border-primary-500" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* View/Edit */}
|
{/* View/Edit - Internal agents can be edited (model config) */}
|
||||||
{agent.isPreset && !agent.isCustomized ? (
|
{isInternal ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setEditingAgent({ name: agent.name, isNew: false })}
|
||||||
|
className="text-blue-400 hover:text-blue-300"
|
||||||
|
title="Edit Model Config"
|
||||||
|
>
|
||||||
|
<Edit3 size={14} />
|
||||||
|
</Button>
|
||||||
|
) : agent.isPreset && !agent.isCustomized ? (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -287,19 +306,21 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Copy */}
|
{/* Copy - not for internal agents */}
|
||||||
<Button
|
{!isInternal && (
|
||||||
variant="ghost"
|
<Button
|
||||||
size="sm"
|
variant="ghost"
|
||||||
onClick={() => handleCopy(agent.name)}
|
size="sm"
|
||||||
className="text-gray-400 hover:text-gray-300"
|
onClick={() => handleCopy(agent.name)}
|
||||||
title="Copy"
|
className="text-gray-400 hover:text-gray-300"
|
||||||
>
|
title="Copy"
|
||||||
<Copy size={14} />
|
>
|
||||||
</Button>
|
<Copy size={14} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Delete (only for custom agents) */}
|
{/* Delete (only for custom agents, not internal) */}
|
||||||
{!agent.isPreset && (
|
{!agent.isPreset && !isInternal && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -475,7 +496,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
Agent Presets
|
Agent Presets
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
{agents.length} agents ({presetAgents.length} preset, {customAgents.length} custom)
|
{agents.length} agents ({internalAgents.length} system, {presetAgents.length} preset, {customAgents.length} custom)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -541,6 +562,21 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
|||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
className={cn('space-y-4', responsive ? 'p-4' : 'p-4')}
|
className={cn('space-y-4', responsive ? 'p-4' : 'p-4')}
|
||||||
>
|
>
|
||||||
|
{/* System Agents (Internal) */}
|
||||||
|
{internalAgents.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xs font-medium text-slate-400 uppercase tracking-wide mb-2 flex items-center gap-2">
|
||||||
|
<Lock size={12} />
|
||||||
|
System Agents
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{internalAgents.map((agent) => (
|
||||||
|
<AgentItem key={agent.name} agent={agent} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Preset Agents */}
|
{/* Preset Agents */}
|
||||||
{presetAgents.length > 0 && (
|
{presetAgents.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -12,15 +12,11 @@ import { cn } from '../utils/cn';
|
|||||||
import { modalOverlay, modalContent, smoothTransition } from '../utils/animations';
|
import { modalOverlay, modalContent, smoothTransition } from '../utils/animations';
|
||||||
import { Button } from '../primitives/Button';
|
import { Button } from '../primitives/Button';
|
||||||
import { Input } from '../primitives/Input';
|
import { Input } from '../primitives/Input';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../primitives/Select';
|
|
||||||
import { Skeleton } from './Skeleton';
|
import { Skeleton } from './Skeleton';
|
||||||
import {
|
import {
|
||||||
getConfig,
|
getConfig,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
getSummaryConfig,
|
|
||||||
updateSummaryConfig,
|
|
||||||
type ServerConfig,
|
type ServerConfig,
|
||||||
type SummaryConfigInfo,
|
|
||||||
} from '../api/client.js';
|
} from '../api/client.js';
|
||||||
|
|
||||||
interface ConfigPanelProps {
|
interface ConfigPanelProps {
|
||||||
@@ -29,58 +25,26 @@ interface ConfigPanelProps {
|
|||||||
responsive?: boolean;
|
responsive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 摘要模型提供商列表
|
|
||||||
const SUMMARY_PROVIDERS = [
|
|
||||||
{ id: 'openai', name: 'OpenAI' },
|
|
||||||
{ id: 'deepseek', name: 'DeepSeek' },
|
|
||||||
{ id: 'anthropic', name: 'Anthropic' },
|
|
||||||
{ id: 'openai-compatible', name: 'OpenAI Compatible' },
|
|
||||||
];
|
|
||||||
|
|
||||||
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({
|
||||||
workdir: '',
|
workdir: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 摘要模型表单状态
|
|
||||||
const [summaryFormData, setSummaryFormData] = useState({
|
|
||||||
provider: '',
|
|
||||||
model: '',
|
|
||||||
apiKey: '',
|
|
||||||
baseUrl: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 加载配置
|
// 加载配置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
try {
|
try {
|
||||||
// 并行加载主配置和摘要配置
|
const configResponse = await getConfig();
|
||||||
const [configResponse, summaryResponse] = await Promise.all([
|
|
||||||
getConfig(),
|
|
||||||
getSummaryConfig(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
setConfig(configResponse.data);
|
setConfig(configResponse.data);
|
||||||
setFormData({
|
setFormData({
|
||||||
workdir: configResponse.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 {
|
||||||
@@ -105,33 +69,6 @@ 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) {
|
||||||
@@ -228,96 +165,6 @@ 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>
|
|
||||||
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -90,11 +90,9 @@ export {
|
|||||||
deleteProvider,
|
deleteProvider,
|
||||||
addProviderModel,
|
addProviderModel,
|
||||||
deleteProviderModel,
|
deleteProviderModel,
|
||||||
// Context & Summary API
|
// Context API
|
||||||
getContextUsage,
|
getContextUsage,
|
||||||
compressContext,
|
compressContext,
|
||||||
getSummaryConfig,
|
|
||||||
updateSummaryConfig,
|
|
||||||
} from './api/client.js';
|
} from './api/client.js';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
@@ -166,13 +164,11 @@ export type {
|
|||||||
CustomProviderDefinition,
|
CustomProviderDefinition,
|
||||||
ProviderConfig,
|
ProviderConfig,
|
||||||
ConnectionTestResult,
|
ConnectionTestResult,
|
||||||
// Context & Summary types
|
// Context types
|
||||||
TokenUsage,
|
TokenUsage,
|
||||||
ContextUsageInfo,
|
ContextUsageInfo,
|
||||||
CompressionStatus,
|
CompressionStatus,
|
||||||
CompressionResult,
|
CompressionResult,
|
||||||
SummaryConfigInfo,
|
|
||||||
SummaryConfigInput,
|
|
||||||
} from './api/client.js';
|
} from './api/client.js';
|
||||||
|
|
||||||
// Primitives (shadcn/ui style)
|
// Primitives (shadcn/ui style)
|
||||||
|
|||||||
Reference in New Issue
Block a user