← 返回
未分类

Feishu(Lark)Multi-Agent Tool-Use Patch

One-time/on-demand patch for openclaw-lark (no webhook LarkTicket paths). Implements agent-session-context, session-key-feishu, syncTicketContextForToolClien...
一次性/按需补丁,用于 openclaw‑lark(不依赖 webhook LarkTicket 路径)。实现 agent‑session‑context、session‑key‑feishu、syncTicketContextForToolClient 等功能。
zouxiao7777 zouxiao7777 来源
未分类 clawhub v1.0.0 1 版本 100000 Key: 无需
★ 0
Stars
📥 476
下载
💾 0
安装
1
版本
#latest

概述

openclaw-lark:Agent 无飞书票据直调工具(完整补丁 Skill)

执行约定:以下路径相对于 openclaw-lark 扩展根目录(含 index.jssrc/ 的目录)。实施 Agent 须先完成「执行前:向用户说明并待确认」 并得到用户 明确同意 后,再按 §B–§M 顺序修改代码,并跑文末 验收命令,即与下列已验证实现 语义等效

强制要求:不得以“当前版本结构不同/未搜到同名片段”为由直接跳过任一章节;必须在当前代码中找到语义等价位置完成改动(见 §0.3 严格执行与闭环验收协议)。

  • 在飞书插件 2026.3.182026.3.25 对应代码结构上测试可用。
  • 升级风险:飞书官方若后续改动插件目录、before_tool_call 形态、execute 签名或核心模块名,本节中的路径/补丁点可能失效;升级后须逐项对照并重新冒烟(Control UI + 飞书 DM)。

不要与本 Skill 之外的其它 Skill 混读;本文自成闭环。


执行前:向用户说明并待确认(Agent 第一优先级,先于 §B–§M)

在创建/修改任何源码文件(从 §B 起)之前,实施 Agent 必须先向用户发一条 补丁前说明(当前对话渠道:飞书、Control UI、IDE 侧等均可),让用户知情并 明确确认 后再继续。不得在用户仅浏览、提问或未表态的情况下擅自改代码。

说明里应包含的要点(Agent 按需组织成自然语言)

  1. 做什么:本 Skill 会按章节 批量修改 openclaw-lark 扩展(新建 2 个文件、改核心模块与大量工具文件),用于解决「无飞书 webhook 票据时 Agent 直调飞书工具」的身份与账号问题(概要见 §A)。
  2. 使用方式:按 §B–§M 顺序执行;改完后用户需 自行重启 OpenClaw Gateway 才生效(见 「补丁与 OpenClaw 运行态」);验证见 §O
  3. 风险:环境与官方版本差异可能导致合并冲突或补丁点不匹配;飞书插件升级后需重新对照 Skill § 开头的版本说明
  4. 建议:动手前 备份 openclaw.jsonplugins.installs.openclaw-lark.installPath 所指目录,或使用 git 提交/分支,便于回滚。
  5. 失败或补丁后飞书仍不可用:可先按 §P 回滚 手工改动;若仍异常、希望恢复 官方可安装基线,在用户 知情并接受可能被覆盖的本地修改 的前提下,可执行官方安装命令重装飞书插件(需本机 Node / npx 可用):
npx -y @larksuite/openclaw-lark install

重装会按 @larksuite/openclaw-lark 发布的流程处理扩展,可能覆盖你对该插件目录的手写修改;执行前请用户自行确认。重装后仍须按 OpenClaw 文档 启用插件并重启 Gateway

  1. 一次性 Skill:若将本 Skill 挂进 OpenClaw 技能列表,建议 短期使用,见 §0.2

何时可以开始改代码

  • 必须:用户用明确措辞表示 同意执行本补丁(例如「确认执行」「开始打补丁」「同意,请按 Skill 修改」等;随 locale 理解同义表达)。
  • 禁止:用户仅说「讲讲 Skill」「什么是补丁」而未确认执行 → 只解释,不改文件
  • 用户确认后,§B 起执行;执行全过程仍须遵守 §0 的收官汇报与 §「补丁与 OpenClaw 运行态」 的重启说明。

