mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 06:15:03 +00:00
feat(websearch): add managed mcp runtime and provider cooldowns
- switch ccs-websearch MCP to stdio-first framing and keep legacy compatibility - add cooldown persistence and bounded retries for provider failures - surface cooldown status in readiness output and regression tests
This commit is contained in:
1 parent
cae5b710b8
commit
7f83e041b7
10 files changed
+1115
-63
No files matched your search
@@ -20,6 +20,13 @@ interface ClaudeUserConfig {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface ManagedWebSearchMcpConfig {
|
||||
type: 'stdio';
|
||||
command: 'node';
|
||||
args: [string];
|
||||
env: Record<string, string>;
|
||||
}
|
||||
|
||||
function getCcsMcpDir(): string {
|
||||
return path.join(getCcsDir(), 'mcp');
|
||||
}
|
||||
@@ -91,9 +98,11 @@ function readClaudeUserConfig(configPath: string): ClaudeUserConfig | null {
|
||||
|
||||
function writeClaudeUserConfig(configPath: string, config: ClaudeUserConfig): boolean {
|
||||
const tempPath = getTempPath(configPath);
|
||||
const fileMode = fs.existsSync(configPath) ? fs.statSync(configPath).mode & 0o777 : 0o600;
|
||||
|
||||
try {
|
||||
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
||||
fs.chmodSync(tempPath, fileMode);
|
||||
fs.renameSync(tempPath, configPath);
|
||||
return true;
|
||||
} finally {
|
||||
@@ -252,9 +261,11 @@ export function ensureWebSearchMcpConfig(): boolean {
|
||||
config.mcpServers && typeof config.mcpServers === 'object' && !Array.isArray(config.mcpServers)
|
||||
? (config.mcpServers as Record<string, unknown>)
|
||||
: {};
|
||||
const desiredServerConfig = {
|
||||
const desiredServerConfig: ManagedWebSearchMcpConfig = {
|
||||
type: 'stdio',
|
||||
command: 'node',
|
||||
args: [getWebSearchMcpServerPath()],
|
||||
env: {},
|
||||
};
|
||||
|
||||
const currentConfig = existingServers[WEBSEARCH_MCP_SERVER_NAME];
|
||||
|
||||
@@ -6,18 +6,105 @@
|
||||
* @module utils/websearch/status
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { ok, warn, fail, info } from '../ui';
|
||||
import { getWebSearchConfig } from '../../config/unified-config-loader';
|
||||
import { getCcsDir } from '../config-manager';
|
||||
import { getGeminiCliStatus, isGeminiAuthenticated } from './gemini-cli';
|
||||
import { getGrokCliStatus } from './grok-cli';
|
||||
import { getOpenCodeCliStatus } from './opencode-cli';
|
||||
import { getWebSearchApiKeyStates } from './provider-secrets';
|
||||
import type { WebSearchCliInfo, WebSearchStatus } from './types';
|
||||
|
||||
const PROVIDER_STATE_FILE = 'websearch-provider-state.json';
|
||||
|
||||
type ProviderCooldown = {
|
||||
reason: string;
|
||||
until: number;
|
||||
};
|
||||
|
||||
function hasEnvValue(name: string): boolean {
|
||||
return (process.env[name] || '').trim().length > 0;
|
||||
}
|
||||
|
||||
function getProviderStatePath(): string {
|
||||
return join(getCcsDir(), 'cache', PROVIDER_STATE_FILE);
|
||||
}
|
||||
|
||||
function readProviderCooldowns(now = Date.now()): Record<string, ProviderCooldown> {
|
||||
try {
|
||||
const statePath = getProviderStatePath();
|
||||
if (!existsSync(statePath)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(readFileSync(statePath, 'utf8')) as {
|
||||
cooldowns?: Record<string, { reason?: unknown; until?: unknown }>;
|
||||
};
|
||||
const nextCooldowns: Record<string, ProviderCooldown> = {};
|
||||
|
||||
for (const [providerId, entry] of Object.entries(parsed.cooldowns || {})) {
|
||||
const until = Number.parseInt(String(entry?.until || ''), 10);
|
||||
if (!Number.isFinite(until) || until <= now) {
|
||||
continue;
|
||||
}
|
||||
|
||||
nextCooldowns[providerId] = {
|
||||
reason: typeof entry?.reason === 'string' ? entry.reason : 'rate_limited',
|
||||
until,
|
||||
};
|
||||
}
|
||||
|
||||
return nextCooldowns;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function formatCooldownDuration(until: number, now = Date.now()): string {
|
||||
const remainingSec = Math.max(1, Math.ceil((until - now) / 1000));
|
||||
if (remainingSec >= 3600) {
|
||||
return `~${Math.ceil(remainingSec / 3600)}h`;
|
||||
}
|
||||
if (remainingSec >= 60) {
|
||||
return `~${Math.ceil(remainingSec / 60)}m`;
|
||||
}
|
||||
return `~${remainingSec}s`;
|
||||
}
|
||||
|
||||
function formatCooldownReason(reason: string): string {
|
||||
switch (reason) {
|
||||
case 'quota_exhausted':
|
||||
return 'quota exhaustion';
|
||||
case 'rate_limited':
|
||||
return 'rate limiting';
|
||||
default:
|
||||
return 'a temporary provider error';
|
||||
}
|
||||
}
|
||||
|
||||
function applyCooldownStatus(
|
||||
provider: WebSearchCliInfo,
|
||||
cooldowns: Record<string, ProviderCooldown>,
|
||||
now = Date.now()
|
||||
): WebSearchCliInfo {
|
||||
if (!(provider.enabled && provider.available)) {
|
||||
return provider;
|
||||
}
|
||||
|
||||
const cooldown = cooldowns[provider.id];
|
||||
if (!cooldown) {
|
||||
return provider;
|
||||
}
|
||||
|
||||
return {
|
||||
...provider,
|
||||
available: false,
|
||||
detail: `Cooling down ${formatCooldownDuration(cooldown.until, now)} after ${formatCooldownReason(cooldown.reason)}`,
|
||||
};
|
||||
}
|
||||
|
||||
function getLegacyProviderStatuses(): WebSearchCliInfo[] {
|
||||
const wsConfig = getWebSearchConfig();
|
||||
const geminiStatus = getGeminiCliStatus();
|
||||
@@ -86,6 +173,7 @@ function getLegacyProviderStatuses(): WebSearchCliInfo[] {
|
||||
export function getWebSearchCliProviders(): WebSearchCliInfo[] {
|
||||
const wsConfig = getWebSearchConfig();
|
||||
const apiKeyStates = getWebSearchApiKeyStates();
|
||||
const cooldowns = readProviderCooldowns();
|
||||
const providers: WebSearchCliInfo[] = [
|
||||
{
|
||||
id: 'exa',
|
||||
@@ -152,7 +240,9 @@ export function getWebSearchCliProviders(): WebSearchCliInfo[] {
|
||||
},
|
||||
];
|
||||
|
||||
return [...providers, ...getLegacyProviderStatuses()];
|
||||
return [...providers, ...getLegacyProviderStatuses()].map((provider) =>
|
||||
applyCooldownStatus(provider, cooldowns)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user