diff --git a/packages/web-ui/src/api.ts b/packages/web-ui/src/api.ts index 140df639..13e9e5fa 100644 --- a/packages/web-ui/src/api.ts +++ b/packages/web-ui/src/api.ts @@ -1351,21 +1351,6 @@ export const api = { getRemote: () => request('/settings/remote'), enableRemote: () => request<{ ok: boolean; status: RemoteStatus }>('/settings/remote/enable', { method: 'POST' }), disableRemote: () => request<{ ok: boolean }>('/settings/remote/disable', { method: 'POST' }), - getFeishuIntegration: () => request<{ - appId?: string; appSecret?: string; verifyToken?: string; encryptKey?: string; - webhookPath?: string; enabled: boolean; connected: boolean; - notifyOnApproval: boolean; notifyOnNotification: boolean; notifyPriority: string[]; - }>('/settings/integrations/feishu'), - saveFeishuIntegration: (config: { - appId: string; appSecret: string; verifyToken?: string; encryptKey?: string; - webhookPath?: string; enabled?: boolean; - notifyOnApproval?: boolean; notifyOnNotification?: boolean; notifyPriority?: string[]; - }) => request<{ - appId?: string; connected: boolean; enabled: boolean; - }>('/settings/integrations/feishu', { method: 'POST', body: JSON.stringify(config) }), - testFeishuConnection: (creds: { appId: string; appSecret: string }) => - request<{ success: boolean; message?: string }>('/settings/integrations/feishu/test', { method: 'POST', body: JSON.stringify(creds) }), - deleteFeishuIntegration: () => request<{ ok: boolean }>('/settings/integrations/feishu', { method: 'DELETE' }), }, modelCatalog: { getByProvider: (provider: string) => request<{ provider: string; models: CatalogModel[] }>(`/models/catalog/${provider}`), diff --git a/packages/web-ui/src/components/FeishuIntegrationSection.tsx b/packages/web-ui/src/components/FeishuIntegrationSection.tsx deleted file mode 100644 index 2abdec58..00000000 --- a/packages/web-ui/src/components/FeishuIntegrationSection.tsx +++ /dev/null @@ -1,515 +0,0 @@ -import { useEffect, useState, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { api } from '../api.ts'; - -export interface FeishuConfig { - appId: string; - appSecret: string; - verifyToken?: string; - encryptKey?: string; - webhookPath?: string; - enabled: boolean; - connected: boolean; - notifyOnApproval: boolean; - notifyOnNotification: boolean; - notifyPriority: string[]; -} - -const DEFAULT_CONFIG: FeishuConfig = { - appId: '', - appSecret: '', - verifyToken: '', - encryptKey: '', - webhookPath: '/webhook/feishu', - enabled: false, - connected: false, - notifyOnApproval: true, - notifyOnNotification: false, - notifyPriority: ['high', 'urgent'], -}; - -const PRIORITY_OPTIONS = [ - { value: 'low', color: 'bg-gray-400' }, - { value: 'medium', color: 'bg-blue-400' }, - { value: 'high', color: 'bg-amber-400' }, - { value: 'urgent', color: 'bg-red-400' }, -] as const; - -function StatusBadge({ connected }: { connected: boolean }) { - return ( - - - {connected ? 'Connected' : 'Disconnected'} - - ); -} - -function Section({ title, children }: { title: string; children: React.ReactNode }) { - return ( -
-

{title}

- {children} -
- ); -} - -function Msg({ type, text }: { type: 'ok' | 'err'; text: string }) { - return ( -
- {type === 'ok' ? ( - - ) : ( - - )} - {text} -
- ); -} - -export function FeishuIntegrationSection() { - const { t } = useTranslation(['settings', 'common']); - - const [config, setConfig] = useState(DEFAULT_CONFIG); - const [dirty, setDirty] = useState(false); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [testing, setTesting] = useState(false); - const [msg, setMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null); - - // Show/hide secret values - const [showAppSecret, setShowAppSecret] = useState(false); - const [showEncryptKey, setShowEncryptKey] = useState(false); - - const loadConfig = useCallback(async () => { - setLoading(true); - try { - const data = await api.settings.getFeishuIntegration(); - if (data) { - setConfig({ - appId: data.appId ?? '', - appSecret: data.appSecret ?? '', - verifyToken: data.verifyToken ?? '', - encryptKey: data.encryptKey ?? '', - webhookPath: data.webhookPath ?? '/webhook/feishu', - enabled: data.enabled ?? false, - connected: data.connected ?? false, - notifyOnApproval: data.notifyOnApproval ?? true, - notifyOnNotification: data.notifyOnNotification ?? false, - notifyPriority: data.notifyPriority ?? ['high', 'urgent'], - }); - } - } catch { - // Not configured yet — use defaults - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { loadConfig(); }, [loadConfig]); - - const updateField = (key: K, value: FeishuConfig[K]) => { - setConfig(prev => ({ ...prev, [key]: value })); - setDirty(true); - setMsg(null); - }; - - const togglePriority = (priority: string) => { - setConfig(prev => { - const current = prev.notifyPriority; - const next = current.includes(priority) - ? current.filter(p => p !== priority) - : [...current, priority]; - return { ...prev, notifyPriority: next }; - }); - setDirty(true); - setMsg(null); - }; - - const handleSave = async () => { - setSaving(true); - setMsg(null); - try { - const result = await api.settings.saveFeishuIntegration({ - appId: config.appId, - appSecret: config.appSecret, - verifyToken: config.verifyToken || undefined, - encryptKey: config.encryptKey || undefined, - webhookPath: config.webhookPath || '/webhook/feishu', - enabled: config.enabled, - notifyOnApproval: config.notifyOnApproval, - notifyOnNotification: config.notifyOnNotification, - notifyPriority: config.notifyPriority, - }); - if (result.connected !== undefined) { - setConfig(prev => ({ ...prev, connected: result.connected })); - } - setDirty(false); - setMsg({ type: 'ok', text: t('settings:feishu.saved', { defaultValue: 'Feishu configuration saved' }) }); - } catch (err) { - setMsg({ type: 'err', text: String(err instanceof Error ? err.message : err) }); - } finally { - setSaving(false); - } - }; - - const handleTest = async () => { - if (!config.appId || !config.appSecret) { - setMsg({ type: 'err', text: t('settings:feishu.fillRequired', { defaultValue: 'Please fill in App ID and App Secret first' }) }); - return; - } - setTesting(true); - setMsg(null); - try { - const result = await api.settings.testFeishuConnection({ - appId: config.appId, - appSecret: config.appSecret, - }); - if (result.success) { - setConfig(prev => ({ ...prev, connected: true })); - setMsg({ type: 'ok', text: result.message || t('settings:feishu.testSuccess', { defaultValue: 'Connection successful' }) }); - } else { - setMsg({ type: 'err', text: result.message || t('settings:feishu.testFailed', { defaultValue: 'Connection failed' }) }); - } - } catch (err) { - setMsg({ type: 'err', text: String(err instanceof Error ? err.message : err) }); - } finally { - setTesting(false); - } - }; - - const handleDisconnect = async () => { - setSaving(true); - setMsg(null); - try { - await api.settings.deleteFeishuIntegration(); - setConfig(DEFAULT_CONFIG); - setDirty(false); - setMsg({ type: 'ok', text: t('settings:feishu.disconnected', { defaultValue: 'Disconnected from Feishu' }) }); - } catch (err) { - setMsg({ type: 'err', text: String(err instanceof Error ? err.message : err) }); - } finally { - setSaving(false); - } - }; - - if (loading) { - return ( -
-
-
- {t('common:loading', { defaultValue: 'Loading...' })} -
-
- ); - } - - return ( -
- {/* Header — Connection status + Feishu branding */} -
-
-
- - - -
-
-

飞书 (Feishu / Lark)

-

{t('settings:feishu.description', { defaultValue: 'Configure Feishu integration for notifications and approvals' })}

-
-
- -
- - {/* Connection Settings */} -
-
- {/* App ID */} -
- - updateField('appId', e.target.value)} - placeholder="cli_xxxxxxxxxxxxxx" - className="w-full px-3 py-2 text-sm bg-surface-primary border border-border-default rounded-lg text-fg-primary placeholder-fg-tertiary focus:outline-none focus:ring-2 focus:ring-brand-500/40 focus:border-brand-500 transition-colors font-mono" - /> -
- - {/* App Secret */} -
- -
- updateField('appSecret', e.target.value)} - placeholder="Enter your Feishu app secret" - className="w-full px-3 py-2 pr-10 text-sm bg-surface-primary border border-border-default rounded-lg text-fg-primary placeholder-fg-tertiary focus:outline-none focus:ring-2 focus:ring-brand-500/40 focus:border-brand-500 transition-colors font-mono" - /> - -
-
- - {/* Verify Token */} -
- - updateField('verifyToken', e.target.value)} - placeholder="Event verification token from Feishu" - className="w-full px-3 py-2 text-sm bg-surface-primary border border-border-default rounded-lg text-fg-primary placeholder-fg-tertiary focus:outline-none focus:ring-2 focus:ring-brand-500/40 focus:border-brand-500 transition-colors font-mono" - /> -
- - {/* Encrypt Key */} -
- -
- updateField('encryptKey', e.target.value)} - placeholder="AES encryption key for event decryption" - className="w-full px-3 py-2 pr-10 text-sm bg-surface-primary border border-border-default rounded-lg text-fg-primary placeholder-fg-tertiary focus:outline-none focus:ring-2 focus:ring-brand-500/40 focus:border-brand-500 transition-colors font-mono" - /> - -
-
- - {/* Webhook Path */} -
- - updateField('webhookPath', e.target.value)} - placeholder="/webhook/feishu" - className="w-full px-3 py-2 text-sm bg-surface-primary border border-border-default rounded-lg text-fg-primary placeholder-fg-tertiary focus:outline-none focus:ring-2 focus:ring-brand-500/40 focus:border-brand-500 transition-colors font-mono" - /> -

