@openclaw/feishu 2026.2.2 → 2026.2.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.
- package/index.ts +49 -1
- package/openclaw.plugin.json +1 -0
- package/package.json +9 -5
- package/skills/feishu-doc/SKILL.md +105 -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.ts +144 -0
- package/src/bitable.ts +461 -0
- package/src/bot.ts +871 -0
- package/src/channel.ts +284 -201
- package/src/client.ts +118 -0
- package/src/config-schema.ts +152 -26
- package/src/directory.ts +177 -0
- package/src/doc-schema.ts +47 -0
- package/src/docx.ts +521 -0
- package/src/drive-schema.ts +46 -0
- package/src/drive.ts +227 -0
- package/src/media.ts +527 -0
- package/src/mention.ts +126 -0
- package/src/monitor.ts +190 -0
- package/src/onboarding.ts +281 -200
- package/src/outbound.ts +55 -0
- package/src/perm-schema.ts +52 -0
- package/src/perm.ts +173 -0
- package/src/policy.ts +104 -0
- package/src/probe.ts +44 -0
- package/src/reactions.ts +160 -0
- package/src/reply-dispatcher.ts +179 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +358 -0
- package/src/targets.ts +78 -0
- package/src/tools-config.ts +21 -0
- package/src/types.ts +75 -0
- package/src/typing.ts +80 -0
- package/src/wiki-schema.ts +55 -0
- package/src/wiki.ts +232 -0
package/src/onboarding.ts
CHANGED
|
@@ -1,124 +1,113 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ChannelOnboardingAdapter,
|
|
3
3
|
ChannelOnboardingDmPolicy,
|
|
4
|
+
ClawdbotConfig,
|
|
4
5
|
DmPolicy,
|
|
5
|
-
OpenClawConfig,
|
|
6
6
|
WizardPrompter,
|
|
7
7
|
} from "openclaw/plugin-sdk";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
normalizeAccountId,
|
|
13
|
-
promptAccountId,
|
|
14
|
-
} from "openclaw/plugin-sdk";
|
|
15
|
-
import {
|
|
16
|
-
listFeishuAccountIds,
|
|
17
|
-
resolveDefaultFeishuAccountId,
|
|
18
|
-
resolveFeishuAccount,
|
|
19
|
-
} from "openclaw/plugin-sdk";
|
|
8
|
+
import { addWildcardAllowFrom, DEFAULT_ACCOUNT_ID, formatDocsLink } from "openclaw/plugin-sdk";
|
|
9
|
+
import type { FeishuConfig } from "./types.js";
|
|
10
|
+
import { resolveFeishuCredentials } from "./accounts.js";
|
|
11
|
+
import { probeFeishu } from "./probe.js";
|
|
20
12
|
|
|
21
13
|
const channel = "feishu" as const;
|
|
22
14
|
|
|
23
|
-
function setFeishuDmPolicy(cfg:
|
|
15
|
+
function setFeishuDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy): ClawdbotConfig {
|
|
24
16
|
const allowFrom =
|
|
25
|
-
|
|
17
|
+
dmPolicy === "open"
|
|
18
|
+
? addWildcardAllowFrom(cfg.channels?.feishu?.allowFrom)?.map((entry) => String(entry))
|
|
19
|
+
: undefined;
|
|
26
20
|
return {
|
|
27
21
|
...cfg,
|
|
28
22
|
channels: {
|
|
29
23
|
...cfg.channels,
|
|
30
24
|
feishu: {
|
|
31
25
|
...cfg.channels?.feishu,
|
|
32
|
-
|
|
33
|
-
dmPolicy: policy,
|
|
26
|
+
dmPolicy,
|
|
34
27
|
...(allowFrom ? { allowFrom } : {}),
|
|
35
28
|
},
|
|
36
29
|
},
|
|
37
30
|
};
|
|
38
31
|
}
|
|
39
32
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function normalizeAllowEntry(entry: string): string {
|
|
53
|
-
return entry.replace(/^(feishu|lark):/i, "").trim();
|
|
33
|
+
function setFeishuAllowFrom(cfg: ClawdbotConfig, allowFrom: string[]): ClawdbotConfig {
|
|
34
|
+
return {
|
|
35
|
+
...cfg,
|
|
36
|
+
channels: {
|
|
37
|
+
...cfg.channels,
|
|
38
|
+
feishu: {
|
|
39
|
+
...cfg.channels?.feishu,
|
|
40
|
+
allowFrom,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
54
44
|
}
|
|
55
45
|
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return "feishu";
|
|
46
|
+
function parseAllowFromInput(raw: string): string[] {
|
|
47
|
+
return raw
|
|
48
|
+
.split(/[\n,;]+/g)
|
|
49
|
+
.map((entry) => entry.trim())
|
|
50
|
+
.filter(Boolean);
|
|
62
51
|
}
|
|
63
52
|
|
|
64
53
|
async function promptFeishuAllowFrom(params: {
|
|
65
|
-
cfg:
|
|
54
|
+
cfg: ClawdbotConfig;
|
|
66
55
|
prompter: WizardPrompter;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
|
80
|
-
validate: (value) => {
|
|
81
|
-
const raw = String(value ?? "").trim();
|
|
82
|
-
if (!raw) {
|
|
83
|
-
return "Required";
|
|
84
|
-
}
|
|
85
|
-
const entries = raw
|
|
86
|
-
.split(/[\n,;]+/g)
|
|
87
|
-
.map((item) => normalizeAllowEntry(item))
|
|
88
|
-
.filter(Boolean);
|
|
89
|
-
const invalid = entries.filter((item) => item !== "*" && !/^o[un]_[a-zA-Z0-9]+$/.test(item));
|
|
90
|
-
if (invalid.length > 0) {
|
|
91
|
-
return `Invalid Feishu ids: ${invalid.join(", ")}`;
|
|
92
|
-
}
|
|
93
|
-
return undefined;
|
|
94
|
-
},
|
|
95
|
-
});
|
|
56
|
+
}): Promise<ClawdbotConfig> {
|
|
57
|
+
const existing = params.cfg.channels?.feishu?.allowFrom ?? [];
|
|
58
|
+
await params.prompter.note(
|
|
59
|
+
[
|
|
60
|
+
"Allowlist Feishu DMs by open_id or user_id.",
|
|
61
|
+
"You can find user open_id in Feishu admin console or via API.",
|
|
62
|
+
"Examples:",
|
|
63
|
+
"- ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
64
|
+
"- on_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
65
|
+
].join("\n"),
|
|
66
|
+
"Feishu allowlist",
|
|
67
|
+
);
|
|
96
68
|
|
|
97
|
-
|
|
98
|
-
.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
69
|
+
while (true) {
|
|
70
|
+
const entry = await params.prompter.text({
|
|
71
|
+
message: "Feishu allowFrom (user open_ids)",
|
|
72
|
+
placeholder: "ou_xxxxx, ou_yyyyy",
|
|
73
|
+
initialValue: existing[0] ? String(existing[0]) : undefined,
|
|
74
|
+
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
75
|
+
});
|
|
76
|
+
const parts = parseAllowFromInput(String(entry));
|
|
77
|
+
if (parts.length === 0) {
|
|
78
|
+
await params.prompter.note("Enter at least one user.", "Feishu allowlist");
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
106
81
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
enabled: true,
|
|
115
|
-
dmPolicy: "allowlist",
|
|
116
|
-
allowFrom: unique,
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
};
|
|
82
|
+
const unique = [
|
|
83
|
+
...new Set([
|
|
84
|
+
...existing.map((v: string | number) => String(v).trim()).filter(Boolean),
|
|
85
|
+
...parts,
|
|
86
|
+
]),
|
|
87
|
+
];
|
|
88
|
+
return setFeishuAllowFrom(params.cfg, unique);
|
|
120
89
|
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function noteFeishuCredentialHelp(prompter: WizardPrompter): Promise<void> {
|
|
93
|
+
await prompter.note(
|
|
94
|
+
[
|
|
95
|
+
"1) Go to Feishu Open Platform (open.feishu.cn)",
|
|
96
|
+
"2) Create a self-built app",
|
|
97
|
+
"3) Get App ID and App Secret from Credentials page",
|
|
98
|
+
"4) Enable required permissions: im:message, im:chat, contact:user.base:readonly",
|
|
99
|
+
"5) Publish the app or add it to a test group",
|
|
100
|
+
"Tip: you can also set FEISHU_APP_ID / FEISHU_APP_SECRET env vars.",
|
|
101
|
+
`Docs: ${formatDocsLink("/channels/feishu", "feishu")}`,
|
|
102
|
+
].join("\n"),
|
|
103
|
+
"Feishu credentials",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
121
106
|
|
|
107
|
+
function setFeishuGroupPolicy(
|
|
108
|
+
cfg: ClawdbotConfig,
|
|
109
|
+
groupPolicy: "open" | "allowlist" | "disabled",
|
|
110
|
+
): ClawdbotConfig {
|
|
122
111
|
return {
|
|
123
112
|
...cfg,
|
|
124
113
|
channels: {
|
|
@@ -126,15 +115,20 @@ async function promptFeishuAllowFrom(params: {
|
|
|
126
115
|
feishu: {
|
|
127
116
|
...cfg.channels?.feishu,
|
|
128
117
|
enabled: true,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
118
|
+
groupPolicy,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function setFeishuGroupAllowFrom(cfg: ClawdbotConfig, groupAllowFrom: string[]): ClawdbotConfig {
|
|
125
|
+
return {
|
|
126
|
+
...cfg,
|
|
127
|
+
channels: {
|
|
128
|
+
...cfg.channels,
|
|
129
|
+
feishu: {
|
|
130
|
+
...cfg.channels?.feishu,
|
|
131
|
+
groupAllowFrom,
|
|
138
132
|
},
|
|
139
133
|
},
|
|
140
134
|
};
|
|
@@ -145,134 +139,221 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
|
145
139
|
channel,
|
|
146
140
|
policyKey: "channels.feishu.dmPolicy",
|
|
147
141
|
allowFromKey: "channels.feishu.allowFrom",
|
|
148
|
-
getCurrent: (cfg) => cfg.channels?.feishu?.dmPolicy ?? "pairing",
|
|
142
|
+
getCurrent: (cfg) => (cfg.channels?.feishu as FeishuConfig | undefined)?.dmPolicy ?? "pairing",
|
|
149
143
|
setPolicy: (cfg, policy) => setFeishuDmPolicy(cfg, policy),
|
|
150
144
|
promptAllowFrom: promptFeishuAllowFrom,
|
|
151
145
|
};
|
|
152
146
|
|
|
153
|
-
function updateFeishuConfig(
|
|
154
|
-
cfg: OpenClawConfig,
|
|
155
|
-
accountId: string,
|
|
156
|
-
updates: { appId?: string; appSecret?: string; domain?: string; enabled?: boolean },
|
|
157
|
-
): OpenClawConfig {
|
|
158
|
-
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
159
|
-
const next = { ...cfg } as OpenClawConfig;
|
|
160
|
-
const feishu = { ...next.channels?.feishu } as Record<string, unknown>;
|
|
161
|
-
const accounts = feishu.accounts
|
|
162
|
-
? { ...(feishu.accounts as Record<string, unknown>) }
|
|
163
|
-
: undefined;
|
|
164
|
-
|
|
165
|
-
if (isDefault && !accounts) {
|
|
166
|
-
return {
|
|
167
|
-
...next,
|
|
168
|
-
channels: {
|
|
169
|
-
...next.channels,
|
|
170
|
-
feishu: {
|
|
171
|
-
...feishu,
|
|
172
|
-
...updates,
|
|
173
|
-
enabled: updates.enabled ?? true,
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const resolvedAccounts = accounts ?? {};
|
|
180
|
-
const existing = (resolvedAccounts[accountId] as Record<string, unknown>) ?? {};
|
|
181
|
-
resolvedAccounts[accountId] = {
|
|
182
|
-
...existing,
|
|
183
|
-
...updates,
|
|
184
|
-
enabled: updates.enabled ?? true,
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
...next,
|
|
189
|
-
channels: {
|
|
190
|
-
...next.channels,
|
|
191
|
-
feishu: {
|
|
192
|
-
...feishu,
|
|
193
|
-
accounts: resolvedAccounts,
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
147
|
export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
200
148
|
channel,
|
|
201
|
-
dmPolicy,
|
|
202
149
|
getStatus: async ({ cfg }) => {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
150
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
151
|
+
const configured = Boolean(resolveFeishuCredentials(feishuCfg));
|
|
152
|
+
|
|
153
|
+
// Try to probe if configured
|
|
154
|
+
let probeResult = null;
|
|
155
|
+
if (configured && feishuCfg) {
|
|
156
|
+
try {
|
|
157
|
+
probeResult = await probeFeishu(feishuCfg);
|
|
158
|
+
} catch {
|
|
159
|
+
// Ignore probe errors
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const statusLines: string[] = [];
|
|
164
|
+
if (!configured) {
|
|
165
|
+
statusLines.push("Feishu: needs app credentials");
|
|
166
|
+
} else if (probeResult?.ok) {
|
|
167
|
+
statusLines.push(
|
|
168
|
+
`Feishu: connected as ${probeResult.botName ?? probeResult.botOpenId ?? "bot"}`,
|
|
169
|
+
);
|
|
170
|
+
} else {
|
|
171
|
+
statusLines.push("Feishu: configured (connection not verified)");
|
|
172
|
+
}
|
|
173
|
+
|
|
207
174
|
return {
|
|
208
175
|
channel,
|
|
209
176
|
configured,
|
|
210
|
-
statusLines
|
|
211
|
-
selectionHint: configured ? "configured" : "
|
|
212
|
-
quickstartScore: configured ?
|
|
177
|
+
statusLines,
|
|
178
|
+
selectionHint: configured ? "configured" : "needs app creds",
|
|
179
|
+
quickstartScore: configured ? 2 : 0,
|
|
213
180
|
};
|
|
214
181
|
},
|
|
215
|
-
|
|
182
|
+
|
|
183
|
+
configure: async ({ cfg, prompter }) => {
|
|
184
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
185
|
+
const resolved = resolveFeishuCredentials(feishuCfg);
|
|
186
|
+
const hasConfigCreds = Boolean(feishuCfg?.appId?.trim() && feishuCfg?.appSecret?.trim());
|
|
187
|
+
const canUseEnv = Boolean(
|
|
188
|
+
!hasConfigCreds && process.env.FEISHU_APP_ID?.trim() && process.env.FEISHU_APP_SECRET?.trim(),
|
|
189
|
+
);
|
|
190
|
+
|
|
216
191
|
let next = cfg;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
192
|
+
let appId: string | null = null;
|
|
193
|
+
let appSecret: string | null = null;
|
|
194
|
+
|
|
195
|
+
if (!resolved) {
|
|
196
|
+
await noteFeishuCredentialHelp(prompter);
|
|
197
|
+
}
|
|
220
198
|
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
label: "Feishu",
|
|
226
|
-
currentId: accountId,
|
|
227
|
-
listAccountIds: listFeishuAccountIds,
|
|
228
|
-
defaultAccountId: defaultId,
|
|
199
|
+
if (canUseEnv) {
|
|
200
|
+
const keepEnv = await prompter.confirm({
|
|
201
|
+
message: "FEISHU_APP_ID + FEISHU_APP_SECRET detected. Use env vars?",
|
|
202
|
+
initialValue: true,
|
|
229
203
|
});
|
|
204
|
+
if (keepEnv) {
|
|
205
|
+
next = {
|
|
206
|
+
...next,
|
|
207
|
+
channels: {
|
|
208
|
+
...next.channels,
|
|
209
|
+
feishu: { ...next.channels?.feishu, enabled: true },
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
} else {
|
|
213
|
+
appId = String(
|
|
214
|
+
await prompter.text({
|
|
215
|
+
message: "Enter Feishu App ID",
|
|
216
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
217
|
+
}),
|
|
218
|
+
).trim();
|
|
219
|
+
appSecret = String(
|
|
220
|
+
await prompter.text({
|
|
221
|
+
message: "Enter Feishu App Secret",
|
|
222
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
223
|
+
}),
|
|
224
|
+
).trim();
|
|
225
|
+
}
|
|
226
|
+
} else if (hasConfigCreds) {
|
|
227
|
+
const keep = await prompter.confirm({
|
|
228
|
+
message: "Feishu credentials already configured. Keep them?",
|
|
229
|
+
initialValue: true,
|
|
230
|
+
});
|
|
231
|
+
if (!keep) {
|
|
232
|
+
appId = String(
|
|
233
|
+
await prompter.text({
|
|
234
|
+
message: "Enter Feishu App ID",
|
|
235
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
236
|
+
}),
|
|
237
|
+
).trim();
|
|
238
|
+
appSecret = String(
|
|
239
|
+
await prompter.text({
|
|
240
|
+
message: "Enter Feishu App Secret",
|
|
241
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
242
|
+
}),
|
|
243
|
+
).trim();
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
appId = String(
|
|
247
|
+
await prompter.text({
|
|
248
|
+
message: "Enter Feishu App ID",
|
|
249
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
250
|
+
}),
|
|
251
|
+
).trim();
|
|
252
|
+
appSecret = String(
|
|
253
|
+
await prompter.text({
|
|
254
|
+
message: "Enter Feishu App Secret",
|
|
255
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
256
|
+
}),
|
|
257
|
+
).trim();
|
|
230
258
|
}
|
|
231
259
|
|
|
232
|
-
|
|
260
|
+
if (appId && appSecret) {
|
|
261
|
+
next = {
|
|
262
|
+
...next,
|
|
263
|
+
channels: {
|
|
264
|
+
...next.channels,
|
|
265
|
+
feishu: {
|
|
266
|
+
...next.channels?.feishu,
|
|
267
|
+
enabled: true,
|
|
268
|
+
appId,
|
|
269
|
+
appSecret,
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// Test connection
|
|
275
|
+
const testCfg = next.channels?.feishu as FeishuConfig;
|
|
276
|
+
try {
|
|
277
|
+
const probe = await probeFeishu(testCfg);
|
|
278
|
+
if (probe.ok) {
|
|
279
|
+
await prompter.note(
|
|
280
|
+
`Connected as ${probe.botName ?? probe.botOpenId ?? "bot"}`,
|
|
281
|
+
"Feishu connection test",
|
|
282
|
+
);
|
|
283
|
+
} else {
|
|
284
|
+
await prompter.note(
|
|
285
|
+
`Connection failed: ${probe.error ?? "unknown error"}`,
|
|
286
|
+
"Feishu connection test",
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
} catch (err) {
|
|
290
|
+
await prompter.note(`Connection test failed: ${String(err)}`, "Feishu connection test");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Domain selection
|
|
295
|
+
const currentDomain = (next.channels?.feishu as FeishuConfig | undefined)?.domain ?? "feishu";
|
|
296
|
+
const domain = await prompter.select({
|
|
297
|
+
message: "Which Feishu domain?",
|
|
298
|
+
options: [
|
|
299
|
+
{ value: "feishu", label: "Feishu (feishu.cn) - China" },
|
|
300
|
+
{ value: "lark", label: "Lark (larksuite.com) - International" },
|
|
301
|
+
],
|
|
302
|
+
initialValue: currentDomain,
|
|
303
|
+
});
|
|
304
|
+
if (domain) {
|
|
305
|
+
next = {
|
|
306
|
+
...next,
|
|
307
|
+
channels: {
|
|
308
|
+
...next.channels,
|
|
309
|
+
feishu: {
|
|
310
|
+
...next.channels?.feishu,
|
|
311
|
+
domain: domain as "feishu" | "lark",
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
233
316
|
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
message: "
|
|
317
|
+
// Group policy
|
|
318
|
+
const groupPolicy = await prompter.select({
|
|
319
|
+
message: "Group chat policy",
|
|
237
320
|
options: [
|
|
238
|
-
{ value: "
|
|
239
|
-
{ value: "
|
|
321
|
+
{ value: "allowlist", label: "Allowlist - only respond in specific groups" },
|
|
322
|
+
{ value: "open", label: "Open - respond in all groups (requires mention)" },
|
|
323
|
+
{ value: "disabled", label: "Disabled - don't respond in groups" },
|
|
240
324
|
],
|
|
241
|
-
initialValue:
|
|
325
|
+
initialValue: (next.channels?.feishu as FeishuConfig | undefined)?.groupPolicy ?? "allowlist",
|
|
242
326
|
});
|
|
243
|
-
|
|
327
|
+
if (groupPolicy) {
|
|
328
|
+
next = setFeishuGroupPolicy(next, groupPolicy as "open" | "allowlist" | "disabled");
|
|
329
|
+
}
|
|
244
330
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
initialValue:
|
|
331
|
+
// Group allowlist if needed
|
|
332
|
+
if (groupPolicy === "allowlist") {
|
|
333
|
+
const existing = (next.channels?.feishu as FeishuConfig | undefined)?.groupAllowFrom ?? [];
|
|
334
|
+
const entry = await prompter.text({
|
|
335
|
+
message: "Group chat allowlist (chat_ids)",
|
|
336
|
+
placeholder: "oc_xxxxx, oc_yyyyy",
|
|
337
|
+
initialValue: existing.length > 0 ? existing.map(String).join(", ") : undefined,
|
|
252
338
|
});
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
339
|
+
if (entry) {
|
|
340
|
+
const parts = parseAllowFromInput(String(entry));
|
|
341
|
+
if (parts.length > 0) {
|
|
342
|
+
next = setFeishuGroupAllowFrom(next, parts);
|
|
343
|
+
}
|
|
256
344
|
}
|
|
257
345
|
}
|
|
258
|
-
const appId = String(
|
|
259
|
-
await prompter.text({
|
|
260
|
-
message: "Feishu App ID (cli_...)",
|
|
261
|
-
initialValue: resolved.config.appId?.trim() || undefined,
|
|
262
|
-
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
263
|
-
}),
|
|
264
|
-
).trim();
|
|
265
346
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
message: "Feishu App Secret",
|
|
269
|
-
initialValue: resolved.config.appSecret?.trim() || undefined,
|
|
270
|
-
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
271
|
-
}),
|
|
272
|
-
).trim();
|
|
347
|
+
return { cfg: next, accountId: DEFAULT_ACCOUNT_ID };
|
|
348
|
+
},
|
|
273
349
|
|
|
274
|
-
|
|
350
|
+
dmPolicy,
|
|
275
351
|
|
|
276
|
-
|
|
277
|
-
|
|
352
|
+
disable: (cfg) => ({
|
|
353
|
+
...cfg,
|
|
354
|
+
channels: {
|
|
355
|
+
...cfg.channels,
|
|
356
|
+
feishu: { ...cfg.channels?.feishu, enabled: false },
|
|
357
|
+
},
|
|
358
|
+
}),
|
|
278
359
|
};
|
package/src/outbound.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk";
|
|
2
|
+
import { sendMediaFeishu } from "./media.js";
|
|
3
|
+
import { getFeishuRuntime } from "./runtime.js";
|
|
4
|
+
import { sendMessageFeishu } from "./send.js";
|
|
5
|
+
|
|
6
|
+
export const feishuOutbound: ChannelOutboundAdapter = {
|
|
7
|
+
deliveryMode: "direct",
|
|
8
|
+
chunker: (text, limit) => getFeishuRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
9
|
+
chunkerMode: "markdown",
|
|
10
|
+
textChunkLimit: 4000,
|
|
11
|
+
sendText: async ({ cfg, to, text, accountId }) => {
|
|
12
|
+
const result = await sendMessageFeishu({ cfg, to, text, accountId: accountId ?? undefined });
|
|
13
|
+
return { channel: "feishu", ...result };
|
|
14
|
+
},
|
|
15
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
|
|
16
|
+
// Send text first if provided
|
|
17
|
+
if (text?.trim()) {
|
|
18
|
+
await sendMessageFeishu({ cfg, to, text, accountId: accountId ?? undefined });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Upload and send media if URL provided
|
|
22
|
+
if (mediaUrl) {
|
|
23
|
+
try {
|
|
24
|
+
const result = await sendMediaFeishu({
|
|
25
|
+
cfg,
|
|
26
|
+
to,
|
|
27
|
+
mediaUrl,
|
|
28
|
+
accountId: accountId ?? undefined,
|
|
29
|
+
});
|
|
30
|
+
return { channel: "feishu", ...result };
|
|
31
|
+
} catch (err) {
|
|
32
|
+
// Log the error for debugging
|
|
33
|
+
console.error(`[feishu] sendMediaFeishu failed:`, err);
|
|
34
|
+
// Fallback to URL link if upload fails
|
|
35
|
+
const fallbackText = `📎 ${mediaUrl}`;
|
|
36
|
+
const result = await sendMessageFeishu({
|
|
37
|
+
cfg,
|
|
38
|
+
to,
|
|
39
|
+
text: fallbackText,
|
|
40
|
+
accountId: accountId ?? undefined,
|
|
41
|
+
});
|
|
42
|
+
return { channel: "feishu", ...result };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// No media URL, just return text result
|
|
47
|
+
const result = await sendMessageFeishu({
|
|
48
|
+
cfg,
|
|
49
|
+
to,
|
|
50
|
+
text: text ?? "",
|
|
51
|
+
accountId: accountId ?? undefined,
|
|
52
|
+
});
|
|
53
|
+
return { channel: "feishu", ...result };
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
2
|
+
|
|
3
|
+
const TokenType = Type.Union([
|
|
4
|
+
Type.Literal("doc"),
|
|
5
|
+
Type.Literal("docx"),
|
|
6
|
+
Type.Literal("sheet"),
|
|
7
|
+
Type.Literal("bitable"),
|
|
8
|
+
Type.Literal("folder"),
|
|
9
|
+
Type.Literal("file"),
|
|
10
|
+
Type.Literal("wiki"),
|
|
11
|
+
Type.Literal("mindnote"),
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const MemberType = Type.Union([
|
|
15
|
+
Type.Literal("email"),
|
|
16
|
+
Type.Literal("openid"),
|
|
17
|
+
Type.Literal("userid"),
|
|
18
|
+
Type.Literal("unionid"),
|
|
19
|
+
Type.Literal("openchat"),
|
|
20
|
+
Type.Literal("opendepartmentid"),
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const Permission = Type.Union([
|
|
24
|
+
Type.Literal("view"),
|
|
25
|
+
Type.Literal("edit"),
|
|
26
|
+
Type.Literal("full_access"),
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export const FeishuPermSchema = Type.Union([
|
|
30
|
+
Type.Object({
|
|
31
|
+
action: Type.Literal("list"),
|
|
32
|
+
token: Type.String({ description: "File token" }),
|
|
33
|
+
type: TokenType,
|
|
34
|
+
}),
|
|
35
|
+
Type.Object({
|
|
36
|
+
action: Type.Literal("add"),
|
|
37
|
+
token: Type.String({ description: "File token" }),
|
|
38
|
+
type: TokenType,
|
|
39
|
+
member_type: MemberType,
|
|
40
|
+
member_id: Type.String({ description: "Member ID (email, open_id, user_id, etc.)" }),
|
|
41
|
+
perm: Permission,
|
|
42
|
+
}),
|
|
43
|
+
Type.Object({
|
|
44
|
+
action: Type.Literal("remove"),
|
|
45
|
+
token: Type.String({ description: "File token" }),
|
|
46
|
+
type: TokenType,
|
|
47
|
+
member_type: MemberType,
|
|
48
|
+
member_id: Type.String({ description: "Member ID to remove" }),
|
|
49
|
+
}),
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
export type FeishuPermParams = Static<typeof FeishuPermSchema>;
|