小熊奶糖(BearCandy)
小熊奶糖(BearCandy)
发布于 2026-03-24 / 0 阅读
0
0

OpenClaw 升级故障修复

一、背景概述

当将 OpenClaw 从 v2026.3.13 升级到 v2026.3.22 后,用户可能会遇到两类主要故障:

  1. 飞书插件(openclaw-lark)崩溃:消息无法发送,网关不断报错,甚至进程重启。
  2. 控制台 UI(Dashboard)无法访问:网关返回 503,提示“Control UI assets not found”。

这两类问题均为发布版本中的疏漏所致,但可以通过本文提供的修复方法逐一解决。


二、飞书插件崩溃修复

2.1 错误表现与根因分析

  • 现象:插件加载后消息发送失败,日志中出现 (0 , _pluginSdk.readStringParam) is not a function;Windows 环境下可能伴随 Received protocol 'c:' 错误。
  • 根因:OpenClaw 2026.3.22 将 plugin-sdk 重构为狭窄子路径导出(破坏性更改),而飞书插件(v2026.3.18)仍从旧根路径导入,导致 22 个运行时符号丢失。此外,Windows 下动态导入的绝对路径触发了 ESM 安全限制,且新版要求插件实现 describeMessageTool 接口。

2.2 修复步骤

2.2.1 建立依赖符号链接

Windows(以管理员身份运行 CMD)

cd C:\Users\Admin\.openclaw\extensions\openclaw-lark
mkdir node_modules
mklink /D node_modules\openclaw C:\Users\Admin\AppData\Roaming\npm\node_modules\openclaw

Linux / macOS / WSL

cd ~/.openclaw/extensions/openclaw-lark
mkdir -p node_modules
ln -sf ~/.nvm/versions/node/v24.14.0/lib/node_modules/openclaw node_modules/openclaw

2.2.2 替换核心调度文件 plugin.js

将以下完整代码覆盖至 src/channel/plugin.js(路径根据系统调整):

"use strict";
/**
 * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
 * SPDX-License-Identifier: MIT
 *
 * ChannelPlugin interface implementation for the Lark/Feishu channel.
 *
 * This is the top-level entry point that the OpenClaw plugin system uses to
 * discover capabilities, resolve accounts, obtain outbound adapters, and
 * start the inbound event gateway.
 */
import { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE } from 'openclaw/plugin-sdk';

// [修复 1] 将动态导入提升为顶层静态导入,规避 Windows ESM 路径解析 Bug (Received protocol 'c:')
import { monitorFeishuProvider } from './monitor.js'; 

