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 };

View File

@@ -22,6 +22,7 @@ import {
const OPENCLAW_DIR = join(homedir(), '.openclaw');
const CONFIG_FILE = join(OPENCLAW_DIR, 'openclaw.json');
const WECOM_PLUGIN_ID = 'wecom';
const QQBOT_PLUGIN_ID = 'openclaw-qqbot';
const WECHAT_PLUGIN_ID = OPENCLAW_WECHAT_CHANNEL_TYPE;
const FEISHU_PLUGIN_ID_CANDIDATES = ['openclaw-lark', 'feishu-openclaw-plugin'] as const;
const DEFAULT_ACCOUNT_ID = 'default';
@@ -465,14 +466,33 @@ async function ensurePluginAllowlist(currentConfig: OpenClawConfig, channelType:
if (channelType === 'qqbot') {
if (!currentConfig.plugins) {
currentConfig.plugins = {};
}
currentConfig.plugins.enabled = true;
const allow = Array.isArray(currentConfig.plugins.allow)
? currentConfig.plugins.allow as string[]
: [];
if (!allow.includes('qqbot')) {
currentConfig.plugins.allow = [...allow, 'qqbot'];
currentConfig.plugins = {
allow: [QQBOT_PLUGIN_ID],
enabled: true,
entries: {
[QQBOT_PLUGIN_ID]: { enabled: true }
}
};
} else {
currentConfig.plugins.enabled = true;
const allow: string[] = Array.isArray(currentConfig.plugins.allow)
? (currentConfig.plugins.allow as string[])
: [];
// Normalize: remove bare 'qqbot' and ensure the actual manifest ID is present.
const normalizedAllow = allow.filter((pluginId) => pluginId !== 'qqbot');
if (!normalizedAllow.includes(QQBOT_PLUGIN_ID)) {
currentConfig.plugins.allow = [...normalizedAllow, QQBOT_PLUGIN_ID];
} else if (normalizedAllow.length !== allow.length) {
currentConfig.plugins.allow = normalizedAllow;
}
if (!currentConfig.plugins.entries) {
currentConfig.plugins.entries = {};
}
if (!currentConfig.plugins.entries[QQBOT_PLUGIN_ID]) {
currentConfig.plugins.entries[QQBOT_PLUGIN_ID] = {};
}
currentConfig.plugins.entries[QQBOT_PLUGIN_ID].enabled = true;
}
}

View File

@@ -1436,6 +1436,35 @@ export async function sanitizeOpenClawConfig(): Promise<void> {
modified = true;
}
// ── qqbot → openclaw-qqbot migration ────────────────────────
// The qqbot npm package (@tencent-connect/openclaw-qqbot) declares
// id="openclaw-qqbot" in its manifest, but older ClawX versions
// wrote bare "qqbot" into plugins.allow. Migrate to the manifest ID
// so the Gateway can resolve the plugin correctly.
const LEGACY_QQBOT_ID = 'qqbot';
const NEW_QQBOT_ID = 'openclaw-qqbot';
if (Array.isArray(pluginsObj.allow)) {
const allowArr = pluginsObj.allow as string[];
const legacyIdx = allowArr.indexOf(LEGACY_QQBOT_ID);
if (legacyIdx !== -1) {
if (!allowArr.includes(NEW_QQBOT_ID)) {
allowArr[legacyIdx] = NEW_QQBOT_ID;
} else {
allowArr.splice(legacyIdx, 1);
}
console.log(`[sanitize] Migrated plugins.allow: ${LEGACY_QQBOT_ID}${NEW_QQBOT_ID}`);
modified = true;
}
}
if (pEntries?.[LEGACY_QQBOT_ID]) {
if (!pEntries[NEW_QQBOT_ID]) {
pEntries[NEW_QQBOT_ID] = pEntries[LEGACY_QQBOT_ID];
}
delete pEntries[LEGACY_QQBOT_ID];
console.log(`[sanitize] Migrated plugins.entries: ${LEGACY_QQBOT_ID}${NEW_QQBOT_ID}`);
modified = true;
}
// ── Remove bare 'feishu' when canonical feishu plugin is present ──
// The Gateway binary automatically adds bare 'feishu' to plugins.allow
// because the official plugin registers the 'feishu' channel.

View File

@@ -204,7 +204,7 @@ describe('WeCom plugin configuration', () => {
const config = await readOpenClawJson();
const plugins = config.plugins as { allow: string[] };
expect(plugins.allow).toEqual(expect.arrayContaining(['qqbot', 'discord', 'whatsapp']));
expect(plugins.allow).toEqual(expect.arrayContaining(['openclaw-qqbot', 'discord', 'whatsapp']));
});
});