0. 执行收尾与用户告知(实施 Agent 必做)

本节适用于 各类 AI 辅助开发工具中的 Agent(如 Cursor、GitHub Copilot、Codeium、Windsurf 等)、OpenClaw 对话里的 Agent,以及 任何按本 Skill 执行补丁的实施 Agent。在 §B–§M 的代码改动全部完成(含 §M 的 rg 验收,若环境可运行)之后,必须向用户发出 一条清晰的收官消息(飞书回复、Control UI 回复、或当前会话通道均可),内容至少包括:

  1. 结果状态:成功完成 / 部分完成(并说明卡在哪一步)/ 未改文件(仅咨询)。
  2. 改动了哪些文件:逐项列出路径(相对于 openclaw-lark 扩展根目录),并标明 新建修改,例如:
    • 新建:src/core/agent-session-context.jssrc/core/session-key-feishu.js
    • 修改:src/core/lark-ticket.jssrc/core/lark-ticket.d.tssrc/core/tool-client.jssrc/core/tool-client.d.tssrc/tools/helpers.jssrc/tools/helpers.d.tsindex.jssrc/core/auth-errors.jssrc/tools/oapi/calendar/event.js,以及 §M 列出的各 toolClient(_toolCallId) 工具文件(若你还改了超出清单的文件,一并列出)。
  3. 做了哪些类别的调整(给用户看的短摘要,无需贴大段代码),例如:新增会话上下文与 sessionKey 解析、合成 Lark ticket 并同步 ALS、createToolClient 增加 toolCallId、插件 hook 绑定 sessionKey、日历身份优先 getTicket()、权限错误带 appId、全量工具传入 toolCallId 等。
  4. 网关重启提醒(强制):说明 磁盘上的修改在重启 OpenClaw Gateway(或加载该插件的 Node 进程)之前不会生效;请用户 自行按环境重启(实施 Agent 若无明确授权与能力,不要声称已替用户重启)。
  5. 可选后续:提醒用户重启后可按 §O 做飞书 DM + Control UI 冒烟;若曾临时把本 Skill 挂进 OpenClaw 技能目录,可按 §0.2 拆除以免常驻。
  6. 若执行失败或补丁后飞书仍异常:提醒用户可先 §P 回滚;仍不可用则可在接受「官方重装可能覆盖本地修改」的前提下执行

npx -y @larksuite/openclaw-lark install(与 「执行前:向用户说明并待确认」 一致),重装后重启 Gateway。


0.2 一次性补丁:如何避免「常态化待在」OpenClaw 技能列表

本 Skill 是 按需执行的一次性工程补丁,不是日常对话要常驻引用的业务技能。建议按下面方式使用,避免它 长期出现在待选技能里、干扰其它 Agent:

做法说明
------------
只用本地 AI 工具、不挂 OpenClawagent-feishu-direct-tools-patch 放在所用工具支持的 skills / 规则 / 上下文 目录,或在对话里 @ / 粘贴 SKILL.md 路径 执行;不要写入 openclaw.jsonskills。这样 OpenClaw Gateway 运行时 完全看不到 本 Skill。
临时挂到 OpenClaw若必须让 Gateway 里的 Agent 读本文:可 短期 将本目录 复制 到已有 skills.load.extraDirs 下的某个文件夹,用完即删该副本(或移除 extraDirs 里为补丁专加的目录),并 重启 Gateway。列表里不再出现即恢复常态。
勿长期 enabled: true若配置里有 skills.entries.:对本 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,或 临时挂载 → 打完补丁即拆


0.3 严格执行与闭环验收协议(实施 Agent 必须遵守)

