feat(core): 将 Tavily API Key 从环境变量迁移到系统配置
- 新增 ServiceConfig 类型和 services 配置字段 - 添加 getServiceApiKey/saveServiceConfig/deleteServiceConfig API - 更新 web_search 和 web_extract 工具使用配置系统 - 新增 /api/services REST 端点管理第三方服务配置
This commit is contained in:
@@ -297,6 +297,11 @@ export {
|
|||||||
createOpenAICompatibleFactory,
|
createOpenAICompatibleFactory,
|
||||||
isValidProviderId,
|
isValidProviderId,
|
||||||
isValidUrl,
|
isValidUrl,
|
||||||
|
// Service config
|
||||||
|
getServiceConfig,
|
||||||
|
getServiceApiKey,
|
||||||
|
saveServiceConfig,
|
||||||
|
deleteServiceConfig,
|
||||||
} from './provider/index.js';
|
} from './provider/index.js';
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
@@ -313,6 +318,8 @@ export type {
|
|||||||
ProvidersConfigFile,
|
ProvidersConfigFile,
|
||||||
ProviderListItem,
|
ProviderListItem,
|
||||||
ProviderDetail,
|
ProviderDetail,
|
||||||
|
ServiceConfig,
|
||||||
|
ServiceType,
|
||||||
} from './provider/index.js';
|
} from './provider/index.js';
|
||||||
|
|
||||||
// File Index
|
// File Index
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import { existsSync } from 'node:fs';
|
import { existsSync } from 'node:fs';
|
||||||
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
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';
|
import { getConfigDir as getGlobalConfigDir } from '../constants/paths.js';
|
||||||
|
|
||||||
/** 配置文件名 */
|
/** 配置文件名 */
|
||||||
@@ -28,7 +28,7 @@ export async function loadProvidersConfig(): Promise<ProvidersConfigFile> {
|
|||||||
const configPath = getConfigPath();
|
const configPath = getConfigPath();
|
||||||
|
|
||||||
if (!existsSync(configPath)) {
|
if (!existsSync(configPath)) {
|
||||||
return { providers: {}, configs: {} };
|
return { providers: {}, configs: {}, services: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -37,10 +37,11 @@ export async function loadProvidersConfig(): Promise<ProvidersConfigFile> {
|
|||||||
return {
|
return {
|
||||||
providers: config.providers ?? {},
|
providers: config.providers ?? {},
|
||||||
configs: config.configs ?? {},
|
configs: config.configs ?? {},
|
||||||
|
services: config.services ?? {},
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
// 解析失败返回空配置
|
// 解析失败返回空配置
|
||||||
return { providers: {}, configs: {} };
|
return { providers: {}, configs: {}, services: {} };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,3 +84,43 @@ export function mergeProviderConfig(
|
|||||||
enabled: config?.enabled ?? true,
|
enabled: config?.enabled ?? true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取服务配置
|
||||||
|
*/
|
||||||
|
export async function getServiceConfig(serviceId: ServiceType): Promise<ServiceConfig | undefined> {
|
||||||
|
const config = await loadProvidersConfig();
|
||||||
|
return config.services?.[serviceId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取服务 API Key
|
||||||
|
*/
|
||||||
|
export async function getServiceApiKey(serviceId: ServiceType): Promise<string | undefined> {
|
||||||
|
const serviceConfig = await getServiceConfig(serviceId);
|
||||||
|
if (serviceConfig?.enabled === false) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return serviceConfig?.apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存服务配置
|
||||||
|
*/
|
||||||
|
export async function saveServiceConfig(serviceId: ServiceType, serviceConfig: Omit<ServiceConfig, 'id'>): Promise<void> {
|
||||||
|
const config = await loadProvidersConfig();
|
||||||
|
config.services = config.services ?? {};
|
||||||
|
config.services[serviceId] = { ...serviceConfig, id: serviceId };
|
||||||
|
await saveProvidersConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除服务配置
|
||||||
|
*/
|
||||||
|
export async function deleteServiceConfig(serviceId: ServiceType): Promise<void> {
|
||||||
|
const config = await loadProvidersConfig();
|
||||||
|
if (config.services) {
|
||||||
|
delete config.services[serviceId];
|
||||||
|
await saveProvidersConfig(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export type {
|
|||||||
ProvidersConfigFile,
|
ProvidersConfigFile,
|
||||||
ProviderListItem,
|
ProviderListItem,
|
||||||
ProviderDetail,
|
ProviderDetail,
|
||||||
|
ServiceConfig,
|
||||||
|
ServiceType,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
// Registry
|
// Registry
|
||||||
@@ -42,6 +44,10 @@ export {
|
|||||||
saveProvidersConfig,
|
saveProvidersConfig,
|
||||||
resolveApiKey,
|
resolveApiKey,
|
||||||
getConfigPath,
|
getConfigPath,
|
||||||
|
getServiceConfig,
|
||||||
|
getServiceApiKey,
|
||||||
|
saveServiceConfig,
|
||||||
|
deleteServiceConfig,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|||||||
@@ -122,8 +122,23 @@ export interface ProvidersConfigFile {
|
|||||||
providers?: Record<string, CustomProviderDefinition>;
|
providers?: Record<string, CustomProviderDefinition>;
|
||||||
/** 提供商配置(API Key、选项等) */
|
/** 提供商配置(API Key、选项等) */
|
||||||
configs?: Record<string, ProviderConfig>;
|
configs?: Record<string, ProviderConfig>;
|
||||||
|
/** 第三方服务配置(如 Tavily) */
|
||||||
|
services?: Record<string, ServiceConfig>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 第三方服务配置 */
|
||||||
|
export interface ServiceConfig {
|
||||||
|
/** 服务 ID */
|
||||||
|
id: string;
|
||||||
|
/** API Key */
|
||||||
|
apiKey?: string;
|
||||||
|
/** 是否启用 */
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 已知的服务类型 */
|
||||||
|
export type ServiceType = 'tavily';
|
||||||
|
|
||||||
/** 提供商列表项(API 响应用) */
|
/** 提供商列表项(API 响应用) */
|
||||||
export interface ProviderListItem {
|
export interface ProviderListItem {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { ToolResult } from '../../types/index.js';
|
|||||||
import type { ToolWithMetadata } from '../types.js';
|
import type { ToolWithMetadata } from '../types.js';
|
||||||
import { loadDescription } from '../load_description.js';
|
import { loadDescription } from '../load_description.js';
|
||||||
import { getPermissionManager } from '../../permission/index.js';
|
import { getPermissionManager } from '../../permission/index.js';
|
||||||
|
import { getServiceApiKey } from '../../provider/index.js';
|
||||||
|
|
||||||
export const webExtractTool: ToolWithMetadata = {
|
export const webExtractTool: ToolWithMetadata = {
|
||||||
name: 'web_extract',
|
name: 'web_extract',
|
||||||
@@ -72,14 +73,14 @@ export const webExtractTool: ToolWithMetadata = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取 Tavily API Key(从环境变量)
|
// 获取 Tavily API Key(从配置文件)
|
||||||
const apiKey = process.env.TAVILY_API_KEY;
|
const apiKey = await getServiceApiKey('tavily');
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
output: '',
|
output: '',
|
||||||
error: '未配置 Tavily API Key。请设置环境变量 TAVILY_API_KEY。',
|
error: '未配置 Tavily API Key。请在设置中配置 Tavily 服务的 API Key。',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { ToolResult } from '../../types/index.js';
|
|||||||
import type { ToolWithMetadata } from '../types.js';
|
import type { ToolWithMetadata } from '../types.js';
|
||||||
import { loadDescription } from '../load_description.js';
|
import { loadDescription } from '../load_description.js';
|
||||||
import { getPermissionManager } from '../../permission/index.js';
|
import { getPermissionManager } from '../../permission/index.js';
|
||||||
|
import { getServiceApiKey } from '../../provider/index.js';
|
||||||
|
|
||||||
export const webSearchTool: ToolWithMetadata = {
|
export const webSearchTool: ToolWithMetadata = {
|
||||||
name: 'web_search',
|
name: 'web_search',
|
||||||
@@ -74,14 +75,14 @@ export const webSearchTool: ToolWithMetadata = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取 Tavily API Key(从环境变量)
|
// 获取 Tavily API Key(从配置文件)
|
||||||
const apiKey = process.env.TAVILY_API_KEY;
|
const apiKey = await getServiceApiKey('tavily');
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
output: '',
|
output: '',
|
||||||
error: '未配置 Tavily API Key。请设置环境变量 TAVILY_API_KEY。',
|
error: '未配置 Tavily API Key。请在设置中配置 Tavily 服务的 API Key。',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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, 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 {
|
import {
|
||||||
handleWebSocket,
|
handleWebSocket,
|
||||||
handleWebSocketMessage,
|
handleWebSocketMessage,
|
||||||
@@ -88,6 +88,7 @@ api.route('/hooks', hooksRouter);
|
|||||||
api.route('/agents', agentsRouter);
|
api.route('/agents', agentsRouter);
|
||||||
api.route('/checkpoints', checkpointsRouter);
|
api.route('/checkpoints', checkpointsRouter);
|
||||||
api.route('/providers', providersRouter);
|
api.route('/providers', providersRouter);
|
||||||
|
api.route('/services', servicesRouter);
|
||||||
api.route('/lsp', lspRouter);
|
api.route('/lsp', lspRouter);
|
||||||
|
|
||||||
// 上下文压缩相关(挂载到根路径,内部路由包含 /sessions/:id/context)
|
// 上下文压缩相关(挂载到根路径,内部路由包含 /sessions/:id/context)
|
||||||
|
|||||||
@@ -14,5 +14,6 @@ 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 { servicesRouter } from './services.js';
|
||||||
export { contextRouter } from './context.js';
|
export { contextRouter } from './context.js';
|
||||||
export { lspRouter } from './lsp.js';
|
export { lspRouter } from './lsp.js';
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user