import { getLarkAccount, getLarkAccountIds, getDefaultLarkAccountId } from '../core/accounts';
import { listFeishuDirectoryPeers, listFeishuDirectoryGroups, listFeishuDirectoryPeersLive, listFeishuDirectoryGroupsLive, } from './directory';
import { feishuOnboardingAdapter } from './onboarding';
import { feishuOutbound } from '../messaging/outbound/outbound';
import { feishuMessageActions } from '../messaging/outbound/actions';
import { resolveFeishuGroupToolPolicy } from '../messaging/inbound/policy';
import { LarkClient } from '../core/lark-client';
import { sendMessageFeishu } from '../messaging/outbound/send';
import { normalizeFeishuTarget, looksLikeFeishuId } from '../core/targets';
import { triggerOnboarding } from '../tools/onboarding-auth';
import { setAccountEnabled, applyAccountConfig, deleteAccount, collectFeishuSecurityWarnings } from './config-adapter';
import { larkLogger } from '../core/lark-logger';
import { FEISHU_CONFIG_JSON_SCHEMA } from '../core/config-schema';
const pluginLog = larkLogger('channel/plugin');
/** 状态轮询的探针结果缓存时长(10 分钟)。 */
const PROBE_CACHE_TTL_MS = 10 * 60 * 1000;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Convert nullable SDK params to optional params for directory functions. */
function adaptDirectoryParams(params) {
    return {
        cfg: params.cfg,
        query: params.query ?? undefined,
        limit: params.limit ?? undefined,
        accountId: params.accountId ?? undefined,
    };
}
// ---------------------------------------------------------------------------
// Meta
// ---------------------------------------------------------------------------
const meta = {
    id: 'feishu',
    label: 'Feishu',
    selectionLabel: 'Lark/Feishu (\u98DE\u4E66)',
    docsPath: '/channels/feishu',
    docsLabel: 'feishu',
    blurb: '\u98DE\u4E66/Lark enterprise messaging.',
    aliases: ['lark'],
    order: 70,
};
// ---------------------------------------------------------------------------
// Channel plugin definition
// ---------------------------------------------------------------------------
export const feishuPlugin = {
    id: 'feishu',
    meta: {
        ...meta,
    },
    // -------------------------------------------------------------------------
    // Pairing
    // -------------------------------------------------------------------------
    pairing: {
        idLabel: 'feishuUserId',
        normalizeAllowEntry: (entry) => entry.replace(/^(feishu|user|open_id):/i, ''),
        notifyApproval: async ({ cfg, id }) => {
            const accountId = getDefaultLarkAccountId(cfg);
            pluginLog.info('notifyApproval called', { id, accountId });
            // 1. 发送配对成功消息
            await sendMessageFeishu({
                cfg,
                to: id,
                text: PAIRING_APPROVED_MESSAGE,
                accountId,
            });
            // 2. 触发 onboarding
            try {
                await triggerOnboarding({ cfg, userOpenId: id, accountId });
                pluginLog.info('onboarding completed', { id });
            }
            catch (err) {
                pluginLog.warn('onboarding failed', { id, error: String(err) });
            }
        },
    },
    // -------------------------------------------------------------------------
    // Capabilities
    // -------------------------------------------------------------------------
    capabilities: {
        chatTypes: ['direct', 'group'],
        media: true,
        reactions: true,
        threads: true,
        polls: false,
        nativeCommands: true,
        blockStreaming: true,
    },
    // -------------------------------------------------------------------------
    // Agent prompt
    // -------------------------------------------------------------------------
    agentPrompt: {
        messageToolHints: () => [
            '- Feishu targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:open_id` or `chat:chat_id`.',
            '- Feishu supports interactive cards for rich messages.',
            '- Feishu reactions use UPPERCASE emoji type names (e.g. `OK`,`THUMBSUP`,`THANKS`,`MUSCLE`,`FINGERHEART`,`APPLAUSE`,`FISTBUMP`,`JIAYI`,`DONE`,`SMILE`,`BLUSH` ), not Unicode emoji characters.',
            "- Feishu `action=delete`/`action=unsend` only deletes messages sent by the bot. When the user quotes a message and says 'delete this', use the **quoted message's** message_id, not the user's own message_id.",
        ],
    },
    // -------------------------------------------------------------------------
    // Groups
    // -------------------------------------------------------------------------
    groups: {
        resolveToolPolicy: resolveFeishuGroupToolPolicy,
    },
    // -------------------------------------------------------------------------
    // Reload
    // -------------------------------------------------------------------------
    reload: { configPrefixes: ['channels.feishu'] },
    // -------------------------------------------------------------------------
    // Config schema (JSON Schema)
    // -------------------------------------------------------------------------
    configSchema: {
        schema: FEISHU_CONFIG_JSON_SCHEMA,
    },
    // -------------------------------------------------------------------------
    // Config adapter
    // -------------------------------------------------------------------------
    config: {
        listAccountIds: (cfg) => getLarkAccountIds(cfg),
        resolveAccount: (cfg, accountId) => getLarkAccount(cfg, accountId),
        defaultAccountId: (cfg) => getDefaultLarkAccountId(cfg),
        setAccountEnabled: ({ cfg, accountId, enabled }) => {
            return setAccountEnabled(cfg, accountId, enabled);
        },
        deleteAccount: ({ cfg, accountId }) => {
            return deleteAccount(cfg, accountId);
        },
        isConfigured: (account) => account.configured,
        describeAccount: (account) => ({
            accountId: account.accountId,
            enabled: account.enabled,
            configured: account.configured,
            name: account.name,
            appId: account.appId,
            brand: account.brand,
        }),
        resolveAllowFrom: ({ cfg, accountId }) => {
            const account = getLarkAccount(cfg, accountId);
            return (account.config?.allowFrom ?? []).map((entry) => String(entry));
        },
        formatAllowFrom: ({ allowFrom }) => allowFrom
            .map((entry) => String(entry).trim())
            .filter(Boolean)
            .map((entry) => entry.toLowerCase()),
    },
    // -------------------------------------------------------------------------
    // Security
    // -------------------------------------------------------------------------
    security: {
        collectWarnings: ({ cfg, accountId }) => collectFeishuSecurityWarnings({ cfg, accountId: accountId ?? DEFAULT_ACCOUNT_ID }),
    },
    // -------------------------------------------------------------------------
    // Setup
    // -------------------------------------------------------------------------
    setup: {
        resolveAccountId: () => DEFAULT_ACCOUNT_ID,
        applyAccountConfig: ({ cfg, accountId }) => {
            return applyAccountConfig(cfg, accountId, { enabled: true });
        },
    },
    // -------------------------------------------------------------------------
    // Onboarding
    // -------------------------------------------------------------------------
    onboarding: feishuOnboardingAdapter,
    // -------------------------------------------------------------------------
    // Messaging
    // -------------------------------------------------------------------------
    messaging: {
        normalizeTarget: (raw) => normalizeFeishuTarget(raw) ?? undefined,
        targetResolver: {
            looksLikeId: looksLikeFeishuId,
            hint: '<chatId|user:openId|chat:chatId>',
        },
    },
    // -------------------------------------------------------------------------
    // Directory
    // -------------------------------------------------------------------------
    directory: {
        self: async () => null,
        listPeers: async (p) => listFeishuDirectoryPeers(adaptDirectoryParams(p)),
        listGroups: async (p) => listFeishuDirectoryGroups(adaptDirectoryParams(p)),
        listPeersLive: async (p) => listFeishuDirectoryPeersLive(adaptDirectoryParams(p)),
        listGroupsLive: async (p) => listFeishuDirectoryGroupsLive(adaptDirectoryParams(p)),
    },
    // -------------------------------------------------------------------------
    // Outbound
    // -------------------------------------------------------------------------
    outbound: feishuOutbound,
    // -------------------------------------------------------------------------
    // Threading
    // -------------------------------------------------------------------------
    threading: {
        buildToolContext: ({ context, hasRepliedRef }) => ({
            currentChannelId: normalizeFeishuTarget(context.To ?? '') ?? undefined,
            currentThreadTs: context.MessageThreadId != null ? String(context.MessageThreadId) : undefined,
            currentMessageId: context.CurrentMessageId,
            hasRepliedRef,
        }),
    },
    // -------------------------------------------------------------------------
    // Actions
    // -------------------------------------------------------------------------
    actions: {
        ...feishuMessageActions,
        // [修复 2] 兼容性垫片:为 OpenClaw 2026.3.22+ 补充缺失的 describeMessageTool 接口实现
        describeMessageTool: typeof feishuMessageActions.describeMessageTool === 'function' 
            ? feishuMessageActions.describeMessageTool 
            : () => "Interact with Feishu messages. Supported actions: reply, reaction, edit, delete.",
    },
    // -------------------------------------------------------------------------
    // Status
    // -------------------------------------------------------------------------
    status: {
        defaultRuntime: {
            accountId: DEFAULT_ACCOUNT_ID,
            running: false,
            lastStartAt: null,
            lastStopAt: null,
            lastError: null,
            port: null,
        },
        buildChannelSummary: ({ snapshot }) => ({
            configured: snapshot.configured ?? false,
            running: snapshot.running ?? false,
            lastStartAt: snapshot.lastStartAt ?? null,
            lastStopAt: snapshot.lastStopAt ?? null,
            lastError: snapshot.lastError ?? null,
            port: snapshot.port ?? null,
            probe: snapshot.probe,
            lastProbeAt: snapshot.lastProbeAt ?? null,
        }),
        probeAccount: async ({ account }) => {
            return await LarkClient.fromAccount(account).probe({ maxAgeMs: PROBE_CACHE_TTL_MS });
        },
        buildAccountSnapshot: ({ account, runtime, probe }) => ({
            accountId: account.accountId,
            enabled: account.enabled,
            configured: account.configured,
            name: account.name,
            appId: account.appId,
            brand: account.brand,
            running: runtime?.running ?? false,
            lastStartAt: runtime?.lastStartAt ?? null,
            lastStopAt: runtime?.lastStopAt ?? null,
            lastError: runtime?.lastError ?? null,
            port: runtime?.port ?? null,
            probe,
        }),
    },
    // -------------------------------------------------------------------------
    // Gateway
    // -------------------------------------------------------------------------
    gateway: {
        startAccount: async (ctx) => {
            // 已移除导致 Windows ESM 崩溃的动态导入:const { monitorFeishuProvider } = await import('./monitor.js');
            const account = getLarkAccount(ctx.cfg, ctx.accountId);
            const port = account.config?.webhookPort ?? null;
            ctx.setStatus({ accountId: ctx.accountId, port });
            ctx.log?.info(`starting feishu[${ctx.accountId}] (mode: ${account.config?.connectionMode ?? 'websocket'})`);
            return monitorFeishuProvider({
                config: ctx.cfg,
                runtime: ctx.runtime,
                abortSignal: ctx.abortSignal,
                accountId: ctx.accountId,
            });
        },
        stopAccount: async (ctx) => {
            ctx.log?.info(`stopping feishu[${ctx.accountId}]`);
            LarkClient.clearCache(ctx.accountId);
            ctx.log?.info(`stopped feishu[${ctx.accountId}]`);
        },
    },
};