本节用于避免“执行了但漏改/误改/跳过”。实施 Agent 在用户确认执行后,必须按以下闭环进行,直到全部通过:

  1. 逐条施工:按 §B→§M 顺序执行;每节完成后立即自检,不得一次性粗放改完再回头补。
  2. 禁止跳节:任何一节(含 §L)都不可因“版本差异”直接标记不适用;若代码结构不同,必须在当前实现中定位语义等价位置并完成同等改动。
  3. 双重校验:每节至少做
    • 文本/结构校验(关键符号、函数签名、调用形态、导出项);
    • 行为意图校验(是否满足该节目标语义,而非仅字符串出现)。
  4. 失败即继续修复:只要有任一校验不通过,必须继续修改并重验;禁止在“部分通过”状态下收官。
  5. 收官门槛(全部同时满足)
    • §B–§M 每一节均通过;
    • §M 的 toolClient() 无参调用检查通过(非注释/文档语境);
    • 对关键高风险点做定向复核:index.js hook 参数兼容、createToolClient(config, accountIndex, toolCallId) 三参链路、calendar/event.jseffectiveSenderOpenId 优先顺序。
  6. 证据化汇报:收官消息必须带“章节验收清单”,至少列出 §B–§M 每节“已通过”的一句证据(例如命中函数名/签名/调用形态),而非只说“已完成”。
  7. 未全绿不得结束:若工具环境限制导致某条无法验证,状态必须标为“未完成/待验证”,并继续处理可处理项;禁止宣称“补丁完成”。

补丁与 OpenClaw 运行态(AI 辅助工具与网关通用,必读)

