feat: 重构为 Monorepo 架构并实现 HTTP Server
架构变更: - 采用 pnpm workspaces 实现 Monorepo 结构 - 将现有代码迁移到 packages/core - 新增 packages/server HTTP 服务层 Server 功能: - REST API: 会话管理、工具管理、配置管理 - WebSocket: 实时双向通信支持 - SSE: 服务端事件推送 - Hono + Bun 作为运行时 API 端点: - GET/POST /api/sessions - 会话 CRUD - GET/POST /api/sessions/:id/messages - 消息管理 - GET /api/sessions/:id/events - SSE 事件流 - WS /api/ws/:sessionId - WebSocket 连接 - GET/POST /api/tools - 工具管理 - GET/PUT /api/config - 配置管理
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import type { AgentConfig, ProviderType } from '../types/index.js';
|
||||
|
||||
const CONFIG_DIR = path.join(os.homedir(), '.ai-terminal-assistant');
|
||||
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
||||
|
||||
interface StoredConfig {
|
||||
provider?: ProviderType;
|
||||
apiKey?: string;
|
||||
deepseekApiKey?: string;
|
||||
openaiApiKey?: string;
|
||||
model?: string;
|
||||
maxTokens?: number;
|
||||
tavilyApiKey?: string;
|
||||
/** 自定义 API 基础 URL(用于 OpenAI 兼容服务,如阿里云百炼) */
|
||||
baseUrl?: string;
|
||||
// Vision 配置
|
||||
visionProvider?: ProviderType;
|
||||
visionModel?: string;
|
||||
/** Vision 专用的 API Key(可选,不设置则使用对应 provider 的 key) */
|
||||
visionApiKey?: string;
|
||||
/** Vision 专用的 Base URL(用于 OpenAI 兼容的 Vision 服务) */
|
||||
visionBaseUrl?: string;
|
||||
}
|
||||
|
||||
// Vision 配置接口
|
||||
export interface VisionConfig {
|
||||
provider: ProviderType;
|
||||
apiKey: string;
|
||||
model: string;
|
||||
/** 自定义 Base URL(用于 OpenAI 兼容的 Vision 服务) */
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
// 默认模型配置
|
||||
const DEFAULT_MODELS: Record<ProviderType, string> = {
|
||||
anthropic: 'claude-sonnet-4-20250514',
|
||||
deepseek: 'deepseek-chat',
|
||||
openai: 'gpt-4o',
|
||||
};
|
||||
|
||||
// 默认 Vision 模型(需要支持图片理解)
|
||||
const DEFAULT_VISION_MODELS: Record<ProviderType, string> = {
|
||||
anthropic: 'claude-sonnet-4-20250514',
|
||||
deepseek: 'deepseek-chat', // DeepSeek 暂不支持 vision,占位用
|
||||
openai: 'gpt-4o',
|
||||
};
|
||||
|
||||
// 默认系统提示词
|
||||
const DEFAULT_SYSTEM_PROMPT = `你是一个运行在终端中的 AI 编程助手。你可以帮助用户:
|
||||
- 读取和写入文件
|
||||
- 执行 bash 命令
|
||||
- 搜索代码和文件
|
||||
- 回答编程问题
|
||||
|
||||
使用工具时请注意:
|
||||
1. 在修改文件前,先读取文件内容
|
||||
2. 执行可能有风险的命令前,先向用户确认
|
||||
3. 给出清晰、简洁的回答
|
||||
|
||||
重要的工具使用规则:
|
||||
- 创建或修改文件时,必须使用 write_file 或 edit_file 工具,不要使用 bash 命令(如 cat、echo 等)
|
||||
- write_file 和 edit_file 工具集成了代码诊断功能,可以在写入后自动检查代码错误
|
||||
- bash 工具仅用于运行命令、安装依赖、执行脚本等操作,不要用于文件内容的创建和修改
|
||||
|
||||
当前工作目录: ${process.cwd()}
|
||||
操作系统: ${process.platform}`;
|
||||
|
||||
// 获取原始配置(包含所有字段)
|
||||
export function getConfig(): StoredConfig {
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
try {
|
||||
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// 加载配置
|
||||
export function loadConfig(): AgentConfig {
|
||||
// 从环境变量获取
|
||||
const provider = (process.env.AI_PROVIDER as ProviderType) || 'anthropic';
|
||||
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
|
||||
const deepseekApiKey = process.env.DEEPSEEK_API_KEY;
|
||||
const openaiApiKey = process.env.OPENAI_API_KEY;
|
||||
const model = process.env.AI_MODEL;
|
||||
const maxTokens = parseInt(process.env.AI_MAX_TOKENS || '4096', 10);
|
||||
const baseUrl = process.env.AI_BASE_URL;
|
||||
|
||||
// 从配置文件读取
|
||||
let storedConfig: StoredConfig = {};
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
try {
|
||||
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
||||
storedConfig = JSON.parse(content);
|
||||
} catch {
|
||||
// 忽略解析错误
|
||||
}
|
||||
}
|
||||
|
||||
// 确定最终的 provider
|
||||
const finalProvider = storedConfig.provider || provider;
|
||||
|
||||
// 根据 provider 获取对应的 API Key
|
||||
let finalApiKey: string | undefined;
|
||||
if (finalProvider === 'anthropic') {
|
||||
finalApiKey = anthropicApiKey || storedConfig.apiKey;
|
||||
} else if (finalProvider === 'deepseek') {
|
||||
finalApiKey = deepseekApiKey || storedConfig.deepseekApiKey;
|
||||
} else if (finalProvider === 'openai') {
|
||||
finalApiKey = openaiApiKey || storedConfig.openaiApiKey;
|
||||
}
|
||||
|
||||
if (!finalApiKey) {
|
||||
const envVarMap: Record<ProviderType, string> = {
|
||||
anthropic: 'ANTHROPIC_API_KEY',
|
||||
deepseek: 'DEEPSEEK_API_KEY',
|
||||
openai: 'OPENAI_API_KEY',
|
||||
};
|
||||
const envVar = envVarMap[finalProvider];
|
||||
console.error(`❌ 错误: 未设置 ${envVar}`);
|
||||
console.error(`请设置环境变量: export ${envVar}=your-api-key`);
|
||||
console.error('或运行: ai-assist init 进行初始化配置');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 确定模型
|
||||
const finalModel = model || storedConfig.model || DEFAULT_MODELS[finalProvider];
|
||||
|
||||
// 确定 baseUrl(环境变量优先)
|
||||
const finalBaseUrl = baseUrl || storedConfig.baseUrl;
|
||||
|
||||
return {
|
||||
provider: finalProvider,
|
||||
apiKey: finalApiKey,
|
||||
model: finalModel,
|
||||
maxTokens: storedConfig.maxTokens || maxTokens,
|
||||
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
||||
baseUrl: finalBaseUrl,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 Vision 配置
|
||||
* Vision 用于图片理解,当主模型不支持 vision 时使用
|
||||
* 优先级:环境变量 > 配置文件 > 默认使用 Anthropic Claude
|
||||
*/
|
||||
export function loadVisionConfig(): VisionConfig | null {
|
||||
// 从环境变量获取
|
||||
const visionProvider = process.env.VISION_PROVIDER as ProviderType | undefined;
|
||||
const visionModel = process.env.VISION_MODEL;
|
||||
const visionApiKey = process.env.VISION_API_KEY;
|
||||
const visionBaseUrl = process.env.VISION_BASE_URL;
|
||||
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
|
||||
const deepseekApiKey = process.env.DEEPSEEK_API_KEY;
|
||||
const openaiApiKey = process.env.OPENAI_API_KEY;
|
||||
|
||||
// 从配置文件读取
|
||||
const storedConfig = getConfig();
|
||||
|
||||
// 确定 vision provider(默认使用 anthropic,因为 Claude 支持 vision)
|
||||
const finalProvider = visionProvider || storedConfig.visionProvider || 'anthropic';
|
||||
|
||||
// 获取 Vision 专用的 API Key(优先级:环境变量 > 配置文件专用 key > provider 对应的 key)
|
||||
let finalApiKey: string | undefined;
|
||||
finalApiKey = visionApiKey || storedConfig.visionApiKey;
|
||||
|
||||
// 如果没有专用 key,回退到对应 provider 的 key
|
||||
if (!finalApiKey) {
|
||||
if (finalProvider === 'anthropic') {
|
||||
finalApiKey = anthropicApiKey || storedConfig.apiKey;
|
||||
} else if (finalProvider === 'deepseek') {
|
||||
finalApiKey = deepseekApiKey || storedConfig.deepseekApiKey;
|
||||
} else if (finalProvider === 'openai') {
|
||||
finalApiKey = openaiApiKey || storedConfig.openaiApiKey;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有 API Key,返回 null
|
||||
if (!finalApiKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 确定模型
|
||||
const finalModel = visionModel || storedConfig.visionModel || DEFAULT_VISION_MODELS[finalProvider];
|
||||
|
||||
// 确定 baseUrl(Vision 专用)
|
||||
const finalBaseUrl = visionBaseUrl || storedConfig.visionBaseUrl;
|
||||
|
||||
return {
|
||||
provider: finalProvider,
|
||||
apiKey: finalApiKey,
|
||||
model: finalModel,
|
||||
baseUrl: finalBaseUrl,
|
||||
};
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
export function saveConfig(config: Partial<StoredConfig>): void {
|
||||
// 确保目录存在
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// 读取现有配置
|
||||
let existingConfig: StoredConfig = {};
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
try {
|
||||
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
||||
existingConfig = JSON.parse(content);
|
||||
} catch {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
|
||||
// 合并并保存
|
||||
const newConfig = { ...existingConfig, ...config };
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2));
|
||||
}
|
||||
|
||||
// 初始化配置向导
|
||||
export async function initConfig(): Promise<void> {
|
||||
const { default: inquirer } = await import('inquirer');
|
||||
|
||||
console.log('\n🔧 初始化 AI Terminal Assistant 配置\n');
|
||||
|
||||
// 选择 provider
|
||||
const { provider } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'provider',
|
||||
message: '选择 AI 服务商:',
|
||||
choices: [
|
||||
{ name: 'Anthropic (Claude)', value: 'anthropic' },
|
||||
{ name: 'OpenAI (GPT)', value: 'openai' },
|
||||
{ name: 'OpenAI 兼容服务 (阿里云百炼、Azure 等)', value: 'openai-compatible' },
|
||||
{ name: 'DeepSeek', value: 'deepseek' },
|
||||
],
|
||||
default: 'anthropic',
|
||||
},
|
||||
]);
|
||||
|
||||
// 是否是 OpenAI 兼容服务
|
||||
const isOpenAICompatible = provider === 'openai-compatible';
|
||||
const actualProvider = isOpenAICompatible ? 'openai' : provider;
|
||||
|
||||
// 如果是 OpenAI 兼容服务,询问 base URL
|
||||
let baseUrl: string | undefined;
|
||||
if (isOpenAICompatible) {
|
||||
const { customBaseUrl } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'customBaseUrl',
|
||||
message: '请输入 API 基础 URL (如: https://dashscope.aliyuncs.com/compatible-mode/v1):',
|
||||
validate: (input: string) => {
|
||||
if (!input) return 'Base URL 不能为空';
|
||||
try {
|
||||
new URL(input);
|
||||
return true;
|
||||
} catch {
|
||||
return '请输入有效的 URL';
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
baseUrl = customBaseUrl;
|
||||
}
|
||||
|
||||
// 根据 provider 显示不同的模型选项
|
||||
let modelChoices: Array<{ name: string; value: string }>;
|
||||
let allowCustomModel = false;
|
||||
|
||||
if (actualProvider === 'anthropic') {
|
||||
modelChoices = [
|
||||
{ name: 'Claude Sonnet 4 (推荐,平衡性能和成本)', value: 'claude-sonnet-4-20250514' },
|
||||
{ name: 'Claude Opus 4 (最强,成本较高)', value: 'claude-opus-4-20250514' },
|
||||
{ name: 'Claude 3.5 Haiku (快速,成本低)', value: 'claude-3-5-haiku-20241022' },
|
||||
];
|
||||
} else if (actualProvider === 'openai') {
|
||||
if (isOpenAICompatible) {
|
||||
// OpenAI 兼容服务允许自定义模型名称
|
||||
modelChoices = [
|
||||
{ name: 'qwen-plus (通义千问)', value: 'qwen-plus' },
|
||||
{ name: 'qwen-turbo (通义千问快速版)', value: 'qwen-turbo' },
|
||||
{ name: 'qwen-max (通义千问最强版)', value: 'qwen-max' },
|
||||
{ name: 'gpt-4o', value: 'gpt-4o' },
|
||||
{ name: '自定义模型名称...', value: '__custom__' },
|
||||
];
|
||||
allowCustomModel = true;
|
||||
} else {
|
||||
modelChoices = [
|
||||
{ name: 'GPT-4o (推荐,支持 vision)', value: 'gpt-4o' },
|
||||
{ name: 'GPT-4o mini (快速,成本低)', value: 'gpt-4o-mini' },
|
||||
{ name: 'GPT-4 Turbo', value: 'gpt-4-turbo' },
|
||||
{ name: 'o1 (推理增强)', value: 'o1' },
|
||||
{ name: 'o1-mini (推理,成本低)', value: 'o1-mini' },
|
||||
];
|
||||
}
|
||||
} else {
|
||||
modelChoices = [
|
||||
{ name: 'DeepSeek Chat (推荐)', value: 'deepseek-chat' },
|
||||
{ name: 'DeepSeek Reasoner (推理增强)', value: 'deepseek-reasoner' },
|
||||
];
|
||||
}
|
||||
|
||||
const apiKeyMessageMap: Record<string, string> = {
|
||||
anthropic: '请输入你的 Anthropic API Key:',
|
||||
openai: '请输入你的 OpenAI API Key:',
|
||||
deepseek: '请输入你的 DeepSeek API Key:',
|
||||
};
|
||||
|
||||
// 分开询问 API Key
|
||||
const { apiKey } = await inquirer.prompt([
|
||||
{
|
||||
type: 'password',
|
||||
name: 'apiKey',
|
||||
message: isOpenAICompatible ? '请输入你的 API Key:' : apiKeyMessageMap[actualProvider],
|
||||
validate: (input: string) => input.length > 0 || 'API Key 不能为空',
|
||||
},
|
||||
]);
|
||||
|
||||
// 询问模型配置
|
||||
const { model: selectedModel } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'model',
|
||||
message: '选择默认模型:',
|
||||
choices: modelChoices,
|
||||
default: DEFAULT_MODELS[actualProvider as ProviderType],
|
||||
},
|
||||
]);
|
||||
|
||||
// 如果选择自定义模型,询问模型名称
|
||||
let finalModel = selectedModel;
|
||||
if (allowCustomModel && selectedModel === '__custom__') {
|
||||
const { customModel } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'customModel',
|
||||
message: '请输入模型名称:',
|
||||
validate: (input: string) => input.length > 0 || '模型名称不能为空',
|
||||
},
|
||||
]);
|
||||
finalModel = customModel;
|
||||
}
|
||||
|
||||
// 询问 token 配置
|
||||
const { maxTokens } = await inquirer.prompt([
|
||||
{
|
||||
type: 'number',
|
||||
name: 'maxTokens',
|
||||
message: '最大输出 token 数:',
|
||||
default: 4096,
|
||||
},
|
||||
]);
|
||||
|
||||
// 根据 provider 构建配置对象
|
||||
const configToSave: Partial<StoredConfig> = {
|
||||
provider: actualProvider as ProviderType,
|
||||
model: finalModel,
|
||||
maxTokens,
|
||||
};
|
||||
|
||||
// 存储 API Key 到对应字段
|
||||
if (actualProvider === 'anthropic') {
|
||||
configToSave.apiKey = apiKey;
|
||||
} else if (actualProvider === 'openai') {
|
||||
configToSave.openaiApiKey = apiKey;
|
||||
} else if (actualProvider === 'deepseek') {
|
||||
configToSave.deepseekApiKey = apiKey;
|
||||
}
|
||||
|
||||
// 存储 base URL
|
||||
if (baseUrl) {
|
||||
configToSave.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
saveConfig(configToSave);
|
||||
console.log('\n✅ 配置已保存到', CONFIG_FILE);
|
||||
console.log('现在可以运行 ai-assist 开始使用了!\n');
|
||||
}
|
||||
Reference in New Issue
Block a user