2.2.3 补充 SDK 垫片(解决符号缺失)

找到全局 OpenClaw 的 dist/plugin-sdk/index.js,在文件末尾追加以下代码:

// --- 向后兼容垫片 (Backward-compat shim) ---
// 解决 @larksuite/openclaw-lark v2026.3.18 找不到 22 个核心符号的问题
export { addWildcardAllowFrom, buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, createReplyPrefixContext, DEFAULT_ACCOUNT_ID, DEFAULT_GROUP_HISTORY_LIMIT, formatDocsLink, logTypingFailure, PAIRING_APPROVED_MESSAGE, recordPendingHistoryEntryIfEnabled } from "./feishu.js";
export { readStringParam, readNumberParam } from "./param-readers.js";
export { jsonResult, readReactionParams } from "./agent-runtime.js";
export { extractToolSend } from "./tool-send.js";
export { createTypingCallbacks } from "./channel-runtime.js";
export { isNormalizedSenderAllowed } from "./allow-from.js";
export { buildRandomTempFilePath } from "./temp-path.js";
export { resolveSenderCommandAuthorization } from "./command-auth.js";
export { SILENT_REPLY_TOKEN } from "./msteams.js";
export { normalizeAccountId, resolveThreadSessionKeys } from "./core.js";

2.2.4 加固插件入口文件 index.js

