@nextclaw/channel-plugin-feishu 0.2.20 → 0.2.22

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/channel-plugin-feishu",
3
- "version": "0.2.20",
3
+ "version": "0.2.22",
4
4
  "private": false,
5
5
  "description": "NextClaw Feishu/Lark channel plugin with doc/wiki/drive tools.",
6
6
  "type": "module",
@@ -2,11 +2,18 @@ import type { OpenClawConfig } from "./nextclaw-sdk/feishu.js";
2
2
  import { describe, expect, it, vi } from "vitest";
3
3
 
4
4
  const probeFeishuMock = vi.hoisted(() => vi.fn());
5
+ const monitorFeishuProviderMock = vi.hoisted(() => vi.fn());
6
+ const stopFeishuMonitorMock = vi.hoisted(() => vi.fn());
5
7
 
6
8
  vi.mock("./probe.js", () => ({
7
9
  probeFeishu: probeFeishuMock,
8
10
  }));
9
11
 
12
+ vi.mock("./monitor.js", () => ({
13
+ monitorFeishuProvider: monitorFeishuProviderMock,
14
+ stopFeishuMonitor: stopFeishuMonitorMock,
15
+ }));
16
+
10
17
  import { feishuPlugin } from "./channel.js";
11
18
 
12
19
  describe("feishuPlugin.status.probeAccount", () => {
@@ -45,4 +52,62 @@ describe("feishuPlugin.status.probeAccount", () => {
45
52
  );
46
53
  expect(result).toMatchObject({ ok: true, appId: "cli_main" });
47
54
  });
55
+
56
+ it("starts gateway monitor without blocking service startup", async () => {
57
+ let resolveMonitor!: () => void;
58
+ const monitorDone = new Promise<void>((resolve) => {
59
+ resolveMonitor = resolve;
60
+ });
61
+ monitorFeishuProviderMock.mockReturnValueOnce(monitorDone);
62
+
63
+ const abortController = new AbortController();
64
+ const ctx = {
65
+ cfg: {
66
+ channels: {
67
+ feishu: {
68
+ enabled: true,
69
+ appId: "cli_default",
70
+ appSecret: "secret_default",
71
+ connectionMode: "websocket",
72
+ },
73
+ },
74
+ } as OpenClawConfig,
75
+ accountId: "default",
76
+ abortSignal: abortController.signal,
77
+ setStatus: vi.fn(),
78
+ log: {
79
+ info: vi.fn(),
80
+ error: vi.fn(),
81
+ },
82
+ runtime: {
83
+ log: vi.fn(),
84
+ info: vi.fn(),
85
+ warn: vi.fn(),
86
+ error: vi.fn(),
87
+ debug: vi.fn(),
88
+ },
89
+ };
90
+
91
+ const timeoutToken = Symbol("timeout");
92
+ const started = await Promise.race([
93
+ feishuPlugin.gateway?.startAccount?.(ctx),
94
+ new Promise<symbol>((resolve) => setTimeout(() => resolve(timeoutToken), 20)),
95
+ ]);
96
+
97
+ expect(started).not.toBe(timeoutToken);
98
+ expect(monitorFeishuProviderMock).toHaveBeenCalledWith({
99
+ config: ctx.cfg,
100
+ runtime: ctx.runtime,
101
+ abortSignal: ctx.abortSignal,
102
+ accountId: "default",
103
+ });
104
+ expect(ctx.setStatus).toHaveBeenCalledWith({ accountId: "default", port: null });
105
+ expect(typeof (started as { stop?: () => Promise<void> }).stop).toBe("function");
106
+
107
+ abortController.abort();
108
+ resolveMonitor();
109
+ await (started as { stop?: () => Promise<void> }).stop?.();
110
+
111
+ expect(stopFeishuMonitorMock).toHaveBeenCalledWith("default");
112
+ });
48
113
  });
package/src/channel.ts CHANGED
@@ -109,7 +109,8 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
109
109
  },
110
110
  agentPrompt: {
111
111
  messageToolHints: () => [
112
- "- Feishu targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:open_id` or `chat:chat_id`.",
112
+ "- Feishu targeting: omit `target` only when replying in the current Feishu conversation. For proactive sends from UI/CLI/another channel, pass an explicit target such as `user:open_id` or `chat:chat_id`.",
113
+ "- If the current session is not Feishu, never rely on `channel=feishu` alone; resolve the route first (for example from an existing Feishu session) and then send to that explicit target.",
113
114
  "- Feishu supports interactive cards for rich messages.",
114
115
  ],
115
116
  },
@@ -351,19 +352,40 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
351
352
  },
352
353
  gateway: {
353
354
  startAccount: async (ctx) => {
354
- const { monitorFeishuProvider } = await import("./monitor.js");
355
+ const { monitorFeishuProvider, stopFeishuMonitor } = await import("./monitor.js");
355
356
  const account = resolveFeishuAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
357
+ const accountId = account.accountId;
356
358
  const port = account.config?.webhookPort ?? null;
357
- ctx.setStatus({ accountId: ctx.accountId, port });
359
+ ctx.setStatus({ accountId, port });
358
360
  ctx.log?.info(
359
- `starting feishu[${ctx.accountId}] (mode: ${account.config?.connectionMode ?? "websocket"})`,
361
+ `starting feishu[${accountId}] (mode: ${account.config?.connectionMode ?? "websocket"})`,
360
362
  );
361
- return monitorFeishuProvider({
362
- config: ctx.cfg,
363
- runtime: ctx.runtime,
364
- abortSignal: ctx.abortSignal,
365
- accountId: ctx.accountId,
366
- });
363
+
364
+ // Start the long-running monitor in the background so service startup can continue
365
+ // into ChannelManager.startAll() for other channels.
366
+ const monitorTask = (async () => {
367
+ try {
368
+ await monitorFeishuProvider({
369
+ config: ctx.cfg,
370
+ runtime: ctx.runtime,
371
+ abortSignal: ctx.abortSignal,
372
+ accountId,
373
+ });
374
+ } catch (error) {
375
+ if (ctx.abortSignal?.aborted) {
376
+ return;
377
+ }
378
+ const message = error instanceof Error ? error.stack ?? error.message : String(error);
379
+ ctx.log?.error?.(`feishu[${accountId}]: gateway monitor stopped unexpectedly: ${message}`);
380
+ }
381
+ })();
382
+
383
+ return {
384
+ stop: async () => {
385
+ stopFeishuMonitor(accountId);
386
+ await monitorTask;
387
+ },
388
+ };
367
389
  },
368
390
  },
369
391
  };