feat: 添加swarms团队编排功能并优化agent委派系统
- 引入AgentTeamOrchestrator支持多agent协同任务执行 - 增加第三方swarms库依赖并配置git协议替换以改善包管理 - 扩展DelegationManager支持团队任务调度和进度跟踪 - 实现中文bigram分词算法提升中文任务检索准确性 - 调整A2AClient和DelegationManager超时时间从30秒增至600秒 - 优化AgentRunResult状态判断逻辑增加有意义摘要检测 - 修改Dockerfile配置npm仓库镜像地址和git协议映射 - 更新CLI命令行接口支持网关端口配置传递 - 调整提供者超时配置机制增强请求稳定性 - 移除过时的support_group字段简化agent描述符结构 - 增强错误处理和进度事件报告机制改进用户体验
This commit is contained in:
@ -54,6 +54,9 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import type { AppLocale } from '@/lib/i18n/core';
|
||||
import { pickAppText } from '@/lib/i18n/core';
|
||||
import { useAppI18n } from '@/lib/i18n/provider';
|
||||
|
||||
type OutlookFormState = OutlookConnectionPayload;
|
||||
type OutlookView = 'inbox' | 'sent' | 'calendar' | 'settings';
|
||||
@ -88,11 +91,11 @@ function toFormState(status: OutlookStatus | null): OutlookFormState {
|
||||
};
|
||||
}
|
||||
|
||||
function formatDateTime(value?: string | null): string {
|
||||
function formatDateTime(value?: string | null, locale: AppLocale = 'zh-CN'): string {
|
||||
if (!value) return '-';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
@ -115,19 +118,19 @@ function formatDateKey(value?: string | null): string | null {
|
||||
return toLocalDateKey(date);
|
||||
}
|
||||
|
||||
function formatDayLabel(value: Date): string {
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
function formatDayLabel(value: Date, locale: AppLocale = 'zh-CN'): string {
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
weekday: 'short',
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
function formatTime(value?: string | null): string {
|
||||
function formatTime(value?: string | null, locale: AppLocale = 'zh-CN'): string {
|
||||
if (!value) return '-';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(date);
|
||||
@ -241,9 +244,9 @@ function sanitizeEmailHtml(html: string): string {
|
||||
return documentRef.body.innerHTML;
|
||||
}
|
||||
|
||||
function buildEmailPreviewDocument(html: string): string {
|
||||
function buildEmailPreviewDocument(html: string, locale: AppLocale = 'zh-CN'): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<html lang="${locale}">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
@ -334,6 +337,8 @@ function renderPlainText(content: string): React.ReactNode[] {
|
||||
}
|
||||
|
||||
export default function OutlookPage() {
|
||||
const { locale } = useAppI18n();
|
||||
const t = (zh: string, en: string) => pickAppText(locale, zh, en);
|
||||
const [status, setStatus] = useState<OutlookStatus | null>(null);
|
||||
const [form, setForm] = useState<OutlookFormState>(EMPTY_FORM);
|
||||
const [formDirty, setFormDirty] = useState(false);
|
||||
@ -382,11 +387,11 @@ export default function OutlookPage() {
|
||||
if (!preserveExisting) {
|
||||
setOverview(null);
|
||||
}
|
||||
setError(err.message || '加载 Outlook 概览失败');
|
||||
setError(err.message || t('加载 Outlook 概览失败', 'Failed to load the Outlook overview'));
|
||||
} finally {
|
||||
setOverviewLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
const loadMailboxPage = useCallback(async (view: OutlookMailboxView, skip = 0) => {
|
||||
setMailboxLoading((current) => ({ ...current, [view]: true }));
|
||||
@ -402,11 +407,17 @@ export default function OutlookPage() {
|
||||
}
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
setError(err.message || `加载${view === 'inbox' ? '收件箱' : '发件箱'}失败`);
|
||||
setError(
|
||||
err.message
|
||||
|| t(
|
||||
`加载${view === 'inbox' ? '收件箱' : '发件箱'}失败`,
|
||||
`Failed to load the ${view === 'inbox' ? 'inbox' : 'sent mailbox'}`
|
||||
)
|
||||
);
|
||||
} finally {
|
||||
setMailboxLoading((current) => ({ ...current, [view]: false }));
|
||||
}
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
const loadCalendarPage = useCallback(async (anchorKey: string) => {
|
||||
setCalendarLoading(true);
|
||||
@ -420,11 +431,11 @@ export default function OutlookPage() {
|
||||
setCalendarPage(nextPage);
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
setError(err.message || '加载日程失败');
|
||||
setError(err.message || t('加载日程失败', 'Failed to load calendar events'));
|
||||
} finally {
|
||||
setCalendarLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
const loadStatus = useCallback(async (
|
||||
background = false,
|
||||
@ -452,7 +463,7 @@ export default function OutlookPage() {
|
||||
setOverviewLoading(false);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || '加载 Outlook 集成状态失败');
|
||||
setError(err.message || t('加载 Outlook 集成状态失败', 'Failed to load Outlook integration status'));
|
||||
setOverviewLoading(false);
|
||||
} finally {
|
||||
if (background) {
|
||||
@ -461,7 +472,7 @@ export default function OutlookPage() {
|
||||
setStatusLoading(false);
|
||||
}
|
||||
}
|
||||
}, [applyStatus, loadOverview]);
|
||||
}, [applyStatus, loadOverview, t]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadStatus();
|
||||
@ -483,7 +494,7 @@ export default function OutlookPage() {
|
||||
})
|
||||
.catch((err: any) => {
|
||||
if (!cancelled) {
|
||||
setError(err.message || '加载邮件详情失败');
|
||||
setError(err.message || t('加载邮件详情失败', 'Failed to load message details'));
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
@ -495,7 +506,7 @@ export default function OutlookPage() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [selectedMessageRef]);
|
||||
}, [selectedMessageRef, t]);
|
||||
|
||||
const canTest = useMemo(
|
||||
() => Boolean(
|
||||
@ -519,8 +530,8 @@ export default function OutlookPage() {
|
||||
return [
|
||||
{
|
||||
id: 'settings' as const,
|
||||
label: '设置',
|
||||
hint: '配置 Outlook 连接',
|
||||
label: t('设置', 'Settings'),
|
||||
hint: t('配置 Outlook 连接', 'Configure the Outlook connection'),
|
||||
icon: Settings2,
|
||||
count: null,
|
||||
},
|
||||
@ -530,34 +541,34 @@ export default function OutlookPage() {
|
||||
return [
|
||||
{
|
||||
id: 'inbox' as const,
|
||||
label: '收件箱',
|
||||
hint: '最近接收邮件',
|
||||
label: t('收件箱', 'Inbox'),
|
||||
hint: t('最近接收邮件', 'Recently received mail'),
|
||||
icon: Inbox,
|
||||
count: null,
|
||||
},
|
||||
{
|
||||
id: 'sent' as const,
|
||||
label: '发件箱',
|
||||
hint: '最近发送记录',
|
||||
label: t('发件箱', 'Sent'),
|
||||
hint: t('最近发送记录', 'Recently sent messages'),
|
||||
icon: Send,
|
||||
count: null,
|
||||
},
|
||||
{
|
||||
id: 'calendar' as const,
|
||||
label: '日程',
|
||||
hint: '未来 7 天',
|
||||
label: t('日程', 'Calendar'),
|
||||
hint: t('未来 7 天', 'Next 7 days'),
|
||||
icon: CalendarDays,
|
||||
count: overviewPending ? null : eventCount,
|
||||
},
|
||||
{
|
||||
id: 'settings' as const,
|
||||
label: '设置',
|
||||
hint: '连接与状态',
|
||||
label: t('设置', 'Settings'),
|
||||
hint: t('连接与状态', 'Connection and status'),
|
||||
icon: Settings2,
|
||||
count: null,
|
||||
},
|
||||
];
|
||||
}, [eventCount, inboxCount, isConfigured, overviewPending, sentCount]);
|
||||
}, [eventCount, inboxCount, isConfigured, overviewPending, sentCount, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!availableViews.some((view) => view.id === activeView)) {
|
||||
@ -604,7 +615,7 @@ export default function OutlookPage() {
|
||||
const result = await testOutlookConnection(form);
|
||||
setTestResult(result);
|
||||
} catch (err: any) {
|
||||
setError(err.message || '测试连接失败');
|
||||
setError(err.message || t('测试连接失败', 'Failed to test the connection'));
|
||||
setTestResult(null);
|
||||
} finally {
|
||||
setTesting(false);
|
||||
@ -626,7 +637,7 @@ export default function OutlookPage() {
|
||||
await loadStatus(true, { forceFormSync: true });
|
||||
setActiveView('inbox');
|
||||
} catch (err: any) {
|
||||
setError(err.message || '保存 Outlook 配置失败');
|
||||
setError(err.message || t('保存 Outlook 配置失败', 'Failed to save Outlook settings'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@ -649,7 +660,7 @@ export default function OutlookPage() {
|
||||
setFormDirty(false);
|
||||
await loadStatus(true, { forceFormSync: true });
|
||||
} catch (err: any) {
|
||||
setError(err.message || '断开 Outlook 连接失败');
|
||||
setError(err.message || t('断开 Outlook 连接失败', 'Failed to disconnect Outlook'));
|
||||
} finally {
|
||||
setDisconnecting(false);
|
||||
}
|
||||
@ -688,16 +699,16 @@ export default function OutlookPage() {
|
||||
) : (
|
||||
<>
|
||||
<Badge variant={statusVariant(isConnected)}>
|
||||
{isConnected ? '已连通' : isConfigured ? '已配置' : '未配置'}
|
||||
{isConnected ? t('已连通', 'Connected') : isConfigured ? t('已配置', 'Configured') : t('未配置', 'Not configured')}
|
||||
</Badge>
|
||||
<Badge variant={status?.mcp_registered ? 'default' : 'secondary'}>
|
||||
{status?.mcp_registered ? 'MCP 已注册' : 'MCP 未注册'}
|
||||
{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">邮箱 {overview?.mailbox || status?.saved?.email || '-'}</span>
|
||||
<span className="text-muted-foreground">时区 {status?.saved?.default_timezone || overview?.timezone || form.default_timezone}</span>
|
||||
<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">
|
||||
最近刷新 {formatDateTime((overview?.meta?.last_overview_refresh_at || status?.meta?.last_overview_refresh_at) as string | undefined)}
|
||||
{t('最近刷新', 'Last refreshed')} {formatDateTime((overview?.meta?.last_overview_refresh_at || status?.meta?.last_overview_refresh_at) as string | undefined, locale)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
@ -706,14 +717,14 @@ export default function OutlookPage() {
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{isConfigured ? (
|
||||
<>
|
||||
<TopStat label="收件箱" value={String(inboxCount)} loading={overviewPending} />
|
||||
<TopStat label="发件箱" value={String(sentCount)} loading={overviewPending} />
|
||||
<TopStat label="日程" value={String(eventCount)} loading={overviewPending} />
|
||||
<TopStat label={t('收件箱', 'Inbox')} value={String(inboxCount)} loading={overviewPending} />
|
||||
<TopStat label={t('发件箱', 'Sent')} value={String(sentCount)} loading={overviewPending} />
|
||||
<TopStat label={t('日程', 'Calendar')} value={String(eventCount)} loading={overviewPending} />
|
||||
</>
|
||||
) : null}
|
||||
<Button variant="outline" size="sm" onClick={() => void refreshOverview()}>
|
||||
<RefreshCw className={`mr-2 h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
{t('刷新', 'Refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -765,7 +776,7 @@ export default function OutlookPage() {
|
||||
<div className="text-left">
|
||||
<p className="text-sm font-semibold">{view.label}</p>
|
||||
{typeof view.count === 'number' ? (
|
||||
<p className="text-xs text-muted-foreground">{view.count} 条</p>
|
||||
<p className="text-xs text-muted-foreground">{t(`${view.count} 条`, `${view.count} items`)}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
@ -777,12 +788,13 @@ export default function OutlookPage() {
|
||||
|
||||
<TabsContent value="inbox" className="mt-0">
|
||||
<MessageCard
|
||||
title="收件箱"
|
||||
title={t('收件箱', 'Inbox')}
|
||||
icon={<MailOpen className="h-4 w-4" />}
|
||||
items={inboxPage?.value || []}
|
||||
page={inboxPage?.page || null}
|
||||
loading={mailboxLoading.inbox || (activeView === 'inbox' && !inboxPage)}
|
||||
emptyLabel="还没有读取到收件箱邮件"
|
||||
emptyLabel={t('还没有读取到收件箱邮件', 'No inbox messages have been loaded yet')}
|
||||
locale={locale}
|
||||
onOpen={(item) => setSelectedMessageRef(item.id ? { id: item.id, changekey: item.changekey } : null)}
|
||||
onRefresh={() => void loadMailboxPage('inbox', inboxPage?.page.skip ?? 0)}
|
||||
refreshing={mailboxLoading.inbox}
|
||||
@ -798,12 +810,13 @@ export default function OutlookPage() {
|
||||
|
||||
<TabsContent value="sent" className="mt-0">
|
||||
<MessageCard
|
||||
title="发件箱"
|
||||
title={t('发件箱', 'Sent')}
|
||||
icon={<Send className="h-4 w-4" />}
|
||||
items={sentPage?.value || []}
|
||||
page={sentPage?.page || null}
|
||||
loading={mailboxLoading.sent || (activeView === 'sent' && !sentPage)}
|
||||
emptyLabel="还没有读取到已发送邮件"
|
||||
emptyLabel={t('还没有读取到已发送邮件', 'No sent messages have been loaded yet')}
|
||||
locale={locale}
|
||||
onOpen={(item) => setSelectedMessageRef(item.id ? { id: item.id, changekey: item.changekey } : null)}
|
||||
onRefresh={() => void loadMailboxPage('sent', sentPage?.page.skip ?? 0)}
|
||||
refreshing={mailboxLoading.sent}
|
||||
@ -822,6 +835,7 @@ export default function OutlookPage() {
|
||||
items={calendarPage?.value || []}
|
||||
startDate={calendarAnchorKey}
|
||||
loading={calendarLoading || (activeView === 'calendar' && !calendarPage)}
|
||||
locale={locale}
|
||||
onOpen={(item) => setSelectedEvent(item)}
|
||||
onRefresh={() => void loadCalendarPage(calendarAnchorKey)}
|
||||
refreshing={calendarLoading}
|
||||
@ -849,33 +863,33 @@ export default function OutlookPage() {
|
||||
<div className="grid gap-6 xl:grid-cols-[1.08fr,0.92fr]">
|
||||
<Card className="rounded-[28px] shadow-sm">
|
||||
<CardHeader className="border-b pb-5">
|
||||
<CardTitle className="text-xl text-foreground">连接设置</CardTitle>
|
||||
<CardTitle className="text-xl text-foreground">{t('连接设置', 'Connection settings')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-5 pt-6">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Field label="邮箱地址" required>
|
||||
<Field label={t('邮箱地址', 'Email address')} required>
|
||||
<Input
|
||||
value={form.email}
|
||||
onChange={(event) => updateField('email', event.target.value)}
|
||||
placeholder="you@boardware.com"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="用户名">
|
||||
<Field label={t('用户名', 'Username')}>
|
||||
<Input
|
||||
value={form.username}
|
||||
onChange={(event) => updateField('username', event.target.value)}
|
||||
placeholder="留空时默认取邮箱前缀"
|
||||
placeholder={t('留空时默认取邮箱前缀', 'Leave blank to default to the email prefix')}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="密码" required>
|
||||
<Field label={t('密码', 'Password')} required>
|
||||
<Input
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(event) => updateField('password', event.target.value)}
|
||||
placeholder="请输入邮箱密码"
|
||||
placeholder={t('请输入邮箱密码', 'Enter the mailbox password')}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="域">
|
||||
<Field label={t('域', 'Domain')}>
|
||||
<Input
|
||||
value={form.domain}
|
||||
onChange={(event) => updateField('domain', event.target.value)}
|
||||
@ -898,7 +912,7 @@ export default function OutlookPage() {
|
||||
disabled={form.autodiscover}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="时区">
|
||||
<Field label={t('时区', 'Timezone')}>
|
||||
<Input
|
||||
value={form.default_timezone}
|
||||
onChange={(event) => updateField('default_timezone', event.target.value)}
|
||||
@ -912,7 +926,7 @@ export default function OutlookPage() {
|
||||
Autodiscover
|
||||
</Label>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
开启后优先使用 Exchange 自动发现,不再强依赖手填 EWS URL。
|
||||
{t('开启后优先使用 Exchange 自动发现,不再强依赖手填 EWS URL。', 'When enabled, Exchange autodiscover is preferred so the EWS URL does not need to be entered manually.')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
@ -927,11 +941,11 @@ export default function OutlookPage() {
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<Button variant="outline" 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}>
|
||||
{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"
|
||||
@ -939,21 +953,21 @@ export default function OutlookPage() {
|
||||
disabled={!status?.configured || disconnecting}
|
||||
>
|
||||
{disconnecting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Unplug className="mr-2 h-4 w-4" />}
|
||||
断开连接
|
||||
{t('断开连接', 'Disconnect')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{testResult && (
|
||||
<div className="rounded-3xl border bg-muted/30 p-4 text-sm">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="default">测试成功</Badge>
|
||||
<Badge variant="default">{t('测试成功', 'Test succeeded')}</Badge>
|
||||
<span className="text-muted-foreground">{testResult.mailbox}</span>
|
||||
<span className="text-muted-foreground">用户名: {testResult.resolved_username}</span>
|
||||
<span className="text-muted-foreground">{t('用户名', 'Username')}: {testResult.resolved_username}</span>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-3 md:grid-cols-3">
|
||||
<MiniStat label="检测到文件夹" value={String(testResult.sample.folders.length)} />
|
||||
<MiniStat label="收件箱样本" value={String(testResult.sample.inbox.length)} />
|
||||
<MiniStat label="日程样本" value={String(testResult.sample.events.length)} />
|
||||
<MiniStat label={t('检测到文件夹', 'Detected folders')} value={String(testResult.sample.folders.length)} />
|
||||
<MiniStat label={t('收件箱样本', 'Inbox samples')} value={String(testResult.sample.inbox.length)} />
|
||||
<MiniStat label={t('日程样本', 'Calendar samples')} value={String(testResult.sample.events.length)} />
|
||||
</div>
|
||||
{testWarnings.length > 0 && (
|
||||
<div className="mt-4 space-y-2 rounded-2xl border border-amber-300 bg-amber-50/80 p-3 text-amber-900">
|
||||
@ -972,7 +986,7 @@ export default function OutlookPage() {
|
||||
|
||||
<Card className="rounded-[28px] shadow-sm">
|
||||
<CardHeader className="border-b pb-5">
|
||||
<CardTitle className="text-xl text-foreground">连接状态</CardTitle>
|
||||
<CardTitle className="text-xl text-foreground">{t('连接状态', 'Connection status')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 pt-6">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@ -985,27 +999,27 @@ export default function OutlookPage() {
|
||||
) : (
|
||||
<>
|
||||
<Badge variant={statusVariant(isConnected)}>
|
||||
{isConnected ? '已连通' : isConfigured ? '已配置' : '未配置'}
|
||||
{isConnected ? t('已连通', 'Connected') : isConfigured ? t('已配置', 'Configured') : t('未配置', 'Not configured')}
|
||||
</Badge>
|
||||
<Badge variant={status?.mcp_registered ? 'default' : 'secondary'}>
|
||||
{status?.mcp_registered ? 'MCP 已注册' : 'MCP 未注册'}
|
||||
{status?.mcp_registered ? t('MCP 已注册', 'MCP registered') : t('MCP 未注册', 'MCP not registered')}
|
||||
</Badge>
|
||||
<Badge variant="secondary">{status?.provider || 'ews'}</Badge>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<InfoRow label="邮箱" value={status?.saved?.email || '-'} loading={statusPending} />
|
||||
<InfoRow label="用户名" value={status?.saved?.username || '-'} loading={statusPending} />
|
||||
<InfoRow label="域" value={status?.saved?.domain || '-'} loading={statusPending} />
|
||||
<InfoRow label={t('邮箱', 'Email')} value={status?.saved?.email || '-'} loading={statusPending} />
|
||||
<InfoRow label={t('用户名', 'Username')} value={status?.saved?.username || '-'} loading={statusPending} />
|
||||
<InfoRow label={t('域', 'Domain')} value={status?.saved?.domain || '-'} loading={statusPending} />
|
||||
<InfoRow label="EWS URL" value={status?.saved?.service_endpoint || '-'} loading={statusPending} />
|
||||
<InfoRow label="Server Host" value={status?.saved?.server || '-'} loading={statusPending} />
|
||||
<InfoRow label="时区" value={status?.saved?.default_timezone || status?.defaults.fields.default_timezone || '-'} loading={statusPending} />
|
||||
<InfoRow label="最近验证" value={formatDateTime(status?.meta?.last_verified_at as string | undefined)} loading={statusPending} />
|
||||
<InfoRow label="最近接入" value={formatDateTime(status?.meta?.last_connected_at as string | undefined)} loading={statusPending} />
|
||||
<InfoRow label={t('时区', 'Timezone')} value={status?.saved?.default_timezone || status?.defaults.fields.default_timezone || '-'} loading={statusPending} />
|
||||
<InfoRow label={t('最近验证', 'Last verified')} value={formatDateTime(status?.meta?.last_verified_at as string | undefined, locale)} loading={statusPending} />
|
||||
<InfoRow label={t('最近接入', 'Last connected')} value={formatDateTime(status?.meta?.last_connected_at as string | undefined, locale)} loading={statusPending} />
|
||||
<InfoRow
|
||||
label="最近刷新"
|
||||
value={formatDateTime((overview?.meta?.last_overview_refresh_at || status?.meta?.last_overview_refresh_at) as string | undefined)}
|
||||
label={t('最近刷新', 'Last refreshed')}
|
||||
value={formatDateTime((overview?.meta?.last_overview_refresh_at || status?.meta?.last_overview_refresh_at) as string | undefined, locale)}
|
||||
loading={statusPending || overviewPending}
|
||||
/>
|
||||
|
||||
@ -1017,12 +1031,15 @@ export default function OutlookPage() {
|
||||
|
||||
<div className="rounded-3xl border bg-muted/30 p-4">
|
||||
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
|
||||
当前存储位置
|
||||
{t('当前存储位置', 'Current storage mode')}
|
||||
</p>
|
||||
<p className="mt-2 text-sm font-medium text-foreground">
|
||||
{status?.storage_mode === 'authz'
|
||||
? '当前为 AuthZ 模式。Outlook 凭据保存在 AuthZ Service,由外置 Outlook MCP 按 backend 身份读取。'
|
||||
: <>当前为 workspace 模式。Outlook 状态文件会写入当前 workspace 的 <code>state/bw_outlook_mcp</code>。</>}
|
||||
? t(
|
||||
'当前为 AuthZ 模式。Outlook 凭据保存在 AuthZ Service,由外置 Outlook MCP 按 backend 身份读取。',
|
||||
'AuthZ mode is active. Outlook credentials are stored in the AuthZ service and read by the external Outlook MCP using the backend identity.'
|
||||
)
|
||||
: <>{t('当前为 workspace 模式。Outlook 状态文件会写入当前 workspace 的', 'Workspace mode is active. Outlook state files are written to')} <code>state/bw_outlook_mcp</code>.</>}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
@ -1034,9 +1051,9 @@ export default function OutlookPage() {
|
||||
<Dialog open={Boolean(selectedMessageRef)} onOpenChange={(open) => !open && setSelectedMessageRef(null)}>
|
||||
<DialogContent className="sm:max-w-5xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{selectedMessage?.subject || '邮件详情'}</DialogTitle>
|
||||
<DialogTitle>{selectedMessage?.subject || t('邮件详情', 'Message details')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{selectedMessage?.receivedDateTime ? formatDateTime(selectedMessage.receivedDateTime) : '正在加载'}
|
||||
{selectedMessage?.receivedDateTime ? formatDateTime(selectedMessage.receivedDateTime, locale) : t('正在加载', 'Loading')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{messageLoading ? (
|
||||
@ -1046,32 +1063,33 @@ export default function OutlookPage() {
|
||||
) : selectedMessage ? (
|
||||
<div className="grid gap-4 lg:grid-cols-[280px,1fr]">
|
||||
<div className="space-y-4 rounded-2xl border bg-muted/20 p-4 text-sm">
|
||||
<InfoRow label="发件人" value={mailboxLabel(selectedMessage.from)} />
|
||||
<InfoRow label={t('发件人', 'From')} value={mailboxLabel(selectedMessage.from)} />
|
||||
<InfoRow
|
||||
label="收件人"
|
||||
value={(selectedMessage.toRecipients || []).map(mailboxLabel).filter(Boolean).join(';') || '-'}
|
||||
label={t('收件人', 'To')}
|
||||
value={(selectedMessage.toRecipients || []).map(mailboxLabel).filter(Boolean).join(locale === 'en-US' ? '; ' : ';') || '-'}
|
||||
/>
|
||||
<InfoRow
|
||||
label="抄送"
|
||||
value={(selectedMessage.ccRecipients || []).map(mailboxLabel).filter(Boolean).join(';') || '-'}
|
||||
label={t('抄送', 'Cc')}
|
||||
value={(selectedMessage.ccRecipients || []).map(mailboxLabel).filter(Boolean).join(locale === 'en-US' ? '; ' : ';') || '-'}
|
||||
/>
|
||||
<InfoRow label="接收时间" value={formatDateTime(selectedMessage.receivedDateTime)} />
|
||||
<InfoRow label={t('接收时间', 'Received at')} value={formatDateTime(selectedMessage.receivedDateTime, locale)} />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant={selectedMessage.isRead ? 'secondary' : 'default'}>
|
||||
{selectedMessage.isRead ? '已读' : '未读'}
|
||||
{selectedMessage.isRead ? t('已读', 'Read') : t('未读', 'Unread')}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="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>
|
||||
{selectedMessage.body?.contentType?.toLowerCase() === 'html' ? (
|
||||
<iframe
|
||||
title="邮件正文"
|
||||
title={t('邮件正文', 'Message body')}
|
||||
srcDoc={buildEmailPreviewDocument(
|
||||
selectedMessage.body?.content || selectedMessage.bodyPreview || ''
|
||||
selectedMessage.body?.content || selectedMessage.bodyPreview || '',
|
||||
locale
|
||||
)}
|
||||
className="h-[60vh] w-full bg-white"
|
||||
sandbox="allow-popups allow-popups-to-escape-sandbox"
|
||||
@ -1086,7 +1104,7 @@ export default function OutlookPage() {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">未加载到邮件详情。</p>
|
||||
<p className="text-sm text-muted-foreground">{t('未加载到邮件详情。', 'Message details were not loaded.')}</p>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@ -1094,26 +1112,26 @@ export default function OutlookPage() {
|
||||
<Dialog open={Boolean(selectedEvent)} onOpenChange={(open) => !open && setSelectedEvent(null)}>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{selectedEvent?.subject || '日程详情'}</DialogTitle>
|
||||
<DialogTitle>{selectedEvent?.subject || t('日程详情', 'Event details')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{selectedEvent
|
||||
? `${formatDateTime(selectedEvent.start?.dateTime)} - ${formatDateTime(selectedEvent.end?.dateTime)}`
|
||||
: '日程详情'}
|
||||
? `${formatDateTime(selectedEvent.start?.dateTime, locale)} - ${formatDateTime(selectedEvent.end?.dateTime, locale)}`
|
||||
: t('日程详情', 'Event details')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{selectedEvent && (
|
||||
<div className="space-y-4 text-sm">
|
||||
<InfoRow label="组织者" value={mailboxLabel(selectedEvent.organizer)} />
|
||||
<InfoRow label="地点" value={selectedEvent.location?.displayName || '-'} />
|
||||
<InfoRow label={t('组织者', 'Organizer')} value={mailboxLabel(selectedEvent.organizer)} />
|
||||
<InfoRow label={t('地点', 'Location')} value={selectedEvent.location?.displayName || '-'} />
|
||||
<InfoRow
|
||||
label="参会人"
|
||||
value={(selectedEvent.attendees || []).map(mailboxLabel).filter(Boolean).join(';') || '-'}
|
||||
label={t('参会人', 'Attendees')}
|
||||
value={(selectedEvent.attendees || []).map(mailboxLabel).filter(Boolean).join(locale === 'en-US' ? '; ' : ';') || '-'}
|
||||
/>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">说明</p>
|
||||
<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">
|
||||
{selectedEvent.bodyPreview || '没有更多说明。'}
|
||||
{selectedEvent.bodyPreview || t('没有更多说明。', 'No additional notes.')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1185,6 +1203,7 @@ function MessageCard({
|
||||
icon,
|
||||
items,
|
||||
page,
|
||||
locale,
|
||||
loading = false,
|
||||
emptyLabel,
|
||||
onOpen,
|
||||
@ -1197,6 +1216,7 @@ function MessageCard({
|
||||
icon: React.ReactNode;
|
||||
items: OutlookMessageSummary[];
|
||||
page: OutlookPageInfo | null;
|
||||
locale: AppLocale;
|
||||
loading?: boolean;
|
||||
emptyLabel: string;
|
||||
onOpen: (item: OutlookMessageSummary) => void;
|
||||
@ -1205,8 +1225,9 @@ function MessageCard({
|
||||
onPreviousPage: () => void;
|
||||
onNextPage: () => void;
|
||||
}) {
|
||||
const t = (zh: string, en: string) => pickAppText(locale, zh, en);
|
||||
const currentPage = page ? Math.floor(page.skip / Math.max(page.top, 1)) + 1 : 1;
|
||||
const pageLabel = page ? `第 ${currentPage} 页 · 本页 ${page.returned} 封` : '正在读取邮件…';
|
||||
const pageLabel = page ? t(`第 ${currentPage} 页 · 本页 ${page.returned} 封`, `Page ${currentPage} · ${page.returned} messages`) : t('正在读取邮件…', 'Loading messages...');
|
||||
|
||||
return (
|
||||
<Card className="rounded-[28px] shadow-sm">
|
||||
@ -1216,14 +1237,14 @@ function MessageCard({
|
||||
{icon}
|
||||
{title}
|
||||
</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">{loading ? '正在读取邮件…' : pageLabel}</p>
|
||||
<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}>
|
||||
<RefreshCw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={onPreviousPage} disabled={!page || page.skip === 0 || refreshing}>
|
||||
上一页
|
||||
{t('上一页', 'Previous')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@ -1231,7 +1252,7 @@ function MessageCard({
|
||||
onClick={onNextPage}
|
||||
disabled={!page || !page.has_more || refreshing}
|
||||
>
|
||||
下一页
|
||||
{t('下一页', 'Next')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@ -1262,17 +1283,17 @@ function MessageCard({
|
||||
>
|
||||
<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 || '(无主题)'}</p>
|
||||
<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">
|
||||
{item.bodyPreview || '没有预览内容。'}
|
||||
{item.bodyPreview || t('没有预览内容。', 'No preview available.')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2 lg:flex-col lg:items-end">
|
||||
<Badge variant={item.isRead ? 'secondary' : 'default'}>
|
||||
{item.isRead ? '已读' : '未读'}
|
||||
{item.isRead ? t('已读', 'Read') : t('未读', 'Unread')}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">{formatDateTime(item.receivedDateTime)}</span>
|
||||
<span className="text-xs text-muted-foreground">{formatDateTime(item.receivedDateTime, locale)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@ -1287,6 +1308,7 @@ function MessageCard({
|
||||
function EventCard({
|
||||
items,
|
||||
startDate,
|
||||
locale,
|
||||
loading = false,
|
||||
onOpen,
|
||||
onRefresh,
|
||||
@ -1297,6 +1319,7 @@ function EventCard({
|
||||
}: {
|
||||
items: OutlookEventSummary[];
|
||||
startDate?: string | null;
|
||||
locale: AppLocale;
|
||||
loading?: boolean;
|
||||
onOpen: (item: OutlookEventSummary) => void;
|
||||
onRefresh: () => void;
|
||||
@ -1305,6 +1328,7 @@ function EventCard({
|
||||
onNextWeek: () => void;
|
||||
onCurrentWeek: () => void;
|
||||
}) {
|
||||
const t = (zh: string, en: string) => pickAppText(locale, zh, en);
|
||||
const initialAnchor = startDate ? new Date(startDate) : new Date();
|
||||
const anchor = Number.isNaN(initialAnchor.getTime()) ? new Date() : initialAnchor;
|
||||
const weekDays = Array.from({ length: 7 }, (_, index) => {
|
||||
@ -1316,7 +1340,7 @@ function EventCard({
|
||||
const key = toLocalDateKey(day);
|
||||
return {
|
||||
key,
|
||||
label: formatDayLabel(day),
|
||||
label: formatDayLabel(day, locale),
|
||||
items: items
|
||||
.filter((item) => formatDateKey(item.start?.dateTime) === key)
|
||||
.sort((left, right) => {
|
||||
@ -1333,21 +1357,21 @@ function EventCard({
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<CalendarDays className="h-4 w-4" />
|
||||
日程安排
|
||||
{t('日程安排', 'Schedule')}
|
||||
</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatDayLabel(weekDays[0])} - {formatDayLabel(weekDays[weekDays.length - 1])}
|
||||
{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}>
|
||||
上一周
|
||||
{t('上一周', 'Previous week')}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={onCurrentWeek} disabled={refreshing}>
|
||||
本周
|
||||
{t('本周', 'This week')}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={onNextWeek} disabled={refreshing}>
|
||||
下一周
|
||||
{t('下一周', 'Next week')}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={onRefresh} disabled={refreshing}>
|
||||
<RefreshCw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
@ -1372,11 +1396,11 @@ function EventCard({
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="font-medium text-foreground">{day.label}</p>
|
||||
<p className="text-xs text-muted-foreground">{day.items.length} 条安排</p>
|
||||
<p className="text-xs text-muted-foreground">{t(`${day.items.length} 条安排`, `${day.items.length} events`)}</p>
|
||||
</div>
|
||||
</div>
|
||||
{day.items.length === 0 ? (
|
||||
<p className="mt-4 text-sm text-muted-foreground">暂无安排</p>
|
||||
<p className="mt-4 text-sm text-muted-foreground">{t('暂无安排', 'No events')}</p>
|
||||
) : (
|
||||
<div className="mt-4 space-y-3">
|
||||
{day.items.map((item) => (
|
||||
@ -1386,12 +1410,12 @@ function EventCard({
|
||||
onClick={() => onOpen(item)}
|
||||
className="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 || '(无主题)'}</p>
|
||||
<p className="font-medium text-foreground">{item.subject || t('(无主题)', '(No subject)')}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{formatTime(item.start?.dateTime)} - {formatTime(item.end?.dateTime)}
|
||||
{formatTime(item.start?.dateTime, locale)} - {formatTime(item.end?.dateTime, locale)}
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{item.location?.displayName || '未设置地点'}
|
||||
{item.location?.displayName || t('未设置地点', 'No location set')}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user