用以下完整代码覆盖飞书插件根目录下的 index.js

"use strict";
/**
 * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
 * SPDX-License-Identifier: MIT
 *
 * OpenClaw Lark/Feishu plugin entry point.
 */

import { pathToFileURL } from 'url'; // [修复] 引入 URL 转换模块,预防 Windows 绝对路径 Bug
import { emptyPluginConfigSchema } from 'openclaw/plugin-sdk';
import { feishuPlugin } from './src/channel/plugin';
import { LarkClient } from './src/core/lark-client';
import { registerOapiTools } from './src/tools/oapi/index';
import { registerFeishuMcpDocTools } from './src/tools/mcp/doc/index';
import { registerFeishuOAuthTool } from './src/tools/oauth';
import { registerFeishuOAuthBatchAuthTool } from './src/tools/oauth-batch-auth';
import { runDiagnosis, formatDiagReportCli, traceByMessageId, formatTraceOutput, analyzeTrace, } from './src/commands/diagnose';
import { registerCommands } from './src/commands/index';
import { larkLogger } from './src/core/lark-logger';
import { emitSecurityWarnings } from './src/core/security-check';
const log = larkLogger('plugin');

// ---------------------------------------------------------------------------
// Re-exports for external consumers
// ---------------------------------------------------------------------------
export { monitorFeishuProvider } from './src/channel/monitor';
export { sendMessageFeishu, sendCardFeishu, updateCardFeishu, editMessageFeishu } from './src/messaging/outbound/send';
export { getMessageFeishu } from './src/messaging/outbound/fetch';
export { uploadImageLark, uploadFileLark, sendImageLark, sendFileLark, sendAudioLark, uploadAndSendMediaLark, } from './src/messaging/outbound/media';
export { sendTextLark, sendCardLark, sendMediaLark, } from './src/messaging/outbound/deliver';
export { probeFeishu } from './src/channel/probe';
export { addReactionFeishu, removeReactionFeishu, listReactionsFeishu, FeishuEmoji, VALID_FEISHU_EMOJI_TYPES, } from './src/messaging/outbound/reactions';
export { forwardMessageFeishu } from './src/messaging/outbound/forward';
export { updateChatFeishu, addChatMembersFeishu, removeChatMembersFeishu, listChatMembersFeishu, } from './src/messaging/outbound/chat-manage';
export { feishuMessageActions } from './src/messaging/outbound/actions';
export { mentionedBot, nonBotMentions, extractMessageBody, formatMentionForText, formatMentionForCard, formatMentionAllForText, formatMentionAllForCard, buildMentionedMessage, buildMentionedCardContent, } from './src/messaging/inbound/mention';
export { feishuPlugin } from './src/channel/plugin';
export { handleFeishuReaction } from './src/messaging/inbound/reaction-handler';
export { parseMessageEvent } from './src/messaging/inbound/parse';
export { checkMessageGate } from './src/messaging/inbound/gate';
export { isMessageExpired } from './src/messaging/inbound/dedup';

