Install
openclaw skills install agent-feishu-direct-tools-patchOne-time/on-demand patch for openclaw-lark (no webhook LarkTicket paths). Implements agent-session-context, session-key-feishu, syncTicketContextForToolClient, createToolClient third arg, hooks, calendar effectiveSenderOpenId, toolClient sweep. After file edits, tell the user what changed and that they MUST restart the Gateway. BEFORE starting file edits, Agent must brief the user and wait for explicit confirmation. On failure or Feishu broken after patch, user may reinstall with: npx -y @larksuite/openclaw-lark install. Prefer AI-assisted dev tools or temporary OpenClaw skill load—avoid keeping this skill permanently enabled in skills.entries. Use when porting or re-applying after merge conflicts.
openclaw skills install agent-feishu-direct-tools-patch执行约定:以下路径相对于 openclaw-lark 扩展根目录(含 index.js、src/ 的目录)。实施 Agent 须先完成「执行前:向用户说明并待确认」 并得到用户 明确同意 后,再按 §B–§M 顺序修改代码,并跑文末 验收命令,即与下列已验证实现 语义等效。
强制要求:不得以“当前版本结构不同/未搜到同名片段”为由直接跳过任一章节;必须在当前代码中找到语义等价位置完成改动(见 §0.3 严格执行与闭环验收协议)。
before_tool_call 形态、execute 签名或核心模块名,本节中的路径/补丁点可能失效;升级后须逐项对照并重新冒烟(Control UI + 飞书 DM)。不要与本 Skill 之外的其它 Skill 混读;本文自成闭环。
在创建/修改任何源码文件(从 §B 起)之前,实施 Agent 必须先向用户发一条 补丁前说明(当前对话渠道:飞书、Control UI、IDE 侧等均可),让用户知情并 明确确认 后再继续。不得在用户仅浏览、提问或未表态的情况下擅自改代码。
openclaw-lark 扩展(新建 2 个文件、改核心模块与大量工具文件),用于解决「无飞书 webhook 票据时 Agent 直调飞书工具」的身份与账号问题(概要见 §A)。openclaw.json 中 plugins.installs.openclaw-lark.installPath 所指目录,或使用 git 提交/分支,便于回滚。npx -y @larksuite/openclaw-lark install
重装会按 @larksuite/openclaw-lark 发布的流程处理扩展,可能覆盖你对该插件目录的手写修改;执行前请用户自行确认。重装后仍须按 OpenClaw 文档 启用插件并重启 Gateway。
本节适用于 各类 AI 辅助开发工具中的 Agent(如 Cursor、GitHub Copilot、Codeium、Windsurf 等)、OpenClaw 对话里的 Agent,以及 任何按本 Skill 执行补丁的实施 Agent。在 §B–§M 的代码改动全部完成(含 §M 的 rg 验收,若环境可运行)之后,必须向用户发出 一条清晰的收官消息(飞书回复、Control UI 回复、或当前会话通道均可),内容至少包括:
openclaw-lark 扩展根目录),并标明 新建 或 修改,例如:
src/core/agent-session-context.js、src/core/session-key-feishu.jssrc/core/lark-ticket.js、src/core/lark-ticket.d.ts、src/core/tool-client.js、src/core/tool-client.d.ts、src/tools/helpers.js、src/tools/helpers.d.ts、index.js、src/core/auth-errors.js、src/tools/oapi/calendar/event.js,以及 §M 列出的各 toolClient(_toolCallId) 工具文件(若你还改了超出清单的文件,一并列出)。createToolClient 增加 toolCallId、插件 hook 绑定 sessionKey、日历身份优先 getTicket()、权限错误带 appId、全量工具传入 toolCallId 等。npx -y @larksuite/openclaw-lark install(与 「执行前:向用户说明并待确认」 一致),重装后重启 Gateway。本 Skill 是 按需执行的一次性工程补丁,不是日常对话要常驻引用的业务技能。建议按下面方式使用,避免它 长期出现在待选技能里、干扰其它 Agent:
| 做法 | 说明 |
|---|---|
| 只用本地 AI 工具、不挂 OpenClaw | 把 agent-feishu-direct-tools-patch 放在所用工具支持的 skills / 规则 / 上下文 目录,或在对话里 @ / 粘贴 SKILL.md 路径 执行;不要写入 openclaw.json 的 skills。这样 OpenClaw Gateway 运行时 完全看不到 本 Skill。 |
| 临时挂到 OpenClaw | 若必须让 Gateway 里的 Agent 读本文:可 短期 将本目录 复制 到已有 skills.load.extraDirs 下的某个文件夹,用完即删该副本(或移除 extraDirs 里为补丁专加的目录),并 重启 Gateway。列表里不再出现即恢复常态。 |
勿长期 enabled: true | 若配置里有 skills.entries.<name>:对本 Skill 不要设为长期启用;需要打补丁时再打开(若你的 OpenClaw 版本支持按条目开关),打完改回 禁用 或删掉对应条目。 |
| 不要依赖「Agent 自动选技能」 | 由用户 明确一句话触发(例如:「按 openclaw-lark 的 agent-feishu-direct-tools-patch 打补丁」),避免把补丁 Skill 与日常 team-agent-onboarding 类技能混在同一常驻集合里。 |
| 随仓库或压缩包分发 | 将整个 agent-feishu-direct-tools-patch 目录纳入 Git 或打包发给别人即可复用;是否让运行时装载,与 是否在磁盘保留 是两回事——保留文件 ≠ 必须加入 skills.entries 长期启用。 |
与 §「让 OpenClaw 里的对话 Agent 也能加载本 Skill」的关系:那几条是「短期需要时怎么挂载」;默认仍推荐 在 AI 辅助开发工具里执行 + 用完不挂 Gateway,或 临时挂载 → 打完补丁即拆。
本节用于避免“执行了但漏改/误改/跳过”。实施 Agent 在用户确认执行后,必须按以下闭环进行,直到全部通过:
toolClient() 无参调用检查通过(非注释/文档语境);index.js hook 参数兼容、createToolClient(config, accountIndex, toolCallId) 三参链路、calendar/event.js 的 effectiveSenderOpenId 优先顺序。openclaw-lark)加载进 Node 进程;改磁盘上的 .js 不会自动热替换已加载模块。openclaw.json → plugins.installs.openclaw-lark.installPath(或你本机实际启用的扩展目录)应与正在编辑的 openclaw-lark 根目录 一致;若从不一致的副本改代码,重启后仍会用旧代码。src/core/*.js)。openclaw、负责加载插件的 Node 进程):
openclaw gateway 或项目文档中的启动命令)。本仓库里 Skill 目录名为 agent-feishu-direct-tools-patch/(内含 SKILL.md)。OpenClaw 默认 不会自动扫描飞书扩展内部子目录;若 临时 要在 运行时 Agent 侧可读,任选其一:
agent-feishu-direct-tools-patch 到 openclaw.json 里 skills.load.extraDirs 已列出的目录之一,使存在 .../agent-feishu-direct-tools-patch/SKILL.md。打完补丁后 删除该副本 并重启 Gateway,见 §0.2。skills.load.extraDirs 中 短期增加 一条指向「仅含本 Skill 父目录」的路径;用完从配置中移除 后重启 Gateway。skills.entries:不要长期把本 Skill 对应条目设为 enabled: true;需要执行补丁时再打开,执行完改回禁用(行为因 OpenClaw 版本而异,以文档为准)。说明:插件代码补丁与 把 SKILL 给运行时读 是两件事——前者改 openclaw-lark 源码并重启即生效;后者只是把 操作说明 暴露给 Agent,不能替代源码修改与重启。默认推荐在本机 AI 辅助开发工具中执行本 Skill,避免补丁文档常驻 Gateway 技能列表(§0.2)。
| 类型 | 说明 |
|---|---|
| 入口 | Control UI、sessions_send 等 无 webhook,无完整 LarkTicket。 |
| 账号 | createToolClient 回退到默认账号 → 误用 main 等,报错里 appId 不对。 |
| 身份 | OpenClaw 已有 sessionKey(agent:…:feishu:…:direct:ou_…),未进工具栈 → 无法选 account + senderOpenId。 |
| ALS 不一致 | 合成 ticket 后未写回 ALS → getTicket() 仍陈旧 → open_id 跨应用(日历参会人等)。 |
| Hook 边界 | toolCallId 缺失或 (event,ctx) 顺序不同、await 丢 ALS → 需 Map + 短 TTL lastSessionKey。 |
目标行为:sessionKey → 解析 accountId/senderOpenId → 合成 ticket(且优先于残留 LarkTicket)→ syncTicketContextForToolClient → ToolClient 与 getTicket 一致 → 所有工具 toolClient(toolCallId)`。**
src/core/agent-session-context.js完整落盘为以下文件(逐字一致):
"use strict";
/**
* Propagate OpenClaw agent sessionKey into the Feishu tool stack without a Feishu webhook.
*
* `before_tool_call` receives ctx.sessionKey; we store it in AsyncLocalStorage and by
* toolCallId so createToolClient() can synthesize Lark identity for Control UI / sessions_send.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.bindToolCallContext = bindToolCallContext;
exports.bindAgentSessionKeyForToolCall = bindAgentSessionKeyForToolCall;
exports.getToolCallContext = getToolCallContext;
exports.getAgentSessionKey = getAgentSessionKey;
exports.resolveAgentSessionKeyForToolCall = resolveAgentSessionKeyForToolCall;
exports.registerSessionKeyForToolCall = registerSessionKeyForToolCall;
exports.clearSessionKeyForToolCall = clearSessionKeyForToolCall;
const node_async_hooks_1 = require("node:async_hooks");
const store = new node_async_hooks_1.AsyncLocalStorage();
/** Fallback when ALS does not propagate into tool execute (await boundaries). */
const sessionKeyByToolCallId = new Map();
/** Last-bound sessionKey (short TTL) when both ALS and Map miss. */
let lastSessionKey = "";
let lastSessionKeyAt = 0;
const LAST_SESSION_KEY_TTL_MS = 5000;
const MAX_TOOL_CALL_SESSION_KEYS = 512;
function bindToolCallContext(params) {
const sessionKey = typeof params?.sessionKey === "string" ? params.sessionKey.trim() : "";
const agentId = typeof params?.agentId === "string" ? params.agentId.trim() : "";
if (!sessionKey && !agentId) {
return;
}
if (sessionKey) {
lastSessionKey = sessionKey;
lastSessionKeyAt = Date.now();
}
store.enterWith({
...(sessionKey ? { sessionKey } : {}),
...(agentId ? { agentId } : {}),
});
}
function bindAgentSessionKeyForToolCall(sessionKey) {
bindToolCallContext({ sessionKey });
}
function getToolCallContext() {
return store.getStore();
}
function getAgentSessionKey() {
return store.getStore()?.sessionKey;
}
/**
* Prefer ALS; else sessionKey registered for this toolCallId in before_tool_call.
*/
function resolveAgentSessionKeyForToolCall(toolCallId) {
const fromAls = store.getStore()?.sessionKey;
if (fromAls) {
return fromAls;
}
if (toolCallId && typeof toolCallId === "string") {
const fromMap = sessionKeyByToolCallId.get(toolCallId);
if (fromMap) {
return fromMap;
}
}
// Last-resort fallback: if the hook did not bind Map (toolCallId optional)
// and ALS did not survive across async boundaries, still use a very recent sessionKey.
if (lastSessionKey && Date.now() - lastSessionKeyAt <= LAST_SESSION_KEY_TTL_MS) {
return lastSessionKey;
}
return undefined;
}
function registerSessionKeyForToolCall(toolCallId, sessionKey) {
if (!toolCallId || typeof toolCallId !== "string") {
return;
}
const sk = typeof sessionKey === "string" ? sessionKey.trim() : "";
if (!sk) {
return;
}
if (sessionKeyByToolCallId.size >= MAX_TOOL_CALL_SESSION_KEYS) {
const oldest = sessionKeyByToolCallId.keys().next().value;
if (oldest) {
sessionKeyByToolCallId.delete(oldest);
}
}
sessionKeyByToolCallId.set(toolCallId, sk);
}
function clearSessionKeyForToolCall(toolCallId) {
if (!toolCallId || typeof toolCallId !== "string") {
return;
}
sessionKeyByToolCallId.delete(toolCallId);
}
src/core/session-key-feishu.js完整落盘:
"use strict";
/**
* Parse OpenClaw sessionKey for per-account Feishu DM binding:
* agent:<agentId>:feishu:direct:<user_open_id>
* agent:<agentId>:feishu:<accountId>:direct:<user_open_id>
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseFeishuDirectSessionIdentity = parseFeishuDirectSessionIdentity;
exports.resolveFeishuAccountIdForAgent = resolveFeishuAccountIdForAgent;
/** open_id segment; allow optional tail segments (lanes, etc.) */
const FEISHU_DIRECT_SESSION_RE = /^agent:([^:]+):feishu:direct:(ou_[0-9a-f]{32})(?::|$)/i;
const FEISHU_ACCOUNT_SCOPED_DIRECT_RE = /^agent:([^:]+):feishu:([^:]+):direct:(ou_[0-9a-f]{32})(?::|$)/i;
function resolveFeishuAccountIdForAgent(agentId, cfg) {
if (!agentId || !cfg) {
return undefined;
}
const bindings = cfg.bindings;
if (Array.isArray(bindings)) {
const hit = bindings.find((b) => b?.agentId === agentId && b?.match?.channel === "feishu");
const aid = hit?.match?.accountId;
if (aid && typeof aid === "string") {
return aid.trim();
}
}
return agentId;
}
/**
* @returns {{ accountId: string, senderOpenId: string } | undefined}
*/
function parseFeishuDirectSessionIdentity(sessionKey, cfg) {
if (!sessionKey || typeof sessionKey !== "string") {
return undefined;
}
const raw = sessionKey.trim();
let m = raw.match(FEISHU_DIRECT_SESSION_RE);
if (m) {
const agentId = m[1];
const senderOpenId = m[2];
const accountId = resolveFeishuAccountIdForAgent(agentId, cfg);
if (!accountId) {
return undefined;
}
return { accountId, senderOpenId };
}
m = raw.match(FEISHU_ACCOUNT_SCOPED_DIRECT_RE);
if (m) {
const accountIdFromKey = m[2];
const senderOpenId = m[3];
if (!accountIdFromKey) {
return undefined;
}
return { accountId: accountIdFromKey.trim(), senderOpenId };
}
return undefined;
}
src/core/lark-ticket.jsexports 块增加:exports.syncTicketContextForToolClient = syncTicketContextForToolClient;(与其它 export 并列)。getTicket 与 ticketElapsed 之间增加函数与注释(保持与现网一致):/**
* After {@link createToolClient} merges sessionKey-derived identity into the
* working ticket snapshot, push the same ticket into AsyncLocalStorage so
* {@link getTicket} matches for the remainder of the tool run.
*
* Without this, tool bodies that call `getTicket()` can see a stale ticket
* from another app/round while ToolClient already uses the synthetic account
* (e.g. open_id cross-app on attendee APIs).
*/
function syncTicketContextForToolClient(ticket) {
if (!ticket || typeof ticket !== "object") {
return;
}
store.enterWith({ ...ticket });
}
src/core/lark-ticket.d.ts在 ticketElapsed 声明之后追加:
/**
* Align AsyncLocalStorage ticket with the merged identity used by ToolClient
* (sessionKey synthetic over feishu:direct), so {@link getTicket} matches.
*/
export declare function syncTicketContextForToolClient(ticket: LarkTicket): void;
src/core/tool-client.js./auth-errors require 之后即可):const agent_session_context_1 = require("./agent-session-context");
const session_key_feishu_1 = require("./session-key-feishu");
createToolClient 函数为(保留文件其余类定义不变;若上游 JSDoc 不同,以函数体为准):function createToolClient(config, accountIndex = 0, toolCallId) {
let ticket = (0, lark_ticket_1.getTicket)();
const sk = (0, agent_session_context_1.resolveAgentSessionKeyForToolCall)(toolCallId);
const resolveConfig = (0, lark_client_1.getResolvedConfig)(config);
const synthetic = sk ? (0, session_key_feishu_1.parseFeishuDirectSessionIdentity)(sk, resolveConfig) : undefined;
// 0. feishu:direct sessionKey(Control UI / sessions_send / 非 webhook 回合)
//
// 必须**始终优先**于 LarkTicket:网关或其它路径可能仍带着 withTicket 残留,
// 若仅在 !ticket.senderOpenId 时才合成,会导致 session 里仍用「别的回合」的
// accountId/senderOpenId,日历 delete 等与飞书私聊表现不一致。
if (synthetic) {
const prevAccount = ticket?.accountId;
const prevSender = ticket?.senderOpenId;
const overridden = Boolean(prevSender && prevSender !== synthetic.senderOpenId) ||
Boolean(prevAccount && prevAccount !== synthetic.accountId);
if (overridden) {
tcLog.warn("createToolClient: sessionKey overrides LarkTicket (feishu:direct takes precedence)", {
sessionKey: sk,
prevAccountId: prevAccount,
prevSenderOpenId: prevSender,
accountId: synthetic.accountId,
senderOpenId: synthetic.senderOpenId,
});
}
ticket = {
...(ticket || {}),
messageId: ticket?.messageId ?? "synthetic:openclaw-session",
chatId: ticket?.chatId ?? "synthetic:openclaw-session",
startTime: ticket?.startTime ?? Date.now(),
accountId: synthetic.accountId,
senderOpenId: synthetic.senderOpenId,
};
tcLog.info("createToolClient: synthetic Lark identity from sessionKey (feishu:direct)", {
accountId: synthetic.accountId,
senderOpenId: synthetic.senderOpenId,
});
// Keep getTicket() aligned with this merged ticket for any tool code that reads ALS directly.
(0, lark_ticket_1.syncTicketContextForToolClient)(ticket);
}
// 1. 解析账号
let account;
if (ticket?.accountId) {
const resolved = (0, accounts_1.getLarkAccount)(resolveConfig, ticket.accountId);
if (!resolved.configured) {
throw new Error(`Feishu account "${ticket.accountId}" is not configured (missing appId or appSecret). ` +
`Please check channels.feishu.accounts.${ticket.accountId} in your config.`);
}
if (!resolved.enabled) {
throw new Error(`Feishu account "${ticket.accountId}" is disabled. ` +
`Set channels.feishu.accounts.${ticket.accountId}.enabled to true, or remove it to use defaults.`);
}
account = resolved;
}
if (!account) {
const accounts = (0, accounts_1.getEnabledLarkAccounts)(resolveConfig);
if (accounts.length === 0) {
throw new Error('No enabled Feishu accounts configured. ' + 'Please add appId and appSecret in config under channels.feishu');
}
if (accountIndex >= accounts.length) {
throw new Error(`Requested account index ${accountIndex} but only ${accounts.length} accounts available`);
}
const fallback = accounts[accountIndex];
if (!fallback.configured) {
throw new Error(`Account at index ${accountIndex} is not fully configured (missing appId or appSecret)`);
}
account = fallback;
}
// 2. 获取 SDK 实例(复用 LarkClient 的缓存)
const larkClient = lark_client_1.LarkClient.fromAccount(account);
// 3. 组装 ToolClient
return new ToolClient({
account,
senderOpenId: ticket?.senderOpenId,
sdk: larkClient.sdk,
config,
});
}
src/core/tool-client.d.ts将 createToolClient 声明改为三参数(第三参可选):
export declare function createToolClient(config: ClawdbotConfig, accountIndex?: number, toolCallId?: string): ToolClient;
(并视需要在上方 JSDoc 增加 @param toolCallId 一行。)
src/tools/helpers.js在 createToolContext 返回对象中,toolClient 必须为:
toolClient: (toolCallId) => (0, tool_client_1.createToolClient)(config, accountIndex, toolCallId),
src/tools/helpers.d.tsToolContext 接口中:
toolClient: (toolCallId?: string) => ToolClient;
(替换原 () => ToolClient。)
index.jsrequire("./src/core/...") 旁增加:const agent_session_context_1 = require("./src/core/agent-session-context.js");
const fs_1 = require("node:fs");
const path_1 = require("node:path");
const os_1 = require("node:os");
(若已有 fs/path/os 的 require,则合并去重,只保证下文函数可用。)
const log = ... 之后、emptyPluginConfigSchema 或 plugin 定义之前,插入 整段 sessionKeyByAgentSessionId、getOpenclawStateDir、resolveSessionKeyFromStore(逻辑与现网一致):process.env.OPENCLAW_STATE_DIR 非空则用;否则 path.join(homedir, ".openclaw")。path.join(stateDir, "agents", agentId, "sessions", "sessions.json")。JSON.parse 后取 parsed.sessions 数组,按 s.sessionId === sessionId 找条目,取 hit.key 字符串为 sessionKey。agentId:sessionId → 结果(含 undefined)。api.register(...) 内、before_tool_call / after_tool_call:before_tool_call:必须兼容双参(arg1, arg2),按 arg1?.toolName ? arg1 : arg2 取 event,按 arg1?.sessionKey ? arg1 : arg2 取 ctx。解析 ctx.sessionKey 或 event.sessionKey;若无则 resolveSessionKeyFromStore(ctx.agentId, ctx.sessionId)。解析 toolCallId 自 event 或 ctx。若有 resolvedSk:bindToolCallContext({ sessionKey: resolvedSk, agentId: ctx?.agentId });若 toolCallId 存在则 registerSessionKeyForToolCall(toolCallId, resolvedSk)。可选:当 event.toolName === "feishu_calendar_event" 时打一条 log.info 含 agentId、sessionId、toolCallId、resolvedSkPresent、resolvedFromStore、resolvedSk。event 当 ctx 的形式(会导致 sessionKey 丢失)。after_tool_call:同样解析 toolCallId,若存在则 clearSessionKeyForToolCall(toolCallId)。(完整可参考你仓库中已合并的 index.js 第 23–77 行与 166–213 行。)
src/core/auth-errors.js(AppScopeMissingError)constructor 里两条 super(...) 字符串末尾均带上 appId=${info.appId ?? 'unknown'}(与现网 AppScopeMissingError 一致)。
src/tools/oapi/calendar/event.js在 所有 计算「当前用户 open_id / UAT 身份」的分支中,在已有 const larkTicket = getTicket() 与 sessionBoundSenderOpenId = resolveSessionBoundSenderOpenId(...) 之后,必须为:
const effectiveSenderOpenId = larkTicket?.senderOpenId ?? sessionBoundSenderOpenId;
不得再使用「仅信 sessionBound、忽略已同步 ticket」等 其它优先顺序,否则与 syncTicketContextForToolClient 的设计不一致。
若当前版本 event.js 结构与示例不同,仍必须在等价身份分支中落实同一优先顺序;不得将 §L 标记为不适用后跳过。
toolClient 必须带 toolCallId规则:凡通过 createToolContext 拿到 toolClient 的注册工具,execute 第一形参为 _toolCallId(或 toolCallId),且客户端获取写为 toolClient(_toolCallId)(MCP 封装里变量名 toolCallId 则用 toolClient(toolCallId))。
以下路径在 2026.3.25 结构 中已对齐(移植时逐项 rg 校验;若官方新增工具,按同一规则补):
src/tools/oapi/calendar/event.jssrc/tools/oapi/calendar/event-attendee.jssrc/tools/oapi/calendar/calendar.jssrc/tools/oapi/calendar/freebusy.jssrc/tools/oapi/bitable/app.jssrc/tools/oapi/bitable/app-table.jssrc/tools/oapi/bitable/app-table-field.jssrc/tools/oapi/bitable/app-table-record.jssrc/tools/oapi/bitable/app-table-view.jssrc/tools/oapi/chat/chat.jssrc/tools/oapi/chat/members.jssrc/tools/oapi/common/get-user.jssrc/tools/oapi/common/search-user.jssrc/tools/oapi/drive/doc-comments.jssrc/tools/oapi/drive/doc-media.jssrc/tools/oapi/drive/file.jssrc/tools/oapi/im/message.jssrc/tools/oapi/im/message-read.jssrc/tools/oapi/im/resource.jssrc/tools/oapi/search/doc-search.jssrc/tools/oapi/sheets/sheet.jssrc/tools/oapi/task/comment.jssrc/tools/oapi/task/subtask.jssrc/tools/oapi/task/task.jssrc/tools/oapi/task/tasklist.jssrc/tools/oapi/wiki/space.jssrc/tools/oapi/wiki/space-node.jssrc/tools/mcp/shared.js验收(在 src/tools 下):
rg 'toolClient\(\)' --glob '*.js' src/tools
除 注释或文档字符串 外应 无 toolClient() 无参调用。
openclaw.json(或等价配置):bindings 中 match.channel === "feishu" 且 match.accountId 映射「仅 agent 段的 feishu:direct key」到 channels.feishu.accounts。agent:…:feishu:<accountId>:direct:ou_… 显式写账号。前置条件:已完成 「补丁与 OpenClaw 运行态」 中的重启;否则本节结果不可靠。
feishu_calendar_event 创建日程。createToolClient: synthetic Lark identity from sessionKey;若曾存在残留 LarkTicket 且被覆盖,可有 sessionKey overrides LarkTicket。_debug:核对 ticket_account_id、ticket_sender_open_id 与当前会话身份一致。若验证失败:先确认 重启的是加载 installPath 下该扩展的进程,再查 before_tool_call 是否拿到 sessionKey、bindings 是否指向正确 accountId(见 §N),并回到 §0.3 按失败点继续修复与重验,直到全部通过。
若仍无法恢复飞书能力、且用户同意放弃未备份的本地插件改动,可建议用户使用官方命令 重新安装飞书插件(同一 「执行前」 节中的命令),安装完成后 重启 Gateway 再测:
npx -y @larksuite/openclaw-lark install
删除两新文件,还原 lark-ticket*、tool-client*、helpers*、index.js、auth-errors.js、calendar/event.js 及所有 toolClient callsite 至补丁前;禁止只回滚其中一两处以免半套状态。
若无备份、手工回滚困难,且用户同意以官方包为准,可改用 「执行前」 与 §O 中的
npx -y @larksuite/openclaw-lark install 重装插件基线。