mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-20 05:11:48 +00:00
feat(websearch): add multi-tier MCP fallback for third-party profiles
Implements CCS WebSearch Native Infrastructure (Phases 1-3): Phase 1 - Multi-tier MCP Fallback: - Add mcp-manager.ts with ensureMcpWebSearch() for auto-configuration - Support web-search-prime (primary), Brave Search, and Tavily MCPs - Case-insensitive detection of existing web search MCPs - Block-websearch hook for optional native WebSearch blocking Phase 2 - User Configuration: - Add WebSearchConfig interface to unified-config-types.ts - Add getWebSearchConfig() to unified-config-loader.ts - Support configurable webSearchPrimeUrl for security - Add GET/PUT /api/websearch endpoints in routes.ts - Add WebSearch settings UI in settings.tsx dashboard Phase 3 - Documentation & Testing: - Add WebSearch section to README.md - Create comprehensive docs/websearch.md guide - Add 23 unit tests for MCP manager logic Enables web search for gemini, codex, agy, qwen, glm, kimi profiles that cannot access Anthropic's native WebSearch API.
This commit is contained in:
1 parent
8ec7e99038
commit
071ec041ed
11 files changed
+1314
-29
No files matched your search
@@ -14,6 +14,7 @@ import { detectClaudeCli } from './utils/claude-detector';
|
||||
import { getSettingsPath } from './utils/config-manager';
|
||||
import { ErrorManager } from './utils/error-manager';
|
||||
import { execClaudeWithCLIProxy, CLIProxyProvider } from './cliproxy';
|
||||
import { ensureMcpWebSearch } from './utils/mcp-manager';
|
||||
|
||||
// Import extracted command handlers
|
||||
import { handleVersionCommand } from './commands/version-command';
|
||||
@@ -378,6 +379,10 @@ async function main(): Promise<void> {
|
||||
const customSettingsPath = profileInfo.settingsPath; // undefined for hardcoded profiles
|
||||
await execClaudeWithCLIProxy(claudeCli, provider, remainingArgs, { customSettingsPath });
|
||||
} else if (profileInfo.type === 'settings') {
|
||||
// Settings-based profiles (glm, glmt, kimi) are third-party providers
|
||||
// WebSearch is server-side tool - third-party providers have no access
|
||||
ensureMcpWebSearch();
|
||||
|
||||
// Check if this is GLMT profile (requires proxy)
|
||||
if (profileInfo.name === 'glmt') {
|
||||
// GLMT FLOW: Settings-based with embedded proxy for thinking support
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
} from './account-manager';
|
||||
import { getPortCheckCommand, getCatCommand, killProcessOnPort } from '../utils/platform-commands';
|
||||
import { getPortProcess, isCLIProxyProcess } from '../utils/port-utils';
|
||||
import { ensureMcpWebSearch } from '../utils/mcp-manager';
|
||||
|
||||
/** Default executor configuration */
|
||||
const DEFAULT_CONFIG: ExecutorConfig = {
|
||||
@@ -112,6 +113,11 @@ export async function execClaudeWithCLIProxy(
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure MCP web-search is configured for third-party profiles
|
||||
// WebSearch is a server-side tool executed by Anthropic's API
|
||||
// Third-party providers don't have access, so we use MCP fallback
|
||||
ensureMcpWebSearch();
|
||||
|
||||
// Validate provider
|
||||
const providerConfig = getProviderConfig(provider);
|
||||
log(`Provider: ${providerConfig.displayName}`);
|
||||
|
||||
@@ -115,6 +115,13 @@ function mergeWithDefaults(partial: Partial<UnifiedConfig>): UnifiedConfig {
|
||||
...defaults.preferences,
|
||||
...partial.preferences,
|
||||
},
|
||||
websearch: {
|
||||
enabled: partial.websearch?.enabled ?? defaults.websearch?.enabled ?? true,
|
||||
provider: partial.websearch?.provider ?? defaults.websearch?.provider ?? 'auto',
|
||||
fallback: partial.websearch?.fallback ?? defaults.websearch?.fallback ?? true,
|
||||
webSearchPrimeUrl:
|
||||
partial.websearch?.webSearchPrimeUrl ?? defaults.websearch?.webSearchPrimeUrl,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -149,8 +156,7 @@ function generateYamlHeader(): string {
|
||||
#
|
||||
# To customize a profile:
|
||||
# 1. Edit the *.settings.json file directly (e.g., ~/.ccs/glm.settings.json)
|
||||
# 2. The file format matches Claude's settings.json: { "env": { ... } }
|
||||
#
|
||||
# 2. The file format matches Claude's settings.json: { "env": { ... } }\n#
|
||||
# Structure:
|
||||
# ┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
# │ profiles - References to *.settings.json files for API providers │
|
||||
@@ -228,6 +234,23 @@ function generateYamlWithComments(config: UnifiedConfig): string {
|
||||
);
|
||||
lines.push('');
|
||||
|
||||
// WebSearch section
|
||||
if (config.websearch) {
|
||||
lines.push('# ----------------------------------------------------------------------------');
|
||||
lines.push('# WebSearch: MCP auto-configuration for third-party profiles');
|
||||
lines.push('# enabled: true/false - Enable/disable MCP web-search auto-config');
|
||||
lines.push('# provider: auto | web-search-prime | brave | tavily');
|
||||
lines.push('# fallback: true/false - Enable fallback chain when provider fails');
|
||||
lines.push('# API keys: Set BRAVE_API_KEY or TAVILY_API_KEY env vars');
|
||||
lines.push('# ----------------------------------------------------------------------------');
|
||||
lines.push(
|
||||
yaml
|
||||
.dump({ websearch: config.websearch }, { indent: 2, lineWidth: -1, quotingType: '"' })
|
||||
.trim()
|
||||
);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
@@ -292,3 +315,22 @@ export function getDefaultProfile(): string | undefined {
|
||||
export function setDefaultProfile(name: string): void {
|
||||
updateUnifiedConfig({ default: name });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get websearch configuration.
|
||||
* Returns defaults if not configured.
|
||||
*/
|
||||
export function getWebSearchConfig(): {
|
||||
enabled: boolean;
|
||||
provider: 'auto' | 'web-search-prime' | 'brave' | 'tavily';
|
||||
fallback: boolean;
|
||||
webSearchPrimeUrl?: string;
|
||||
} {
|
||||
const config = loadOrCreateUnifiedConfig();
|
||||
return {
|
||||
enabled: config.websearch?.enabled ?? true,
|
||||
provider: config.websearch?.provider ?? 'auto',
|
||||
fallback: config.websearch?.fallback ?? true,
|
||||
webSearchPrimeUrl: config.websearch?.webSearchPrimeUrl,
|
||||
};
|
||||
}
|
||||
@@ -100,6 +100,21 @@ export interface PreferencesConfig {
|
||||
auto_update?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSearch configuration.
|
||||
* Controls MCP web-search auto-configuration for third-party profiles.
|
||||
*/
|
||||
export interface WebSearchConfig {
|
||||
/** Enable auto-configuration of MCP web-search (default: true) */
|
||||
enabled?: boolean;
|
||||
/** Preferred provider: auto uses fallback chain, or specify one */
|
||||
provider?: 'auto' | 'web-search-prime' | 'brave' | 'tavily';
|
||||
/** Enable fallback chain when preferred provider fails (default: true) */
|
||||
fallback?: boolean;
|
||||
/** Custom URL for web-search-prime provider (optional, overrides default) */
|
||||
webSearchPrimeUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main unified configuration structure.
|
||||
* Stored in ~/.ccs/config.yaml
|
||||
@@ -117,6 +132,8 @@ export interface UnifiedConfig {
|
||||
cliproxy: CLIProxyConfig;
|
||||
/** User preferences */
|
||||
preferences: PreferencesConfig;
|
||||
/** WebSearch configuration */
|
||||
websearch?: WebSearchConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,6 +171,11 @@ export function createEmptyUnifiedConfig(): UnifiedConfig {
|
||||
telemetry: false,
|
||||
auto_update: true,
|
||||
},
|
||||
websearch: {
|
||||
enabled: true,
|
||||
provider: 'auto',
|
||||
fallback: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
/**
|
||||
* MCP Manager - Manages MCP server configuration for CCS
|
||||
*
|
||||
* Ensures web-search-prime MCP is available for third-party profiles
|
||||
* that cannot use Claude's native WebSearch tool.
|
||||
*
|
||||
* WebSearch is a server-side tool executed by Anthropic's API.
|
||||
* Third-party providers (gemini, agy, codex, qwen) don't have access.
|
||||
* This manager auto-configures MCP web-search as a fallback.
|
||||
*
|
||||
* @module utils/mcp-manager
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { info, warn } from './ui';
|
||||
import { getWebSearchConfig } from '../config/unified-config-loader';
|
||||
|
||||
// MCP configuration file path
|
||||
const MCP_CONFIG_PATH = path.join(os.homedir(), '.claude', '.mcp.json');
|
||||
|
||||
/**
|
||||
* MCP server configuration interface
|
||||
*/
|
||||
interface McpServerConfig {
|
||||
type: 'http' | 'stdio';
|
||||
url?: string;
|
||||
headers?: Record<string, string>;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP configuration file structure
|
||||
*/
|
||||
interface McpConfig {
|
||||
mcpServers?: Record<string, McpServerConfig>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default web-search-prime MCP configuration (primary)
|
||||
* HTTP-based, no API key needed
|
||||
*/
|
||||
const WEB_SEARCH_PRIME_CONFIG: McpServerConfig = {
|
||||
type: 'http',
|
||||
url: 'https://api.z.ai/api/mcp/web_search_prime/mcp',
|
||||
headers: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Brave Search MCP configuration (secondary fallback)
|
||||
* Requires BRAVE_API_KEY env var
|
||||
* Free tier: 15k queries/month, 1 query/sec
|
||||
* Package: @modelcontextprotocol/server-brave-search
|
||||
*/
|
||||
const BRAVE_SEARCH_CONFIG: McpServerConfig = {
|
||||
type: 'stdio',
|
||||
command: 'npx',
|
||||
args: ['-y', '@modelcontextprotocol/server-brave-search'],
|
||||
env: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Tavily MCP configuration (tertiary fallback)
|
||||
* Requires TAVILY_API_KEY env var
|
||||
* AI-optimized search, paid service
|
||||
* Package: @tavily/mcp-server (official)
|
||||
*/
|
||||
const TAVILY_CONFIG: McpServerConfig = {
|
||||
type: 'stdio',
|
||||
command: 'npx',
|
||||
args: ['-y', '@tavily/mcp-server'],
|
||||
env: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Ensure MCP web-search is configured for third-party profiles
|
||||
*
|
||||
* Called before spawning Claude CLI for CLIProxy profiles.
|
||||
* Respects user configuration from ~/.ccs/config.yaml:
|
||||
* - enabled: false → skip auto-config
|
||||
* - provider: specific → add only that provider
|
||||
* - fallback: false → don't add fallback providers
|
||||
*
|
||||
* Multi-tier MCP fallback chain:
|
||||
* 1. web-search-prime (primary, no API key needed)
|
||||
* 2. brave-search (if BRAVE_API_KEY set)
|
||||
* 3. tavily (if TAVILY_API_KEY set)
|
||||
*
|
||||
* Only adds MCPs if no web search MCP is already configured.
|
||||
*
|
||||
* @returns true if web search MCP is available, false on error
|
||||
*/
|
||||
export function ensureMcpWebSearch(): boolean {
|
||||
try {
|
||||
// Check user configuration
|
||||
const wsConfig = getWebSearchConfig();
|
||||
|
||||
// If disabled by user, skip auto-configuration
|
||||
if (!wsConfig.enabled) {
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(info('WebSearch auto-config disabled by user'));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let config: McpConfig = { mcpServers: {} };
|
||||
|
||||
// Read existing config if present
|
||||
if (fs.existsSync(MCP_CONFIG_PATH)) {
|
||||
const content = fs.readFileSync(MCP_CONFIG_PATH, 'utf8');
|
||||
try {
|
||||
config = JSON.parse(content);
|
||||
} catch {
|
||||
// Malformed JSON - start fresh but preserve file as backup
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(warn('Malformed .mcp.json - starting fresh'));
|
||||
}
|
||||
config = { mcpServers: {} };
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize mcpServers if missing
|
||||
if (!config.mcpServers) {
|
||||
config.mcpServers = {};
|
||||
}
|
||||
|
||||
// Check if any web search MCP already configured (case-insensitive)
|
||||
const hasWebSearch = Object.keys(config.mcpServers).some((key) => {
|
||||
const lowerKey = key.toLowerCase();
|
||||
return (
|
||||
lowerKey.includes('web-search') ||
|
||||
lowerKey.includes('websearch') ||
|
||||
lowerKey.includes('tavily') ||
|
||||
lowerKey.includes('brave')
|
||||
);
|
||||
});
|
||||
|
||||
if (hasWebSearch) {
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(info('MCP web-search already configured'));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Track what we add for logging
|
||||
const addedMcps: string[] = [];
|
||||
|
||||
// Helper to add a specific provider
|
||||
const addProvider = (provider: 'web-search-prime' | 'brave' | 'tavily'): boolean => {
|
||||
// mcpServers is guaranteed to exist here (initialized above)
|
||||
const servers = config.mcpServers as Record<string, McpServerConfig>;
|
||||
|
||||
if (provider === 'web-search-prime') {
|
||||
const webSearchPrimeConfig = { ...WEB_SEARCH_PRIME_CONFIG };
|
||||
// Use configurable URL if provided
|
||||
if (wsConfig.webSearchPrimeUrl) {
|
||||
webSearchPrimeConfig.url = wsConfig.webSearchPrimeUrl;
|
||||
}
|
||||
servers['web-search-prime'] = webSearchPrimeConfig;
|
||||
addedMcps.push('web-search-prime');
|
||||
return true;
|
||||
}
|
||||
|
||||
if (provider === 'brave') {
|
||||
const braveApiKey = process.env.BRAVE_API_KEY;
|
||||
if (braveApiKey) {
|
||||
const braveConfig = { ...BRAVE_SEARCH_CONFIG };
|
||||
braveConfig.env = { BRAVE_API_KEY: braveApiKey };
|
||||
servers['brave-search'] = braveConfig;
|
||||
addedMcps.push('brave-search');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (provider === 'tavily') {
|
||||
const tavilyApiKey = process.env.TAVILY_API_KEY;
|
||||
if (tavilyApiKey) {
|
||||
const tavilyConfig = { ...TAVILY_CONFIG };
|
||||
tavilyConfig.env = { TAVILY_API_KEY: tavilyApiKey };
|
||||
servers['tavily'] = tavilyConfig;
|
||||
addedMcps.push('tavily');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Apply user's provider preference
|
||||
if (wsConfig.provider === 'auto') {
|
||||
// Auto mode: add all available providers
|
||||
addProvider('web-search-prime');
|
||||
if (wsConfig.fallback) {
|
||||
addProvider('brave');
|
||||
addProvider('tavily');
|
||||
}
|
||||
} else {
|
||||
// Specific provider requested
|
||||
const added = addProvider(wsConfig.provider);
|
||||
if (!added && wsConfig.fallback) {
|
||||
// Fallback if preferred provider not available
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(
|
||||
warn(`Preferred provider ${wsConfig.provider} not available, using fallback`)
|
||||
);
|
||||
}
|
||||
addProvider('web-search-prime');
|
||||
addProvider('brave');
|
||||
addProvider('tavily');
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing was added, return false
|
||||
if (addedMcps.length === 0) {
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(warn('No web search MCP could be configured'));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure ~/.claude directory exists
|
||||
const claudeDir = path.dirname(MCP_CONFIG_PATH);
|
||||
if (!fs.existsSync(claudeDir)) {
|
||||
fs.mkdirSync(claudeDir, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
|
||||
// Write config with proper formatting
|
||||
fs.writeFileSync(MCP_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
|
||||
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(info(`Added MCP servers for web search: ${addedMcps.join(', ')}`));
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(warn(`Failed to configure MCP: ${(error as Error).message}`));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if MCP web-search is configured
|
||||
*
|
||||
* @returns true if any web search MCP is present
|
||||
*/
|
||||
export function hasMcpWebSearch(): boolean {
|
||||
try {
|
||||
if (!fs.existsSync(MCP_CONFIG_PATH)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(MCP_CONFIG_PATH, 'utf8');
|
||||
const config: McpConfig = JSON.parse(content);
|
||||
|
||||
if (!config.mcpServers) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Case-insensitive check for web search MCP
|
||||
return Object.keys(config.mcpServers).some((key) => {
|
||||
const lowerKey = key.toLowerCase();
|
||||
return (
|
||||
lowerKey.includes('web-search') ||
|
||||
lowerKey.includes('websearch') ||
|
||||
lowerKey.includes('tavily') ||
|
||||
lowerKey.includes('brave')
|
||||
);
|
||||
});
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path to MCP config file
|
||||
*
|
||||
* @returns absolute path to .mcp.json
|
||||
*/
|
||||
export function getMcpConfigPath(): string {
|
||||
return MCP_CONFIG_PATH;
|
||||
}
|
||||
|
||||
// CCS hooks directory
|
||||
const CCS_HOOKS_DIR = path.join(os.homedir(), '.ccs', 'hooks');
|
||||
const BLOCK_WEBSEARCH_HOOK = 'block-websearch.cjs';
|
||||
|
||||
/**
|
||||
* Install WebSearch blocking hook to ~/.ccs/hooks/
|
||||
*
|
||||
* This hook blocks native WebSearch and directs Claude to use MCP.
|
||||
* Optional feature - must be explicitly enabled.
|
||||
*
|
||||
* @returns true if hook installed successfully
|
||||
*/
|
||||
export function installWebSearchHook(): boolean {
|
||||
try {
|
||||
// Ensure hooks directory exists
|
||||
if (!fs.existsSync(CCS_HOOKS_DIR)) {
|
||||
fs.mkdirSync(CCS_HOOKS_DIR, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
|
||||
const hookPath = path.join(CCS_HOOKS_DIR, BLOCK_WEBSEARCH_HOOK);
|
||||
|
||||
// Find the bundled hook script
|
||||
// In npm package: node_modules/ccs/lib/hooks/block-websearch.cjs
|
||||
// In development: lib/hooks/block-websearch.cjs
|
||||
const possiblePaths = [
|
||||
path.join(__dirname, '..', '..', 'lib', 'hooks', BLOCK_WEBSEARCH_HOOK),
|
||||
path.join(__dirname, '..', 'lib', 'hooks', BLOCK_WEBSEARCH_HOOK),
|
||||
];
|
||||
|
||||
let sourcePath: string | null = null;
|
||||
for (const p of possiblePaths) {
|
||||
if (fs.existsSync(p)) {
|
||||
sourcePath = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sourcePath) {
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(warn('WebSearch hook source not found'));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Copy hook to ~/.ccs/hooks/
|
||||
fs.copyFileSync(sourcePath, hookPath);
|
||||
fs.chmodSync(hookPath, 0o755);
|
||||
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(info(`Installed WebSearch blocking hook to ${hookPath}`));
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(warn(`Failed to install WebSearch hook: ${(error as Error).message}`));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WebSearch hook configuration for settings.json
|
||||
*
|
||||
* @returns hook configuration object
|
||||
*/
|
||||
export function getWebSearchHookConfig(): Record<string, unknown> {
|
||||
const hookPath = path.join(CCS_HOOKS_DIR, BLOCK_WEBSEARCH_HOOK);
|
||||
|
||||
return {
|
||||
PreToolUse: [
|
||||
{
|
||||
matcher: 'WebSearch',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command: `node "${hookPath}"`,
|
||||
timeout: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WebSearch blocking hook is installed
|
||||
*
|
||||
* @returns true if hook exists
|
||||
*/
|
||||
export function hasWebSearchHook(): boolean {
|
||||
const hookPath = path.join(CCS_HOOKS_DIR, BLOCK_WEBSEARCH_HOOK);
|
||||
return fs.existsSync(hookPath);
|
||||
}
|
||||
@@ -53,6 +53,8 @@ import {
|
||||
getBackupDirectories,
|
||||
} from '../config/migration-manager';
|
||||
import { getProfileSecrets, setProfileSecrets } from '../config/secrets-manager';
|
||||
import { getWebSearchConfig } from '../config/unified-config-loader';
|
||||
import type { WebSearchConfig } from '../config/unified-config-types';
|
||||
import { isUnifiedConfig } from '../config/unified-config-types';
|
||||
import { isSensitiveKey, maskSensitiveValue } from '../utils/sensitive-keys';
|
||||
|
||||
@@ -1397,3 +1399,79 @@ apiRoutes.delete('/cliproxy/openai-compat/:name', (req: Request, res: Response):
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== WebSearch Configuration ====================
|
||||
|
||||
/**
|
||||
* GET /api/websearch - Get WebSearch configuration
|
||||
* Returns: WebSearchConfig with enabled, provider, fallback
|
||||
*/
|
||||
apiRoutes.get('/websearch', (_req: Request, res: Response): void => {
|
||||
try {
|
||||
const config = getWebSearchConfig();
|
||||
res.json(config);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/websearch - Update WebSearch configuration
|
||||
* Body: { enabled?: boolean, provider?: string, fallback?: boolean }
|
||||
*/
|
||||
apiRoutes.put('/websearch', (req: Request, res: Response): void => {
|
||||
const { enabled, provider, fallback, webSearchPrimeUrl } = req.body as Partial<WebSearchConfig>;
|
||||
|
||||
// Validate enabled
|
||||
if (enabled !== undefined && typeof enabled !== 'boolean') {
|
||||
res.status(400).json({ error: 'Invalid value for enabled. Must be a boolean.' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate fallback
|
||||
if (fallback !== undefined && typeof fallback !== 'boolean') {
|
||||
res.status(400).json({ error: 'Invalid value for fallback. Must be a boolean.' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate provider if specified
|
||||
const validProviders = ['auto', 'web-search-prime', 'brave', 'tavily'];
|
||||
if (provider && !validProviders.includes(provider)) {
|
||||
res.status(400).json({
|
||||
error: `Invalid provider. Must be one of: ${validProviders.join(', ')}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate webSearchPrimeUrl if specified
|
||||
if (webSearchPrimeUrl !== undefined && typeof webSearchPrimeUrl !== 'string') {
|
||||
res.status(400).json({ error: 'Invalid value for webSearchPrimeUrl. Must be a string.' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Load existing config and update websearch section
|
||||
const existingConfig = loadUnifiedConfig();
|
||||
if (!existingConfig) {
|
||||
res.status(500).json({ error: 'Failed to load config' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Merge updates
|
||||
existingConfig.websearch = {
|
||||
enabled: enabled ?? existingConfig.websearch?.enabled ?? true,
|
||||
provider: provider ?? existingConfig.websearch?.provider ?? 'auto',
|
||||
fallback: fallback ?? existingConfig.websearch?.fallback ?? true,
|
||||
webSearchPrimeUrl: webSearchPrimeUrl ?? existingConfig.websearch?.webSearchPrimeUrl,
|
||||
};
|
||||
|
||||
saveUnifiedConfig(existingConfig);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
websearch: existingConfig.websearch,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user