diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f597a47..0949000 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -297,6 +297,11 @@ export { createOpenAICompatibleFactory, isValidProviderId, isValidUrl, + // Service config + getServiceConfig, + getServiceApiKey, + saveServiceConfig, + deleteServiceConfig, } from './provider/index.js'; export type { @@ -313,6 +318,8 @@ export type { ProvidersConfigFile, ProviderListItem, ProviderDetail, + ServiceConfig, + ServiceType, } from './provider/index.js'; // File Index diff --git a/packages/core/src/provider/config.ts b/packages/core/src/provider/config.ts index 16fb083..7342bbd 100644 --- a/packages/core/src/provider/config.ts +++ b/packages/core/src/provider/config.ts @@ -7,7 +7,7 @@ import { existsSync } from 'node:fs'; import { readFile, writeFile, mkdir } from 'node:fs/promises'; import { join } from 'node:path'; -import type { ProvidersConfigFile, CustomProviderDefinition, ProviderConfig } from './types.js'; +import type { ProvidersConfigFile, CustomProviderDefinition, ProviderConfig, ServiceConfig, ServiceType } from './types.js'; import { getConfigDir as getGlobalConfigDir } from '../constants/paths.js'; /** 配置文件名 */ @@ -28,7 +28,7 @@ export async function loadProvidersConfig(): Promise { const configPath = getConfigPath(); if (!existsSync(configPath)) { - return { providers: {}, configs: {} }; + return { providers: {}, configs: {}, services: {} }; } try { @@ -37,10 +37,11 @@ export async function loadProvidersConfig(): Promise { return { providers: config.providers ?? {}, configs: config.configs ?? {}, + services: config.services ?? {}, }; } catch { // 解析失败返回空配置 - return { providers: {}, configs: {} }; + return { providers: {}, configs: {}, services: {} }; } } @@ -83,3 +84,43 @@ export function mergeProviderConfig( enabled: config?.enabled ?? true, }; } + +/** + * 获取服务配置 + */ +export async function getServiceConfig(serviceId: ServiceType): Promise { + const config = await loadProvidersConfig(); + return config.services?.[serviceId]; +} + +/** + * 获取服务 API Key + */ +export async function getServiceApiKey(serviceId: ServiceType): Promise { + const serviceConfig = await getServiceConfig(serviceId); + if (serviceConfig?.enabled === false) { + return undefined; + } + return serviceConfig?.apiKey; +} + +/** + * 保存服务配置 + */ +export async function saveServiceConfig(serviceId: ServiceType, serviceConfig: Omit): Promise { + const config = await loadProvidersConfig(); + config.services = config.services ?? {}; + config.services[serviceId] = { ...serviceConfig, id: serviceId }; + await saveProvidersConfig(config); +} + +/** + * 删除服务配置 + */ +export async function deleteServiceConfig(serviceId: ServiceType): Promise { + const config = await loadProvidersConfig(); + if (config.services) { + delete config.services[serviceId]; + await saveProvidersConfig(config); + } +} diff --git a/packages/core/src/provider/index.ts b/packages/core/src/provider/index.ts index 1132b95..15b0d34 100644 --- a/packages/core/src/provider/index.ts +++ b/packages/core/src/provider/index.ts @@ -19,6 +19,8 @@ export type { ProvidersConfigFile, ProviderListItem, ProviderDetail, + ServiceConfig, + ServiceType, } from './types.js'; // Registry @@ -42,6 +44,10 @@ export { saveProvidersConfig, resolveApiKey, getConfigPath, + getServiceConfig, + getServiceApiKey, + saveServiceConfig, + deleteServiceConfig, } from './config.js'; // Utils diff --git a/packages/core/src/provider/types.ts b/packages/core/src/provider/types.ts index b6ee0c9..5658fc9 100644 --- a/packages/core/src/provider/types.ts +++ b/packages/core/src/provider/types.ts @@ -122,8 +122,23 @@ export interface ProvidersConfigFile { providers?: Record; /** 提供商配置(API Key、选项等) */ configs?: Record; + /** 第三方服务配置(如 Tavily) */ + services?: Record; } +/** 第三方服务配置 */ +export interface ServiceConfig { + /** 服务 ID */ + id: string; + /** API Key */ + apiKey?: string; + /** 是否启用 */ + enabled?: boolean; +} + +/** 已知的服务类型 */ +export type ServiceType = 'tavily'; + /** 提供商列表项(API 响应用) */ export interface ProviderListItem { id: string; diff --git a/packages/core/src/tools/web/web_extract.ts b/packages/core/src/tools/web/web_extract.ts index f435d5f..5fd9c1d 100644 --- a/packages/core/src/tools/web/web_extract.ts +++ b/packages/core/src/tools/web/web_extract.ts @@ -3,6 +3,7 @@ import type { ToolResult } from '../../types/index.js'; import type { ToolWithMetadata } from '../types.js'; import { loadDescription } from '../load_description.js'; import { getPermissionManager } from '../../permission/index.js'; +import { getServiceApiKey } from '../../provider/index.js'; export const webExtractTool: ToolWithMetadata = { name: 'web_extract', @@ -72,14 +73,14 @@ export const webExtractTool: ToolWithMetadata = { }; } - // 获取 Tavily API Key(从环境变量) - const apiKey = process.env.TAVILY_API_KEY; + // 获取 Tavily API Key(从配置文件) + const apiKey = await getServiceApiKey('tavily'); if (!apiKey) { return { success: false, output: '', - error: '未配置 Tavily API Key。请设置环境变量 TAVILY_API_KEY。', + error: '未配置 Tavily API Key。请在设置中配置 Tavily 服务的 API Key。', }; } diff --git a/packages/core/src/tools/web/web_search.ts b/packages/core/src/tools/web/web_search.ts index aaadef2..94af535 100644 --- a/packages/core/src/tools/web/web_search.ts +++ b/packages/core/src/tools/web/web_search.ts @@ -3,6 +3,7 @@ import type { ToolResult } from '../../types/index.js'; import type { ToolWithMetadata } from '../types.js'; import { loadDescription } from '../load_description.js'; import { getPermissionManager } from '../../permission/index.js'; +import { getServiceApiKey } from '../../provider/index.js'; export const webSearchTool: ToolWithMetadata = { name: 'web_search', @@ -74,14 +75,14 @@ export const webSearchTool: ToolWithMetadata = { }; } - // 获取 Tavily API Key(从环境变量) - const apiKey = process.env.TAVILY_API_KEY; + // 获取 Tavily API Key(从配置文件) + const apiKey = await getServiceApiKey('tavily'); if (!apiKey) { return { success: false, output: '', - error: '未配置 Tavily API Key。请设置环境变量 TAVILY_API_KEY。', + error: '未配置 Tavily API Key。请在设置中配置 Tavily 服务的 API Key。', }; } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 2bca182..fdb8b03 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -9,7 +9,7 @@ import { cors } from 'hono/cors'; import { logger } from 'hono/logger'; import { createBunWebSocket } from 'hono/bun'; -import { sessionsRouter, toolsRouter, configRouter, filesRouter, commandsRouter, mcpRouter, hooksRouter, agentsRouter, checkpointsRouter, providersRouter, contextRouter, lspRouter } from './routes/index.js'; +import { sessionsRouter, toolsRouter, configRouter, filesRouter, commandsRouter, mcpRouter, hooksRouter, agentsRouter, checkpointsRouter, providersRouter, servicesRouter, contextRouter, lspRouter } from './routes/index.js'; import { handleWebSocket, handleWebSocketMessage, @@ -88,6 +88,7 @@ api.route('/hooks', hooksRouter); api.route('/agents', agentsRouter); api.route('/checkpoints', checkpointsRouter); api.route('/providers', providersRouter); +api.route('/services', servicesRouter); api.route('/lsp', lspRouter); // 上下文压缩相关(挂载到根路径,内部路由包含 /sessions/:id/context) diff --git a/packages/server/src/routes/index.ts b/packages/server/src/routes/index.ts index 3b380a2..2590c75 100644 --- a/packages/server/src/routes/index.ts +++ b/packages/server/src/routes/index.ts @@ -14,5 +14,6 @@ export { hooksRouter } from './hooks.js'; export { agentsRouter } from './agents.js'; export { checkpointsRouter } from './checkpoints.js'; export { providersRouter } from './providers.js'; +export { servicesRouter } from './services.js'; export { contextRouter } from './context.js'; export { lspRouter } from './lsp.js'; diff --git a/packages/server/src/routes/services.ts b/packages/server/src/routes/services.ts new file mode 100644 index 0000000..10cae94 --- /dev/null +++ b/packages/server/src/routes/services.ts @@ -0,0 +1,155 @@ +/** + * Services API Routes + * + * 第三方服务配置管理 REST API(如 Tavily) + */ + +import { Hono } from 'hono'; +import type { ServiceConfig, ServiceType } from '@ai-assistant/core'; +import { + loadProvidersConfig, + getServiceConfig, + saveServiceConfig, + deleteServiceConfig, +} from '@ai-assistant/core'; + +export const servicesRouter = new Hono(); + +/** 服务元信息 */ +interface ServiceInfo { + id: ServiceType; + name: string; + description: string; + website: string; +} + +/** 已知服务列表 */ +const KNOWN_SERVICES: ServiceInfo[] = [ + { + id: 'tavily', + name: 'Tavily', + description: '网络搜索和网页内容提取 API', + website: 'https://tavily.com', + }, +]; + +/** + * GET /services - List all services + */ +servicesRouter.get('/', async (c) => { + try { + const config = await loadProvidersConfig(); + const services = config.services ?? {}; + + const data = KNOWN_SERVICES.map((info) => { + const serviceConfig = services[info.id]; + return { + ...info, + enabled: serviceConfig?.enabled ?? false, + hasApiKey: !!serviceConfig?.apiKey, + }; + }); + + return c.json({ + success: true, + data, + }); + } catch (error) { + return c.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to list services', + }, + 500 + ); + } +}); + +/** + * GET /services/:id - Get service config + */ +servicesRouter.get('/:id', async (c) => { + const id = c.req.param('id') as ServiceType; + + try { + const info = KNOWN_SERVICES.find((s) => s.id === id); + if (!info) { + return c.json({ success: false, error: `Unknown service: ${id}` }, 404); + } + + const serviceConfig = await getServiceConfig(id); + + return c.json({ + success: true, + data: { + ...info, + enabled: serviceConfig?.enabled ?? false, + hasApiKey: !!serviceConfig?.apiKey, + }, + }); + } catch (error) { + return c.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to get service', + }, + 500 + ); + } +}); + +/** + * PUT /services/:id - Update service config + */ +servicesRouter.put('/:id', async (c) => { + const id = c.req.param('id') as ServiceType; + + try { + const info = KNOWN_SERVICES.find((s) => s.id === id); + if (!info) { + return c.json({ success: false, error: `Unknown service: ${id}` }, 404); + } + + const body = await c.req.json(); + const { apiKey, enabled } = body as { apiKey?: string; enabled?: boolean }; + + await saveServiceConfig(id, { apiKey, enabled }); + + return c.json({ + success: true, + message: `Service ${id} config updated`, + }); + } catch (error) { + return c.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to update service config', + }, + 400 + ); + } +}); + +/** + * DELETE /services/:id - Delete service config + */ +servicesRouter.delete('/:id', async (c) => { + const id = c.req.param('id') as ServiceType; + + try { + await deleteServiceConfig(id); + + return c.json({ + success: true, + message: `Service ${id} config removed`, + }); + } catch (error) { + return c.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to remove service config', + }, + 400 + ); + } +});