// @ts-nocheck import { CONFIG, loadToken } from './config.js'; const TOKEN = loadToken(); const _subsysSessions = new Map(); const _toolToSubsys = new Map(); const FALLBACK_SUBSYSTEMS = [ { id: 'assignment', name: 'Assignment', descPrefix: '司机任务排班', healthUrl: 'http://127.0.0.1:7777/health' }, { id: 'profile', name: 'Profile', descPrefix: '员工能力画像', healthUrl: 'http://127.0.0.1:7778/health' }, ]; function genId() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 6); } async function discoverSubsystems() { try { const r = await fetch(`${CONFIG.ZHONGTAI_BASE}/subsystems`, { signal: AbortSignal.timeout(2000), }); if (!r.ok) return FALLBACK_SUBSYSTEMS; const data = await r.json(); const list = data.subsystems || []; return list.length > 0 ? list : FALLBACK_SUBSYSTEMS; } catch (e) { return FALLBACK_SUBSYSTEMS; } } async function checkHealth(healthUrl) { try { const r = await fetch(healthUrl, { signal: AbortSignal.timeout(2000) }); if (!r.ok) return false; const data = await r.json(); if (data.ok !== true) return false; if (String(healthUrl).includes(':7777')) { if (data.service === 'profile-mcp-http') return false; if (data.service && data.service !== 'assignment-mcp-http') return false; if (!data.service && !(data.result && typeof data.result.appStarted !== 'undefined')) return false; } return true; } catch (e) { return false; } } function mcpUrlFromHealth(healthUrl) { const url = new URL(healthUrl); return `${url.protocol}//${url.host}/mcp`; } async function ensureSubsystemInitialized(subsys) { if (!subsys.healthUrl) return null; const existing = _subsysSessions.get(subsys.id); if (existing?.initialized) return existing; const healthy = await checkHealth(subsys.healthUrl); if (!healthy) return null; const mcpUrl = mcpUrlFromHealth(subsys.healthUrl); const r = await fetch(mcpUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(TOKEN ? { 'X-MCP-Token': TOKEN } : {}), }, body: JSON.stringify({ jsonrpc: '2.0', id: genId(), method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'ccsparkle-webui', version: '1.0.0' }, }, }), }); if (!r.ok) return null; const sessionHeader = r.headers.get('mcp-session-id'); const msg = await r.json(); if (msg.error) return null; const entry = { mcpUrl, sessionId: sessionHeader, initialized: true, healthUrl: subsys.healthUrl, }; _subsysSessions.set(subsys.id, entry); try { await fetch(mcpUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(TOKEN ? { 'X-MCP-Token': TOKEN } : {}), ...(sessionHeader ? { 'Mcp-Session-Id': sessionHeader } : {}), }, body: JSON.stringify({ jsonrpc: '2.0', method: 'initialized' }), }); } catch (e) {} return entry; } export async function initialize() { const subsystems = await discoverSubsystems(); for (const subsys of subsystems) { if (!subsys.healthUrl) continue; try { await ensureSubsystemInitialized(subsys); } catch (e) { console.warn(`[mcp-client] init ${subsys.id} failed:`, e.message); } } } async function rpcOnSubsystem(subsysId, method, params, extraHeaders = {}) { const entry = _subsysSessions.get(subsysId); if (!entry) throw new Error(`Subsystem ${subsysId} not initialized`); const headers = { 'Content-Type': 'application/json', ...extraHeaders }; if (TOKEN) headers['X-MCP-Token'] = TOKEN; if (entry.sessionId) headers['Mcp-Session-Id'] = entry.sessionId; const r = await fetch(entry.mcpUrl, { method: 'POST', headers, body: JSON.stringify({ jsonrpc: '2.0', id: genId(), method, params }), }); if (!r.ok) { throw new Error(`MCP HTTP ${r.status}: ${await r.text()}`); } const text = await r.text(); if (!text) return null; const msg = JSON.parse(text); if (msg.error) { throw new Error(`MCP error [${msg.error.code}]: ${msg.error.message}`); } return msg.result; } export async function listTools() { await initialize(); const subsystems = await discoverSubsystems(); const allTools = []; _toolToSubsys.clear(); for (const subsys of subsystems) { if (!subsys.healthUrl) continue; const entry = _subsysSessions.get(subsys.id); if (!entry?.initialized) continue; try { const result = await rpcOnSubsystem(subsys.id, 'tools/list', {}); const tools = result?.tools || []; for (const t of tools) { _toolToSubsys.set(t.name, subsys.id); allTools.push(t); } } catch (e) { console.warn(`[mcp-client] listTools from ${subsys.id} failed:`, e.message); } } return allTools; } function prefixToSubsys(name) { if (name.startsWith('metro_')) return 'assignment'; if (name.startsWith('profile_')) return 'profile'; return null; } export function getToolSubsystemId(toolName) { const name = String(toolName || ''); if (!name) return null; return _toolToSubsys.get(name) || prefixToSubsys(name); } export async function callTool(name, args, opts = {}) { await initialize(); const t0 = performance.now(); let subsysId = _toolToSubsys.get(name); if (!subsysId) { subsysId = prefixToSubsys(name); if (subsysId) { const subsystems = await discoverSubsystems(); const subsys = subsystems.find(s => s.id === subsysId); if (subsys) { try { await ensureSubsystemInitialized(subsys); } catch (e) {} } } } if (!subsysId) { throw new Error(`Tool ${name} not routed to any subsystem`); } const entry = _subsysSessions.get(subsysId); if (!entry) { throw new Error(`Subsystem ${subsysId} not available for tool ${name}`); } const headers = {}; if (opts.traceId) headers['X-Trace-Id'] = opts.traceId; const result = await rpcOnSubsystem(subsysId, 'tools/call', { name, arguments: args }, headers); return parseToolResult(result, t0); } function parseToolResult(result, t0) { const durationMs = performance.now() - t0; const content = (result?.content || []) .filter(c => c.type === 'text') .map(c => c.text) .join('\n'); const trimmed = String(content || '').trim(); let peeled; if (trimmed.startsWith('{')) { let parsed; try { parsed = JSON.parse(trimmed); } catch (e) { peeled = content; parsed = null; } if (parsed !== null) { peeled = peelHandlerEnvelope(parsed); } } else { peeled = content; } return { ok: !result?.isError, result: peeled, rawContent: content, error: result?.isError ? { message: content } : null, meta: result?._meta, durationMs, protocol: 'mcp', }; } function peelHandlerEnvelope(parsed) { if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return parsed; if (typeof parsed.ok !== 'boolean') return parsed; if (parsed.ok === false) return parsed; if (parsed.result !== undefined) return parsed.result; const { ok: _ok, ...rest } = parsed; return Object.keys(rest).length ? rest : parsed; } export function getSessionId() { return _subsysSessions.get('assignment')?.sessionId || null; } export function isSubsystemOnline(subsysId) { const entry = _subsysSessions.get(subsysId); return !!entry?.initialized; } export function listOnlineSubsystems() { const ids = []; for (const [id, entry] of _subsysSessions.entries()) { if (entry?.initialized) ids.push(id); } return ids; } export async function notifyContextCleared() { const targets = []; for (const [id, entry] of _subsysSessions.entries()) { if (entry?.initialized) targets.push([id, entry]); } await Promise.all(targets.map(async ([id, entry]) => { try { await fetch(entry.mcpUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(TOKEN ? { 'X-MCP-Token': TOKEN } : {}), ...(entry.sessionId ? { 'Mcp-Session-Id': entry.sessionId } : {}), }, body: JSON.stringify({ jsonrpc: '2.0', method: 'notifications/context_cleared' }), }); } catch (e) { console.warn(`[mcp-client] notifyContextCleared to ${id} failed:`, e.message); } })); } const _maskingConfigListeners = new Set(); export function onMaskingConfigChanged(fn) { if (typeof fn === 'function') _maskingConfigListeners.add(fn); return () => _maskingConfigListeners.delete(fn); } function _emitMaskingConfigChanged(payload) { for (const fn of _maskingConfigListeners) { try { fn(payload); } catch (e) { console.warn('[mcp-client] masking_config_changed listener error:', e.message); } } } export function _handleIncomingNotification(msg) { if (!msg || msg.jsonrpc !== '2.0' || typeof msg.method !== 'string') return; if (msg.method === 'notifications/masking_config_changed') { _emitMaskingConfigChanged(msg.params || {}); } else if (msg.method === 'notifications/context_cleared') { } }