@openclaw/feishu 2026.3.2 → 2026.3.7
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/index.ts +2 -2
- package/package.json +1 -1
- package/src/accounts.test.ts +199 -13
- package/src/accounts.ts +45 -17
- package/src/bitable.ts +40 -28
- package/src/bot.checkBotMentioned.test.ts +8 -0
- package/src/bot.stripBotMention.test.ts +118 -22
- package/src/bot.test.ts +516 -9
- package/src/bot.ts +366 -109
- package/src/card-action.ts +1 -1
- package/src/channel.test.ts +1 -1
- package/src/channel.ts +52 -64
- package/src/chat.test.ts +2 -2
- package/src/chat.ts +1 -1
- package/src/client.test.ts +207 -4
- package/src/client.ts +70 -5
- package/src/config-schema.test.ts +14 -6
- package/src/config-schema.ts +5 -1
- package/src/dedup.ts +1 -1
- package/src/directory.test.ts +40 -0
- package/src/directory.ts +29 -50
- package/src/docx-batch-insert.test.ts +90 -0
- package/src/docx-batch-insert.ts +8 -11
- package/src/docx.account-selection.test.ts +3 -3
- package/src/docx.ts +1 -1
- package/src/drive.ts +13 -17
- package/src/dynamic-agent.ts +1 -1
- package/src/feishu-command-handler.ts +59 -0
- package/src/media.test.ts +60 -13
- package/src/media.ts +23 -9
- package/src/monitor.account.ts +19 -8
- package/src/monitor.reaction.test.ts +111 -105
- package/src/monitor.startup.test.ts +11 -10
- package/src/monitor.startup.ts +20 -7
- package/src/monitor.state.ts +4 -1
- package/src/monitor.test-mocks.ts +42 -9
- package/src/monitor.transport.ts +4 -1
- package/src/monitor.ts +4 -4
- package/src/monitor.webhook-security.test.ts +8 -23
- package/src/onboarding.status.test.ts +1 -1
- package/src/onboarding.test.ts +143 -0
- package/src/onboarding.ts +86 -71
- package/src/outbound.test.ts +178 -0
- package/src/outbound.ts +39 -6
- package/src/perm.ts +11 -15
- package/src/policy.test.ts +40 -0
- package/src/policy.ts +9 -10
- package/src/probe.test.ts +18 -18
- package/src/reactions.ts +1 -1
- package/src/reply-dispatcher.test.ts +175 -0
- package/src/reply-dispatcher.ts +69 -21
- package/src/runtime.ts +1 -1
- package/src/secret-input.ts +8 -14
- package/src/send-message.ts +71 -0
- package/src/send-target.test.ts +1 -1
- package/src/send-target.ts +1 -1
- package/src/send.reply-fallback.test.ts +74 -0
- package/src/send.test.ts +1 -1
- package/src/send.ts +88 -49
- package/src/streaming-card.test.ts +54 -0
- package/src/streaming-card.ts +96 -28
- package/src/targets.ts +5 -1
- package/src/tool-account-routing.test.ts +3 -3
- package/src/tool-account.ts +1 -1
- package/src/tool-factory-test-harness.ts +1 -1
- package/src/tool-result.test.ts +32 -0
- package/src/tool-result.ts +14 -0
- package/src/types.ts +2 -3
- package/src/typing.ts +1 -1
- package/src/wiki.ts +15 -19
package/src/dedup.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
createDedupeCache,
|
|
5
5
|
createPersistentDedupe,
|
|
6
6
|
readJsonFileWithFallback,
|
|
7
|
-
} from "openclaw/plugin-sdk";
|
|
7
|
+
} from "openclaw/plugin-sdk/feishu";
|
|
8
8
|
|
|
9
9
|
// Persistent TTL: 24 hours — survives restarts & WebSocket reconnects.
|
|
10
10
|
const DEDUP_TTL_MS = 24 * 60 * 60 * 1000;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
vi.mock("./accounts.js", () => ({
|
|
5
|
+
resolveFeishuAccount: vi.fn(() => ({
|
|
6
|
+
configured: false,
|
|
7
|
+
config: {
|
|
8
|
+
allowFrom: ["user:alice", "user:bob"],
|
|
9
|
+
dms: {
|
|
10
|
+
"user:carla": {},
|
|
11
|
+
},
|
|
12
|
+
groups: {
|
|
13
|
+
"chat-1": {},
|
|
14
|
+
},
|
|
15
|
+
groupAllowFrom: ["chat-2"],
|
|
16
|
+
},
|
|
17
|
+
})),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import { listFeishuDirectoryGroups, listFeishuDirectoryPeers } from "./directory.js";
|
|
21
|
+
|
|
22
|
+
describe("feishu directory (config-backed)", () => {
|
|
23
|
+
const cfg = {} as ClawdbotConfig;
|
|
24
|
+
|
|
25
|
+
it("merges allowFrom + dms into peer entries", async () => {
|
|
26
|
+
const peers = await listFeishuDirectoryPeers({ cfg, query: "a" });
|
|
27
|
+
expect(peers).toEqual([
|
|
28
|
+
{ kind: "user", id: "alice" },
|
|
29
|
+
{ kind: "user", id: "carla" },
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("merges groups map + groupAllowFrom into group entries", async () => {
|
|
34
|
+
const groups = await listFeishuDirectoryGroups({ cfg });
|
|
35
|
+
expect(groups).toEqual([
|
|
36
|
+
{ kind: "group", id: "chat-1" },
|
|
37
|
+
{ kind: "group", id: "chat-2" },
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
});
|
package/src/directory.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
listDirectoryGroupEntriesFromMapKeysAndAllowFrom,
|
|
3
|
+
listDirectoryUserEntriesFromAllowFromAndMapKeys,
|
|
4
|
+
} from "openclaw/plugin-sdk/compat";
|
|
5
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
|
|
2
6
|
import { resolveFeishuAccount } from "./accounts.js";
|
|
3
7
|
import { createFeishuClient } from "./client.js";
|
|
4
8
|
import { normalizeFeishuTarget } from "./targets.js";
|
|
@@ -15,6 +19,14 @@ export type FeishuDirectoryGroup = {
|
|
|
15
19
|
name?: string;
|
|
16
20
|
};
|
|
17
21
|
|
|
22
|
+
function toFeishuDirectoryPeers(ids: string[]): FeishuDirectoryPeer[] {
|
|
23
|
+
return ids.map((id) => ({ kind: "user", id }));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function toFeishuDirectoryGroups(ids: string[]): FeishuDirectoryGroup[] {
|
|
27
|
+
return ids.map((id) => ({ kind: "group", id }));
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
export async function listFeishuDirectoryPeers(params: {
|
|
19
31
|
cfg: ClawdbotConfig;
|
|
20
32
|
query?: string;
|
|
@@ -22,31 +34,15 @@ export async function listFeishuDirectoryPeers(params: {
|
|
|
22
34
|
accountId?: string;
|
|
23
35
|
}): Promise<FeishuDirectoryPeer[]> {
|
|
24
36
|
const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
for (const userId of Object.keys(feishuCfg?.dms ?? {})) {
|
|
37
|
-
const trimmed = userId.trim();
|
|
38
|
-
if (trimmed) {
|
|
39
|
-
ids.add(trimmed);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return Array.from(ids)
|
|
44
|
-
.map((raw) => raw.trim())
|
|
45
|
-
.filter(Boolean)
|
|
46
|
-
.map((raw) => normalizeFeishuTarget(raw) ?? raw)
|
|
47
|
-
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
|
48
|
-
.slice(0, params.limit && params.limit > 0 ? params.limit : undefined)
|
|
49
|
-
.map((id) => ({ kind: "user" as const, id }));
|
|
37
|
+
const entries = listDirectoryUserEntriesFromAllowFromAndMapKeys({
|
|
38
|
+
allowFrom: account.config.allowFrom,
|
|
39
|
+
map: account.config.dms,
|
|
40
|
+
query: params.query,
|
|
41
|
+
limit: params.limit,
|
|
42
|
+
normalizeAllowFromId: (entry) => normalizeFeishuTarget(entry) ?? entry,
|
|
43
|
+
normalizeMapKeyId: (entry) => normalizeFeishuTarget(entry) ?? entry,
|
|
44
|
+
});
|
|
45
|
+
return toFeishuDirectoryPeers(entries.map((entry) => entry.id));
|
|
50
46
|
}
|
|
51
47
|
|
|
52
48
|
export async function listFeishuDirectoryGroups(params: {
|
|
@@ -56,30 +52,13 @@ export async function listFeishuDirectoryGroups(params: {
|
|
|
56
52
|
accountId?: string;
|
|
57
53
|
}): Promise<FeishuDirectoryGroup[]> {
|
|
58
54
|
const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
ids.add(trimmed);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
for (const entry of feishuCfg?.groupAllowFrom ?? []) {
|
|
71
|
-
const trimmed = String(entry).trim();
|
|
72
|
-
if (trimmed && trimmed !== "*") {
|
|
73
|
-
ids.add(trimmed);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return Array.from(ids)
|
|
78
|
-
.map((raw) => raw.trim())
|
|
79
|
-
.filter(Boolean)
|
|
80
|
-
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
|
81
|
-
.slice(0, params.limit && params.limit > 0 ? params.limit : undefined)
|
|
82
|
-
.map((id) => ({ kind: "group" as const, id }));
|
|
55
|
+
const entries = listDirectoryGroupEntriesFromMapKeysAndAllowFrom({
|
|
56
|
+
groups: account.config.groups,
|
|
57
|
+
allowFrom: account.config.groupAllowFrom,
|
|
58
|
+
query: params.query,
|
|
59
|
+
limit: params.limit,
|
|
60
|
+
});
|
|
61
|
+
return toFeishuDirectoryGroups(entries.map((entry) => entry.id));
|
|
83
62
|
}
|
|
84
63
|
|
|
85
64
|
export async function listFeishuDirectoryPeersLive(params: {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { BATCH_SIZE, insertBlocksInBatches } from "./docx-batch-insert.js";
|
|
3
|
+
|
|
4
|
+
function createCountingIterable<T>(values: T[]) {
|
|
5
|
+
let iterations = 0;
|
|
6
|
+
return {
|
|
7
|
+
values: {
|
|
8
|
+
[Symbol.iterator]: function* () {
|
|
9
|
+
iterations += 1;
|
|
10
|
+
yield* values;
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
getIterations: () => iterations,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("insertBlocksInBatches", () => {
|
|
18
|
+
it("builds the source block map once for large flat trees", async () => {
|
|
19
|
+
const blockCount = BATCH_SIZE + 200;
|
|
20
|
+
const blocks = Array.from({ length: blockCount }, (_, index) => ({
|
|
21
|
+
block_id: `block_${index}`,
|
|
22
|
+
block_type: 2,
|
|
23
|
+
}));
|
|
24
|
+
const counting = createCountingIterable(blocks);
|
|
25
|
+
const createMock = vi.fn(async ({ data }: { data: { children_id: string[] } }) => ({
|
|
26
|
+
code: 0,
|
|
27
|
+
data: {
|
|
28
|
+
children: data.children_id.map((id) => ({ block_id: id })),
|
|
29
|
+
},
|
|
30
|
+
}));
|
|
31
|
+
const client = {
|
|
32
|
+
docx: {
|
|
33
|
+
documentBlockDescendant: {
|
|
34
|
+
create: createMock,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
} as any;
|
|
38
|
+
|
|
39
|
+
const result = await insertBlocksInBatches(
|
|
40
|
+
client,
|
|
41
|
+
"doc_1",
|
|
42
|
+
counting.values as any[],
|
|
43
|
+
blocks.map((block) => block.block_id),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(counting.getIterations()).toBe(1);
|
|
47
|
+
expect(createMock).toHaveBeenCalledTimes(2);
|
|
48
|
+
expect(createMock.mock.calls[0]?.[0]?.data.children_id).toHaveLength(BATCH_SIZE);
|
|
49
|
+
expect(createMock.mock.calls[1]?.[0]?.data.children_id).toHaveLength(200);
|
|
50
|
+
expect(result.children).toHaveLength(blockCount);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("keeps nested descendants grouped with their root blocks", async () => {
|
|
54
|
+
const createMock = vi.fn(
|
|
55
|
+
async ({
|
|
56
|
+
data,
|
|
57
|
+
}: {
|
|
58
|
+
data: { children_id: string[]; descendants: Array<{ block_id: string }> };
|
|
59
|
+
}) => ({
|
|
60
|
+
code: 0,
|
|
61
|
+
data: {
|
|
62
|
+
children: data.children_id.map((id) => ({ block_id: id })),
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
const client = {
|
|
67
|
+
docx: {
|
|
68
|
+
documentBlockDescendant: {
|
|
69
|
+
create: createMock,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
} as any;
|
|
73
|
+
const blocks = [
|
|
74
|
+
{ block_id: "root_a", block_type: 1, children: ["child_a"] },
|
|
75
|
+
{ block_id: "child_a", block_type: 2 },
|
|
76
|
+
{ block_id: "root_b", block_type: 1, children: ["child_b"] },
|
|
77
|
+
{ block_id: "child_b", block_type: 2 },
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
await insertBlocksInBatches(client, "doc_1", blocks as any[], ["root_a", "root_b"]);
|
|
81
|
+
|
|
82
|
+
expect(createMock).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(createMock.mock.calls[0]?.[0]?.data.children_id).toEqual(["root_a", "root_b"]);
|
|
84
|
+
expect(
|
|
85
|
+
createMock.mock.calls[0]?.[0]?.data.descendants.map(
|
|
86
|
+
(block: { block_id: string }) => block.block_id,
|
|
87
|
+
),
|
|
88
|
+
).toEqual(["root_a", "child_a", "root_b", "child_b"]);
|
|
89
|
+
});
|
|
90
|
+
});
|
package/src/docx-batch-insert.ts
CHANGED
|
@@ -14,16 +14,11 @@ export const BATCH_SIZE = 1000; // Feishu API limit per request
|
|
|
14
14
|
type Logger = { info?: (msg: string) => void };
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Collect all descendant blocks for a given
|
|
17
|
+
* Collect all descendant blocks for a given first-level block ID.
|
|
18
18
|
* Recursively traverses the block tree to gather all children.
|
|
19
19
|
*/
|
|
20
20
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block types
|
|
21
|
-
function collectDescendants(
|
|
22
|
-
const blockMap = new Map<string, any>();
|
|
23
|
-
for (const block of blocks) {
|
|
24
|
-
blockMap.set(block.block_id, block);
|
|
25
|
-
}
|
|
26
|
-
|
|
21
|
+
function collectDescendants(blockMap: Map<string, any>, rootId: string): any[] {
|
|
27
22
|
const result: any[] = [];
|
|
28
23
|
const visited = new Set<string>();
|
|
29
24
|
|
|
@@ -47,9 +42,7 @@ function collectDescendants(blocks: any[], firstLevelIds: string[]): any[] {
|
|
|
47
42
|
}
|
|
48
43
|
}
|
|
49
44
|
|
|
50
|
-
|
|
51
|
-
collect(id);
|
|
52
|
-
}
|
|
45
|
+
collect(rootId);
|
|
53
46
|
|
|
54
47
|
return result;
|
|
55
48
|
}
|
|
@@ -123,9 +116,13 @@ export async function insertBlocksInBatches(
|
|
|
123
116
|
const batches: { firstLevelIds: string[]; blocks: any[] }[] = [];
|
|
124
117
|
let currentBatch: { firstLevelIds: string[]; blocks: any[] } = { firstLevelIds: [], blocks: [] };
|
|
125
118
|
const usedBlockIds = new Set<string>();
|
|
119
|
+
const blockMap = new Map<string, any>();
|
|
120
|
+
for (const block of blocks) {
|
|
121
|
+
blockMap.set(block.block_id, block);
|
|
122
|
+
}
|
|
126
123
|
|
|
127
124
|
for (const firstLevelId of firstLevelBlockIds) {
|
|
128
|
-
const descendants = collectDescendants(
|
|
125
|
+
const descendants = collectDescendants(blockMap, firstLevelId);
|
|
129
126
|
const newBlocks = descendants.filter((b) => !usedBlockIds.has(b.block_id));
|
|
130
127
|
|
|
131
128
|
// A single block whose subtree exceeds the API limit cannot be split
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
|
|
2
2
|
import { describe, expect, test, vi } from "vitest";
|
|
3
3
|
import { registerFeishuDocTools } from "./docx.js";
|
|
4
4
|
import { createToolFactoryHarness } from "./tool-factory-test-harness.js";
|
|
@@ -27,8 +27,8 @@ describe("feishu_doc account selection", () => {
|
|
|
27
27
|
feishu: {
|
|
28
28
|
enabled: true,
|
|
29
29
|
accounts: {
|
|
30
|
-
a: { appId: "app-a", appSecret: "sec-a", tools: { doc: true } },
|
|
31
|
-
b: { appId: "app-b", appSecret: "sec-b", tools: { doc: true } },
|
|
30
|
+
a: { appId: "app-a", appSecret: "sec-a", tools: { doc: true } }, // pragma: allowlist secret
|
|
31
|
+
b: { appId: "app-b", appSecret: "sec-b", tools: { doc: true } }, // pragma: allowlist secret
|
|
32
32
|
},
|
|
33
33
|
},
|
|
34
34
|
},
|
package/src/docx.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { isAbsolute } from "node:path";
|
|
|
4
4
|
import { basename } from "node:path";
|
|
5
5
|
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
6
6
|
import { Type } from "@sinclair/typebox";
|
|
7
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
7
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
|
|
8
8
|
import { listEnabledFeishuAccounts } from "./accounts.js";
|
|
9
9
|
import { FeishuDocSchema, type FeishuDocParams } from "./doc-schema.js";
|
|
10
10
|
import { BATCH_SIZE, insertBlocksInBatches } from "./docx-batch-insert.js";
|
package/src/drive.ts
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
|
|
3
3
|
import { listEnabledFeishuAccounts } from "./accounts.js";
|
|
4
4
|
import { FeishuDriveSchema, type FeishuDriveParams } from "./drive-schema.js";
|
|
5
5
|
import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
12
|
-
details: data,
|
|
13
|
-
};
|
|
14
|
-
}
|
|
6
|
+
import {
|
|
7
|
+
jsonToolResult,
|
|
8
|
+
toolExecutionErrorResult,
|
|
9
|
+
unknownToolActionResult,
|
|
10
|
+
} from "./tool-result.js";
|
|
15
11
|
|
|
16
12
|
// ============ Actions ============
|
|
17
13
|
|
|
@@ -206,21 +202,21 @@ export function registerFeishuDriveTools(api: OpenClawPluginApi) {
|
|
|
206
202
|
});
|
|
207
203
|
switch (p.action) {
|
|
208
204
|
case "list":
|
|
209
|
-
return
|
|
205
|
+
return jsonToolResult(await listFolder(client, p.folder_token));
|
|
210
206
|
case "info":
|
|
211
|
-
return
|
|
207
|
+
return jsonToolResult(await getFileInfo(client, p.file_token));
|
|
212
208
|
case "create_folder":
|
|
213
|
-
return
|
|
209
|
+
return jsonToolResult(await createFolder(client, p.name, p.folder_token));
|
|
214
210
|
case "move":
|
|
215
|
-
return
|
|
211
|
+
return jsonToolResult(await moveFile(client, p.file_token, p.type, p.folder_token));
|
|
216
212
|
case "delete":
|
|
217
|
-
return
|
|
213
|
+
return jsonToolResult(await deleteFile(client, p.file_token, p.type));
|
|
218
214
|
default:
|
|
219
215
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- exhaustive check fallback
|
|
220
|
-
return
|
|
216
|
+
return unknownToolActionResult((p as { action?: unknown }).action);
|
|
221
217
|
}
|
|
222
218
|
} catch (err) {
|
|
223
|
-
return
|
|
219
|
+
return toolExecutionErrorResult(err);
|
|
224
220
|
}
|
|
225
221
|
},
|
|
226
222
|
};
|
package/src/dynamic-agent.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
4
|
+
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/feishu";
|
|
5
5
|
import type { DynamicAgentCreationConfig } from "./types.js";
|
|
6
6
|
|
|
7
7
|
export type MaybeCreateDynamicAgentResult = {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const DEFAULT_RESET_TRIGGERS = ["/new", "/reset"] as const;
|
|
2
|
+
|
|
3
|
+
type FeishuBeforeResetContext = {
|
|
4
|
+
cfg: Record<string, unknown>;
|
|
5
|
+
sessionEntry: Record<string, unknown>;
|
|
6
|
+
previousSessionEntry?: Record<string, unknown>;
|
|
7
|
+
commandSource: string;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type FeishuBeforeResetEvent = {
|
|
12
|
+
type: "command";
|
|
13
|
+
action: "new" | "reset";
|
|
14
|
+
context: FeishuBeforeResetContext;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type FeishuBeforeResetRunner = {
|
|
18
|
+
runBeforeReset: (
|
|
19
|
+
event: FeishuBeforeResetEvent,
|
|
20
|
+
ctx: { agentId: string; sessionKey: string },
|
|
21
|
+
) => Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Handle Feishu command messages and trigger reset hooks.
|
|
26
|
+
*/
|
|
27
|
+
export async function handleFeishuCommand(
|
|
28
|
+
messageText: string,
|
|
29
|
+
sessionKey: string,
|
|
30
|
+
hookRunner: FeishuBeforeResetRunner,
|
|
31
|
+
context: FeishuBeforeResetContext,
|
|
32
|
+
): Promise<boolean> {
|
|
33
|
+
const trimmed = messageText.trim().toLowerCase();
|
|
34
|
+
const isResetCommand = DEFAULT_RESET_TRIGGERS.some(
|
|
35
|
+
(trigger) => trimmed === trigger || trimmed.startsWith(`${trigger} `),
|
|
36
|
+
);
|
|
37
|
+
if (!isResetCommand) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const command = trimmed.split(" ")[0];
|
|
42
|
+
const action: "new" | "reset" = command === "/new" ? "new" : "reset";
|
|
43
|
+
await hookRunner.runBeforeReset(
|
|
44
|
+
{
|
|
45
|
+
type: "command",
|
|
46
|
+
action,
|
|
47
|
+
context: {
|
|
48
|
+
...context,
|
|
49
|
+
commandSource: "feishu",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
agentId: "main",
|
|
54
|
+
sessionKey,
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return true;
|
|
59
|
+
}
|
package/src/media.test.ts
CHANGED
|
@@ -10,11 +10,14 @@ const resolveReceiveIdTypeMock = vi.hoisted(() => vi.fn());
|
|
|
10
10
|
const loadWebMediaMock = vi.hoisted(() => vi.fn());
|
|
11
11
|
|
|
12
12
|
const fileCreateMock = vi.hoisted(() => vi.fn());
|
|
13
|
+
const imageCreateMock = vi.hoisted(() => vi.fn());
|
|
13
14
|
const imageGetMock = vi.hoisted(() => vi.fn());
|
|
14
15
|
const messageCreateMock = vi.hoisted(() => vi.fn());
|
|
15
16
|
const messageResourceGetMock = vi.hoisted(() => vi.fn());
|
|
16
17
|
const messageReplyMock = vi.hoisted(() => vi.fn());
|
|
17
18
|
|
|
19
|
+
const FEISHU_MEDIA_HTTP_TIMEOUT_MS = 120_000;
|
|
20
|
+
|
|
18
21
|
vi.mock("./client.js", () => ({
|
|
19
22
|
createFeishuClient: createFeishuClientMock,
|
|
20
23
|
}));
|
|
@@ -53,6 +56,14 @@ function expectPathIsolatedToTmpRoot(pathValue: string, key: string): void {
|
|
|
53
56
|
expect(rel === ".." || rel.startsWith(`..${path.sep}`)).toBe(false);
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
function expectMediaTimeoutClientConfigured(): void {
|
|
60
|
+
expect(createFeishuClientMock).toHaveBeenCalledWith(
|
|
61
|
+
expect.objectContaining({
|
|
62
|
+
httpTimeoutMs: FEISHU_MEDIA_HTTP_TIMEOUT_MS,
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
56
67
|
describe("sendMediaFeishu msg_type routing", () => {
|
|
57
68
|
beforeEach(() => {
|
|
58
69
|
vi.clearAllMocks();
|
|
@@ -75,6 +86,7 @@ describe("sendMediaFeishu msg_type routing", () => {
|
|
|
75
86
|
create: fileCreateMock,
|
|
76
87
|
},
|
|
77
88
|
image: {
|
|
89
|
+
create: imageCreateMock,
|
|
78
90
|
get: imageGetMock,
|
|
79
91
|
},
|
|
80
92
|
message: {
|
|
@@ -91,6 +103,10 @@ describe("sendMediaFeishu msg_type routing", () => {
|
|
|
91
103
|
code: 0,
|
|
92
104
|
data: { file_key: "file_key_1" },
|
|
93
105
|
});
|
|
106
|
+
imageCreateMock.mockResolvedValue({
|
|
107
|
+
code: 0,
|
|
108
|
+
data: { image_key: "image_key_1" },
|
|
109
|
+
});
|
|
94
110
|
|
|
95
111
|
messageCreateMock.mockResolvedValue({
|
|
96
112
|
code: 0,
|
|
@@ -113,7 +129,7 @@ describe("sendMediaFeishu msg_type routing", () => {
|
|
|
113
129
|
messageResourceGetMock.mockResolvedValue(Buffer.from("resource-bytes"));
|
|
114
130
|
});
|
|
115
131
|
|
|
116
|
-
it("uses msg_type=
|
|
132
|
+
it("uses msg_type=media for mp4 video", async () => {
|
|
117
133
|
await sendMediaFeishu({
|
|
118
134
|
cfg: {} as any,
|
|
119
135
|
to: "user:ou_target",
|
|
@@ -129,7 +145,7 @@ describe("sendMediaFeishu msg_type routing", () => {
|
|
|
129
145
|
|
|
130
146
|
expect(messageCreateMock).toHaveBeenCalledWith(
|
|
131
147
|
expect.objectContaining({
|
|
132
|
-
data: expect.objectContaining({ msg_type: "
|
|
148
|
+
data: expect.objectContaining({ msg_type: "media" }),
|
|
133
149
|
}),
|
|
134
150
|
);
|
|
135
151
|
});
|
|
@@ -176,7 +192,23 @@ describe("sendMediaFeishu msg_type routing", () => {
|
|
|
176
192
|
);
|
|
177
193
|
});
|
|
178
194
|
|
|
179
|
-
it("
|
|
195
|
+
it("configures the media client timeout for image uploads", async () => {
|
|
196
|
+
await sendMediaFeishu({
|
|
197
|
+
cfg: {} as any,
|
|
198
|
+
to: "user:ou_target",
|
|
199
|
+
mediaBuffer: Buffer.from("image"),
|
|
200
|
+
fileName: "photo.png",
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expectMediaTimeoutClientConfigured();
|
|
204
|
+
expect(messageCreateMock).toHaveBeenCalledWith(
|
|
205
|
+
expect.objectContaining({
|
|
206
|
+
data: expect.objectContaining({ msg_type: "image" }),
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("uses msg_type=media when replying with mp4", async () => {
|
|
180
212
|
await sendMediaFeishu({
|
|
181
213
|
cfg: {} as any,
|
|
182
214
|
to: "user:ou_target",
|
|
@@ -188,7 +220,7 @@ describe("sendMediaFeishu msg_type routing", () => {
|
|
|
188
220
|
expect(messageReplyMock).toHaveBeenCalledWith(
|
|
189
221
|
expect.objectContaining({
|
|
190
222
|
path: { message_id: "om_parent" },
|
|
191
|
-
data: expect.objectContaining({ msg_type: "
|
|
223
|
+
data: expect.objectContaining({ msg_type: "media" }),
|
|
192
224
|
}),
|
|
193
225
|
);
|
|
194
226
|
|
|
@@ -208,7 +240,10 @@ describe("sendMediaFeishu msg_type routing", () => {
|
|
|
208
240
|
expect(messageReplyMock).toHaveBeenCalledWith(
|
|
209
241
|
expect.objectContaining({
|
|
210
242
|
path: { message_id: "om_parent" },
|
|
211
|
-
data: expect.objectContaining({
|
|
243
|
+
data: expect.objectContaining({
|
|
244
|
+
msg_type: "media",
|
|
245
|
+
reply_in_thread: true,
|
|
246
|
+
}),
|
|
212
247
|
}),
|
|
213
248
|
);
|
|
214
249
|
});
|
|
@@ -288,6 +323,12 @@ describe("sendMediaFeishu msg_type routing", () => {
|
|
|
288
323
|
imageKey,
|
|
289
324
|
});
|
|
290
325
|
|
|
326
|
+
expect(imageGetMock).toHaveBeenCalledWith(
|
|
327
|
+
expect.objectContaining({
|
|
328
|
+
path: { image_key: imageKey },
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
expectMediaTimeoutClientConfigured();
|
|
291
332
|
expect(result.buffer).toEqual(Buffer.from("image-data"));
|
|
292
333
|
expect(capturedPath).toBeDefined();
|
|
293
334
|
expectPathIsolatedToTmpRoot(capturedPath as string, imageKey);
|
|
@@ -473,10 +514,13 @@ describe("downloadMessageResourceFeishu", () => {
|
|
|
473
514
|
type: "file",
|
|
474
515
|
});
|
|
475
516
|
|
|
476
|
-
expect(messageResourceGetMock).toHaveBeenCalledWith(
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
517
|
+
expect(messageResourceGetMock).toHaveBeenCalledWith(
|
|
518
|
+
expect.objectContaining({
|
|
519
|
+
path: { message_id: "om_audio_msg", file_key: "file_key_audio" },
|
|
520
|
+
params: { type: "file" },
|
|
521
|
+
}),
|
|
522
|
+
);
|
|
523
|
+
expectMediaTimeoutClientConfigured();
|
|
480
524
|
expect(result.buffer).toBeInstanceOf(Buffer);
|
|
481
525
|
});
|
|
482
526
|
|
|
@@ -490,10 +534,13 @@ describe("downloadMessageResourceFeishu", () => {
|
|
|
490
534
|
type: "image",
|
|
491
535
|
});
|
|
492
536
|
|
|
493
|
-
expect(messageResourceGetMock).toHaveBeenCalledWith(
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
537
|
+
expect(messageResourceGetMock).toHaveBeenCalledWith(
|
|
538
|
+
expect.objectContaining({
|
|
539
|
+
path: { message_id: "om_img_msg", file_key: "img_key_1" },
|
|
540
|
+
params: { type: "image" },
|
|
541
|
+
}),
|
|
542
|
+
);
|
|
543
|
+
expectMediaTimeoutClientConfigured();
|
|
497
544
|
expect(result.buffer).toBeInstanceOf(Buffer);
|
|
498
545
|
});
|
|
499
546
|
});
|