Merge Feishu IM setup guidance into local main

Bring the desktop Feishu onboarding prompt from the worktree onto local main while preserving the existing local Web UI startup commit.

Constraint: Local main already had an independent commit after the worktree base, so fast-forward merge was not possible.
Confidence: high
Scope-risk: narrow
Directive: Keep future Feishu setup help concise in the settings page and leave detailed platform instructions in docs.
Tested: merge completed without conflicts
Tested: cd desktop && bunx vitest run src/pages/AdapterSettings.test.tsx src/i18n/index.test.tsx
Not-tested: Full post-merge check:desktop was not rerun; it had passed in the source worktree before merge.
This commit is contained in:
程序员阿江(Relakkes) 2026-05-19 00:08:47 +08:00
commit 82609f6db6
4 changed files with 93 additions and 0 deletions

View File

@ -376,6 +376,11 @@ export const en = {
'settings.adapters.appIdPlaceholder': 'e.g. cli_xxx',
'settings.adapters.appSecret': 'App Secret',
'settings.adapters.appSecretPlaceholder': 'From Feishu Open Platform',
'settings.adapters.feishuCreateBotTitle': 'Need a Feishu bot?',
'settings.adapters.feishuCreateBotDesc': 'Use the documented OpenClaw template to create a Feishu bot with the required permissions already configured. Then paste the App ID and App Secret below.',
'settings.adapters.feishuCreateBotAction': 'Create Feishu bot',
'settings.adapters.feishuCreateBotStepCreate': 'Create the bot from the template.',
'settings.adapters.feishuCreateBotStepFill': 'Copy its App ID and App Secret, then fill them in here.',
'settings.adapters.encryptKey': 'Encrypt Key',
'settings.adapters.encryptKeyPlaceholder': 'Optional',
'settings.adapters.verificationToken': 'Verification Token',

View File

@ -378,6 +378,11 @@ export const zh: Record<TranslationKey, string> = {
'settings.adapters.appIdPlaceholder': '如 cli_xxx',
'settings.adapters.appSecret': 'App Secret',
'settings.adapters.appSecretPlaceholder': '从飞书开放平台获取',
'settings.adapters.feishuCreateBotTitle': '还没有飞书机器人?',
'settings.adapters.feishuCreateBotDesc': '使用文档中的官方 OpenClaw 模板,一键创建已预配权限的飞书机器人。创建后把 App ID 和 App Secret 填到下方。',
'settings.adapters.feishuCreateBotAction': '一键创建飞书机器人',
'settings.adapters.feishuCreateBotStepCreate': '点击模板创建机器人。',
'settings.adapters.feishuCreateBotStepFill': '复制 App ID 和 App Secret回到这里填写。',
'settings.adapters.encryptKey': 'Encrypt Key',
'settings.adapters.encryptKeyPlaceholder': '可选',
'settings.adapters.verificationToken': 'Verification Token',

View File

@ -0,0 +1,54 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { cleanup, render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
import { AdapterSettings } from './AdapterSettings'
import { useAdapterStore } from '../stores/adapterStore'
import { useSettingsStore } from '../stores/settingsStore'
import type { AdapterFileConfig } from '../types/adapter'
const FEISHU_CREATE_BOT_URL = 'https://open.feishu.cn/page/openclaw?form=multiAgent'
function renderAdapterSettings(config: AdapterFileConfig) {
useSettingsStore.setState({ locale: 'en' })
useAdapterStore.setState({
config,
isLoading: false,
fetchConfig: vi.fn(async () => {}),
} as Partial<ReturnType<typeof useAdapterStore.getState>>)
render(<AdapterSettings />)
}
afterEach(() => {
cleanup()
useAdapterStore.setState(useAdapterStore.getInitialState(), true)
useSettingsStore.setState(useSettingsStore.getInitialState(), true)
})
describe('AdapterSettings Feishu onboarding', () => {
it('shows the documented one-click Feishu bot link before credentials are configured', () => {
renderAdapterSettings({})
expect(screen.getByText('Need a Feishu bot?')).toBeInTheDocument()
expect(screen.getByText(/OpenClaw template/)).toBeInTheDocument()
expect(screen.getByText('1. Create the bot from the template.')).toBeInTheDocument()
expect(screen.getByText('2. Copy its App ID and App Secret, then fill them in here.')).toBeInTheDocument()
expect(screen.getByRole('link', { name: /create feishu bot/i })).toHaveAttribute(
'href',
FEISHU_CREATE_BOT_URL,
)
})
it('hides the one-click Feishu bot prompt once saved credentials exist', () => {
renderAdapterSettings({
feishu: {
appId: 'cli_existing',
appSecret: '****cret',
},
})
expect(screen.queryByRole('link', { name: /create feishu bot/i })).not.toBeInTheDocument()
expect(screen.queryByText('Need a Feishu bot?')).not.toBeInTheDocument()
})
})

View File

@ -10,6 +10,8 @@ import QRCode from 'qrcode'
type ImTab = 'feishu' | 'wechat' | 'dingtalk' | 'telegram'
type ImPlatform = 'telegram' | 'feishu' | 'wechat' | 'dingtalk'
const FEISHU_CREATE_BOT_URL = 'https://open.feishu.cn/page/openclaw?form=multiAgent'
export function AdapterSettings() {
const t = useTranslation()
const {
@ -375,6 +377,7 @@ export function AdapterSettings() {
const pairingExpiry = config.pairing?.expiresAt
const isPairingActive = pairingExpiry ? Date.now() < pairingExpiry : false
const minutesLeft = pairingExpiry ? Math.max(0, Math.ceil((pairingExpiry - Date.now()) / 60000)) : 0
const hasSavedFeishuCredentials = Boolean(config.feishu?.appId && config.feishu?.appSecret)
if (isLoading) {
return (
@ -505,6 +508,32 @@ export function AdapterSettings() {
{activeIm === 'feishu' && (
<div className="p-4 space-y-4">
{!hasSavedFeishuCredentials && (
<div className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface)] p-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="flex min-w-0 gap-3">
<span className="material-symbols-outlined mt-0.5 text-[20px] text-[var(--color-brand)]">smart_toy</span>
<div className="min-w-0">
<h4 className="text-sm font-semibold text-[var(--color-text-primary)]">{t('settings.adapters.feishuCreateBotTitle')}</h4>
<p className="mt-1 text-xs leading-5 text-[var(--color-text-tertiary)]">{t('settings.adapters.feishuCreateBotDesc')}</p>
<ol className="mt-2 space-y-1 text-xs leading-5 text-[var(--color-text-secondary)]">
<li>1. {t('settings.adapters.feishuCreateBotStepCreate')}</li>
<li>2. {t('settings.adapters.feishuCreateBotStepFill')}</li>
</ol>
</div>
</div>
<a
href={FEISHU_CREATE_BOT_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex h-9 shrink-0 items-center justify-center gap-1.5 rounded-[var(--radius-md)] bg-[image:var(--gradient-btn-primary)] px-3 text-xs font-medium text-[var(--color-btn-primary-fg)] shadow-[var(--shadow-button-primary)] transition-colors hover:bg-[image:var(--gradient-btn-primary-hover)] hover:brightness-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-surface)]"
>
{t('settings.adapters.feishuCreateBotAction')}
<span className="material-symbols-outlined text-[14px]">open_in_new</span>
</a>
</div>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<Input
label={t('settings.adapters.appId')}