一、背景概述
当将 OpenClaw 从 v2026.3.13 升级到 v2026.3.22 后,用户可能会遇到两类主要故障:
- 飞书插件(openclaw-lark)崩溃:消息无法发送,网关不断报错,甚至进程重启。
- 控制台 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.html、assets/、favicons 等静态资源,但在新版本中只有 dist/control-ui-assets-8FlHCc2H.js 和 dist/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 版本中两个已知的严重问题及其修复方法:
- 飞书插件因 SDK 重构崩溃:通过符号链接、代码垫片和入口加固解决。
- 控制台 UI 资产缺失:通过从旧版本包中提取静态资源补齐。
这套修复思路(依赖链接 → 代码垫片 → 资源补全)同样适用于类似版本迭代带来的兼容性问题。