- {t('settings:feishu.webhookPathHint', { defaultValue: 'Set this path in your Feishu app event subscription' })} -

-
-
-
- - {/* Test & Save Actions */} -
-
- - - - - {config.connected && config.enabled && ( - - )} -
- - {msg && } -
- - {/* Enable / Disable Toggle */} -
-
-
-
-
- {t('settings:feishu.enableIntegration', { defaultValue: 'Enable Feishu Integration' })} -
-
- {t('settings:feishu.enableHint', { defaultValue: 'When enabled, Markus will connect to Feishu and start processing events' })} -
-
- -
-
-
- - {/* Notification Forwarding Settings */} -
-
- {/* Toggle switches */} -
- {/* Approval notifications */} -
-
-
{t('settings:feishu.forwardApprovals', { defaultValue: 'Forward Approval Requests' })}
-
{t('settings:feishu.forwardApprovalsHint', { defaultValue: 'Send approval requests to Feishu as interactive cards' })}
-
- -
- - {/* General notifications */} -
-
-
{t('settings:feishu.forwardNotifications', { defaultValue: 'Forward General Notifications' })}
-
{t('settings:feishu.forwardNotificationsHint', { defaultValue: 'Send system notifications to Feishu chat' })}
-
- -
-
- - {/* Priority filter */} - {(config.notifyOnApproval || config.notifyOnNotification) && ( -
- -
- {PRIORITY_OPTIONS.map(({ value, color }) => { - const selected = config.notifyPriority.includes(value); - return ( - - ); - })} -
-

- {t('settings:feishu.priorityHint', { defaultValue: 'Only notifications with the selected priority levels will be forwarded' })} -

-
- )} -
-
- - {/* Setup Guide */} -
-
-

