@openclaw/feishu 2026.3.12 → 2026.5.1-beta.1
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/api.ts +31 -0
- package/channel-entry.ts +20 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +16 -0
- package/index.ts +70 -53
- package/openclaw.plugin.json +1653 -4
- package/package.json +32 -7
- package/runtime-api.ts +55 -0
- package/secret-contract-api.ts +5 -0
- package/security-contract-api.ts +1 -0
- package/session-key-api.ts +1 -0
- package/setup-api.ts +3 -0
- package/setup-entry.test.ts +14 -0
- package/setup-entry.ts +13 -0
- package/src/accounts.test.ts +115 -22
- package/src/accounts.ts +199 -117
- package/src/app-registration.ts +331 -0
- package/src/approval-auth.test.ts +24 -0
- package/src/approval-auth.ts +25 -0
- package/src/async.test.ts +35 -0
- package/src/async.ts +43 -1
- package/src/audio-preflight.runtime.ts +9 -0
- package/src/bitable.test.ts +131 -0
- package/src/bitable.ts +59 -22
- package/src/bot-content.ts +474 -0
- package/src/bot-group-name.test.ts +108 -0
- package/src/bot-runtime-api.ts +12 -0
- package/src/bot-sender-name.ts +125 -0
- package/src/bot.broadcast.test.ts +463 -0
- package/src/bot.card-action.test.ts +519 -5
- package/src/bot.checkBotMentioned.test.ts +92 -20
- package/src/bot.helpers.test.ts +118 -0
- package/src/bot.stripBotMention.test.ts +13 -21
- package/src/bot.test.ts +1334 -401
- package/src/bot.ts +798 -786
- package/src/card-action.ts +408 -40
- package/src/card-interaction.test.ts +129 -0
- package/src/card-interaction.ts +159 -0
- package/src/card-test-helpers.ts +47 -0
- package/src/card-ux-approval.ts +65 -0
- package/src/card-ux-launcher.test.ts +99 -0
- package/src/card-ux-launcher.ts +121 -0
- package/src/card-ux-shared.ts +33 -0
- package/src/channel-runtime-api.ts +16 -0
- package/src/channel.runtime.ts +47 -0
- package/src/channel.test.ts +914 -3
- package/src/channel.ts +1252 -309
- package/src/chat-schema.ts +5 -4
- package/src/chat.test.ts +84 -28
- package/src/chat.ts +68 -10
- package/src/client.test.ts +212 -103
- package/src/client.ts +115 -21
- package/src/comment-dispatcher-runtime-api.ts +6 -0
- package/src/comment-dispatcher.test.ts +169 -0
- package/src/comment-dispatcher.ts +107 -0
- package/src/comment-handler-runtime-api.ts +3 -0
- package/src/comment-handler.test.ts +486 -0
- package/src/comment-handler.ts +309 -0
- package/src/comment-reaction.test.ts +166 -0
- package/src/comment-reaction.ts +259 -0
- package/src/comment-shared.test.ts +182 -0
- package/src/comment-shared.ts +365 -0
- package/src/comment-target.ts +44 -0
- package/src/config-schema.test.ts +77 -25
- package/src/config-schema.ts +31 -4
- package/src/conversation-id.test.ts +18 -0
- package/src/conversation-id.ts +199 -0
- package/src/dedup-runtime-api.ts +1 -0
- package/src/dedup.ts +76 -35
- package/src/directory.static.ts +61 -0
- package/src/directory.test.ts +119 -20
- package/src/directory.ts +61 -91
- package/src/doc-schema.ts +1 -1
- package/src/docx-batch-insert.test.ts +39 -38
- package/src/docx-batch-insert.ts +55 -19
- package/src/docx-color-text.ts +9 -4
- package/src/docx-table-ops.test.ts +53 -0
- package/src/docx-table-ops.ts +52 -34
- package/src/docx-types.ts +38 -0
- package/src/docx.account-selection.test.ts +12 -3
- package/src/docx.test.ts +314 -74
- package/src/docx.ts +278 -122
- package/src/drive-schema.ts +47 -1
- package/src/drive.test.ts +1219 -0
- package/src/drive.ts +614 -13
- package/src/dynamic-agent.ts +10 -4
- package/src/event-types.ts +45 -0
- package/src/external-keys.ts +1 -1
- package/src/lifecycle.test-support.ts +220 -0
- package/src/media.test.ts +413 -87
- package/src/media.ts +488 -154
- package/src/mention-target.types.ts +5 -0
- package/src/mention.ts +32 -51
- package/src/message-action-contract.ts +13 -0
- package/src/monitor-state-runtime-api.ts +7 -0
- package/src/monitor-transport-runtime-api.ts +7 -0
- package/src/monitor.account.ts +220 -313
- package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
- package/src/monitor.bot-identity.ts +86 -0
- package/src/monitor.bot-menu-handler.ts +165 -0
- package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
- package/src/monitor.bot-menu.test.ts +178 -0
- package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
- package/src/monitor.card-action.lifecycle.test-support.ts +373 -0
- package/src/monitor.cleanup.test.ts +376 -0
- package/src/monitor.comment-notice-handler.ts +105 -0
- package/src/monitor.comment.test.ts +937 -0
- package/src/monitor.comment.ts +1386 -0
- package/src/monitor.lifecycle.test.ts +4 -0
- package/src/monitor.message-handler.ts +339 -0
- package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
- package/src/monitor.reaction.test.ts +194 -92
- package/src/monitor.reply-once.lifecycle.test-support.ts +190 -0
- package/src/monitor.startup.test.ts +24 -36
- package/src/monitor.startup.ts +26 -16
- package/src/monitor.state.ts +20 -5
- package/src/monitor.synthetic-error.ts +18 -0
- package/src/monitor.test-mocks.ts +2 -2
- package/src/monitor.transport.ts +297 -39
- package/src/monitor.ts +15 -10
- package/src/monitor.webhook-e2e.test.ts +272 -0
- package/src/monitor.webhook-security.test.ts +125 -91
- package/src/monitor.webhook.test-helpers.ts +116 -0
- package/src/outbound-runtime-api.ts +1 -0
- package/src/outbound.test.ts +627 -53
- package/src/outbound.ts +623 -81
- package/src/perm-schema.ts +1 -1
- package/src/perm.ts +1 -7
- package/src/pins.ts +108 -0
- package/src/policy.test.ts +297 -117
- package/src/policy.ts +142 -29
- package/src/post.ts +7 -6
- package/src/probe.test.ts +122 -118
- package/src/probe.ts +26 -16
- package/src/processing-claims.ts +59 -0
- package/src/qr-terminal.ts +1 -0
- package/src/reactions.ts +23 -60
- package/src/reasoning-preview.test.ts +59 -0
- package/src/reasoning-preview.ts +20 -0
- package/src/reply-dispatcher-runtime-api.ts +7 -0
- package/src/reply-dispatcher.test.ts +721 -168
- package/src/reply-dispatcher.ts +422 -172
- package/src/runtime.ts +6 -3
- package/src/secret-contract.ts +145 -0
- package/src/secret-input.ts +1 -13
- package/src/security-audit-shared.ts +69 -0
- package/src/security-audit.test.ts +61 -0
- package/src/security-audit.ts +1 -0
- package/src/send-result.ts +1 -1
- package/src/send-target.test.ts +9 -3
- package/src/send-target.ts +10 -4
- package/src/send.reply-fallback.test.ts +127 -42
- package/src/send.test.ts +386 -4
- package/src/send.ts +486 -164
- package/src/sequential-key.test.ts +72 -0
- package/src/sequential-key.ts +28 -0
- package/src/sequential-queue.test.ts +92 -0
- package/src/sequential-queue.ts +16 -0
- package/src/session-conversation.ts +42 -0
- package/src/session-route.ts +48 -0
- package/src/setup-core.ts +51 -0
- package/src/{onboarding.test.ts → setup-surface.test.ts} +52 -21
- package/src/setup-surface.ts +581 -0
- package/src/streaming-card.test.ts +138 -2
- package/src/streaming-card.ts +134 -18
- package/src/subagent-hooks.test.ts +603 -0
- package/src/subagent-hooks.ts +397 -0
- package/src/targets.ts +3 -13
- package/src/test-support/lifecycle-test-support.ts +479 -0
- package/src/thread-bindings.test.ts +143 -0
- package/src/thread-bindings.ts +330 -0
- package/src/tool-account-routing.test.ts +66 -8
- package/src/tool-account.test.ts +44 -0
- package/src/tool-account.ts +40 -17
- package/src/tool-factory-test-harness.ts +11 -8
- package/src/tool-result.ts +3 -1
- package/src/tools-config.ts +1 -1
- package/src/types.ts +16 -15
- package/src/typing.ts +10 -6
- package/src/wiki-schema.ts +1 -1
- package/src/wiki.ts +1 -7
- package/subagent-hooks-api.ts +31 -0
- package/tsconfig.json +16 -0
- package/src/feishu-command-handler.ts +0 -59
- package/src/onboarding.status.test.ts +0 -25
- package/src/onboarding.ts +0 -489
- package/src/send-message.ts +0 -71
- package/src/targets.test.ts +0 -70
package/src/onboarding.ts
DELETED
|
@@ -1,489 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ChannelOnboardingAdapter,
|
|
3
|
-
ChannelOnboardingDmPolicy,
|
|
4
|
-
ClawdbotConfig,
|
|
5
|
-
DmPolicy,
|
|
6
|
-
SecretInput,
|
|
7
|
-
WizardPrompter,
|
|
8
|
-
} from "openclaw/plugin-sdk/feishu";
|
|
9
|
-
import {
|
|
10
|
-
buildSingleChannelSecretPromptState,
|
|
11
|
-
DEFAULT_ACCOUNT_ID,
|
|
12
|
-
formatDocsLink,
|
|
13
|
-
hasConfiguredSecretInput,
|
|
14
|
-
mergeAllowFromEntries,
|
|
15
|
-
promptSingleChannelSecretInput,
|
|
16
|
-
setTopLevelChannelAllowFrom,
|
|
17
|
-
setTopLevelChannelDmPolicyWithAllowFrom,
|
|
18
|
-
setTopLevelChannelGroupPolicy,
|
|
19
|
-
splitOnboardingEntries,
|
|
20
|
-
} from "openclaw/plugin-sdk/feishu";
|
|
21
|
-
import { resolveFeishuCredentials } from "./accounts.js";
|
|
22
|
-
import { probeFeishu } from "./probe.js";
|
|
23
|
-
import type { FeishuConfig } from "./types.js";
|
|
24
|
-
|
|
25
|
-
const channel = "feishu" as const;
|
|
26
|
-
|
|
27
|
-
function normalizeString(value: unknown): string | undefined {
|
|
28
|
-
if (typeof value !== "string") {
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
const trimmed = value.trim();
|
|
32
|
-
return trimmed || undefined;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function setFeishuDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy): ClawdbotConfig {
|
|
36
|
-
return setTopLevelChannelDmPolicyWithAllowFrom({
|
|
37
|
-
cfg,
|
|
38
|
-
channel: "feishu",
|
|
39
|
-
dmPolicy,
|
|
40
|
-
}) as ClawdbotConfig;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function setFeishuAllowFrom(cfg: ClawdbotConfig, allowFrom: string[]): ClawdbotConfig {
|
|
44
|
-
return setTopLevelChannelAllowFrom({
|
|
45
|
-
cfg,
|
|
46
|
-
channel: "feishu",
|
|
47
|
-
allowFrom,
|
|
48
|
-
}) as ClawdbotConfig;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function promptFeishuAllowFrom(params: {
|
|
52
|
-
cfg: ClawdbotConfig;
|
|
53
|
-
prompter: WizardPrompter;
|
|
54
|
-
}): Promise<ClawdbotConfig> {
|
|
55
|
-
const existing = params.cfg.channels?.feishu?.allowFrom ?? [];
|
|
56
|
-
await params.prompter.note(
|
|
57
|
-
[
|
|
58
|
-
"Allowlist Feishu DMs by open_id or user_id.",
|
|
59
|
-
"You can find user open_id in Feishu admin console or via API.",
|
|
60
|
-
"Examples:",
|
|
61
|
-
"- ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
62
|
-
"- on_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
63
|
-
].join("\n"),
|
|
64
|
-
"Feishu allowlist",
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
while (true) {
|
|
68
|
-
const entry = await params.prompter.text({
|
|
69
|
-
message: "Feishu allowFrom (user open_ids)",
|
|
70
|
-
placeholder: "ou_xxxxx, ou_yyyyy",
|
|
71
|
-
initialValue: existing[0] ? String(existing[0]) : undefined,
|
|
72
|
-
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
73
|
-
});
|
|
74
|
-
const parts = splitOnboardingEntries(String(entry));
|
|
75
|
-
if (parts.length === 0) {
|
|
76
|
-
await params.prompter.note("Enter at least one user.", "Feishu allowlist");
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const unique = mergeAllowFromEntries(existing, parts);
|
|
81
|
-
return setFeishuAllowFrom(params.cfg, unique);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function noteFeishuCredentialHelp(prompter: WizardPrompter): Promise<void> {
|
|
86
|
-
await prompter.note(
|
|
87
|
-
[
|
|
88
|
-
"1) Go to Feishu Open Platform (open.feishu.cn)",
|
|
89
|
-
"2) Create a self-built app",
|
|
90
|
-
"3) Get App ID and App Secret from Credentials page",
|
|
91
|
-
"4) Enable required permissions: im:message, im:chat, contact:user.base:readonly",
|
|
92
|
-
"5) Publish the app or add it to a test group",
|
|
93
|
-
"Tip: you can also set FEISHU_APP_ID / FEISHU_APP_SECRET env vars.",
|
|
94
|
-
`Docs: ${formatDocsLink("/channels/feishu", "feishu")}`,
|
|
95
|
-
].join("\n"),
|
|
96
|
-
"Feishu credentials",
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function promptFeishuAppId(params: {
|
|
101
|
-
prompter: WizardPrompter;
|
|
102
|
-
initialValue?: string;
|
|
103
|
-
}): Promise<string> {
|
|
104
|
-
const appId = String(
|
|
105
|
-
await params.prompter.text({
|
|
106
|
-
message: "Enter Feishu App ID",
|
|
107
|
-
initialValue: params.initialValue,
|
|
108
|
-
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
109
|
-
}),
|
|
110
|
-
).trim();
|
|
111
|
-
return appId;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function setFeishuGroupPolicy(
|
|
115
|
-
cfg: ClawdbotConfig,
|
|
116
|
-
groupPolicy: "open" | "allowlist" | "disabled",
|
|
117
|
-
): ClawdbotConfig {
|
|
118
|
-
return setTopLevelChannelGroupPolicy({
|
|
119
|
-
cfg,
|
|
120
|
-
channel: "feishu",
|
|
121
|
-
groupPolicy,
|
|
122
|
-
enabled: true,
|
|
123
|
-
}) as ClawdbotConfig;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function setFeishuGroupAllowFrom(cfg: ClawdbotConfig, groupAllowFrom: string[]): ClawdbotConfig {
|
|
127
|
-
return {
|
|
128
|
-
...cfg,
|
|
129
|
-
channels: {
|
|
130
|
-
...cfg.channels,
|
|
131
|
-
feishu: {
|
|
132
|
-
...cfg.channels?.feishu,
|
|
133
|
-
groupAllowFrom,
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
140
|
-
label: "Feishu",
|
|
141
|
-
channel,
|
|
142
|
-
policyKey: "channels.feishu.dmPolicy",
|
|
143
|
-
allowFromKey: "channels.feishu.allowFrom",
|
|
144
|
-
getCurrent: (cfg) => (cfg.channels?.feishu as FeishuConfig | undefined)?.dmPolicy ?? "pairing",
|
|
145
|
-
setPolicy: (cfg, policy) => setFeishuDmPolicy(cfg, policy),
|
|
146
|
-
promptAllowFrom: promptFeishuAllowFrom,
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
150
|
-
channel,
|
|
151
|
-
getStatus: async ({ cfg }) => {
|
|
152
|
-
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
153
|
-
|
|
154
|
-
const isAppIdConfigured = (value: unknown): boolean => {
|
|
155
|
-
const asString = normalizeString(value);
|
|
156
|
-
if (asString) {
|
|
157
|
-
return true;
|
|
158
|
-
}
|
|
159
|
-
if (!value || typeof value !== "object") {
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
const rec = value as Record<string, unknown>;
|
|
163
|
-
const source = normalizeString(rec.source)?.toLowerCase();
|
|
164
|
-
const id = normalizeString(rec.id);
|
|
165
|
-
if (source === "env" && id) {
|
|
166
|
-
return Boolean(normalizeString(process.env[id]));
|
|
167
|
-
}
|
|
168
|
-
return hasConfiguredSecretInput(value);
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const topLevelConfigured = Boolean(
|
|
172
|
-
isAppIdConfigured(feishuCfg?.appId) && hasConfiguredSecretInput(feishuCfg?.appSecret),
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
const accountConfigured = Object.values(feishuCfg?.accounts ?? {}).some((account) => {
|
|
176
|
-
if (!account || typeof account !== "object") {
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
const hasOwnAppId = Object.prototype.hasOwnProperty.call(account, "appId");
|
|
180
|
-
const hasOwnAppSecret = Object.prototype.hasOwnProperty.call(account, "appSecret");
|
|
181
|
-
const accountAppIdConfigured = hasOwnAppId
|
|
182
|
-
? isAppIdConfigured((account as Record<string, unknown>).appId)
|
|
183
|
-
: isAppIdConfigured(feishuCfg?.appId);
|
|
184
|
-
const accountSecretConfigured = hasOwnAppSecret
|
|
185
|
-
? hasConfiguredSecretInput((account as Record<string, unknown>).appSecret)
|
|
186
|
-
: hasConfiguredSecretInput(feishuCfg?.appSecret);
|
|
187
|
-
return Boolean(accountAppIdConfigured && accountSecretConfigured);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
const configured = topLevelConfigured || accountConfigured;
|
|
191
|
-
const resolvedCredentials = resolveFeishuCredentials(feishuCfg, {
|
|
192
|
-
allowUnresolvedSecretRef: true,
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
// Try to probe if configured
|
|
196
|
-
let probeResult = null;
|
|
197
|
-
if (configured && resolvedCredentials) {
|
|
198
|
-
try {
|
|
199
|
-
probeResult = await probeFeishu(resolvedCredentials);
|
|
200
|
-
} catch {
|
|
201
|
-
// Ignore probe errors
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const statusLines: string[] = [];
|
|
206
|
-
if (!configured) {
|
|
207
|
-
statusLines.push("Feishu: needs app credentials");
|
|
208
|
-
} else if (probeResult?.ok) {
|
|
209
|
-
statusLines.push(
|
|
210
|
-
`Feishu: connected as ${probeResult.botName ?? probeResult.botOpenId ?? "bot"}`,
|
|
211
|
-
);
|
|
212
|
-
} else {
|
|
213
|
-
statusLines.push("Feishu: configured (connection not verified)");
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
channel,
|
|
218
|
-
configured,
|
|
219
|
-
statusLines,
|
|
220
|
-
selectionHint: configured ? "configured" : "needs app creds",
|
|
221
|
-
quickstartScore: configured ? 2 : 0,
|
|
222
|
-
};
|
|
223
|
-
},
|
|
224
|
-
|
|
225
|
-
configure: async ({ cfg, prompter }) => {
|
|
226
|
-
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
227
|
-
const resolved = resolveFeishuCredentials(feishuCfg, {
|
|
228
|
-
allowUnresolvedSecretRef: true,
|
|
229
|
-
});
|
|
230
|
-
const hasConfigSecret = hasConfiguredSecretInput(feishuCfg?.appSecret);
|
|
231
|
-
const hasConfigCreds = Boolean(
|
|
232
|
-
typeof feishuCfg?.appId === "string" && feishuCfg.appId.trim() && hasConfigSecret,
|
|
233
|
-
);
|
|
234
|
-
const appSecretPromptState = buildSingleChannelSecretPromptState({
|
|
235
|
-
accountConfigured: Boolean(resolved),
|
|
236
|
-
hasConfigToken: hasConfigSecret,
|
|
237
|
-
allowEnv: !hasConfigCreds && Boolean(process.env.FEISHU_APP_ID?.trim()),
|
|
238
|
-
envValue: process.env.FEISHU_APP_SECRET,
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
let next = cfg;
|
|
242
|
-
let appId: string | null = null;
|
|
243
|
-
let appSecret: SecretInput | null = null;
|
|
244
|
-
let appSecretProbeValue: string | null = null;
|
|
245
|
-
|
|
246
|
-
if (!resolved) {
|
|
247
|
-
await noteFeishuCredentialHelp(prompter);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const appSecretResult = await promptSingleChannelSecretInput({
|
|
251
|
-
cfg: next,
|
|
252
|
-
prompter,
|
|
253
|
-
providerHint: "feishu",
|
|
254
|
-
credentialLabel: "App Secret",
|
|
255
|
-
accountConfigured: appSecretPromptState.accountConfigured,
|
|
256
|
-
canUseEnv: appSecretPromptState.canUseEnv,
|
|
257
|
-
hasConfigToken: appSecretPromptState.hasConfigToken,
|
|
258
|
-
envPrompt: "FEISHU_APP_ID + FEISHU_APP_SECRET detected. Use env vars?",
|
|
259
|
-
keepPrompt: "Feishu App Secret already configured. Keep it?",
|
|
260
|
-
inputPrompt: "Enter Feishu App Secret",
|
|
261
|
-
preferredEnvVar: "FEISHU_APP_SECRET",
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
if (appSecretResult.action === "use-env") {
|
|
265
|
-
next = {
|
|
266
|
-
...next,
|
|
267
|
-
channels: {
|
|
268
|
-
...next.channels,
|
|
269
|
-
feishu: { ...next.channels?.feishu, enabled: true },
|
|
270
|
-
},
|
|
271
|
-
};
|
|
272
|
-
} else if (appSecretResult.action === "set") {
|
|
273
|
-
appSecret = appSecretResult.value;
|
|
274
|
-
appSecretProbeValue = appSecretResult.resolvedValue;
|
|
275
|
-
appId = await promptFeishuAppId({
|
|
276
|
-
prompter,
|
|
277
|
-
initialValue:
|
|
278
|
-
normalizeString(feishuCfg?.appId) ?? normalizeString(process.env.FEISHU_APP_ID),
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (appId && appSecret) {
|
|
283
|
-
next = {
|
|
284
|
-
...next,
|
|
285
|
-
channels: {
|
|
286
|
-
...next.channels,
|
|
287
|
-
feishu: {
|
|
288
|
-
...next.channels?.feishu,
|
|
289
|
-
enabled: true,
|
|
290
|
-
appId,
|
|
291
|
-
appSecret,
|
|
292
|
-
},
|
|
293
|
-
},
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
// Test connection
|
|
297
|
-
try {
|
|
298
|
-
const probe = await probeFeishu({
|
|
299
|
-
appId,
|
|
300
|
-
appSecret: appSecretProbeValue ?? undefined,
|
|
301
|
-
domain: (next.channels?.feishu as FeishuConfig | undefined)?.domain,
|
|
302
|
-
});
|
|
303
|
-
if (probe.ok) {
|
|
304
|
-
await prompter.note(
|
|
305
|
-
`Connected as ${probe.botName ?? probe.botOpenId ?? "bot"}`,
|
|
306
|
-
"Feishu connection test",
|
|
307
|
-
);
|
|
308
|
-
} else {
|
|
309
|
-
await prompter.note(
|
|
310
|
-
`Connection failed: ${probe.error ?? "unknown error"}`,
|
|
311
|
-
"Feishu connection test",
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
} catch (err) {
|
|
315
|
-
await prompter.note(`Connection test failed: ${String(err)}`, "Feishu connection test");
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const currentMode =
|
|
320
|
-
(next.channels?.feishu as FeishuConfig | undefined)?.connectionMode ?? "websocket";
|
|
321
|
-
const connectionMode = (await prompter.select({
|
|
322
|
-
message: "Feishu connection mode",
|
|
323
|
-
options: [
|
|
324
|
-
{ value: "websocket", label: "WebSocket (default)" },
|
|
325
|
-
{ value: "webhook", label: "Webhook" },
|
|
326
|
-
],
|
|
327
|
-
initialValue: currentMode,
|
|
328
|
-
})) as "websocket" | "webhook";
|
|
329
|
-
next = {
|
|
330
|
-
...next,
|
|
331
|
-
channels: {
|
|
332
|
-
...next.channels,
|
|
333
|
-
feishu: {
|
|
334
|
-
...next.channels?.feishu,
|
|
335
|
-
connectionMode,
|
|
336
|
-
},
|
|
337
|
-
},
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
if (connectionMode === "webhook") {
|
|
341
|
-
const currentVerificationToken = (next.channels?.feishu as FeishuConfig | undefined)
|
|
342
|
-
?.verificationToken;
|
|
343
|
-
const verificationTokenPromptState = buildSingleChannelSecretPromptState({
|
|
344
|
-
accountConfigured: hasConfiguredSecretInput(currentVerificationToken),
|
|
345
|
-
hasConfigToken: hasConfiguredSecretInput(currentVerificationToken),
|
|
346
|
-
allowEnv: false,
|
|
347
|
-
});
|
|
348
|
-
const verificationTokenResult = await promptSingleChannelSecretInput({
|
|
349
|
-
cfg: next,
|
|
350
|
-
prompter,
|
|
351
|
-
providerHint: "feishu-webhook",
|
|
352
|
-
credentialLabel: "verification token",
|
|
353
|
-
accountConfigured: verificationTokenPromptState.accountConfigured,
|
|
354
|
-
canUseEnv: verificationTokenPromptState.canUseEnv,
|
|
355
|
-
hasConfigToken: verificationTokenPromptState.hasConfigToken,
|
|
356
|
-
envPrompt: "",
|
|
357
|
-
keepPrompt: "Feishu verification token already configured. Keep it?",
|
|
358
|
-
inputPrompt: "Enter Feishu verification token",
|
|
359
|
-
preferredEnvVar: "FEISHU_VERIFICATION_TOKEN",
|
|
360
|
-
});
|
|
361
|
-
if (verificationTokenResult.action === "set") {
|
|
362
|
-
next = {
|
|
363
|
-
...next,
|
|
364
|
-
channels: {
|
|
365
|
-
...next.channels,
|
|
366
|
-
feishu: {
|
|
367
|
-
...next.channels?.feishu,
|
|
368
|
-
verificationToken: verificationTokenResult.value,
|
|
369
|
-
},
|
|
370
|
-
},
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
const currentEncryptKey = (next.channels?.feishu as FeishuConfig | undefined)?.encryptKey;
|
|
374
|
-
const encryptKeyPromptState = buildSingleChannelSecretPromptState({
|
|
375
|
-
accountConfigured: hasConfiguredSecretInput(currentEncryptKey),
|
|
376
|
-
hasConfigToken: hasConfiguredSecretInput(currentEncryptKey),
|
|
377
|
-
allowEnv: false,
|
|
378
|
-
});
|
|
379
|
-
const encryptKeyResult = await promptSingleChannelSecretInput({
|
|
380
|
-
cfg: next,
|
|
381
|
-
prompter,
|
|
382
|
-
providerHint: "feishu-webhook",
|
|
383
|
-
credentialLabel: "encrypt key",
|
|
384
|
-
accountConfigured: encryptKeyPromptState.accountConfigured,
|
|
385
|
-
canUseEnv: encryptKeyPromptState.canUseEnv,
|
|
386
|
-
hasConfigToken: encryptKeyPromptState.hasConfigToken,
|
|
387
|
-
envPrompt: "",
|
|
388
|
-
keepPrompt: "Feishu encrypt key already configured. Keep it?",
|
|
389
|
-
inputPrompt: "Enter Feishu encrypt key",
|
|
390
|
-
preferredEnvVar: "FEISHU_ENCRYPT_KEY",
|
|
391
|
-
});
|
|
392
|
-
if (encryptKeyResult.action === "set") {
|
|
393
|
-
next = {
|
|
394
|
-
...next,
|
|
395
|
-
channels: {
|
|
396
|
-
...next.channels,
|
|
397
|
-
feishu: {
|
|
398
|
-
...next.channels?.feishu,
|
|
399
|
-
encryptKey: encryptKeyResult.value,
|
|
400
|
-
},
|
|
401
|
-
},
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
const currentWebhookPath = (next.channels?.feishu as FeishuConfig | undefined)?.webhookPath;
|
|
405
|
-
const webhookPath = String(
|
|
406
|
-
await prompter.text({
|
|
407
|
-
message: "Feishu webhook path",
|
|
408
|
-
initialValue: currentWebhookPath ?? "/feishu/events",
|
|
409
|
-
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
410
|
-
}),
|
|
411
|
-
).trim();
|
|
412
|
-
next = {
|
|
413
|
-
...next,
|
|
414
|
-
channels: {
|
|
415
|
-
...next.channels,
|
|
416
|
-
feishu: {
|
|
417
|
-
...next.channels?.feishu,
|
|
418
|
-
webhookPath,
|
|
419
|
-
},
|
|
420
|
-
},
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Domain selection
|
|
425
|
-
const currentDomain = (next.channels?.feishu as FeishuConfig | undefined)?.domain ?? "feishu";
|
|
426
|
-
const domain = await prompter.select({
|
|
427
|
-
message: "Which Feishu domain?",
|
|
428
|
-
options: [
|
|
429
|
-
{ value: "feishu", label: "Feishu (feishu.cn) - China" },
|
|
430
|
-
{ value: "lark", label: "Lark (larksuite.com) - International" },
|
|
431
|
-
],
|
|
432
|
-
initialValue: currentDomain,
|
|
433
|
-
});
|
|
434
|
-
if (domain) {
|
|
435
|
-
next = {
|
|
436
|
-
...next,
|
|
437
|
-
channels: {
|
|
438
|
-
...next.channels,
|
|
439
|
-
feishu: {
|
|
440
|
-
...next.channels?.feishu,
|
|
441
|
-
domain: domain as "feishu" | "lark",
|
|
442
|
-
},
|
|
443
|
-
},
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Group policy
|
|
448
|
-
const groupPolicy = await prompter.select({
|
|
449
|
-
message: "Group chat policy",
|
|
450
|
-
options: [
|
|
451
|
-
{ value: "allowlist", label: "Allowlist - only respond in specific groups" },
|
|
452
|
-
{ value: "open", label: "Open - respond in all groups (requires mention)" },
|
|
453
|
-
{ value: "disabled", label: "Disabled - don't respond in groups" },
|
|
454
|
-
],
|
|
455
|
-
initialValue: (next.channels?.feishu as FeishuConfig | undefined)?.groupPolicy ?? "allowlist",
|
|
456
|
-
});
|
|
457
|
-
if (groupPolicy) {
|
|
458
|
-
next = setFeishuGroupPolicy(next, groupPolicy as "open" | "allowlist" | "disabled");
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Group allowlist if needed
|
|
462
|
-
if (groupPolicy === "allowlist") {
|
|
463
|
-
const existing = (next.channels?.feishu as FeishuConfig | undefined)?.groupAllowFrom ?? [];
|
|
464
|
-
const entry = await prompter.text({
|
|
465
|
-
message: "Group chat allowlist (chat_ids)",
|
|
466
|
-
placeholder: "oc_xxxxx, oc_yyyyy",
|
|
467
|
-
initialValue: existing.length > 0 ? existing.map(String).join(", ") : undefined,
|
|
468
|
-
});
|
|
469
|
-
if (entry) {
|
|
470
|
-
const parts = splitOnboardingEntries(String(entry));
|
|
471
|
-
if (parts.length > 0) {
|
|
472
|
-
next = setFeishuGroupAllowFrom(next, parts);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
return { cfg: next, accountId: DEFAULT_ACCOUNT_ID };
|
|
478
|
-
},
|
|
479
|
-
|
|
480
|
-
dmPolicy,
|
|
481
|
-
|
|
482
|
-
disable: (cfg) => ({
|
|
483
|
-
...cfg,
|
|
484
|
-
channels: {
|
|
485
|
-
...cfg.channels,
|
|
486
|
-
feishu: { ...cfg.channels?.feishu, enabled: false },
|
|
487
|
-
},
|
|
488
|
-
}),
|
|
489
|
-
};
|
package/src/send-message.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { assertFeishuMessageApiSuccess, toFeishuSendResult } from "./send-result.js";
|
|
2
|
-
|
|
3
|
-
type FeishuMessageClient = {
|
|
4
|
-
im: {
|
|
5
|
-
message: {
|
|
6
|
-
reply: (params: {
|
|
7
|
-
path: { message_id: string };
|
|
8
|
-
data: Record<string, unknown>;
|
|
9
|
-
}) => Promise<{ code?: number; msg?: string; data?: { message_id?: string } }>;
|
|
10
|
-
create: (params: {
|
|
11
|
-
params: { receive_id_type: string };
|
|
12
|
-
data: Record<string, unknown>;
|
|
13
|
-
}) => Promise<{ code?: number; msg?: string; data?: { message_id?: string } }>;
|
|
14
|
-
};
|
|
15
|
-
};
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export async function sendFeishuMessageWithOptionalReply(params: {
|
|
19
|
-
client: FeishuMessageClient;
|
|
20
|
-
receiveId: string;
|
|
21
|
-
receiveIdType: string;
|
|
22
|
-
content: string;
|
|
23
|
-
msgType: string;
|
|
24
|
-
replyToMessageId?: string;
|
|
25
|
-
replyInThread?: boolean;
|
|
26
|
-
sendErrorPrefix: string;
|
|
27
|
-
replyErrorPrefix: string;
|
|
28
|
-
fallbackSendErrorPrefix?: string;
|
|
29
|
-
shouldFallbackFromReply?: (response: { code?: number; msg?: string }) => boolean;
|
|
30
|
-
}): Promise<{ messageId: string; chatId: string }> {
|
|
31
|
-
const data = {
|
|
32
|
-
content: params.content,
|
|
33
|
-
msg_type: params.msgType,
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
if (params.replyToMessageId) {
|
|
37
|
-
const response = await params.client.im.message.reply({
|
|
38
|
-
path: { message_id: params.replyToMessageId },
|
|
39
|
-
data: {
|
|
40
|
-
...data,
|
|
41
|
-
...(params.replyInThread ? { reply_in_thread: true } : {}),
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
if (params.shouldFallbackFromReply?.(response)) {
|
|
45
|
-
const fallback = await params.client.im.message.create({
|
|
46
|
-
params: { receive_id_type: params.receiveIdType },
|
|
47
|
-
data: {
|
|
48
|
-
receive_id: params.receiveId,
|
|
49
|
-
...data,
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
assertFeishuMessageApiSuccess(
|
|
53
|
-
fallback,
|
|
54
|
-
params.fallbackSendErrorPrefix ?? params.sendErrorPrefix,
|
|
55
|
-
);
|
|
56
|
-
return toFeishuSendResult(fallback, params.receiveId);
|
|
57
|
-
}
|
|
58
|
-
assertFeishuMessageApiSuccess(response, params.replyErrorPrefix);
|
|
59
|
-
return toFeishuSendResult(response, params.receiveId);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const response = await params.client.im.message.create({
|
|
63
|
-
params: { receive_id_type: params.receiveIdType },
|
|
64
|
-
data: {
|
|
65
|
-
receive_id: params.receiveId,
|
|
66
|
-
...data,
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
assertFeishuMessageApiSuccess(response, params.sendErrorPrefix);
|
|
70
|
-
return toFeishuSendResult(response, params.receiveId);
|
|
71
|
-
}
|
package/src/targets.test.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { looksLikeFeishuId, normalizeFeishuTarget, resolveReceiveIdType } from "./targets.js";
|
|
3
|
-
|
|
4
|
-
describe("resolveReceiveIdType", () => {
|
|
5
|
-
it("resolves chat IDs by oc_ prefix", () => {
|
|
6
|
-
expect(resolveReceiveIdType("oc_123")).toBe("chat_id");
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it("resolves open IDs by ou_ prefix", () => {
|
|
10
|
-
expect(resolveReceiveIdType("ou_123")).toBe("open_id");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("defaults unprefixed IDs to user_id", () => {
|
|
14
|
-
expect(resolveReceiveIdType("u_123")).toBe("user_id");
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("treats explicit group targets as chat_id", () => {
|
|
18
|
-
expect(resolveReceiveIdType("group:oc_123")).toBe("chat_id");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("treats explicit channel targets as chat_id", () => {
|
|
22
|
-
expect(resolveReceiveIdType("channel:oc_123")).toBe("chat_id");
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("treats dm-prefixed open IDs as open_id", () => {
|
|
26
|
-
expect(resolveReceiveIdType("dm:ou_123")).toBe("open_id");
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe("normalizeFeishuTarget", () => {
|
|
31
|
-
it("strips provider and user prefixes", () => {
|
|
32
|
-
expect(normalizeFeishuTarget("feishu:user:ou_123")).toBe("ou_123");
|
|
33
|
-
expect(normalizeFeishuTarget("lark:user:ou_123")).toBe("ou_123");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("strips provider and chat prefixes", () => {
|
|
37
|
-
expect(normalizeFeishuTarget("feishu:chat:oc_123")).toBe("oc_123");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("normalizes group/channel prefixes to chat ids", () => {
|
|
41
|
-
expect(normalizeFeishuTarget("group:oc_123")).toBe("oc_123");
|
|
42
|
-
expect(normalizeFeishuTarget("feishu:group:oc_123")).toBe("oc_123");
|
|
43
|
-
expect(normalizeFeishuTarget("channel:oc_456")).toBe("oc_456");
|
|
44
|
-
expect(normalizeFeishuTarget("lark:channel:oc_456")).toBe("oc_456");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("accepts provider-prefixed raw ids", () => {
|
|
48
|
-
expect(normalizeFeishuTarget("feishu:ou_123")).toBe("ou_123");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("strips provider and dm prefixes", () => {
|
|
52
|
-
expect(normalizeFeishuTarget("lark:dm:ou_123")).toBe("ou_123");
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe("looksLikeFeishuId", () => {
|
|
57
|
-
it("accepts provider-prefixed user targets", () => {
|
|
58
|
-
expect(looksLikeFeishuId("feishu:user:ou_123")).toBe(true);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("accepts provider-prefixed chat targets", () => {
|
|
62
|
-
expect(looksLikeFeishuId("lark:chat:oc_123")).toBe(true);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("accepts group/channel targets", () => {
|
|
66
|
-
expect(looksLikeFeishuId("feishu:group:oc_123")).toBe(true);
|
|
67
|
-
expect(looksLikeFeishuId("group:oc_123")).toBe(true);
|
|
68
|
-
expect(looksLikeFeishuId("channel:oc_456")).toBe(true);
|
|
69
|
-
});
|
|
70
|
-
});
|