// ---------------------------------------------------------------------------
// Plugin definition
// ---------------------------------------------------------------------------
const plugin = {
    id: 'openclaw-lark',
    name: 'Feishu',
    description: 'Lark/Feishu channel plugin with im/doc/wiki/drive/task/calendar tools',
    configSchema: emptyPluginConfigSchema(),
    register(api) {
        LarkClient.setRuntime(api.runtime);

        // ========================================
        // [修复] 拦截并修复 Windows ESM 路径错误
        // 找出可能被用作进程启动入口的字段,并将形如 C:\... 的路径转换为合法的 file:///C:/...
        const patchedFeishuPlugin = { ...feishuPlugin };
        for (const key of ['runner', 'entry', 'worker', 'path', 'script']) {
            if (typeof patchedFeishuPlugin[key] === 'string' && /^[a-zA-Z]:[\\/]/.test(patchedFeishuPlugin[key])) {
                patchedFeishuPlugin[key] = pathToFileURL(patchedFeishuPlugin[key]).href;
            }
        }
        api.registerChannel({ plugin: patchedFeishuPlugin }); 
        // ========================================

        // Register OAPI tools (calendar, task - using Feishu Open API directly)
        registerOapiTools(api);
        // Register MCP doc tools (using Model Context Protocol)
        registerFeishuMcpDocTools(api);
        // Register OAuth tool (UAT device flow authorization)
        registerFeishuOAuthTool(api);
        // Register OAuth batch auth tool (batch authorization for all app scopes)
        registerFeishuOAuthBatchAuthTool(api);
    
        // ---- Tool call hooks (auto-trace AI tool invocations) ----
        api.on('before_tool_call', (event) => {
            log.info(`tool call: ${event.toolName} params=${JSON.stringify(event.params)}`);
        });
        api.on('after_tool_call', (event) => {
            if (event.error) {
                log.error(`tool fail: ${event.toolName} ${event.error} (${event.durationMs ?? 0}ms)`);
            }
            else {
                log.info(`tool done: ${event.toolName} ok (${event.durationMs ?? 0}ms)`);
            }
        });

        // ---- Diagnostic commands ----
        api.registerCli((ctx) => {
            ctx.program
                .command('feishu-diagnose')
                .description('运行飞书插件诊断,检查配置、连通性和权限状态')
                .option('--trace <messageId>', '按 message_id 追踪完整处理链路')
                .option('--analyze', '分析追踪日志(需配合 --trace 使用)')
                .action(async (opts) => {
                try {
                    if (opts.trace) {
                        const lines = await traceByMessageId(opts.trace);
                        // eslint-disable-next-line no-console
                        console.log(formatTraceOutput(lines, opts.trace));
                        if (opts.analyze && lines.length > 0) {
                            // eslint-disable-next-line no-console
                            console.log(analyzeTrace(lines, opts.trace));
                        }
                    }
                    else {
                        const report = await runDiagnosis({
                            config: ctx.config,
                            logger: ctx.logger,
                        });
                        // eslint-disable-next-line no-console
                        console.log(formatDiagReportCli(report));
                        if (report.overallStatus === 'unhealthy') {
                            process.exitCode = 1;
                        }
                    }
                }
                catch (err) {
                    ctx.logger.error(`诊断命令执行失败: ${err}`);
                    process.exitCode = 1;
                }
            });
        }, { commands: ['feishu-diagnose'] });
    
        registerCommands(api);
    
        if (api.config) {
            emitSecurityWarnings(api.config, api.logger);
        }
    },
};
export default plugin;

