feat(ui): show required API scopes for Feishu/Lark channels (#268)

* feat(ui): show required API scopes for Feishu/Lark channels

Add a collapsible info panel listing the required Lark/Feishu API
permissions (scopes) on the channel create/edit dialog and config
detail tab. Includes reminder about Contact Range and app publishing.

* fix(feishu): annotate DM messages with sender name

Feishu DMs were missing the [From: ...] annotation, so the agent
couldn't identify who was messaging. Group messages already had this.
Align with Telegram channel which annotates both DM and group messages.

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
This commit is contained in:
Luan VuandLuvu182 authored and GitHub committed 2026-03-19 08:47:48 +07:00
1 parent 23d0b5eb0b
commit ba9b5a6be3
8 files changed
+114 -6

No files matched your search

+11 -6
View File
@@ -143,13 +143,18 @@ func (c *Channel) handleMessageEvent(ctx context.Context, event *MessageEvent) {
metadata["sender_open_id"] = sender.SenderID.OpenID
}
// Build final content with group context (pending history + sender annotation).
if mc.ChatType == "group" && senderName != "" {
annotated := fmt.Sprintf("[From: %s]\n%s", senderName, content)
if c.historyLimit > 0 {
content = c.groupHistory.BuildContext(chatID, annotated, c.historyLimit)
// Annotate content with sender identity so the agent knows who is messaging.
if senderName != "" {
if mc.ChatType == "group" {
annotated := fmt.Sprintf("[From: %s]\n%s", senderName, content)
if c.historyLimit > 0 {
content = c.groupHistory.BuildContext(chatID, annotated, c.historyLimit)
} else {
content = annotated
}
} else {
content = annotated
// DM: annotate with sender identity so the agent knows who is messaging.
content = fmt.Sprintf("[From: %s]\n%s", senderName, content)
}
}
+5
View File
@@ -278,6 +278,11 @@
"formBanner": "After creating, you'll authenticate via QR code and configure allowed users."
}
},
"scopes": {
"title": "Required API Permissions",
"description": "Your Lark/Feishu app must have these scopes enabled in the Developer Console. Publish a new app version after adding permissions.",
"publishReminder": "After adding scopes, set Contact Range to \"All members\" and publish a new app version for changes to take effect."
},
"toast": {
"created": "Channel created",
"createdDesc": "{{name}} has been added",
+5
View File
@@ -278,6 +278,11 @@
"formBanner": "Sau khi tạo, bạn sẽ xác thực bằng mã QR và cấu hình người dùng được phép."
}
},
"scopes": {
"title": "Quyền API cần thiết",
"description": "Ứng dụng Lark/Feishu cần bật các scope này trong Developer Console. Publish phiên bản mới sau khi thêm quyền.",
"publishReminder": "Sau khi thêm scope, đặt Contact Range thành \"All members\" và publish phiên bản mới để thay đổi có hiệu lực."
},
"toast": {
"created": "Đã tạo channel",
"createdDesc": "{{name}} đã được thêm",
+5
View File
@@ -278,6 +278,11 @@
"formBanner": "创建后,您将通过二维码进行认证并配置允许的用户。"
}
},
"scopes": {
"title": "所需API权限",
"description": "您的 Lark/飞书 应用需要在开发者控制台启用这些权限。添加权限后请发布新版本。",
"publishReminder": "添加权限后,将通讯录范围设置为「全部成员」,并发布新版本使更改生效。"
},
"toast": {
"created": "Channel已创建",
"createdDesc": "{{name}} 已添加",
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
import type { ChannelInstanceData } from "@/types/channel";
import { configSchema } from "../channel-schemas";
import { ChannelFields } from "../channel-fields";
import { ChannelScopesInfo } from "../channel-scopes-info";
interface ChannelConfigTabProps {
instance: ChannelInstanceData;
@@ -85,6 +86,8 @@ export function ChannelConfigTab({ instance, onUpdate }: ChannelConfigTabProps)
idPrefix="cd-cfg"
/>
<ChannelScopesInfo channelType={instance.channel_type} />
{showWebhookUrl && (
<div className="flex items-start gap-2 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-sm dark:border-blue-800 dark:bg-blue-950">
<Info className="h-4 w-4 shrink-0 mt-0.5 text-blue-600 dark:text-blue-400" />
@@ -23,6 +23,7 @@ import type { AgentData } from "@/types/agent";
import { slugify, isValidSlug } from "@/lib/slug";
import { credentialsSchema, configSchema, wizardConfig, type FieldDef } from "./channel-schemas";
import { ChannelFields } from "./channel-fields";
import { ChannelScopesInfo } from "./channel-scopes-info";
import { wizardAuthSteps, wizardConfigSteps, wizardEditConfigs } from "./channel-wizard-registry";
import { TelegramGroupOverrides } from "./telegram-group-overrides";
import { CHANNEL_TYPES } from "@/constants/channels";
@@ -307,6 +308,8 @@ export function ChannelInstanceFormDialog({
</fieldset>
)}
<ChannelScopesInfo channelType={channelType} />
{/* Auth status indicator (edit mode, channels with auth wizard step) */}
{instance && wizard?.steps.includes("auth") && (
<div className="rounded-md border border-blue-200 bg-blue-50 dark:border-blue-900 dark:bg-blue-950 p-3">
@@ -163,6 +163,34 @@ export const groupOverrideSchema: FieldDef[] = [
{ key: "system_prompt", label: "System Prompt", type: "textarea", placeholder: "Additional system prompt for this group..." },
];
// --- Required API scopes per channel type ---
// Displayed as a help reference when creating/configuring a channel.
export interface ScopeEntry {
scope: string;
note?: string; // e.g. "Range: All members"
}
export const requiredScopes: Partial<Record<string, ScopeEntry[]>> = {
feishu: [
{ scope: "application:application:self_manage" },
{ scope: "application:bot.menu:write" },
{ scope: "cardkit:card:read" },
{ scope: "cardkit:card:write" },
{ scope: "contact:contact.base:readonly", note: "Range: All members" },
{ scope: "contact:user.base:readonly", note: "Range: All members" },
{ scope: "contact:user.employee_id:readonly", note: "Range: All members" },
{ scope: "event:ip_list" },
{ scope: "im:chat.members:bot_access" },
{ scope: "im:message" },
{ scope: "im:message.group_at_msg:readonly" },
{ scope: "im:message.p2p_msg:readonly" },
{ scope: "im:message:readonly" },
{ scope: "im:message:send_as_bot" },
{ scope: "im:resource" },
],
};
// --- Post-create wizard configuration ---
// Channels with multi-step create flows (e.g. auth then config).
// Channels not listed here use the default single-step create.
@@ -0,0 +1,54 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { ChevronDown, ChevronRight, Shield } from "lucide-react";
import { requiredScopes } from "./channel-schemas";
interface ChannelScopesInfoProps {
channelType: string;
}
export function ChannelScopesInfo({ channelType }: ChannelScopesInfoProps) {
const { t } = useTranslation("channels");
const scopes = requiredScopes[channelType];
const [expanded, setExpanded] = useState(false);
if (!scopes || scopes.length === 0) return null;
return (
<div className="rounded-md border border-amber-200 bg-amber-50 dark:border-amber-900 dark:bg-amber-950 text-sm">
<button
type="button"
onClick={() => setExpanded(!expanded)}
className="flex items-center gap-2 w-full px-3 py-2 text-left text-amber-800 dark:text-amber-200 hover:bg-amber-100 dark:hover:bg-amber-900 rounded-md transition-colors"
>
<Shield className="h-4 w-4 shrink-0" />
<span className="flex-1 font-medium">{t("scopes.title")}</span>
{expanded
? <ChevronDown className="h-4 w-4 shrink-0" />
: <ChevronRight className="h-4 w-4 shrink-0" />}
</button>
{expanded && (
<div className="px-3 pb-3 space-y-2">
<p className="text-xs text-amber-700 dark:text-amber-300">
{t("scopes.description")}
</p>
<div className="space-y-0.5">
{scopes.map((s) => (
<div key={s.scope} className="flex items-baseline gap-2 text-xs font-mono">
<code className="text-amber-900 dark:text-amber-100">{s.scope}</code>
{s.note && (
<span className="text-amber-600 dark:text-amber-400 font-sans text-[11px]">
— {s.note}
</span>
)}
</div>
))}
</div>
<p className="text-xs text-amber-600 dark:text-amber-400 pt-1">
{t("scopes.publishReminder")}
</p>
</div>
)}
</div>
);
}