feat: 添加MinIO文件系统支持并优化外部连接器功能

- 添加MinIO用户文件系统配置选项(BEAVER_MINIO_ROOT_USER等)
- 更新外部连接器配置结构,包括BASE_URL和认证令牌设置
- 改进connector provider支持更多类型(official, feishu_bot等)
- 实现Mistral模型推理模式支持reasoning_effort参数
- 增强外部连接器策略配置和运行时配置管理
- 添加connector bridge事件验证和安全保护机制
- 优化任务路由逻辑,区分simple_chat和new_task场景
- 更新初始技能工具提示配置,分离authoring admin功能
This commit is contained in:
2026-06-05 11:46:40 +08:00
parent 236ac19789
commit 2c5205b06e
120 changed files with 8321 additions and 1865 deletions

View File

@ -318,7 +318,7 @@ function renderPlainText(content: string): React.ReactNode[] {
href={part}
target="_blank"
rel="noreferrer noopener"
className="text-primary underline underline-offset-2 break-all"
className="inline-block min-h-11 max-w-full break-all py-2 text-primary underline underline-offset-2"
>
{part}
</a>
@ -461,7 +461,9 @@ export default function OutlookPage() {
if (!background) {
setStatusLoading(false);
}
if (!nextStatus.configured) {
if (nextStatus.configured) {
await loadOverview(options?.preserveOverview ?? background);
} else {
setOverview(null);
setOverviewLoading(false);
}
@ -647,6 +649,9 @@ export default function OutlookPage() {
};
const handleDisconnect = async () => {
if (!window.confirm(t('确定断开 Outlook 连接吗?已保存的连接凭据会被移除。', 'Disconnect Outlook? Saved connection credentials will be removed.'))) {
return;
}
setDisconnecting(true);
setError(null);
try {
@ -671,9 +676,7 @@ export default function OutlookPage() {
const refreshOverview = async () => {
await loadStatus(true, { preserveOverview: true });
if (activeView === 'settings' && isConfigured) {
await loadOverview(true);
} else if (activeView === 'inbox') {
if (activeView === 'inbox') {
await loadMailboxPage('inbox', inboxPage?.page.skip ?? 0);
} else if (activeView === 'sent') {
await loadMailboxPage('sent', sentPage?.page.skip ?? 0);
@ -684,14 +687,14 @@ export default function OutlookPage() {
return (
<div className="min-h-full">
<div className="mx-auto max-w-7xl space-y-6 p-6">
<div className="mx-auto max-w-7xl space-y-6 p-4 sm:p-6">
<section className="rounded-2xl border bg-card px-4 py-4 shadow-sm">
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
<div className="flex flex-wrap items-center gap-2 text-sm">
<div className="mr-2 flex items-center gap-2 text-lg font-semibold text-foreground">
<div className="flex min-w-0 flex-wrap items-center gap-2 text-sm">
<h1 className="mr-2 flex min-w-0 items-center gap-2 text-lg font-semibold text-foreground">
<Mail className="h-5 w-5" />
Outlook
</div>
</h1>
{statusPending ? (
<>
<Skeleton className="h-6 w-20 rounded-full" />
@ -710,9 +713,9 @@ export default function OutlookPage() {
{status?.mcp_registered ? t('MCP 已注册', 'MCP registered') : t('MCP 未注册', 'MCP not registered')}
</Badge>
<Badge variant="secondary">{status?.provider || 'ews'}</Badge>
<span className="text-muted-foreground">{t('邮箱', 'Mailbox')} {overview?.mailbox || status?.saved?.email || '-'}</span>
<span className="text-muted-foreground">{t('时区', 'Timezone')} {status?.saved?.default_timezone || overview?.timezone || form.default_timezone}</span>
<span className="text-muted-foreground">
<span className="min-w-0 break-all text-muted-foreground">{t('邮箱', 'Mailbox')} {overview?.mailbox || status?.saved?.email || '-'}</span>
<span className="min-w-0 break-all text-muted-foreground">{t('时区', 'Timezone')} {status?.saved?.default_timezone || overview?.timezone || form.default_timezone}</span>
<span className="min-w-0 break-words text-muted-foreground">
{t('最近刷新', 'Last refreshed')} {formatDateTime((overview?.meta?.last_overview_refresh_at || status?.meta?.last_overview_refresh_at) as string | undefined, locale)}
</span>
</>
@ -727,7 +730,7 @@ export default function OutlookPage() {
<TopStat label={t('日程', 'Calendar')} value={String(eventCount)} loading={overviewPending} />
</>
) : null}
<Button variant="outline" size="sm" onClick={() => void refreshOverview()}>
<Button variant="outline" size="sm" className="h-11" onClick={() => void refreshOverview()}>
<RefreshCw className={`mr-2 h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
{t('刷新', 'Refresh')}
</Button>
@ -740,7 +743,7 @@ export default function OutlookPage() {
<CardContent className="pt-6">
<div className="flex items-start gap-3 text-sm text-destructive">
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
<span>{error}</span>
<span className="min-w-0 flex-1 break-all">{error}</span>
</div>
</CardContent>
</Card>
@ -752,7 +755,7 @@ export default function OutlookPage() {
{overviewWarnings.map((warning, index) => (
<div key={`${warning}-${index}`} className="flex items-start gap-3">
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
<span>{warning}</span>
<span className="min-w-0 flex-1 break-all">{warning}</span>
</div>
))}
</CardContent>
@ -771,7 +774,7 @@ export default function OutlookPage() {
<TabsTrigger
key={view.id}
value={view.id}
className="h-auto rounded-xl border border-transparent px-4 py-3 data-[state=active]:border-border data-[state=active]:shadow-sm"
className="min-h-11 rounded-xl border border-transparent px-4 py-3 data-[state=active]:border-border data-[state=active]:shadow-sm"
>
<div className="flex w-full items-center justify-between gap-3">
<div className="flex items-center gap-3">
@ -872,53 +875,67 @@ export default function OutlookPage() {
</CardHeader>
<CardContent className="space-y-5 pt-6">
<div className="grid gap-4 md:grid-cols-2">
<Field label={t('邮箱地址', 'Email address')} required>
<Field id="outlook-email" label={t('邮箱地址', 'Email address')} required>
<Input
id="outlook-email"
className="h-11"
value={form.email}
onChange={(event) => updateField('email', event.target.value)}
placeholder="you@boardware.com"
/>
</Field>
<Field label={t('用户名', 'Username')}>
<Field id="outlook-username" label={t('用户名', 'Username')}>
<Input
id="outlook-username"
className="h-11"
value={form.username}
onChange={(event) => updateField('username', event.target.value)}
placeholder={t('留空时默认取邮箱前缀', 'Leave blank to default to the email prefix')}
/>
</Field>
<Field label={t('密码', 'Password')} required>
<Field id="outlook-password" label={t('密码', 'Password')} required>
<Input
id="outlook-password"
className="h-11"
type="password"
value={form.password}
onChange={(event) => updateField('password', event.target.value)}
placeholder={t('请输入邮箱密码', 'Enter the mailbox password')}
/>
</Field>
<Field label={t('域', 'Domain')}>
<Field id="outlook-domain" label={t('域', 'Domain')}>
<Input
id="outlook-domain"
className="h-11"
value={form.domain}
onChange={(event) => updateField('domain', event.target.value)}
placeholder="boardware.com.mo"
/>
</Field>
<Field label="EWS URL">
<Field id="outlook-service-endpoint" label="EWS URL">
<Input
id="outlook-service-endpoint"
className="h-11"
value={form.service_endpoint}
onChange={(event) => updateField('service_endpoint', event.target.value)}
placeholder="https://mail.boardware.com.mo/EWS/Exchange.asmx"
disabled={form.autodiscover}
/>
</Field>
<Field label="Server Host">
<Field id="outlook-server" label="Server Host">
<Input
id="outlook-server"
className="h-11"
value={form.server}
onChange={(event) => updateField('server', event.target.value)}
placeholder="mail.boardware.com.mo"
disabled={form.autodiscover}
/>
</Field>
<Field label={t('时区', 'Timezone')}>
<Field id="outlook-timezone" label={t('时区', 'Timezone')}>
<Input
id="outlook-timezone"
className="h-11"
value={form.default_timezone}
onChange={(event) => updateField('default_timezone', event.target.value)}
placeholder="Asia/Shanghai"
@ -944,16 +961,17 @@ export default function OutlookPage() {
</div>
<div className="flex flex-wrap justify-end gap-2">
<Button variant="outline" onClick={handleTest} disabled={!canTest || testing}>
<Button variant="outline" className="h-11" onClick={handleTest} disabled={!canTest || testing}>
{testing ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <CheckCircle2 className="mr-2 h-4 w-4" />}
{t('测试连接', 'Test connection')}
</Button>
<Button onClick={handleConnect} disabled={!canTest || saving}>
<Button className="h-11" onClick={handleConnect} disabled={!canTest || saving}>
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Save className="mr-2 h-4 w-4" />}
{t('保存并启用', 'Save and enable')}
</Button>
<Button
variant="outline"
className="h-11"
onClick={handleDisconnect}
disabled={!status?.configured || disconnecting}
>
@ -966,8 +984,8 @@ export default function OutlookPage() {
<div className="rounded-3xl border bg-muted/30 p-4 text-sm">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="default">{t('测试成功', 'Test succeeded')}</Badge>
<span className="text-muted-foreground">{testResult.mailbox}</span>
<span className="text-muted-foreground">{t('用户名', 'Username')}: {testResult.resolved_username}</span>
<span className="break-all text-muted-foreground">{testResult.mailbox}</span>
<span className="break-all text-muted-foreground">{t('用户名', 'Username')}: {testResult.resolved_username}</span>
</div>
<div className="mt-3 grid gap-3 md:grid-cols-3">
<MiniStat label={t('检测到文件夹', 'Detected folders')} value={String(testResult.sample.folders.length)} />
@ -979,7 +997,7 @@ export default function OutlookPage() {
{testWarnings.map((warning, index) => (
<div key={`${warning}-${index}`} className="flex items-start gap-2">
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
<span>{warning}</span>
<span className="min-w-0 flex-1 break-all">{warning}</span>
</div>
))}
</div>
@ -1054,10 +1072,10 @@ export default function OutlookPage() {
</Tabs>
<Dialog open={Boolean(selectedMessageRef)} onOpenChange={(open) => !open && setSelectedMessageRef(null)}>
<DialogContent className="sm:max-w-5xl">
<DialogContent className="bottom-4 left-4 right-4 top-4 max-h-none w-auto max-w-none translate-x-0 translate-y-0 overflow-y-auto data-[state=open]:slide-in-from-left-0 data-[state=open]:slide-in-from-top-0 data-[state=closed]:slide-out-to-left-0 data-[state=closed]:slide-out-to-top-0 sm:bottom-auto sm:left-[50%] sm:right-auto sm:top-[50%] sm:max-h-[calc(100dvh-2rem)] sm:w-[calc(100vw-2rem)] sm:max-w-5xl sm:translate-x-[-50%] sm:translate-y-[-50%] sm:data-[state=open]:slide-in-from-left-1/2 sm:data-[state=open]:slide-in-from-top-[48%] sm:data-[state=closed]:slide-out-to-left-1/2 sm:data-[state=closed]:slide-out-to-top-[48%]">
<DialogHeader>
<DialogTitle>{selectedMessage?.subject || t('邮件详情', 'Message details')}</DialogTitle>
<DialogDescription>
<DialogTitle className="break-words pr-8 leading-6">{selectedMessage?.subject || t('邮件详情', 'Message details')}</DialogTitle>
<DialogDescription className="break-words">
{selectedMessage?.receivedDateTime ? formatDateTime(selectedMessage.receivedDateTime, locale) : t('正在加载', 'Loading')}
</DialogDescription>
</DialogHeader>
@ -1066,7 +1084,7 @@ export default function OutlookPage() {
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : selectedMessage ? (
<div className="grid gap-4 lg:grid-cols-[280px,1fr]">
<div className="grid min-w-0 gap-4 lg:grid-cols-[280px,1fr]">
<div className="space-y-4 rounded-2xl border bg-muted/20 p-4 text-sm">
<InfoRow label={t('发件人', 'From')} value={mailboxLabel(selectedMessage.from)} />
<InfoRow
@ -1085,7 +1103,7 @@ export default function OutlookPage() {
</div>
</div>
<div className="overflow-hidden rounded-2xl border bg-background">
<div className="min-w-0 overflow-hidden rounded-2xl border bg-background">
<div className="border-b px-4 py-3 text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">
{t('正文', 'Body')}
</div>
@ -1115,10 +1133,10 @@ export default function OutlookPage() {
</Dialog>
<Dialog open={Boolean(selectedEvent)} onOpenChange={(open) => !open && setSelectedEvent(null)}>
<DialogContent className="sm:max-w-2xl">
<DialogContent className="bottom-4 left-4 right-4 top-4 max-h-none w-auto max-w-none translate-x-0 translate-y-0 overflow-y-auto data-[state=open]:slide-in-from-left-0 data-[state=open]:slide-in-from-top-0 data-[state=closed]:slide-out-to-left-0 data-[state=closed]:slide-out-to-top-0 sm:bottom-auto sm:left-[50%] sm:right-auto sm:top-[50%] sm:max-h-[calc(100dvh-2rem)] sm:w-[calc(100vw-2rem)] sm:max-w-2xl sm:translate-x-[-50%] sm:translate-y-[-50%] sm:data-[state=open]:slide-in-from-left-1/2 sm:data-[state=open]:slide-in-from-top-[48%] sm:data-[state=closed]:slide-out-to-left-1/2 sm:data-[state=closed]:slide-out-to-top-[48%]">
<DialogHeader>
<DialogTitle>{selectedEvent?.subject || t('日程详情', 'Event details')}</DialogTitle>
<DialogDescription>
<DialogTitle className="break-words pr-8 leading-6">{selectedEvent?.subject || t('日程详情', 'Event details')}</DialogTitle>
<DialogDescription className="break-words">
{selectedEvent
? `${formatDateTime(selectedEvent.start?.dateTime, locale)} - ${formatDateTime(selectedEvent.end?.dateTime, locale)}`
: t('日程详情', 'Event details')}
@ -1135,7 +1153,7 @@ export default function OutlookPage() {
<Separator />
<div className="space-y-2">
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">{t('说明', 'Notes')}</p>
<div className="rounded-lg border bg-muted/40 p-3 whitespace-pre-wrap">
<div className="rounded-lg border bg-muted/40 p-3 whitespace-pre-wrap break-words">
{selectedEvent.bodyPreview || t('没有更多说明。', 'No additional notes.')}
</div>
</div>
@ -1149,17 +1167,19 @@ export default function OutlookPage() {
}
function Field({
id,
label,
required = false,
children,
}: {
id: string;
label: string;
required?: boolean;
children: React.ReactNode;
}) {
return (
<div className="space-y-2">
<Label className="text-sm font-medium">
<Label htmlFor={id} className="text-sm font-medium">
{label}
{required ? <span className="ml-1 text-destructive">*</span> : null}
</Label>
@ -1235,25 +1255,26 @@ function MessageCard({
const pageLabel = page ? t(`${currentPage} 页 · 本页 ${page.returned}`, `Page ${currentPage} · ${page.returned} messages`) : t('正在读取邮件…', 'Loading messages...');
return (
<Card className="rounded-[28px] shadow-sm">
<CardHeader className="flex flex-row items-center justify-between gap-4 border-b pb-5">
<div className="space-y-1">
<CardTitle className="flex items-center gap-2 text-base">
<Card className="min-w-0 rounded-[28px] shadow-sm">
<CardHeader className="flex flex-col gap-4 border-b pb-5 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0 space-y-1">
<CardTitle className="flex items-center gap-2 break-words text-base">
{icon}
{title}
</CardTitle>
<p className="text-sm text-muted-foreground">{loading ? t('正在读取邮件…', 'Loading messages...') : pageLabel}</p>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" onClick={onRefresh} disabled={refreshing}>
<div className="flex flex-wrap items-center gap-2">
<Button variant="ghost" size="sm" className="h-11 w-11 p-0" aria-label={t('刷新邮件', 'Refresh mail')} title={t('刷新邮件', 'Refresh mail')} onClick={onRefresh} disabled={refreshing}>
<RefreshCw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
</Button>
<Button variant="outline" size="sm" onClick={onPreviousPage} disabled={!page || page.skip === 0 || refreshing}>
<Button variant="outline" size="sm" className="h-11" onClick={onPreviousPage} disabled={!page || page.skip === 0 || refreshing}>
{t('上一页', 'Previous')}
</Button>
<Button
variant="outline"
size="sm"
className="h-11"
onClick={onNextPage}
disabled={!page || !page.has_more || refreshing}
>
@ -1284,17 +1305,17 @@ function MessageCard({
key={item.id || `${item.subject}-${item.receivedDateTime}`}
type="button"
onClick={() => item.id && onOpen(item)}
className="w-full rounded-2xl border bg-card p-4 text-left transition-colors hover:bg-muted/40"
className="min-h-11 w-full rounded-2xl border bg-card p-4 text-left transition-colors hover:bg-muted/40"
>
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-foreground">{item.subject || t('(无主题)', '(No subject)')}</p>
<p className="mt-1 truncate text-xs text-muted-foreground">{mailboxLabel(item.from)}</p>
<p className="mt-3 line-clamp-2 text-sm leading-6 text-muted-foreground">
<p className="break-words font-medium text-foreground">{item.subject || t('(无主题)', '(No subject)')}</p>
<p className="mt-1 break-all text-xs text-muted-foreground">{mailboxLabel(item.from)}</p>
<p className="mt-3 line-clamp-2 break-words text-sm leading-6 text-muted-foreground">
{item.bodyPreview || t('没有预览内容。', 'No preview available.')}
</p>
</div>
<div className="flex shrink-0 items-center gap-2 lg:flex-col lg:items-end">
<div className="flex shrink-0 flex-wrap items-center gap-2 lg:flex-col lg:items-end">
<Badge variant={item.isRead ? 'secondary' : 'default'}>
{item.isRead ? t('已读', 'Read') : t('未读', 'Unread')}
</Badge>
@ -1357,10 +1378,10 @@ function EventCard({
});
return (
<Card className="rounded-[28px] shadow-sm">
<CardHeader className="flex flex-row items-center justify-between space-y-0 border-b pb-5">
<div className="space-y-1">
<CardTitle className="flex items-center gap-2 text-base">
<Card className="min-w-0 rounded-[28px] shadow-sm">
<CardHeader className="flex flex-col gap-4 space-y-0 border-b pb-5 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0 space-y-1">
<CardTitle className="flex items-center gap-2 break-words text-base">
<CalendarDays className="h-4 w-4" />
{t('日程安排', 'Schedule')}
</CardTitle>
@ -1368,17 +1389,17 @@ function EventCard({
{formatDayLabel(weekDays[0], locale)} - {formatDayLabel(weekDays[weekDays.length - 1], locale)}
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={onPreviousWeek} disabled={refreshing}>
<div className="flex flex-wrap items-center gap-2">
<Button variant="outline" size="sm" className="h-11" onClick={onPreviousWeek} disabled={refreshing}>
{t('上一周', 'Previous week')}
</Button>
<Button variant="outline" size="sm" onClick={onCurrentWeek} disabled={refreshing}>
<Button variant="outline" size="sm" className="h-11" onClick={onCurrentWeek} disabled={refreshing}>
{t('本周', 'This week')}
</Button>
<Button variant="outline" size="sm" onClick={onNextWeek} disabled={refreshing}>
<Button variant="outline" size="sm" className="h-11" onClick={onNextWeek} disabled={refreshing}>
{t('下一周', 'Next week')}
</Button>
<Button variant="ghost" size="sm" onClick={onRefresh} disabled={refreshing}>
<Button variant="ghost" size="sm" className="h-11 w-11 p-0" aria-label={t('刷新日程', 'Refresh calendar')} title={t('刷新日程', 'Refresh calendar')} onClick={onRefresh} disabled={refreshing}>
<RefreshCw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
</Button>
</div>
@ -1397,7 +1418,7 @@ function EventCard({
) : (
<div className="grid gap-3 lg:grid-cols-2 2xl:grid-cols-3">
{eventsByDay.map((day) => (
<div key={day.key} className="rounded-2xl border bg-card p-4">
<div key={day.key} className="min-w-0 rounded-2xl border bg-card p-4">
<div className="flex items-center justify-between gap-3">
<div>
<p className="font-medium text-foreground">{day.label}</p>
@ -1413,13 +1434,13 @@ function EventCard({
key={item.id || `${item.subject}-${item.start?.dateTime}`}
type="button"
onClick={() => onOpen(item)}
className="w-full rounded-xl border bg-background p-3 text-left transition-colors hover:bg-muted/40"
className="min-h-11 w-full rounded-xl border bg-background p-3 text-left transition-colors hover:bg-muted/40"
>
<p className="font-medium text-foreground">{item.subject || t('(无主题)', '(No subject)')}</p>
<p className="break-words font-medium text-foreground">{item.subject || t('(无主题)', '(No subject)')}</p>
<p className="mt-1 text-xs text-muted-foreground">
{formatTime(item.start?.dateTime, locale)} - {formatTime(item.end?.dateTime, locale)}
</p>
<p className="mt-2 text-sm text-muted-foreground">
<p className="mt-2 break-words text-sm text-muted-foreground">
{item.location?.displayName || t('未设置地点', 'No location set')}
</p>
</button>