三、控制台 UI(Dashboard)资产缺失修复

3.1 问题描述

在升级到 v2026.3.22 后,访问控制台 UI(默认 http://localhost:3000)时返回 503 错误,日志显示:

Control UI assets not found. Build them with `pnpm ui:build` (auto-installs UI deps), or run `pnpm ui:dev` during development.

根本原因:发布到 npm 的 openclaw@2026.3.22 包中 缺失了 dist/control-ui/ 目录。对比 v2026.3.13,该目录原本包含 index.htmlassets/、favicons 等静态资源,但在新版本中只有 dist/control-ui-assets-8FlHCc2H.jsdist/control-ui-shared-SxKiMaO4.js 两个文件,缺少完整的静态资源文件夹。

3.2 临时修复方案(无需构建工具链)

以下方法直接从 v2026.3.13 的 tarball 中提取缺失的 control-ui 目录,并将其复制到当前安装的 dist/ 下。

Linux / macOS / WSL 环境

# 进入临时目录并下载旧版本包
cd /tmp
npm pack openclaw@2026.3.13
# 解压并提取 control-ui 目录
tar -xzf openclaw-2026.3.13.tgz package/dist/control-ui/
# 复制到全局安装的 openclaw 目录
cp -r /tmp/package/dist/control-ui $(npm prefix -g)/lib/node_modules/openclaw/dist/
# 重启网关
openclaw gateway restart

Windows 环境

cd %TEMP%
npm pack openclaw@2026.3.13
tar -xzf openclaw-2026.3.13.tgz package/dist/control-ui/
xcopy /E /I /Y %TEMP%\package\dist\control-ui %APPDATA%\npm\node_modules\openclaw\dist\control-ui
openclaw gateway restart

注意:Windows 下若 tar 命令不可用,可使用 7-Zip 或 WinRAR 手动解压并复制。

3.3 验证

重启网关后,再次访问 Dashboard,应能正常加载 UI。如果仍存在问题,请确保复制操作正确覆盖了目标目录。


四、最终验证与收尾

完成以上所有修复后,请依次检查:

  • 飞书插件:向机器人发送消息,观察日志是否出现 dispatch complete
  • Dashboard:在浏览器中打开控制台地址,确认页面正常显示。

若一切正常,则说明 v2026.3.22 的环境已完全修复。建议在后续官方发布包含这些修复的版本后,再通过正常升级方式更新。


五、总结

本文档涵盖了 OpenClaw v2026.3.22 版本中两个已知的严重问题及其修复方法:

  1. 飞书插件因 SDK 重构崩溃:通过符号链接、代码垫片和入口加固解决。
  2. 控制台 UI 资产缺失:通过从旧版本包中提取静态资源补齐。

这套修复思路(依赖链接 → 代码垫片 → 资源补全)同样适用于类似版本迭代带来的兼容性问题。


评论