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:
2025-12-12 10:42:20 +08:00
parent 59dbed926e
commit 5e32375f0e
301 changed files with 3281 additions and 43 deletions
+386
View File
@@ -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];
// 确定 baseUrlVision 专用)
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');
}