mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 04:18:44 +00:00
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:
1 parent
23d0b5eb0b
commit
ba9b5a6be3
8 files changed
+114
-6
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user