@nextclaw/channel-plugin-feishu 0.2.13 → 0.2.14
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/README.md +3 -1
- package/index.ts +65 -0
- package/openclaw.plugin.json +3 -7
- package/package.json +33 -9
- package/skills/feishu-doc/SKILL.md +211 -0
- package/skills/feishu-doc/references/block-types.md +103 -0
- package/skills/feishu-drive/SKILL.md +97 -0
- package/skills/feishu-perm/SKILL.md +119 -0
- package/skills/feishu-wiki/SKILL.md +111 -0
- package/src/accounts.test.ts +371 -0
- package/src/accounts.ts +244 -0
- package/src/async.ts +62 -0
- package/src/bitable.ts +725 -0
- package/src/bot.card-action.test.ts +63 -0
- package/src/bot.checkBotMentioned.test.ts +193 -0
- package/src/bot.stripBotMention.test.ts +134 -0
- package/src/bot.test.ts +2107 -0
- package/src/bot.ts +1556 -0
- package/src/card-action.ts +79 -0
- package/src/channel.test.ts +48 -0
- package/src/channel.ts +369 -0
- package/src/chat-schema.ts +24 -0
- package/src/chat.test.ts +89 -0
- package/src/chat.ts +130 -0
- package/src/client.test.ts +324 -0
- package/src/client.ts +196 -0
- package/src/config-schema.test.ts +247 -0
- package/src/config-schema.ts +306 -0
- package/src/dedup.ts +203 -0
- package/src/directory.test.ts +40 -0
- package/src/directory.ts +156 -0
- package/src/doc-schema.ts +182 -0
- package/src/docx-batch-insert.test.ts +90 -0
- package/src/docx-batch-insert.ts +187 -0
- package/src/docx-color-text.ts +149 -0
- package/src/docx-table-ops.ts +298 -0
- package/src/docx.account-selection.test.ts +70 -0
- package/src/docx.test.ts +445 -0
- package/src/docx.ts +1460 -0
- package/src/drive-schema.ts +46 -0
- package/src/drive.ts +228 -0
- package/src/dynamic-agent.ts +131 -0
- package/src/external-keys.test.ts +20 -0
- package/src/external-keys.ts +19 -0
- package/src/feishu-command-handler.ts +59 -0
- package/src/media.test.ts +523 -0
- package/src/media.ts +484 -0
- package/src/mention.ts +133 -0
- package/src/monitor.account.ts +562 -0
- package/src/monitor.reaction.test.ts +653 -0
- package/src/monitor.startup.test.ts +190 -0
- package/src/monitor.startup.ts +64 -0
- package/src/monitor.state.defaults.test.ts +46 -0
- package/src/monitor.state.ts +155 -0
- package/src/monitor.test-mocks.ts +45 -0
- package/src/monitor.transport.ts +264 -0
- package/src/monitor.ts +95 -0
- package/src/monitor.webhook-e2e.test.ts +214 -0
- package/src/monitor.webhook-security.test.ts +142 -0
- package/src/monitor.webhook.test-helpers.ts +98 -0
- package/src/onboarding.status.test.ts +25 -0
- package/src/onboarding.test.ts +143 -0
- package/src/onboarding.ts +489 -0
- package/src/outbound.test.ts +356 -0
- package/src/outbound.ts +176 -0
- package/src/perm-schema.ts +52 -0
- package/src/perm.ts +176 -0
- package/src/policy.test.ts +154 -0
- package/src/policy.ts +123 -0
- package/src/post.test.ts +105 -0
- package/src/post.ts +274 -0
- package/src/probe.test.ts +270 -0
- package/src/probe.ts +156 -0
- package/src/reactions.ts +153 -0
- package/src/reply-dispatcher.test.ts +513 -0
- package/src/reply-dispatcher.ts +397 -0
- package/src/runtime.ts +6 -0
- package/src/secret-input.ts +13 -0
- package/src/send-message.ts +71 -0
- package/src/send-result.ts +29 -0
- package/src/send-target.test.ts +74 -0
- package/src/send-target.ts +29 -0
- package/src/send.reply-fallback.test.ts +189 -0
- package/src/send.test.ts +168 -0
- package/src/send.ts +481 -0
- package/src/streaming-card.test.ts +54 -0
- package/src/streaming-card.ts +374 -0
- package/src/targets.test.ts +70 -0
- package/src/targets.ts +107 -0
- package/src/tool-account-routing.test.ts +129 -0
- package/src/tool-account.ts +70 -0
- package/src/tool-factory-test-harness.ts +76 -0
- package/src/tool-result.test.ts +32 -0
- package/src/tool-result.ts +14 -0
- package/src/tools-config.test.ts +21 -0
- package/src/tools-config.ts +22 -0
- package/src/types.ts +103 -0
- package/src/typing.test.ts +144 -0
- package/src/typing.ts +210 -0
- package/src/wiki-schema.ts +55 -0
- package/src/wiki.ts +233 -0
- package/index.js +0 -27
package/src/perm.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
|
|
3
|
+
import { listEnabledFeishuAccounts } from "./accounts.js";
|
|
4
|
+
import { FeishuPermSchema, type FeishuPermParams } from "./perm-schema.js";
|
|
5
|
+
import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js";
|
|
6
|
+
import {
|
|
7
|
+
jsonToolResult,
|
|
8
|
+
toolExecutionErrorResult,
|
|
9
|
+
unknownToolActionResult,
|
|
10
|
+
} from "./tool-result.js";
|
|
11
|
+
|
|
12
|
+
type ListTokenType =
|
|
13
|
+
| "doc"
|
|
14
|
+
| "sheet"
|
|
15
|
+
| "file"
|
|
16
|
+
| "wiki"
|
|
17
|
+
| "bitable"
|
|
18
|
+
| "docx"
|
|
19
|
+
| "mindnote"
|
|
20
|
+
| "minutes"
|
|
21
|
+
| "slides";
|
|
22
|
+
type CreateTokenType =
|
|
23
|
+
| "doc"
|
|
24
|
+
| "sheet"
|
|
25
|
+
| "file"
|
|
26
|
+
| "wiki"
|
|
27
|
+
| "bitable"
|
|
28
|
+
| "docx"
|
|
29
|
+
| "folder"
|
|
30
|
+
| "mindnote"
|
|
31
|
+
| "minutes"
|
|
32
|
+
| "slides";
|
|
33
|
+
type MemberType =
|
|
34
|
+
| "email"
|
|
35
|
+
| "openid"
|
|
36
|
+
| "unionid"
|
|
37
|
+
| "openchat"
|
|
38
|
+
| "opendepartmentid"
|
|
39
|
+
| "userid"
|
|
40
|
+
| "groupid"
|
|
41
|
+
| "wikispaceid";
|
|
42
|
+
type PermType = "view" | "edit" | "full_access";
|
|
43
|
+
|
|
44
|
+
// ============ Actions ============
|
|
45
|
+
|
|
46
|
+
async function listMembers(client: Lark.Client, token: string, type: string) {
|
|
47
|
+
const res = await client.drive.permissionMember.list({
|
|
48
|
+
path: { token },
|
|
49
|
+
params: { type: type as ListTokenType },
|
|
50
|
+
});
|
|
51
|
+
if (res.code !== 0) {
|
|
52
|
+
throw new Error(res.msg);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
members:
|
|
57
|
+
res.data?.items?.map((m) => ({
|
|
58
|
+
member_type: m.member_type,
|
|
59
|
+
member_id: m.member_id,
|
|
60
|
+
perm: m.perm,
|
|
61
|
+
name: m.name,
|
|
62
|
+
})) ?? [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function addMember(
|
|
67
|
+
client: Lark.Client,
|
|
68
|
+
token: string,
|
|
69
|
+
type: string,
|
|
70
|
+
memberType: string,
|
|
71
|
+
memberId: string,
|
|
72
|
+
perm: string,
|
|
73
|
+
) {
|
|
74
|
+
const res = await client.drive.permissionMember.create({
|
|
75
|
+
path: { token },
|
|
76
|
+
params: { type: type as CreateTokenType, need_notification: false },
|
|
77
|
+
data: {
|
|
78
|
+
member_type: memberType as MemberType,
|
|
79
|
+
member_id: memberId,
|
|
80
|
+
perm: perm as PermType,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
if (res.code !== 0) {
|
|
84
|
+
throw new Error(res.msg);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
success: true,
|
|
89
|
+
member: res.data?.member,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function removeMember(
|
|
94
|
+
client: Lark.Client,
|
|
95
|
+
token: string,
|
|
96
|
+
type: string,
|
|
97
|
+
memberType: string,
|
|
98
|
+
memberId: string,
|
|
99
|
+
) {
|
|
100
|
+
const res = await client.drive.permissionMember.delete({
|
|
101
|
+
path: { token, member_id: memberId },
|
|
102
|
+
params: { type: type as CreateTokenType, member_type: memberType as MemberType },
|
|
103
|
+
});
|
|
104
|
+
if (res.code !== 0) {
|
|
105
|
+
throw new Error(res.msg);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
success: true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============ Tool Registration ============
|
|
114
|
+
|
|
115
|
+
export function registerFeishuPermTools(api: OpenClawPluginApi) {
|
|
116
|
+
if (!api.config) {
|
|
117
|
+
api.logger.debug?.("feishu_perm: No config available, skipping perm tools");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const accounts = listEnabledFeishuAccounts(api.config);
|
|
122
|
+
if (accounts.length === 0) {
|
|
123
|
+
api.logger.debug?.("feishu_perm: No Feishu accounts configured, skipping perm tools");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const toolsCfg = resolveAnyEnabledFeishuToolsConfig(accounts);
|
|
128
|
+
if (!toolsCfg.perm) {
|
|
129
|
+
api.logger.debug?.("feishu_perm: perm tool disabled in config (default: false)");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
type FeishuPermExecuteParams = FeishuPermParams & { accountId?: string };
|
|
134
|
+
|
|
135
|
+
api.registerTool(
|
|
136
|
+
(ctx) => {
|
|
137
|
+
const defaultAccountId = ctx.agentAccountId;
|
|
138
|
+
return {
|
|
139
|
+
name: "feishu_perm",
|
|
140
|
+
label: "Feishu Perm",
|
|
141
|
+
description: "Feishu permission management. Actions: list, add, remove",
|
|
142
|
+
parameters: FeishuPermSchema,
|
|
143
|
+
async execute(_toolCallId, params) {
|
|
144
|
+
const p = params as FeishuPermExecuteParams;
|
|
145
|
+
try {
|
|
146
|
+
const client = createFeishuToolClient({
|
|
147
|
+
api,
|
|
148
|
+
executeParams: p,
|
|
149
|
+
defaultAccountId,
|
|
150
|
+
});
|
|
151
|
+
switch (p.action) {
|
|
152
|
+
case "list":
|
|
153
|
+
return jsonToolResult(await listMembers(client, p.token, p.type));
|
|
154
|
+
case "add":
|
|
155
|
+
return jsonToolResult(
|
|
156
|
+
await addMember(client, p.token, p.type, p.member_type, p.member_id, p.perm),
|
|
157
|
+
);
|
|
158
|
+
case "remove":
|
|
159
|
+
return jsonToolResult(
|
|
160
|
+
await removeMember(client, p.token, p.type, p.member_type, p.member_id),
|
|
161
|
+
);
|
|
162
|
+
default:
|
|
163
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- exhaustive check fallback
|
|
164
|
+
return unknownToolActionResult((p as { action?: unknown }).action);
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
return toolExecutionErrorResult(err);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
{ name: "feishu_perm" },
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
api.logger.info?.(`feishu_perm: Registered feishu_perm tool`);
|
|
176
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
isFeishuGroupAllowed,
|
|
4
|
+
resolveFeishuAllowlistMatch,
|
|
5
|
+
resolveFeishuGroupConfig,
|
|
6
|
+
} from "./policy.js";
|
|
7
|
+
import type { FeishuConfig } from "./types.js";
|
|
8
|
+
|
|
9
|
+
describe("feishu policy", () => {
|
|
10
|
+
describe("resolveFeishuGroupConfig", () => {
|
|
11
|
+
it("falls back to wildcard group config when direct match is missing", () => {
|
|
12
|
+
const cfg = {
|
|
13
|
+
groups: {
|
|
14
|
+
"*": { requireMention: false },
|
|
15
|
+
"oc-explicit": { requireMention: true },
|
|
16
|
+
},
|
|
17
|
+
} as unknown as FeishuConfig;
|
|
18
|
+
|
|
19
|
+
const resolved = resolveFeishuGroupConfig({
|
|
20
|
+
cfg,
|
|
21
|
+
groupId: "oc-missing",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(resolved).toEqual({ requireMention: false });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("prefers exact group config over wildcard", () => {
|
|
28
|
+
const cfg = {
|
|
29
|
+
groups: {
|
|
30
|
+
"*": { requireMention: false },
|
|
31
|
+
"oc-explicit": { requireMention: true },
|
|
32
|
+
},
|
|
33
|
+
} as unknown as FeishuConfig;
|
|
34
|
+
|
|
35
|
+
const resolved = resolveFeishuGroupConfig({
|
|
36
|
+
cfg,
|
|
37
|
+
groupId: "oc-explicit",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(resolved).toEqual({ requireMention: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("keeps case-insensitive matching for explicit group ids", () => {
|
|
44
|
+
const cfg = {
|
|
45
|
+
groups: {
|
|
46
|
+
"*": { requireMention: false },
|
|
47
|
+
OC_UPPER: { requireMention: true },
|
|
48
|
+
},
|
|
49
|
+
} as unknown as FeishuConfig;
|
|
50
|
+
|
|
51
|
+
const resolved = resolveFeishuGroupConfig({
|
|
52
|
+
cfg,
|
|
53
|
+
groupId: "oc_upper",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(resolved).toEqual({ requireMention: true });
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("resolveFeishuAllowlistMatch", () => {
|
|
61
|
+
it("allows wildcard", () => {
|
|
62
|
+
expect(
|
|
63
|
+
resolveFeishuAllowlistMatch({
|
|
64
|
+
allowFrom: ["*"],
|
|
65
|
+
senderId: "ou-attacker",
|
|
66
|
+
}),
|
|
67
|
+
).toEqual({ allowed: true, matchKey: "*", matchSource: "wildcard" });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("matches normalized ID entries", () => {
|
|
71
|
+
expect(
|
|
72
|
+
resolveFeishuAllowlistMatch({
|
|
73
|
+
allowFrom: ["feishu:user:OU_ALLOWED"],
|
|
74
|
+
senderId: "ou_allowed",
|
|
75
|
+
}),
|
|
76
|
+
).toEqual({ allowed: true, matchKey: "ou_allowed", matchSource: "id" });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("supports user_id as an additional immutable sender candidate", () => {
|
|
80
|
+
expect(
|
|
81
|
+
resolveFeishuAllowlistMatch({
|
|
82
|
+
allowFrom: ["on_user_123"],
|
|
83
|
+
senderId: "ou_other",
|
|
84
|
+
senderIds: ["on_user_123"],
|
|
85
|
+
}),
|
|
86
|
+
).toEqual({ allowed: true, matchKey: "on_user_123", matchSource: "id" });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("does not authorize based on display-name collision", () => {
|
|
90
|
+
const victimOpenId = "ou_4f4ec5aa111122223333444455556666";
|
|
91
|
+
|
|
92
|
+
expect(
|
|
93
|
+
resolveFeishuAllowlistMatch({
|
|
94
|
+
allowFrom: [victimOpenId],
|
|
95
|
+
senderId: "ou_attacker_real_open_id",
|
|
96
|
+
senderIds: ["on_attacker_user_id"],
|
|
97
|
+
senderName: victimOpenId,
|
|
98
|
+
}),
|
|
99
|
+
).toEqual({ allowed: false });
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("isFeishuGroupAllowed", () => {
|
|
104
|
+
it("matches group IDs with chat: prefix", () => {
|
|
105
|
+
expect(
|
|
106
|
+
isFeishuGroupAllowed({
|
|
107
|
+
groupPolicy: "allowlist",
|
|
108
|
+
allowFrom: ["chat:oc_group_123"],
|
|
109
|
+
senderId: "oc_group_123",
|
|
110
|
+
}),
|
|
111
|
+
).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("allows group when groupPolicy is 'open'", () => {
|
|
115
|
+
expect(
|
|
116
|
+
isFeishuGroupAllowed({
|
|
117
|
+
groupPolicy: "open",
|
|
118
|
+
allowFrom: [],
|
|
119
|
+
senderId: "oc_group_999",
|
|
120
|
+
}),
|
|
121
|
+
).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("treats 'allowall' as equivalent to 'open'", () => {
|
|
125
|
+
expect(
|
|
126
|
+
isFeishuGroupAllowed({
|
|
127
|
+
groupPolicy: "allowall",
|
|
128
|
+
allowFrom: [],
|
|
129
|
+
senderId: "oc_group_999",
|
|
130
|
+
}),
|
|
131
|
+
).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("rejects group when groupPolicy is 'disabled'", () => {
|
|
135
|
+
expect(
|
|
136
|
+
isFeishuGroupAllowed({
|
|
137
|
+
groupPolicy: "disabled",
|
|
138
|
+
allowFrom: ["oc_group_999"],
|
|
139
|
+
senderId: "oc_group_999",
|
|
140
|
+
}),
|
|
141
|
+
).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("rejects group when groupPolicy is 'allowlist' and allowFrom is empty", () => {
|
|
145
|
+
expect(
|
|
146
|
+
isFeishuGroupAllowed({
|
|
147
|
+
groupPolicy: "allowlist",
|
|
148
|
+
allowFrom: [],
|
|
149
|
+
senderId: "oc_group_999",
|
|
150
|
+
}),
|
|
151
|
+
).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
package/src/policy.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AllowlistMatch,
|
|
3
|
+
ChannelGroupContext,
|
|
4
|
+
GroupToolPolicyConfig,
|
|
5
|
+
} from "openclaw/plugin-sdk/feishu";
|
|
6
|
+
import { evaluateSenderGroupAccessForPolicy } from "openclaw/plugin-sdk/feishu";
|
|
7
|
+
import { normalizeFeishuTarget } from "./targets.js";
|
|
8
|
+
import type { FeishuConfig, FeishuGroupConfig } from "./types.js";
|
|
9
|
+
|
|
10
|
+
export type FeishuAllowlistMatch = AllowlistMatch<"wildcard" | "id">;
|
|
11
|
+
|
|
12
|
+
function normalizeFeishuAllowEntry(raw: string): string {
|
|
13
|
+
const trimmed = raw.trim();
|
|
14
|
+
if (!trimmed) {
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
if (trimmed === "*") {
|
|
18
|
+
return "*";
|
|
19
|
+
}
|
|
20
|
+
const withoutProviderPrefix = trimmed.replace(/^feishu:/i, "");
|
|
21
|
+
const normalized = normalizeFeishuTarget(withoutProviderPrefix) ?? withoutProviderPrefix;
|
|
22
|
+
return normalized.trim().toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveFeishuAllowlistMatch(params: {
|
|
26
|
+
allowFrom: Array<string | number>;
|
|
27
|
+
senderId: string;
|
|
28
|
+
senderIds?: Array<string | null | undefined>;
|
|
29
|
+
senderName?: string | null;
|
|
30
|
+
}): FeishuAllowlistMatch {
|
|
31
|
+
const allowFrom = params.allowFrom
|
|
32
|
+
.map((entry) => normalizeFeishuAllowEntry(String(entry)))
|
|
33
|
+
.filter(Boolean);
|
|
34
|
+
if (allowFrom.length === 0) {
|
|
35
|
+
return { allowed: false };
|
|
36
|
+
}
|
|
37
|
+
if (allowFrom.includes("*")) {
|
|
38
|
+
return { allowed: true, matchKey: "*", matchSource: "wildcard" };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Feishu allowlists are ID-based; mutable display names must never grant access.
|
|
42
|
+
const senderCandidates = [params.senderId, ...(params.senderIds ?? [])]
|
|
43
|
+
.map((entry) => normalizeFeishuAllowEntry(String(entry ?? "")))
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
|
|
46
|
+
for (const senderId of senderCandidates) {
|
|
47
|
+
if (allowFrom.includes(senderId)) {
|
|
48
|
+
return { allowed: true, matchKey: senderId, matchSource: "id" };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { allowed: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveFeishuGroupConfig(params: {
|
|
56
|
+
cfg?: FeishuConfig;
|
|
57
|
+
groupId?: string | null;
|
|
58
|
+
}): FeishuGroupConfig | undefined {
|
|
59
|
+
const groups = params.cfg?.groups ?? {};
|
|
60
|
+
const wildcard = groups["*"];
|
|
61
|
+
const groupId = params.groupId?.trim();
|
|
62
|
+
if (!groupId) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const direct = groups[groupId];
|
|
67
|
+
if (direct) {
|
|
68
|
+
return direct;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const lowered = groupId.toLowerCase();
|
|
72
|
+
const matchKey = Object.keys(groups).find((key) => key.toLowerCase() === lowered);
|
|
73
|
+
if (matchKey) {
|
|
74
|
+
return groups[matchKey];
|
|
75
|
+
}
|
|
76
|
+
return wildcard;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function resolveFeishuGroupToolPolicy(
|
|
80
|
+
params: ChannelGroupContext,
|
|
81
|
+
): GroupToolPolicyConfig | undefined {
|
|
82
|
+
const cfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
|
|
83
|
+
if (!cfg) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const groupConfig = resolveFeishuGroupConfig({
|
|
88
|
+
cfg,
|
|
89
|
+
groupId: params.groupId,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return groupConfig?.tools;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function isFeishuGroupAllowed(params: {
|
|
96
|
+
groupPolicy: "open" | "allowlist" | "disabled" | "allowall";
|
|
97
|
+
allowFrom: Array<string | number>;
|
|
98
|
+
senderId: string;
|
|
99
|
+
senderIds?: Array<string | null | undefined>;
|
|
100
|
+
senderName?: string | null;
|
|
101
|
+
}): boolean {
|
|
102
|
+
return evaluateSenderGroupAccessForPolicy({
|
|
103
|
+
groupPolicy: params.groupPolicy === "allowall" ? "open" : params.groupPolicy,
|
|
104
|
+
groupAllowFrom: params.allowFrom.map((entry) => String(entry)),
|
|
105
|
+
senderId: params.senderId,
|
|
106
|
+
isSenderAllowed: () => resolveFeishuAllowlistMatch(params).allowed,
|
|
107
|
+
}).allowed;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function resolveFeishuReplyPolicy(params: {
|
|
111
|
+
isDirectMessage: boolean;
|
|
112
|
+
globalConfig?: FeishuConfig;
|
|
113
|
+
groupConfig?: FeishuGroupConfig;
|
|
114
|
+
}): { requireMention: boolean } {
|
|
115
|
+
if (params.isDirectMessage) {
|
|
116
|
+
return { requireMention: false };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const requireMention =
|
|
120
|
+
params.groupConfig?.requireMention ?? params.globalConfig?.requireMention ?? true;
|
|
121
|
+
|
|
122
|
+
return { requireMention };
|
|
123
|
+
}
|
package/src/post.test.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { parsePostContent } from "./post.js";
|
|
3
|
+
|
|
4
|
+
describe("parsePostContent", () => {
|
|
5
|
+
it("renders title and styled text as markdown", () => {
|
|
6
|
+
const content = JSON.stringify({
|
|
7
|
+
title: "Daily *Plan*",
|
|
8
|
+
content: [
|
|
9
|
+
[
|
|
10
|
+
{ tag: "text", text: "Bold", style: { bold: true } },
|
|
11
|
+
{ tag: "text", text: " " },
|
|
12
|
+
{ tag: "text", text: "Italic", style: { italic: true } },
|
|
13
|
+
{ tag: "text", text: " " },
|
|
14
|
+
{ tag: "text", text: "Underline", style: { underline: true } },
|
|
15
|
+
{ tag: "text", text: " " },
|
|
16
|
+
{ tag: "text", text: "Strike", style: { strikethrough: true } },
|
|
17
|
+
{ tag: "text", text: " " },
|
|
18
|
+
{ tag: "text", text: "Code", style: { code: true, bold: true } },
|
|
19
|
+
],
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const result = parsePostContent(content);
|
|
24
|
+
|
|
25
|
+
expect(result.textContent).toBe(
|
|
26
|
+
"Daily \\*Plan\\*\n\n**Bold** *Italic* <u>Underline</u> ~~Strike~~ `Code`",
|
|
27
|
+
);
|
|
28
|
+
expect(result.imageKeys).toEqual([]);
|
|
29
|
+
expect(result.mentionedOpenIds).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("renders links and mentions", () => {
|
|
33
|
+
const content = JSON.stringify({
|
|
34
|
+
title: "",
|
|
35
|
+
content: [
|
|
36
|
+
[
|
|
37
|
+
{ tag: "a", text: "Docs [v2]", href: "https://example.com/guide(a)" },
|
|
38
|
+
{ tag: "text", text: " " },
|
|
39
|
+
{ tag: "at", user_name: "alice_bob" },
|
|
40
|
+
{ tag: "text", text: " " },
|
|
41
|
+
{ tag: "at", open_id: "ou_123" },
|
|
42
|
+
{ tag: "text", text: " " },
|
|
43
|
+
{ tag: "a", href: "https://example.com/no-text" },
|
|
44
|
+
],
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const result = parsePostContent(content);
|
|
49
|
+
|
|
50
|
+
expect(result.textContent).toBe(
|
|
51
|
+
"[Docs \\[v2\\]](https://example.com/guide(a)) @alice\\_bob @ou\\_123 [https://example.com/no\\-text](https://example.com/no-text)",
|
|
52
|
+
);
|
|
53
|
+
expect(result.mentionedOpenIds).toEqual(["ou_123"]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("inserts image placeholders and collects image keys", () => {
|
|
57
|
+
const content = JSON.stringify({
|
|
58
|
+
title: "",
|
|
59
|
+
content: [
|
|
60
|
+
[
|
|
61
|
+
{ tag: "text", text: "Before " },
|
|
62
|
+
{ tag: "img", image_key: "img_1" },
|
|
63
|
+
{ tag: "text", text: " after" },
|
|
64
|
+
],
|
|
65
|
+
[{ tag: "img", image_key: "img_2" }],
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const result = parsePostContent(content);
|
|
70
|
+
|
|
71
|
+
expect(result.textContent).toBe("Before ![image] after\n![image]");
|
|
72
|
+
expect(result.imageKeys).toEqual(["img_1", "img_2"]);
|
|
73
|
+
expect(result.mentionedOpenIds).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("supports locale wrappers", () => {
|
|
77
|
+
const wrappedByPost = JSON.stringify({
|
|
78
|
+
post: {
|
|
79
|
+
zh_cn: {
|
|
80
|
+
title: "标题",
|
|
81
|
+
content: [[{ tag: "text", text: "内容A" }]],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const wrappedByLocale = JSON.stringify({
|
|
86
|
+
zh_cn: {
|
|
87
|
+
title: "标题",
|
|
88
|
+
content: [[{ tag: "text", text: "内容B" }]],
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(parsePostContent(wrappedByPost)).toEqual({
|
|
93
|
+
textContent: "标题\n\n内容A",
|
|
94
|
+
imageKeys: [],
|
|
95
|
+
mediaKeys: [],
|
|
96
|
+
mentionedOpenIds: [],
|
|
97
|
+
});
|
|
98
|
+
expect(parsePostContent(wrappedByLocale)).toEqual({
|
|
99
|
+
textContent: "标题\n\n内容B",
|
|
100
|
+
imageKeys: [],
|
|
101
|
+
mediaKeys: [],
|
|
102
|
+
mentionedOpenIds: [],
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|