- {t('settings:feishu.setupGuideDesc', { defaultValue: 'Follow these steps to set up the Feishu integration:' })} -

-
    -
  1. {t('settings:feishu.guideStep1', { defaultValue: 'Go to Feishu Open Platform (open.feishu.cn) and create a new app' })}
  2. -
  3. {t('settings:feishu.guideStep2', { defaultValue: 'Enable bot capabilities and configure event subscriptions' })}
  4. -
  5. {t('settings:feishu.guideStep3', { defaultValue: 'Copy the App ID and App Secret into the fields above' })}
  6. -
  7. {t('settings:feishu.guideStep4', { defaultValue: 'Set the webhook URL in your Feishu app event subscription' })}
  8. -
  9. {t('settings:feishu.guideStep5', { defaultValue: 'Click "Test Connection" to verify the setup' })}
  10. -
-
-
-
- ); -} diff --git a/packages/web-ui/src/locales/en/settings.json b/packages/web-ui/src/locales/en/settings.json index b6a2b936..3b1a81e8 100644 --- a/packages/web-ui/src/locales/en/settings.json +++ b/packages/web-ui/src/locales/en/settings.json @@ -11,8 +11,7 @@ "remote": "Remote Access", "license": "License", "organization": "Organization", - "account": "Organization & License", - "integrations": "Integrations" + "account": "Organization & License" }, "appearance": { "title": "Appearance", @@ -601,39 +600,5 @@ "roleUpdated": "Role updated", "yourRole": "Your Role", "changeRole": "Change Role" - }, - "feishu": { - "description": "Configure Feishu integration for notifications and approvals", - "connectionSettings": "Connection Settings", - "verifyToken": "Verify Token", - "encryptKey": "Encrypt Key", - "optional": "optional", - "webhookPath": "Webhook Path", - "webhookPathHint": "Set this path in your Feishu app event subscription", - "actions": "Actions", - "testConnection": "Test Connection", - "integrationState": "Integration State", - "enableIntegration": "Enable Feishu Integration", - "enableHint": "When enabled, Markus will connect to Feishu and start processing events", - "notificationForwarding": "Notification Forwarding", - "forwardApprovals": "Forward Approval Requests", - "forwardApprovalsHint": "Send approval requests to Feishu as interactive cards", - "forwardNotifications": "Forward General Notifications", - "forwardNotificationsHint": "Send system notifications to Feishu chat", - "notifyPriority": "Minimum Notification Priority", - "priorityHint": "Only notifications with the selected priority levels will be forwarded", - "setupGuide": "Setup Guide", - "setupGuideDesc": "Follow these steps to set up the Feishu integration:", - "guideStep1": "Go to Feishu Open Platform (open.feishu.cn) and create a new app", - "guideStep2": "Enable bot capabilities and configure event subscriptions", - "guideStep3": "Copy the App ID and App Secret into the fields above", - "guideStep4": "Set the webhook URL in your Feishu app event subscription", - "guideStep5": "Click \"Test Connection\" to verify the setup", - "saved": "Feishu configuration saved", - "fillRequired": "Please fill in App ID and App Secret first", - "testSuccess": "Connection successful", - "testFailed": "Connection failed", - "disconnected": "Disconnected from Feishu", - "disconnect": "Disconnect" } } diff --git a/packages/web-ui/src/locales/zh-CN/settings.json b/packages/web-ui/src/locales/zh-CN/settings.json index d5f498e2..72d32b21 100644 --- a/packages/web-ui/src/locales/zh-CN/settings.json +++ b/packages/web-ui/src/locales/zh-CN/settings.json @@ -11,8 +11,7 @@ "remote": "远程访问", "license": "许可证", "organization": "组织", - "account": "组织与许可证", - "integrations": "集成" + "account": "组织与许可证" }, "appearance": { "title": "外观", @@ -601,39 +600,5 @@ "roleUpdated": "角色已更新", "yourRole": "你的角色", "changeRole": "更改角色" - }, - "feishu": { - "description": "配置飞书集成,用于接收通知和审批请求", - "connectionSettings": "连接设置", - "verifyToken": "验证令牌", - "encryptKey": "加密密钥", - "optional": "可选", - "webhookPath": "Webhook 路径", - "webhookPathHint": "在飞书应用的事件订阅中设置此路径", - "actions": "操作", - "testConnection": "测试连接", - "integrationState": "集成状态", - "enableIntegration": "启用飞书集成", - "enableHint": "启用后,Markus 将连接到飞书并开始处理事件", - "notificationForwarding": "通知转发", - "forwardApprovals": "转发审批请求", - "forwardApprovalsHint": "将审批请求以交互卡片形式发送到飞书", - "forwardNotifications": "转发一般通知", - "forwardNotificationsHint": "将系统通知转发到飞书聊天", - "notifyPriority": "最低通知优先级", - "priorityHint": "仅选择优先级级别及以上的通知会被转发", - "setupGuide": "设置指南", - "setupGuideDesc": "按以下步骤设置飞书集成:", - "guideStep1": "前往飞书开放平台 (open.feishu.cn) 创建新应用", - "guideStep2": "启用机器人能力并配置事件订阅", - "guideStep3": "将 App ID 和 App Secret 填入上方字段", - "guideStep4": "在飞书应用事件订阅中设置 Webhook URL", - "guideStep5": "点击「测试连接」验证设置", - "saved": "飞书配置已保存", - "fillRequired": "请先填写 App ID 和 App Secret", - "testSuccess": "连接成功", - "testFailed": "连接失败", - "disconnected": "已断开飞书连接", - "disconnect": "断开连接" } } diff --git a/packages/web-ui/src/pages/Settings.tsx b/packages/web-ui/src/pages/Settings.tsx index 006d8ea5..5155fa96 100644 --- a/packages/web-ui/src/pages/Settings.tsx +++ b/packages/web-ui/src/pages/Settings.tsx @@ -9,7 +9,6 @@ import { Avatar, AvatarUpload } from '../components/Avatar.tsx'; import { useIsMobile } from '../hooks/useIsMobile.ts'; import { BrowserTestPanel } from '../components/BrowserTestPanel.tsx'; import { ModelPicker } from '../components/ModelPicker.tsx'; -import { FeishuIntegrationSection } from '../components/FeishuIntegrationSection.tsx'; import { PROVIDER_OPTIONS } from '../constants/providers.ts'; interface ModelCost { input: number; output: number; cacheRead?: number; cacheWrite?: number } @@ -40,7 +39,7 @@ interface OllamaDetectResult { models?: Array<{ name: string; fullName: string; size?: number; modifiedAt?: string; parameterSize?: string; family?: string; quantization?: string }>; } -type SettingsTab = 'appearance' | 'providers' | 'execution' | 'browser' | 'search' | 'storage' | 'users' | 'organization' | 'account' | 'remote' | 'license' | 'integrations'; +type SettingsTab = 'appearance' | 'providers' | 'execution' | 'browser' | 'search' | 'storage' | 'users' | 'organization' | 'account' | 'remote' | 'license'; const SETTINGS_TABS: Array<{ id: SettingsTab; labelKey: string; adminOnly?: boolean }> = [ { id: 'appearance', labelKey: 'nav.appearance' }, @@ -51,7 +50,6 @@ const SETTINGS_TABS: Array<{ id: SettingsTab; labelKey: string; adminOnly?: bool { id: 'storage', labelKey: 'nav.storage', adminOnly: true }, { id: 'account', labelKey: 'nav.account' }, { id: 'remote', labelKey: 'nav.remote', adminOnly: true }, - { id: 'integrations', labelKey: 'nav.integrations', adminOnly: true }, ]; const LEGACY_TAB_ALIASES: Record = { users: 'account', organization: 'account', license: 'account' }; @@ -2676,8 +2674,6 @@ export function Settings({ theme, onThemeChange, authUser, onLogout, onUserUpdat {resolvedTab === 'remote' && } - {resolvedTab === 'integrations' && } - )}