为什么必须重启网关(或等价进程)

  • OpenClaw 在 启动 Gateway 时 把插件(含 openclaw-lark加载进 Node 进程;改磁盘上的 .js 不会自动热替换已加载模块。
  • 仅保存文件 ≠ 线上生效。改完 §B–§M 后,必须 重启加载该插件的进程,补丁才会在 飞书 / Control UI / sessions_send 里体现。

建议操作顺序(实施者照做)

  1. 确认改的是运行时要用的目录openclaw.jsonplugins.installs.openclaw-lark.installPath(或你本机实际启用的扩展目录)应与正在编辑的 openclaw-lark 根目录 一致;若从不一致的副本改代码,重启后仍会用旧代码。
  2. 保存全部相关文件(含新建的两个 src/core/*.js)。
  3. 重启 OpenClaw Gateway(或任何托管 openclaw、负责加载插件的 Node 进程):
    • 前台运行:在运行 Gateway 的终端里停止进程(如 Ctrl+C),再按你平时的方式重新启动(例如 openclaw gateway 或项目文档中的启动命令)。
    • 守护进程 / systemd / Docker / 远程机器上的 Gateway:在 实际跑 Gateway 的那台机器 上对 同一服务 执行重启(与官方「改插件配置后重启 Gateway」一致)。
  4. 再跑 §O 的验证(飞书 DM + Control UI 各测一次)。

在 AI 辅助开发工具里用本 Skill

  • 由 Agent 或人工按 §B–§M 改代码 → 必须执行上面第 3 步(重启 Gateway) → 再验证。
  • 若跳过重启,会误以为「补丁无效」。

让 OpenClaw 里的对话 Agent 也能「加载」本 Skill(可选、建议短期)

本仓库里 Skill 目录名为 agent-feishu-direct-tools-patch/(内含 SKILL.md)。OpenClaw 默认 不会自动扫描飞书扩展内部子目录;若 临时 要在 运行时 Agent 侧可读,任选其一:

  1. 复制(优先于软链,便于打完补丁整目录删除)整个 agent-feishu-direct-tools-patchopenclaw.jsonskills.load.extraDirs 已列出的目录之一,使存在 .../agent-feishu-direct-tools-patch/SKILL.md。打完补丁后 删除该副本 并重启 Gateway,见 §0.2
  2. 或在 skills.load.extraDirs短期增加 一条指向「仅含本 Skill 父目录」的路径;用完从配置中移除 后重启 Gateway。
  3. 若使用 skills.entries不要长期把本 Skill 对应条目设为 enabled: true;需要执行补丁时再打开,执行完改回禁用(行为因 OpenClaw 版本而异,以文档为准)。

说明:插件代码补丁把 SKILL 给运行时读 是两件事——前者改 openclaw-lark 源码并重启即生效;后者只是把 操作说明 暴露给 Agent,不能替代源码修改与重启。默认推荐在本机 AI 辅助开发工具中执行本 Skill,避免补丁文档常驻 Gateway 技能列表(§0.2)。


A. 要解决的问题

类型说明
------------
入口Control UI、sessions_send 等 无 webhook,无完整 LarkTicket
账号createToolClient 回退到默认账号 → 误用 main 等,报错里 appId 不对
身份OpenClaw 已有 sessionKeyagent:…: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)`。**


B. 新建文件 1: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);
}

C. 新建文件 2: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;
}

D. 修改 src/core/lark-ticket.js

  1. exports 块增加:exports.syncTicketContextForToolClient = syncTicketContextForToolClient;(与其它 export 并列)。
  2. getTicketticketElapsed 之间增加函数与注释(保持与现网一致):
/**
 * 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 });
}

E. 修改 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;

F. 修改 src/core/tool-client.js

  1. 新增 require(紧挨现有 ./auth-errors require 之后即可):
const agent_session_context_1 = require("./agent-session-context");
const session_key_feishu_1 = require("./session-key-feishu");
  1. 整体替换 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,
    });
}

G. 修改 src/core/tool-client.d.ts

createToolClient 声明改为三参数(第三参可选):

export declare function createToolClient(config: ClawdbotConfig, accountIndex?: number, toolCallId?: string): ToolClient;

(并视需要在上方 JSDoc 增加 @param toolCallId 一行。)


H. 修改 src/tools/helpers.js

createToolContext 返回对象中,toolClient 必须为:

toolClient: (toolCallId) => (0, tool_client_1.createToolClient)(config, accountIndex, toolCallId),

I. 修改 src/tools/helpers.d.ts

ToolContext 接口中:

toolClient: (toolCallId?: string) => ToolClient;

(替换原 () => ToolClient。)


J. 修改根目录 index.js

  1. 在其它 require("./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,则合并去重,只保证下文函数可用。)

  1. const log = ... 之后、emptyPluginConfigSchemaplugin 定义之前,插入 整段 sessionKeyByAgentSessionIdgetOpenclawStateDirresolveSessionKeyFromStore(逻辑与现网一致):
  • 状态目录: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。
  • 使用内存 Map 缓存 agentId:sessionId → 结果(含 undefined)。
  1. api.register(...) 内、before_tool_call / after_tool_call
  • before_tool_call必须兼容双参arg1, arg2),按 arg1?.toolName ? arg1 : arg2event,按 arg1?.sessionKey ? arg1 : arg2ctx。解析 ctx.sessionKeyevent.sessionKey;若无则 resolveSessionKeyFromStore(ctx.agentId, ctx.sessionId)。解析 toolCallIdeventctx。若有 resolvedSkbindToolCallContext({ sessionKey: resolvedSk, agentId: ctx?.agentId });若 toolCallId 存在则 registerSessionKeyForToolCall(toolCallId, resolvedSk)。可选:当 event.toolName === "feishu_calendar_event" 时打一条 log.infoagentIdsessionIdtoolCallIdresolvedSkPresentresolvedFromStoreresolvedSk

禁止写成仅单参并把 eventctx 的形式(会导致 sessionKey 丢失)。

  • after_tool_call:同样解析 toolCallId,若存在则 clearSessionKeyForToolCall(toolCallId)

(完整可参考你仓库中已合并的 index.js 第 23–77 行与 166–213 行。)


K. 修改 src/core/auth-errors.jsAppScopeMissingError

constructor 里两条 super(...) 字符串末尾均带上 appId=${info.appId ?? 'unknown'}(与现网 AppScopeMissingError 一致)。


L. 修改 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 标记为不适用后跳过


M. 全量工具 callsite:toolClient 必须带 toolCallId

规则:凡通过 createToolContext 拿到 toolClient 的注册工具,execute 第一形参为 _toolCallId(或 toolCallId,且客户端获取写为 toolClient(_toolCallId)(MCP 封装里变量名 toolCallId 则用 toolClient(toolCallId))。

以下路径在 2026.3.25 结构 中已对齐(移植时逐项 rg 校验;若官方新增工具,按同一规则补):

  • src/tools/oapi/calendar/event.js
  • src/tools/oapi/calendar/event-attendee.js
  • src/tools/oapi/calendar/calendar.js
  • src/tools/oapi/calendar/freebusy.js
  • src/tools/oapi/bitable/app.js
  • src/tools/oapi/bitable/app-table.js
  • src/tools/oapi/bitable/app-table-field.js
  • src/tools/oapi/bitable/app-table-record.js
  • src/tools/oapi/bitable/app-table-view.js
  • src/tools/oapi/chat/chat.js
  • src/tools/oapi/chat/members.js
  • src/tools/oapi/common/get-user.js
  • src/tools/oapi/common/search-user.js
  • src/tools/oapi/drive/doc-comments.js
  • src/tools/oapi/drive/doc-media.js
  • src/tools/oapi/drive/file.js
  • src/tools/oapi/im/message.js
  • src/tools/oapi/im/message-read.js
  • src/tools/oapi/im/resource.js
  • src/tools/oapi/search/doc-search.js
  • src/tools/oapi/sheets/sheet.js
  • src/tools/oapi/task/comment.js
  • src/tools/oapi/task/subtask.js
  • src/tools/oapi/task/task.js
  • src/tools/oapi/task/tasklist.js
  • src/tools/oapi/wiki/space.js
  • src/tools/oapi/wiki/space-node.js
  • src/tools/mcp/shared.js

验收(在 src/tools 下)

rg 'toolClient\(\)' --glob '*.js' src/tools

注释或文档字符串 外应 toolClient() 无参调用。


N. 配置侧(runtime,非代码)

  • openclaw.json(或等价配置):bindingsmatch.channel === "feishu"match.accountId 映射「仅 agent 段的 feishu:direct key」到 channels.feishu.accounts
  • 或使用 agent:…:feishu::direct:ou_… 显式写账号。

O. 部署与验证(代码已保存且 Gateway 已按上文重启后执行)

前置条件:已完成 「补丁与 OpenClaw 运行态」 中的重启;否则本节结果不可靠。

  1. 飞书私聊(DM):与补丁前一致,仍能正常调用飞书写工具(回归)。
  2. Control UI(或其它无 webhook 入口):执行一次写操作,例如 feishu_calendar_event 创建日程。
  3. 日志(Gateway / 插件日志):期望出现 createToolClient: synthetic Lark identity from sessionKey;若曾存在残留 LarkTicket 且被覆盖,可有 sessionKey overrides LarkTicket
  4. (可选) 若工具支持 _debug:核对 ticket_account_idticket_sender_open_id 与当前会话身份一致。

若验证失败:先确认 重启的是加载 installPath 下该扩展的进程,再查 before_tool_call 是否拿到 sessionKeybindings 是否指向正确 accountId(见 §N),并回到 §0.3 按失败点继续修复与重验,直到全部通过。

若仍无法恢复飞书能力、且用户同意放弃未备份的本地插件改动,可建议用户使用官方命令 重新安装飞书插件(同一 「执行前」 节中的命令),安装完成后 重启 Gateway 再测:

npx -y @larksuite/openclaw-lark install

P. 回滚

删除两新文件,还原 lark-tickettool-clienthelpers*index.jsauth-errors.jscalendar/event.js 及所有 toolClient callsite 至补丁前;禁止只回滚其中一两处以免半套状态。

若无备份、手工回滚困难,且用户同意以官方包为准,可改用 「执行前」§O 中的

npx -y @larksuite/openclaw-lark install 重装插件基线。

版本历史

共 1 个版本

  • v1.0.0 当前
    2026-05-03 05:45 安全 安全

安全检测

腾讯云安全 (Keen)

安全,无风险
查看报告

腾讯云安全 (Sanbu)

安全,无风险
查看报告

🔗 相关推荐

ai-agent

Agent Browser

rez0
用于 AI 代理的浏览器自动化 CLI。当用户需要与网站交互(包括浏览页面、填写表单、点击按钮、截图等)时使用。
★ 861 📥 340,911
ai-agent

Find Skills

root
帮助用户发现和安装智能体技能,当用户询问如「如何做X」、「找X的技能」、「有能做...的吗」等问题时
★ 1,512 📥 568,677
ai-agent

self-improving agent

pskoett
记录自身发现以实现自我改进的技能
★ 4,155 📥 927,797