fix(gateway): resolve startup hang caused by qqbot plugin manifest ID mismatch & stderr flood (#718)

This commit is contained in:
paisley
2026-03-30 18:27:48 +08:00
committed by GitHub
Unverified
parent aa2e4eae14
commit ec8db0be75
6 changed files with 115 additions and 12 deletions

View File

@@ -20,7 +20,7 @@ import { getApiKey, getDefaultProvider, getProvider } from '../utils/secure-stor
import { getProviderEnvVar, getKeyableProviderTypes } from '../utils/provider-registry';
import { getOpenClawDir, getOpenClawEntryPath, isOpenClawPresent } from '../utils/paths';
import { getUvMirrorEnv } from '../utils/uv-env';
import { cleanupDanglingWeChatPluginState, listConfiguredChannels } from '../utils/channel-config';
import { cleanupDanglingWeChatPluginState, listConfiguredChannels, readOpenClawConfig } from '../utils/channel-config';
import { syncGatewayTokenToConfig, syncBrowserConfigToOpenClaw, syncSessionIdleMinutesToOpenClaw, sanitizeOpenClawConfig } from '../utils/openclaw-auth';
import { buildProxyEnv, resolveProxySettings } from '../utils/proxy';
import { syncProxyConfigToOpenClaw } from '../utils/openclaw-proxy';
@@ -131,6 +131,10 @@ function ensureConfiguredPluginsUpgraded(configuredChannels: string[]): void {
} catch (err) {
logger.warn(`[plugin] Failed to ${isInstalled ? 'auto-upgrade' : 'install'} ${channelType} plugin:`, err);
}
} else if (isInstalled) {
// Same version already installed — still patch manifest ID in case it was
// never corrected (e.g. installed before MANIFEST_ID_FIXES included this plugin).
fixupPluginManifest(targetDir);
}
continue;
}
@@ -141,10 +145,14 @@ function ensureConfiguredPluginsUpgraded(configuredChannels: string[]): void {
if (!existsSync(fsPath(join(npmPkgPath, 'openclaw.plugin.json')))) continue;
const sourceVersion = readPluginVersion(join(npmPkgPath, 'package.json'));
if (!sourceVersion) continue;
// Skip only if installed AND same version
if (isInstalled && installedVersion && sourceVersion === installedVersion) continue;
// Skip only if installed AND same version — but still patch manifest ID.
if (isInstalled && installedVersion && sourceVersion === installedVersion) {
fixupPluginManifest(targetDir);
continue;
}
logger.info(`[plugin] ${isInstalled ? 'Auto-upgrading' : 'Installing'} ${channelType} plugin${isInstalled ? `: ${installedVersion}${sourceVersion}` : `: ${sourceVersion}`} (dev/node_modules)`);
try {
mkdirSync(fsPath(join(homedir(), '.openclaw', 'extensions')), { recursive: true });
copyPluginFromNodeModules(npmPkgPath, targetDir, npmName);
@@ -187,6 +195,34 @@ export async function syncGatewayConfigBeforeLaunch(
// the plugin manifest ID matches what sanitize wrote to the config.
try {
const configuredChannels = await listConfiguredChannels();
// Also ensure plugins referenced in plugins.allow are installed even if
// they have no channels.X section yet (e.g. qqbot added via plugins.allow
// but never fully saved through ClawX UI).
try {
const rawCfg = await readOpenClawConfig();
const allowList = Array.isArray(rawCfg.plugins?.allow) ? (rawCfg.plugins!.allow as string[]) : [];
// Build reverse maps: dirName → channelType AND known manifest IDs → channelType
const pluginIdToChannel: Record<string, string> = {};
for (const [channelType, info] of Object.entries(CHANNEL_PLUGIN_MAP)) {
pluginIdToChannel[info.dirName] = channelType;
}
// Known manifest IDs that differ from their dirName/channelType
pluginIdToChannel['openclaw-qqbot'] = 'qqbot';
pluginIdToChannel['openclaw-lark'] = 'feishu';
pluginIdToChannel['feishu-openclaw-plugin'] = 'feishu';
for (const pluginId of allowList) {
const channelType = pluginIdToChannel[pluginId] ?? pluginId;
if (CHANNEL_PLUGIN_MAP[channelType] && !configuredChannels.includes(channelType)) {
configuredChannels.push(channelType);
}
}
} catch (err) {
logger.warn('[plugin] Failed to augment channel list from plugins.allow:', err);
}
ensureConfiguredPluginsUpgraded(configuredChannels);
} catch (err) {
logger.warn('Failed to auto-upgrade plugins:', err);

View File

@@ -705,6 +705,9 @@ export class GatewayManager extends EventEmitter {
await unloadLaunchctlGatewayService();
this.processExitCode = null;
// Per-process dedup map for stderr lines — resets on each new spawn.
const stderrDedup = new Map<string, number>();
const { child, lastSpawnSummary } = await launchGatewayProcess({
port: this.status.port,
launchContext,
@@ -715,6 +718,18 @@ export class GatewayManager extends EventEmitter {
recordGatewayStartupStderrLine(this.recentStartupStderrLines, line);
const classified = classifyGatewayStderrMessage(line);
if (classified.level === 'drop') return;
// Dedup: suppress identical stderr lines after the first occurrence.
const count = (stderrDedup.get(classified.normalized) ?? 0) + 1;
stderrDedup.set(classified.normalized, count);
if (count > 1) {
// Log a summary every 50 duplicates to stay visible without flooding.
if (count % 50 === 0) {
logger.debug(`[Gateway stderr] (suppressed ${count} repeats) ${classified.normalized}`);
}
return;
}
if (classified.level === 'debug') {
logger.debug(`[Gateway stderr] ${classified.normalized}`);
return;

View File

@@ -30,6 +30,9 @@ export function classifyGatewayStderrMessage(message: string): GatewayStderrClas
if (msg.includes('DeprecationWarning')) return { level: 'debug', normalized: msg };
if (msg.includes('Debugger attached')) return { level: 'debug', normalized: msg };
// Gateway config warnings (e.g. stale plugin entries) are informational, not actionable.
if (msg.includes('Config warnings:')) return { level: 'debug', normalized: msg };
// Electron restricts NODE_OPTIONS in packaged apps; this is expected and harmless.
if (msg.includes('node: --require is not allowed in NODE_OPTIONS')) {
return { level: 'debug', normalized: msg };