产品化改造:完善本地集成与启动体验
This commit is contained in:
@@ -5,6 +5,7 @@ import { startComputerServer } from "../services/computerServer";
|
||||
import {
|
||||
mcpProxyManager,
|
||||
DEFAULT_MCP_PROXY_CONFIG,
|
||||
pruneDisabledDefaultMcpServers,
|
||||
} from "../services/packages/mcp";
|
||||
import { getConfiguredPorts } from "../services/startupPorts";
|
||||
import { DEPS_SYNC_TIMEOUT } from "@shared/constants";
|
||||
@@ -55,13 +56,14 @@ export async function runStartupTasks(): Promise<void> {
|
||||
if (savedConfig) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedConfig.value);
|
||||
// 合并默认服务器(如 chrome-devtools),确保内置 MCP 服务始终存在
|
||||
// 合并可用的默认服务器;旧版本持久化的 chrome-devtools npx 默认项
|
||||
// 在本地未安装且未显式启用时会被过滤,避免启动时阻塞 60s。
|
||||
const merged = {
|
||||
...parsed,
|
||||
mcpServers: {
|
||||
mcpServers: pruneDisabledDefaultMcpServers({
|
||||
...DEFAULT_MCP_PROXY_CONFIG.mcpServers,
|
||||
...(parsed.mcpServers || {}),
|
||||
},
|
||||
}),
|
||||
};
|
||||
mcpProxyManager.setConfig(merged);
|
||||
} catch (e) {
|
||||
|
||||
@@ -368,15 +368,71 @@ export function resolveServersConfig(
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
const CHROME_DEVTOOLS_MCP_NAME = "chrome-devtools";
|
||||
const CHROME_DEVTOOLS_MCP_PACKAGE = "chrome-devtools-mcp";
|
||||
|
||||
function findLocalChromeDevtoolsMcpBin(): string | null {
|
||||
const binName = isWindows()
|
||||
? `${CHROME_DEVTOOLS_MCP_PACKAGE}.cmd`
|
||||
: CHROME_DEVTOOLS_MCP_PACKAGE;
|
||||
const candidates = [
|
||||
path.join(
|
||||
os.homedir(),
|
||||
APP_DATA_DIR_NAME,
|
||||
"node_modules",
|
||||
".bin",
|
||||
binName,
|
||||
),
|
||||
path.join(process.cwd(), "node_modules", ".bin", binName),
|
||||
];
|
||||
|
||||
return candidates.find((candidate) => fs.existsSync(candidate)) ?? null;
|
||||
}
|
||||
|
||||
function shouldEnableDefaultChromeDevtoolsMcp(): boolean {
|
||||
return (
|
||||
process.env.QIMINGCLAW_ENABLE_CHROME_DEVTOOLS_MCP === "1" ||
|
||||
!!findLocalChromeDevtoolsMcpBin()
|
||||
);
|
||||
}
|
||||
|
||||
function getDefaultMcpServers(): Record<string, McpServerEntry> {
|
||||
if (!shouldEnableDefaultChromeDevtoolsMcp()) return {};
|
||||
|
||||
const localBin = findLocalChromeDevtoolsMcpBin();
|
||||
return {
|
||||
[CHROME_DEVTOOLS_MCP_NAME]: localBin
|
||||
? {
|
||||
command: localBin,
|
||||
args: [],
|
||||
persistent: true,
|
||||
}
|
||||
: {
|
||||
command: "npx",
|
||||
args: ["-y", `${CHROME_DEVTOOLS_MCP_PACKAGE}@latest`],
|
||||
persistent: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isLegacyDefaultChromeDevtoolsEntry(
|
||||
name: string,
|
||||
entry: McpServerEntry,
|
||||
): boolean {
|
||||
return (
|
||||
name === CHROME_DEVTOOLS_MCP_NAME &&
|
||||
!isRemoteEntry(entry) &&
|
||||
entry.command === "npx" &&
|
||||
Array.isArray(entry.args) &&
|
||||
entry.args.length === 2 &&
|
||||
entry.args[0] === "-y" &&
|
||||
entry.args[1] === `${CHROME_DEVTOOLS_MCP_PACKAGE}@latest`
|
||||
);
|
||||
}
|
||||
|
||||
/** 默认 mcpServers 配置 */
|
||||
export const DEFAULT_MCP_PROXY_CONFIG: McpServersConfig = {
|
||||
mcpServers: {
|
||||
"chrome-devtools": {
|
||||
command: "npx",
|
||||
args: ["-y", "chrome-devtools-mcp@latest"],
|
||||
persistent: true,
|
||||
},
|
||||
},
|
||||
mcpServers: getDefaultMcpServers(),
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -437,6 +493,24 @@ export function isRemoteEntry(
|
||||
return "url" in entry;
|
||||
}
|
||||
|
||||
export function pruneDisabledDefaultMcpServers(
|
||||
servers: Record<string, McpServerEntry> = {},
|
||||
): Record<string, McpServerEntry> {
|
||||
if (shouldEnableDefaultChromeDevtoolsMcp()) return servers;
|
||||
|
||||
const result: Record<string, McpServerEntry> = {};
|
||||
for (const [name, entry] of Object.entries(servers)) {
|
||||
if (isLegacyDefaultChromeDevtoolsEntry(name, entry)) {
|
||||
log.info(
|
||||
"[McpProxy] Default chrome-devtools MCP disabled (not installed locally; set QIMINGCLAW_ENABLE_CHROME_DEVTOOLS_MCP=1 to force npx)",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
result[name] = entry;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** mcpServers 配置(传给 qiming-mcp-stdio-proxy 的 JSON) */
|
||||
export interface McpServersConfig {
|
||||
mcpServers: Record<string, McpServerEntry>;
|
||||
@@ -1298,18 +1372,17 @@ export async function syncMcpConfigToProxyAndReload(
|
||||
// realOnly 为空时(用户删除了所有动态 MCP)不提前返回,
|
||||
// 继续执行以确保 bridge 仅运行默认服务(chrome-devtools)
|
||||
|
||||
// 始终以默认服务为基础,再叠加动态 MCP:
|
||||
// - 用户删除所有动态 MCP → merged 仅含 chrome-devtools
|
||||
// - 用户删除部分动态 MCP → merged 含 chrome-devtools + 剩余动态 MCP
|
||||
// - 用户新增动态 MCP → merged 含 chrome-devtools + 所有动态 MCP
|
||||
// 以可用的默认服务为基础,再叠加动态 MCP。默认 chrome-devtools
|
||||
// 只有本地已安装或显式启用时才注入,避免 npx @latest 在启动时阻塞 60s。
|
||||
const merged: Record<string, McpServerEntry> = {
|
||||
...DEFAULT_MCP_PROXY_CONFIG.mcpServers,
|
||||
...getDefaultMcpServers(),
|
||||
...realOnly,
|
||||
};
|
||||
const prunedMerged = pruneDisabledDefaultMcpServers(merged);
|
||||
|
||||
// 为所有 MCP 服务器注入基础环境变量(包括 PATH)
|
||||
const prepareStartedAt = Date.now();
|
||||
const mergedWithEnv = injectBaseEnvToMcpServers(merged);
|
||||
const mergedWithEnv = injectBaseEnvToMcpServers(prunedMerged);
|
||||
|
||||
// Bridge 只管理 persistent 服务(如 chrome-devtools),动态 MCP 不进 bridge。
|
||||
// 变更检测和重启均只针对 persistent servers,避免动态 MCP 变化时重启 chrome-devtools。
|
||||
|
||||
@@ -1448,10 +1448,26 @@ export async function checkQimingFileServerBundled(): Promise<{
|
||||
export async function checkClaudeCodeAcpBundled(): Promise<{
|
||||
available: boolean;
|
||||
version?: string;
|
||||
binPath?: string;
|
||||
}> {
|
||||
const bundledDir = getClaudeCodeAcpBundledDir();
|
||||
if (!bundledDir) {
|
||||
log.info("[checkClaudeCodeAcpBundled] Bundled not found");
|
||||
const local = await detectNpmPackage(
|
||||
"claude-code-acp-ts",
|
||||
"claude-code-acp-ts",
|
||||
);
|
||||
if (local.installed) {
|
||||
log.info(
|
||||
`[checkClaudeCodeAcpBundled] Bundled not found; using npm-local package: ${local.binPath ?? "unknown"}, version=${local.version ?? "unknown"}`,
|
||||
);
|
||||
return {
|
||||
available: true,
|
||||
version: local.version,
|
||||
binPath: local.binPath,
|
||||
};
|
||||
}
|
||||
|
||||
log.info("[checkClaudeCodeAcpBundled] Bundled and npm-local package not found");
|
||||
return { available: false };
|
||||
}
|
||||
const pkgPath = path.join(bundledDir, "package.json");
|
||||
@@ -1462,10 +1478,10 @@ export async function checkClaudeCodeAcpBundled(): Promise<{
|
||||
log.info(
|
||||
`[checkClaudeCodeAcpBundled] Bundled available: ${bundledDir}, version=${version ?? "unknown"}`,
|
||||
);
|
||||
return { available: true, version };
|
||||
return { available: true, version, binPath: bundledDir };
|
||||
} catch (e) {
|
||||
log.warn("[checkClaudeCodeAcpBundled] Failed to read package.json:", e);
|
||||
return { available: true };
|
||||
return { available: true, binPath: bundledDir };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1960,7 +1976,21 @@ export async function checkAllDependencies(options?: {
|
||||
item.status = "missing";
|
||||
}
|
||||
} else {
|
||||
item.status = "missing";
|
||||
const result = await detectNpmPackage(dep.name, dep.binName);
|
||||
item.version = result.version;
|
||||
item.binPath = result.binPath;
|
||||
if (!result.installed) {
|
||||
item.status = "missing";
|
||||
} else if (dep.installVersion) {
|
||||
const installed = (result.version ?? "0").replace(/^v/, "");
|
||||
const target = dep.installVersion.replace(/^v/, "");
|
||||
item.status =
|
||||
installed === "0" || compareVersions(installed, target) < 0
|
||||
? "outdated"
|
||||
: "installed";
|
||||
} else {
|
||||
item.status = "installed";
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user