openclaw-lark-multi-agent 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -98,6 +98,7 @@ export declare class FeishuBot {
98
98
  private runDiscussionTurn;
99
99
  private replyMessage;
100
100
  private extractBridgeAttachments;
101
+ private pushBridgeAttachment;
101
102
  private validateBridgeAttachmentPath;
102
103
  private inferFeishuFileType;
103
104
  private isImagePath;
@@ -2,14 +2,13 @@ import * as lark from "@larksuiteoapi/node-sdk";
2
2
  import { createRequire } from "module";
3
3
  import { existsSync, readFileSync, statSync } from "fs";
4
4
  import { basename, extname, resolve } from "path";
5
- import { getBridgeAttachmentAllowedRoots, getBridgeAttachmentsDir } from "./paths.js";
5
+ import { getBridgeAttachmentsDir } from "./paths.js";
6
6
  import { buildFeishuCardElements } from "./markdown.js";
7
7
  import { discussionManager } from "./discussion-manager.js";
8
8
  const require = createRequire(import.meta.url);
9
9
  const LMA_VERSION = require("../package.json").version;
10
10
  const MAX_BOT_STREAK = 10;
11
11
  const BRIDGE_ATTACHMENTS_DIR = getBridgeAttachmentsDir();
12
- const BRIDGE_ATTACHMENT_ALLOWED_ROOTS = getBridgeAttachmentAllowedRoots();
13
12
  /**
14
13
  * Manages a single Feishu bot instance.
15
14
  *
@@ -762,9 +761,17 @@ export class FeishuBot {
762
761
  console.warn(`[${this.config.name}] Reply already delivered, skip duplicate for ${chatId.slice(-8)} msgId=${triggerId}`);
763
762
  }
764
763
  else {
765
- await this.enqueueAndDispatchDelivery(chatId, "assistant_visible", this.deliverySourceId("visible", `${(shouldReply ? visibleReply : "").trim()}|${JSON.stringify(parsedReply.attachments)}`), shouldReply ? visibleReply : "", parsedReply.attachments, lastHuman.messageId, `trigger:${triggerId}`);
766
- if (triggerId)
767
- this.store.markDeliveredReply(this.config.name, chatId, triggerId, lastHuman.messageId);
764
+ try {
765
+ await this.enqueueAndDispatchDelivery(chatId, "assistant_visible", this.deliverySourceId("visible", `${(shouldReply ? visibleReply : "").trim()}|${JSON.stringify(parsedReply.attachments)}`), shouldReply ? visibleReply : "", parsedReply.attachments, lastHuman.messageId, `trigger:${triggerId}`);
766
+ if (triggerId)
767
+ this.store.markDeliveredReply(this.config.name, chatId, triggerId, lastHuman.messageId);
768
+ }
769
+ catch (err) {
770
+ // enqueueAndDispatchDelivery already sent a user-visible delivery
771
+ // failure. Do not fall through to the generic provider-error path;
772
+ // that creates a second misleading "bot did not complete" message.
773
+ console.warn(`[${this.config.name}] assistant delivery failed after notification:`, this.errorSummary(err));
774
+ }
768
775
  }
769
776
  }
770
777
  if (isRuntimeFailure && lastHuman.messageId) {
@@ -1178,29 +1185,39 @@ export class FeishuBot {
1178
1185
  try {
1179
1186
  const parsed = JSON.parse(String(jsonText).trim());
1180
1187
  const rawAttachments = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.attachments) ? parsed.attachments : [parsed];
1181
- for (const item of rawAttachments) {
1182
- if (!item || typeof item.path !== "string")
1183
- continue;
1184
- attachments.push({
1185
- type: item.type === "image" || item.type === "document" || item.type === "file" ? item.type : undefined,
1186
- path: item.path,
1187
- caption: typeof item.caption === "string" ? item.caption : undefined,
1188
- });
1189
- }
1188
+ for (const item of rawAttachments)
1189
+ this.pushBridgeAttachment(attachments, item);
1190
1190
  }
1191
1191
  catch (err) {
1192
1192
  console.warn(`[${this.config.name}] Failed to parse bridge attachment marker:`, err.message);
1193
1193
  }
1194
1194
  return "";
1195
+ });
1196
+ // Compatibility fallback for agents that use OpenClaw's channel directive
1197
+ // syntax instead of the LMA marker. In Feishu/LMA, leaving MEDIA:<path> in
1198
+ // plain text only shows the path, so parse it into bridge attachments.
1199
+ const mediaDirectivePattern = /^\s*MEDIA:(.+)$/gm;
1200
+ text = text.replace(mediaDirectivePattern, (_match, rawPath) => {
1201
+ const mediaPath = String(rawPath).trim();
1202
+ if (!mediaPath)
1203
+ return "";
1204
+ const cleanPath = mediaPath.replace(/^['\"]|['\"]$/g, "");
1205
+ attachments.push({ type: this.isImagePath(cleanPath) ? "image" : "file", path: cleanPath });
1206
+ return "";
1195
1207
  }).trim();
1196
1208
  return { text, attachments };
1197
1209
  }
1210
+ pushBridgeAttachment(attachments, item) {
1211
+ if (!item || typeof item.path !== "string")
1212
+ return;
1213
+ attachments.push({
1214
+ type: item.type === "image" || item.type === "document" || item.type === "file" ? item.type : undefined,
1215
+ path: item.path,
1216
+ caption: typeof item.caption === "string" ? item.caption : undefined,
1217
+ });
1218
+ }
1198
1219
  validateBridgeAttachmentPath(filePath) {
1199
1220
  const resolvedPath = resolve(filePath);
1200
- const isAllowed = BRIDGE_ATTACHMENT_ALLOWED_ROOTS.some((root) => resolvedPath === root || resolvedPath.startsWith(root + "/"));
1201
- if (!isAllowed) {
1202
- throw new Error(`Attachment path outside allowed directories (${BRIDGE_ATTACHMENT_ALLOWED_ROOTS.join(", ")}): ${resolvedPath}`);
1203
- }
1204
1221
  if (!existsSync(resolvedPath))
1205
1222
  throw new Error(`Attachment file not found: ${resolvedPath}`);
1206
1223
  const stats = statSync(resolvedPath);
@@ -1634,7 +1651,8 @@ export class FeishuBot {
1634
1651
  async downloadResource(messageId, fileKey, type) {
1635
1652
  const { mkdirSync, writeFileSync } = await import("fs");
1636
1653
  const { resolve } = await import("path");
1637
- const dir = resolve(process.cwd(), "data", "media");
1654
+ const { getLmaMediaDir } = await import("./paths.js");
1655
+ const dir = getLmaMediaDir();
1638
1656
  mkdirSync(dir, { recursive: true });
1639
1657
  const resp = await this.client.im.messageResource.get({
1640
1658
  path: { message_id: messageId, file_key: fileKey },
@@ -2,11 +2,11 @@ import WebSocket from "ws";
2
2
  import { randomUUID } from "crypto";
3
3
  import { mkdirSync, readFileSync, writeFileSync } from "fs";
4
4
  import { basename, extname, join } from "path";
5
- import { getBridgeAttachmentsDir, getStateDir } from "./paths.js";
5
+ import { getBridgeAttachmentsDir, getDataDir } from "./paths.js";
6
6
  export const GATEWAY_PROTOCOL_MIN = 3;
7
7
  export const GATEWAY_PROTOCOL_MAX = 4;
8
8
  const BRIDGE_ATTACHMENTS_DIR = getBridgeAttachmentsDir();
9
- const CONTEXT_SYNC_DIR = join(getStateDir(), "context-sync");
9
+ const CONTEXT_SYNC_DIR = join(getDataDir(), "context-sync");
10
10
  const MAX_INLINE_CONTEXT_MESSAGES = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_MAX_INLINE_CONTEXT_MESSAGES || 1000);
11
11
  const MAX_INLINE_CONTEXT_BYTES = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_MAX_INLINE_CONTEXT_BYTES || 8 * 1024 * 1024);
12
12
  /**
@@ -867,7 +867,7 @@ export class OpenClawClient {
867
867
  return "";
868
868
  return `
869
869
 
870
- [Bridge attachment capability hint: This is an OpenClaw Lark Multi-Agent bridge session. You cannot send Feishu files/images directly from inside OpenClaw. Do NOT call message, sessions_send, Feishu tools, or proactive send tools for this request. If the user asks you to send an image/file/document to Feishu, prefer creating new files under ${BRIDGE_ATTACHMENTS_DIR}/; existing files under the OpenClaw workspace are also allowed. Include this exact marker at the very end of your final reply (do not explain or expose the marker as normal text): <LMA_BRIDGE_ATTACHMENTS>{"attachments":[{"type":"image|file|document","path":"/absolute/path","caption":"optional"}]}</LMA_BRIDGE_ATTACHMENTS>. The bridge layer will parse this marker and send the attachment. Use type=image for images; use type=document for Markdown documents (.md) so the bridge creates a Feishu cloud document and sends its link; use type=file for other ordinary files.]`;
870
+ [Bridge attachment capability hint: This is an OpenClaw Lark Multi-Agent bridge session. You cannot send Feishu files/images directly from inside OpenClaw. Do NOT call message, sessions_send, Feishu tools, or proactive send tools for this request. If the user asks you to send an image/file/document to Feishu, save or copy the real file under ${BRIDGE_ATTACHMENTS_DIR}/, or use an existing real file under the OpenClaw workspace. NEVER use placeholder paths such as /absolute/path, /real/path, /path/to/file, or example paths; the path must be the actual file you created and it must exist. Include this exact marker at the very end of your final reply (do not explain or expose the marker as normal text): <LMA_BRIDGE_ATTACHMENTS>{"attachments":[{"type":"image","path":"${BRIDGE_ATTACHMENTS_DIR}/replace-with-actual-created-file.png","caption":"optional"}]}</LMA_BRIDGE_ATTACHMENTS>. Replace the example path with the actual existing file path before replying. The bridge layer will parse this marker and send the attachment. Use type=image for images; use type=document for Markdown documents (.md) so the bridge creates a Feishu cloud document and sends its link; use type=file for other ordinary files.]`;
871
871
  }
872
872
  /**
873
873
  * Build and send a context catch-up message followed by the actual message.
package/dist/paths.d.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  * for packaged deployments, tests, containers, or systemd installations.
4
4
  */
5
5
  export declare function getStateDir(): string;
6
+ export declare function getDataDir(): string;
6
7
  export declare function getBridgeAttachmentsDir(): string;
7
8
  export declare function getOpenClawWorkspaceDir(): string;
9
+ export declare function getLmaMediaDir(): string;
8
10
  export declare function getBridgeAttachmentAllowedRoots(): string[];
package/dist/paths.js CHANGED
@@ -1,20 +1,39 @@
1
1
  import { homedir } from "os";
2
- import { resolve } from "path";
2
+ import { dirname, resolve } from "path";
3
3
  /**
4
4
  * Runtime state defaults to the user's home directory but can be overridden
5
5
  * for packaged deployments, tests, containers, or systemd installations.
6
6
  */
7
7
  export function getStateDir() {
8
- return resolve(process.env.OPENCLAW_LARK_MULTI_AGENT_STATE_DIR || resolve(homedir(), ".openclaw/openclaw-lark-multi-agent"));
8
+ // Windows services often run as LocalSystem, where homedir() resolves to
9
+ // C:\\Windows\\System32\\config\\systemprofile. When the installer supplies
10
+ // LMA_DATA_DIR, derive the state dir from it instead of falling back to that
11
+ // service profile home.
12
+ if (process.env.OPENCLAW_LARK_MULTI_AGENT_STATE_DIR)
13
+ return resolve(process.env.OPENCLAW_LARK_MULTI_AGENT_STATE_DIR);
14
+ if (process.env.LMA_DATA_DIR)
15
+ return dirname(resolve(process.env.LMA_DATA_DIR));
16
+ return resolve(homedir(), ".openclaw/openclaw-lark-multi-agent");
17
+ }
18
+ export function getDataDir() {
19
+ return resolve(process.env.LMA_DATA_DIR || resolve(getStateDir(), "data"));
9
20
  }
10
21
  export function getBridgeAttachmentsDir() {
11
22
  return resolve(process.env.OPENCLAW_LARK_MULTI_AGENT_ATTACHMENTS_DIR || resolve(getStateDir(), "attachments"));
12
23
  }
13
24
  export function getOpenClawWorkspaceDir() {
14
- return resolve(process.env.OPENCLAW_WORKSPACE_DIR || resolve(homedir(), ".openclaw/workspace"));
25
+ // Prefer the explicit OpenClaw workspace when provided. Otherwise derive it
26
+ // from the LMA state dir instead of homedir(); Windows services may run as
27
+ // LocalSystem, whose homedir is C:\\Windows\\System32\\config\\systemprofile.
28
+ return resolve(process.env.OPENCLAW_WORKSPACE_DIR || resolve(dirname(getStateDir()), "workspace"));
29
+ }
30
+ export function getLmaMediaDir() {
31
+ // Feishu-downloaded media should live inside the OpenClaw workspace so agent
32
+ // tools such as read/image can access it on both Linux and Windows services.
33
+ return resolve(process.env.OPENCLAW_LARK_MULTI_AGENT_MEDIA_DIR || resolve(getOpenClawWorkspaceDir(), ".lma-media"));
15
34
  }
16
35
  export function getBridgeAttachmentAllowedRoots() {
17
- const roots = [getBridgeAttachmentsDir(), getOpenClawWorkspaceDir()];
36
+ const roots = [getBridgeAttachmentsDir(), getOpenClawWorkspaceDir(), getLmaMediaDir()];
18
37
  const extra = process.env.OPENCLAW_LARK_MULTI_AGENT_ATTACHMENT_ALLOW_ROOTS || "";
19
38
  for (const part of extra.split(",")) {
20
39
  const trimmed = part.trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-lark-multi-agent",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Multi-bot Lark/Feishu bridge for OpenClaw, with per-bot model routing and isolated sessions",
5
5
  "type": "module",
6
6
  "scripts": {