mirror of
https://gh-proxy.org/https://github.com/klinsmaya/LobsterAI.git
synced 2026-11-08 01:06:44 +08:00
fix: 修复 MCP 不可用的问题
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
|
||||
|
||||
type McpBridgeToolConfig = {
|
||||
server: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
inputSchema: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type McpBridgePluginConfig = {
|
||||
callbackUrl: string;
|
||||
secret: string;
|
||||
requestTimeoutMs: number;
|
||||
tools: McpBridgeToolConfig[];
|
||||
};
|
||||
|
||||
type ToolResultPayload = {
|
||||
content: Array<{ type: string; text?: string; [key: string]: unknown }>;
|
||||
isError?: boolean;
|
||||
details?: unknown;
|
||||
};
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 120_000;
|
||||
const FALLBACK_INPUT_SCHEMA = Type.Object({}, { additionalProperties: true });
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> => {
|
||||
return !!value && typeof value === 'object' && !Array.isArray(value);
|
||||
};
|
||||
|
||||
const sanitizeToolSegment = (value: string): string => {
|
||||
const sanitized = value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '_')
|
||||
.replace(/^_+|_+$/g, '');
|
||||
return sanitized || 'tool';
|
||||
};
|
||||
|
||||
const buildRegisteredToolName = (
|
||||
server: string,
|
||||
tool: string,
|
||||
usedNames: Set<string>,
|
||||
): string => {
|
||||
const base = `mcp_${sanitizeToolSegment(server)}_${sanitizeToolSegment(tool)}`;
|
||||
let next = base;
|
||||
let index = 2;
|
||||
while (usedNames.has(next)) {
|
||||
next = `${base}_${index}`;
|
||||
index += 1;
|
||||
}
|
||||
usedNames.add(next);
|
||||
return next;
|
||||
};
|
||||
|
||||
const normalizeInputSchema = (value: unknown): Record<string, unknown> => {
|
||||
return isRecord(value) ? value : FALLBACK_INPUT_SCHEMA;
|
||||
};
|
||||
|
||||
const parseToolConfig = (value: unknown): McpBridgeToolConfig | null => {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const server = typeof value.server === 'string' ? value.server.trim() : '';
|
||||
const name = typeof value.name === 'string' ? value.name.trim() : '';
|
||||
if (!server || !name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
server,
|
||||
name,
|
||||
description: typeof value.description === 'string' ? value.description.trim() : undefined,
|
||||
inputSchema: normalizeInputSchema(value.inputSchema),
|
||||
};
|
||||
};
|
||||
|
||||
const parsePluginConfig = (value: unknown): McpBridgePluginConfig => {
|
||||
const raw = isRecord(value) ? value : {};
|
||||
const tools = Array.isArray(raw.tools)
|
||||
? raw.tools.map(parseToolConfig).filter((tool): tool is McpBridgeToolConfig => !!tool)
|
||||
: [];
|
||||
|
||||
return {
|
||||
callbackUrl: typeof raw.callbackUrl === 'string' ? raw.callbackUrl.trim() : '',
|
||||
secret: typeof raw.secret === 'string' ? raw.secret.trim() : '',
|
||||
requestTimeoutMs:
|
||||
typeof raw.requestTimeoutMs === 'number' && Number.isFinite(raw.requestTimeoutMs) && raw.requestTimeoutMs > 0
|
||||
? Math.max(1_000, Math.floor(raw.requestTimeoutMs))
|
||||
: DEFAULT_TIMEOUT_MS,
|
||||
tools,
|
||||
};
|
||||
};
|
||||
|
||||
const extractErrorMessage = (payload: unknown): string | null => {
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
if (typeof payload === 'string') {
|
||||
return payload.trim() || null;
|
||||
}
|
||||
if (!isRecord(payload)) {
|
||||
return null;
|
||||
}
|
||||
if (typeof payload.error === 'string' && payload.error.trim()) {
|
||||
return payload.error.trim();
|
||||
}
|
||||
const content = Array.isArray(payload.content) ? payload.content : [];
|
||||
for (const block of content) {
|
||||
if (!isRecord(block)) {
|
||||
continue;
|
||||
}
|
||||
if (block.type === 'text' && typeof block.text === 'string' && block.text.trim()) {
|
||||
return block.text.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const ensureToolResultPayload = (
|
||||
payload: unknown,
|
||||
details: Record<string, unknown>,
|
||||
): ToolResultPayload => {
|
||||
if (isRecord(payload) && Array.isArray(payload.content)) {
|
||||
return {
|
||||
...payload,
|
||||
details: payload.details ?? details,
|
||||
} as ToolResultPayload;
|
||||
}
|
||||
|
||||
const text =
|
||||
typeof payload === 'string'
|
||||
? payload
|
||||
: JSON.stringify(payload ?? { ok: true }, null, 2);
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text }],
|
||||
details: payload ?? details,
|
||||
};
|
||||
};
|
||||
|
||||
const buildToolDescription = (tool: McpBridgeToolConfig): string => {
|
||||
const parts = [
|
||||
`Proxy to MCP tool "${tool.name}" on server "${tool.server}".`,
|
||||
];
|
||||
if (tool.description) {
|
||||
parts.push(tool.description);
|
||||
}
|
||||
return parts.join(' ');
|
||||
};
|
||||
|
||||
const invokeBridge = async (
|
||||
config: McpBridgePluginConfig,
|
||||
tool: McpBridgeToolConfig,
|
||||
args: Record<string, unknown>,
|
||||
): Promise<unknown> => {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), config.requestTimeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetch(config.callbackUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'x-mcp-bridge-secret': config.secret,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
server: tool.server,
|
||||
tool: tool.name,
|
||||
args,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
let payload: unknown = null;
|
||||
|
||||
if (responseText.trim()) {
|
||||
try {
|
||||
payload = JSON.parse(responseText);
|
||||
} catch {
|
||||
if (!response.ok) {
|
||||
throw new Error(`MCP bridge HTTP ${response.status}: ${responseText.trim()}`);
|
||||
}
|
||||
payload = responseText;
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const message = extractErrorMessage(payload) || response.statusText || 'Unknown MCP bridge error';
|
||||
throw new Error(`MCP bridge HTTP ${response.status}: ${message}`);
|
||||
}
|
||||
|
||||
return payload;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
|
||||
const plugin = {
|
||||
id: 'mcp-bridge',
|
||||
name: 'MCP Bridge',
|
||||
description: 'Expose LobsterAI-managed MCP servers as native OpenClaw tools.',
|
||||
configSchema: {
|
||||
parse(value: unknown): McpBridgePluginConfig {
|
||||
return parsePluginConfig(value);
|
||||
},
|
||||
uiHints: {
|
||||
callbackUrl: { label: 'Callback URL', advanced: true },
|
||||
secret: { label: 'Secret', sensitive: true, advanced: true },
|
||||
requestTimeoutMs: { label: 'Request Timeout (ms)', advanced: true },
|
||||
},
|
||||
},
|
||||
register(api: OpenClawPluginApi) {
|
||||
const config = parsePluginConfig(api.pluginConfig);
|
||||
if (!config.callbackUrl || !config.secret || config.tools.length === 0) {
|
||||
api.logger.info('[mcp-bridge] skipped registration because callbackUrl/secret/tools are incomplete.');
|
||||
return;
|
||||
}
|
||||
|
||||
const usedToolNames = new Set<string>();
|
||||
|
||||
for (const tool of config.tools) {
|
||||
const registeredName = buildRegisteredToolName(tool.server, tool.name, usedToolNames);
|
||||
const details = {
|
||||
alias: registeredName,
|
||||
server: tool.server,
|
||||
tool: tool.name,
|
||||
};
|
||||
|
||||
api.registerTool({
|
||||
name: registeredName,
|
||||
label: `MCP ${tool.server}/${tool.name}`,
|
||||
description: buildToolDescription(tool),
|
||||
parameters: tool.inputSchema,
|
||||
async execute(_id: string, params: Record<string, unknown>) {
|
||||
try {
|
||||
const payload = await invokeBridge(config, tool, params);
|
||||
return ensureToolResultPayload(payload, details);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
content: [{ type: 'text', text: message }],
|
||||
isError: true,
|
||||
details,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
api.logger.info(`[mcp-bridge] registered ${config.tools.length} tool(s).`);
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"id": "mcp-bridge",
|
||||
"name": "MCP Bridge",
|
||||
"description": "Expose LobsterAI-managed MCP servers as native OpenClaw tools.",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"callbackUrl": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"requestTimeoutMs": {
|
||||
"type": "number",
|
||||
"minimum": 1000
|
||||
},
|
||||
"tools": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"server": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@lobsterai/openclaw-mcp-bridge",
|
||||
"version": "0.0.0",
|
||||
"description": "Expose LobsterAI-managed MCP servers as native OpenClaw tools.",
|
||||
"type": "module",
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
+8
-7
@@ -70,15 +70,16 @@
|
||||
"openclaw:ensure": "node scripts/ensure-openclaw-version.cjs",
|
||||
"openclaw:patch": "node scripts/apply-openclaw-patches.cjs",
|
||||
"openclaw:plugins": "node scripts/ensure-openclaw-plugins.cjs",
|
||||
"openclaw:extensions:local": "node scripts/sync-local-openclaw-extensions.cjs",
|
||||
"openclaw:bundle": "node scripts/bundle-openclaw-gateway.cjs",
|
||||
"openclaw:precompile": "node scripts/precompile-openclaw-extensions.cjs",
|
||||
"openclaw:runtime:host": "node scripts/openclaw-runtime-host.cjs",
|
||||
"openclaw:runtime:mac-arm64": "npm run openclaw:ensure && npm run openclaw:patch && node scripts/run-build-openclaw-runtime.cjs mac-arm64 && node scripts/sync-openclaw-runtime-current.cjs mac-arm64 && npm run openclaw:bundle && npm run openclaw:plugins && npm run openclaw:precompile",
|
||||
"openclaw:runtime:mac-x64": "npm run openclaw:ensure && npm run openclaw:patch && node scripts/run-build-openclaw-runtime.cjs mac-x64 && node scripts/sync-openclaw-runtime-current.cjs mac-x64 && npm run openclaw:bundle && npm run openclaw:plugins && npm run openclaw:precompile",
|
||||
"openclaw:runtime:win-x64": "npm run openclaw:ensure && npm run openclaw:patch && node scripts/run-build-openclaw-runtime.cjs win-x64 && node scripts/sync-openclaw-runtime-current.cjs win-x64 && npm run openclaw:bundle && npm run openclaw:plugins && npm run openclaw:precompile",
|
||||
"openclaw:runtime:win-arm64": "npm run openclaw:ensure && npm run openclaw:patch && node scripts/run-build-openclaw-runtime.cjs win-arm64 && node scripts/sync-openclaw-runtime-current.cjs win-arm64 && npm run openclaw:bundle && npm run openclaw:plugins && npm run openclaw:precompile",
|
||||
"openclaw:runtime:linux-x64": "npm run openclaw:ensure && npm run openclaw:patch && node scripts/run-build-openclaw-runtime.cjs linux-x64 && node scripts/sync-openclaw-runtime-current.cjs linux-x64 && npm run openclaw:bundle && npm run openclaw:plugins && npm run openclaw:precompile",
|
||||
"openclaw:runtime:linux-arm64": "npm run openclaw:ensure && npm run openclaw:patch && node scripts/run-build-openclaw-runtime.cjs linux-arm64 && node scripts/sync-openclaw-runtime-current.cjs linux-arm64 && npm run openclaw:bundle && npm run openclaw:plugins && npm run openclaw:precompile",
|
||||
"openclaw:runtime:mac-arm64": "npm run openclaw:ensure && npm run openclaw:patch && node scripts/run-build-openclaw-runtime.cjs mac-arm64 && node scripts/sync-openclaw-runtime-current.cjs mac-arm64 && npm run openclaw:bundle && npm run openclaw:plugins && npm run openclaw:extensions:local && npm run openclaw:precompile",
|
||||
"openclaw:runtime:mac-x64": "npm run openclaw:ensure && npm run openclaw:patch && node scripts/run-build-openclaw-runtime.cjs mac-x64 && node scripts/sync-openclaw-runtime-current.cjs mac-x64 && npm run openclaw:bundle && npm run openclaw:plugins && npm run openclaw:extensions:local && npm run openclaw:precompile",
|
||||
"openclaw:runtime:win-x64": "npm run openclaw:ensure && npm run openclaw:patch && node scripts/run-build-openclaw-runtime.cjs win-x64 && node scripts/sync-openclaw-runtime-current.cjs win-x64 && npm run openclaw:bundle && npm run openclaw:plugins && npm run openclaw:extensions:local && npm run openclaw:precompile",
|
||||
"openclaw:runtime:win-arm64": "npm run openclaw:ensure && npm run openclaw:patch && node scripts/run-build-openclaw-runtime.cjs win-arm64 && node scripts/sync-openclaw-runtime-current.cjs win-arm64 && npm run openclaw:bundle && npm run openclaw:plugins && npm run openclaw:extensions:local && npm run openclaw:precompile",
|
||||
"openclaw:runtime:linux-x64": "npm run openclaw:ensure && npm run openclaw:patch && node scripts/run-build-openclaw-runtime.cjs linux-x64 && node scripts/sync-openclaw-runtime-current.cjs linux-x64 && npm run openclaw:bundle && npm run openclaw:plugins && npm run openclaw:extensions:local && npm run openclaw:precompile",
|
||||
"openclaw:runtime:linux-arm64": "npm run openclaw:ensure && npm run openclaw:patch && node scripts/run-build-openclaw-runtime.cjs linux-arm64 && node scripts/sync-openclaw-runtime-current.cjs linux-arm64 && npm run openclaw:bundle && npm run openclaw:plugins && npm run openclaw:extensions:local && npm run openclaw:precompile",
|
||||
"regenerate:icon": "bash scripts/regenerate-mac-icon.sh",
|
||||
"fix:mac-icon": "bash scripts/fix-mac-icon-display.sh"
|
||||
},
|
||||
@@ -157,4 +158,4 @@
|
||||
"vite-plugin-electron-renderer": "^0.14.5",
|
||||
"wait-on": "^7.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const { spawnSync } = require('child_process');
|
||||
const asar = require('@electron/asar');
|
||||
const { ensurePortableGit } = require('./setup-mingit.js');
|
||||
const { ensurePortablePythonRuntime, checkRuntimeHealth } = require('./setup-python-runtime.js');
|
||||
const { syncLocalOpenClawExtensions } = require('./sync-local-openclaw-extensions.cjs');
|
||||
|
||||
function isWindowsTarget(context) {
|
||||
return context?.electronPlatformName === 'win32';
|
||||
@@ -126,6 +127,11 @@ function ensureBundledOpenClawRuntime(context) {
|
||||
const { runtimeRoot, targetId } = syncCurrentOpenClawRuntimeForTarget(context);
|
||||
const buildHint = getOpenClawRuntimeBuildHint(targetId);
|
||||
|
||||
const localMcpBridgeDir = path.join(runtimeRoot, 'extensions', 'mcp-bridge');
|
||||
if (!existsSync(localMcpBridgeDir)) {
|
||||
syncLocalOpenClawExtensions(runtimeRoot);
|
||||
}
|
||||
|
||||
const requiredExternalPaths = [
|
||||
path.join(runtimeRoot, 'node_modules'),
|
||||
];
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function syncLocalOpenClawExtensions(runtimeRoot) {
|
||||
const rootDir = path.resolve(__dirname, '..');
|
||||
const sourceDir = path.join(rootDir, 'openclaw-extensions');
|
||||
const targetRoot = runtimeRoot
|
||||
? path.resolve(runtimeRoot)
|
||||
: path.join(rootDir, 'vendor', 'openclaw-runtime', 'current');
|
||||
const targetExtensionsDir = path.join(targetRoot, 'extensions');
|
||||
|
||||
if (!fs.existsSync(sourceDir)) {
|
||||
return { sourceDir, targetRoot, copied: [] };
|
||||
}
|
||||
if (!fs.existsSync(targetExtensionsDir)) {
|
||||
throw new Error(`Runtime extensions directory does not exist: ${targetExtensionsDir}`);
|
||||
}
|
||||
|
||||
const copied = [];
|
||||
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
const src = path.join(sourceDir, entry.name);
|
||||
const dest = path.join(targetExtensionsDir, entry.name);
|
||||
fs.cpSync(src, dest, { recursive: true, force: true });
|
||||
copied.push(entry.name);
|
||||
}
|
||||
|
||||
return { sourceDir, targetRoot, copied };
|
||||
}
|
||||
|
||||
function main() {
|
||||
try {
|
||||
const runtimeRoot = (process.argv[2] || '').trim() || undefined;
|
||||
const result = syncLocalOpenClawExtensions(runtimeRoot);
|
||||
if (result.copied.length === 0) {
|
||||
console.log('[sync-local-openclaw-extensions] No local extensions to sync.');
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
`[sync-local-openclaw-extensions] Synced ${result.copied.join(', ')} -> ${path.join(result.targetRoot, 'extensions')}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[sync-local-openclaw-extensions] ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
syncLocalOpenClawExtensions,
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { resolveRawApiConfig } from './claudeSettings';
|
||||
import type { OpenClawEngineManager } from './openclawEngineManager';
|
||||
import { parseChannelSessionKey } from './openclawChannelSessionSync';
|
||||
import type { McpToolManifestEntry } from './mcpServerManager';
|
||||
import { hasBundledOpenClawExtension } from './openclawLocalExtensions';
|
||||
import { buildScheduledTaskEnginePrompt } from './scheduledTaskEnginePrompt';
|
||||
|
||||
export type McpBridgeConfig = {
|
||||
@@ -278,6 +279,10 @@ const readPreinstalledPluginIds = (): string[] => {
|
||||
}
|
||||
};
|
||||
|
||||
const isBundledPluginAvailable = (pluginId: string): boolean => {
|
||||
return hasBundledOpenClawExtension(pluginId);
|
||||
};
|
||||
|
||||
export type OpenClawConfigSyncResult = {
|
||||
ok: boolean;
|
||||
changed: boolean;
|
||||
@@ -367,6 +372,7 @@ export class OpenClawConfigSync {
|
||||
const workspaceDir = (coworkConfig.workingDirectory || '').trim();
|
||||
|
||||
const preinstalledPluginIds = readPreinstalledPluginIds();
|
||||
const hasMcpBridgePlugin = isBundledPluginAvailable('mcp-bridge');
|
||||
|
||||
const dingTalkConfig = this.getDingTalkConfig();
|
||||
// DingTalk runs through OpenClaw plugin but still needs the gateway HTTP endpoint (chatCompletions)
|
||||
@@ -435,46 +441,45 @@ export class OpenClawConfigSync {
|
||||
maxConcurrentRuns: 3,
|
||||
sessionRetention: '7d',
|
||||
},
|
||||
...(preinstalledPluginIds.length > 0
|
||||
? {
|
||||
plugins: {
|
||||
entries: {
|
||||
...Object.fromEntries(
|
||||
preinstalledPluginIds.map((id) => {
|
||||
// Sync plugin enabled state with the corresponding channel config.
|
||||
// When a channel is disabled in the UI, its plugin must also be
|
||||
// disabled so OpenClaw doesn't load it at all.
|
||||
const pluginEnabled = (() => {
|
||||
if (id === 'dingtalk-connector') return !!(dingTalkConfig?.enabled && dingTalkConfig.clientId);
|
||||
if (id === 'feishu-openclaw-plugin') return !!(feishuConfig?.enabled && feishuConfig.appId);
|
||||
if (id === 'qqbot') return !!(qqConfig?.enabled && qqConfig.appId);
|
||||
if (id === 'wecom-openclaw-plugin') return !!(wecomConfig?.enabled && wecomConfig.botId);
|
||||
return true; // other plugins stay enabled
|
||||
})();
|
||||
return [id, { enabled: pluginEnabled }];
|
||||
}),
|
||||
),
|
||||
// Disable the built-in feishu plugin when the official one is preinstalled
|
||||
...(preinstalledPluginIds.includes('feishu-openclaw-plugin')
|
||||
? { feishu: { enabled: false } }
|
||||
: {}),
|
||||
'mcp-bridge': { enabled: true },
|
||||
...((() => {
|
||||
const pluginEntries: Record<string, unknown> = {
|
||||
...Object.fromEntries(
|
||||
preinstalledPluginIds.map((id) => {
|
||||
// Sync plugin enabled state with the corresponding channel config.
|
||||
// When a channel is disabled in the UI, its plugin must also be
|
||||
// disabled so OpenClaw doesn't load it at all.
|
||||
const pluginEnabled = (() => {
|
||||
if (id === 'dingtalk-connector') return !!(dingTalkConfig?.enabled && dingTalkConfig.clientId);
|
||||
if (id === 'feishu-openclaw-plugin') return !!(feishuConfig?.enabled && feishuConfig.appId);
|
||||
if (id === 'qqbot') return !!(qqConfig?.enabled && qqConfig.appId);
|
||||
if (id === 'wecom-openclaw-plugin') return !!(wecomConfig?.enabled && wecomConfig.botId);
|
||||
return true; // other plugins stay enabled
|
||||
})();
|
||||
return [id, { enabled: pluginEnabled }];
|
||||
}),
|
||||
),
|
||||
...(preinstalledPluginIds.includes('feishu-openclaw-plugin')
|
||||
? { feishu: { enabled: false } }
|
||||
: {}),
|
||||
...(hasMcpBridgePlugin
|
||||
? { 'mcp-bridge': { enabled: true } }
|
||||
: {}),
|
||||
};
|
||||
|
||||
return Object.keys(pluginEntries).length > 0
|
||||
? {
|
||||
plugins: {
|
||||
entries: pluginEntries,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
plugins: {
|
||||
entries: {
|
||||
'mcp-bridge': { enabled: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
: {};
|
||||
})())
|
||||
};
|
||||
|
||||
// Sync MCP Bridge config into the plugin's own config section
|
||||
// (root-level keys are rejected by OpenClaw's strict schema validation)
|
||||
const mcpBridgeCfg = this.getMcpBridgeConfig?.();
|
||||
if (mcpBridgeCfg && mcpBridgeCfg.tools.length > 0) {
|
||||
if (hasMcpBridgePlugin && mcpBridgeCfg && mcpBridgeCfg.tools.length > 0 && managedConfig.plugins) {
|
||||
const plugins = managedConfig.plugins as Record<string, unknown>;
|
||||
const entries = plugins.entries as Record<string, Record<string, unknown>>;
|
||||
entries['mcp-bridge'] = {
|
||||
|
||||
@@ -5,6 +5,7 @@ import fs from 'fs';
|
||||
import net from 'net';
|
||||
import path from 'path';
|
||||
import { getElectronNodeRuntimePath } from './coworkUtil';
|
||||
import { syncLocalOpenClawExtensionsIntoRuntime } from './openclawLocalExtensions';
|
||||
import { applyBundledOpenClawRuntimeHotfixes } from './openclawRuntimeHotfix';
|
||||
|
||||
const DEFAULT_OPENCLAW_VERSION = '2026.2.23';
|
||||
@@ -256,6 +257,11 @@ export class OpenClawEngineManager extends EventEmitter {
|
||||
return this.getStatus();
|
||||
}
|
||||
|
||||
const localExtensionSync = syncLocalOpenClawExtensionsIntoRuntime(runtime.root);
|
||||
if (localExtensionSync.copied.length > 0) {
|
||||
console.log(`[OpenClaw] synced local extensions: ${localExtensionSync.copied.join(', ')}`);
|
||||
}
|
||||
|
||||
if (this.status.phase === 'running') {
|
||||
return this.getStatus();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { app } from 'electron';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const LOCAL_EXTENSIONS_DIR = 'openclaw-extensions';
|
||||
|
||||
const findLocalExtensionsSourceDir = (): string | null => {
|
||||
if (app.isPackaged) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidates = [
|
||||
path.join(app.getAppPath(), LOCAL_EXTENSIONS_DIR),
|
||||
path.join(process.cwd(), LOCAL_EXTENSIONS_DIR),
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
if (fs.statSync(candidate).isDirectory()) {
|
||||
return candidate;
|
||||
}
|
||||
} catch {
|
||||
// Ignore missing candidates.
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const findBundledExtensionsDir = (): string | null => {
|
||||
const candidates = app.isPackaged
|
||||
? [path.join(process.resourcesPath, 'cfmind', 'extensions')]
|
||||
: [
|
||||
path.join(app.getAppPath(), 'vendor', 'openclaw-runtime', 'current', 'extensions'),
|
||||
path.join(process.cwd(), 'vendor', 'openclaw-runtime', 'current', 'extensions'),
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
if (fs.statSync(candidate).isDirectory()) {
|
||||
return candidate;
|
||||
}
|
||||
} catch {
|
||||
// Ignore missing candidates.
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const syncLocalOpenClawExtensionsIntoRuntime = (
|
||||
runtimeRoot: string,
|
||||
): { sourceDir: string | null; copied: string[] } => {
|
||||
const sourceDir = findLocalExtensionsSourceDir();
|
||||
if (!sourceDir) {
|
||||
return { sourceDir: null, copied: [] };
|
||||
}
|
||||
|
||||
const targetExtensionsDir = path.join(runtimeRoot, 'extensions');
|
||||
try {
|
||||
if (!fs.statSync(targetExtensionsDir).isDirectory()) {
|
||||
return { sourceDir, copied: [] };
|
||||
}
|
||||
} catch {
|
||||
return { sourceDir, copied: [] };
|
||||
}
|
||||
|
||||
const copied: string[] = [];
|
||||
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
fs.cpSync(
|
||||
path.join(sourceDir, entry.name),
|
||||
path.join(targetExtensionsDir, entry.name),
|
||||
{ recursive: true, force: true },
|
||||
);
|
||||
copied.push(entry.name);
|
||||
}
|
||||
|
||||
return { sourceDir, copied };
|
||||
};
|
||||
|
||||
export const listLocalOpenClawExtensionIds = (): string[] => {
|
||||
const sourceDir = findLocalExtensionsSourceDir();
|
||||
if (!sourceDir) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return fs.readdirSync(sourceDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.filter((entry) => fs.existsSync(path.join(sourceDir, entry.name, 'openclaw.plugin.json')))
|
||||
.map((entry) => entry.name);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const listBundledOpenClawExtensionIds = (): string[] => {
|
||||
const extensionsDir = findBundledExtensionsDir();
|
||||
if (!extensionsDir) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return fs.readdirSync(extensionsDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.filter((entry) => fs.existsSync(path.join(extensionsDir, entry.name, 'openclaw.plugin.json')))
|
||||
.map((entry) => entry.name);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const hasBundledOpenClawExtension = (extensionId: string): boolean => {
|
||||
return listBundledOpenClawExtensionIds().includes(extensionId)
|
||||
|| listLocalOpenClawExtensionIds().includes(extensionId);
|
||||
};
|
||||
Reference in New Issue
Block a user