diff --git a/contentlayer.config.ts b/contentlayer.config.ts index 9901105..3d951bf 100644 --- a/contentlayer.config.ts +++ b/contentlayer.config.ts @@ -1,24 +1,7 @@ -import { defineDocumentType, makeSource } from 'contentlayer/source-files' - -export const Workshop = defineDocumentType(() => ({ - name: 'Workshop', - filePathPattern: '**/*.mdx', - contentType: 'mdx', - fields: { - title: { type: 'string', required: true }, - summary: { type: 'string', required: true }, - level: { type: 'string', default: 'Intro' }, - duration: { type: 'string', required: false }, - tags: { type: 'list', of: { type: 'string' }, default: [] }, - updatedAt: { type: 'date', required: false }, - }, - computedFields: { - slug: { type: 'string', resolve: (doc) => doc._raw.flattenedPath }, - url: { type: 'string', resolve: (doc) => `/workshop/${doc._raw.flattenedPath}` }, - }, -})) +import { makeSource } from 'contentlayer/source-files' export default makeSource({ - contentDirPath: 'src/content/workshop', - documentTypes: [Workshop], + contentDirPath: 'src/content', + documentTypes: [], }) + diff --git a/src/app/(tenant)/[slug]/mail/compose/page.tsx b/src/app/(tenant)/[slug]/mail/compose/page.tsx deleted file mode 100644 index a72be98..0000000 --- a/src/app/(tenant)/[slug]/mail/compose/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -export const dynamic = 'error' - -import ComposeForm from '../../../../components/mail/ComposeForm' - -type PageProps = { - params: Promise<{ - slug: string - }> -} - -export default async function ComposePage({ params }: PageProps) { - const { slug: tenantId } = await params - return -} diff --git a/src/app/(tenant)/[slug]/mail/message/[id]/page.tsx b/src/app/(tenant)/[slug]/mail/message/[id]/page.tsx deleted file mode 100644 index dd338bf..0000000 --- a/src/app/(tenant)/[slug]/mail/message/[id]/page.tsx +++ /dev/null @@ -1,20 +0,0 @@ -'use client' - -import { useRouter } from 'next/navigation' - -import MessageView from '../../../../../components/mail/MessageView' - -export default function MessageDetailPage({ params }: { params: { slug: string; id: string } }) { - const router = useRouter() - const tenantId = params.slug - return ( -
- router.back()} - /> -
- ) -} diff --git a/src/app/(tenant)/[slug]/mail/page.tsx b/src/app/(tenant)/[slug]/mail/page.tsx deleted file mode 100644 index b081faf..0000000 --- a/src/app/(tenant)/[slug]/mail/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -export const dynamic = 'error' - -import MailDashboard from '../../../components/mail/MailDashboard' - -type PageProps = { - params: Promise<{ - slug: string - }> -} - -export default async function TenantMailPage({ params }: PageProps) { - const { slug: tenantId } = await params - return -} diff --git a/src/app/(tenant)/[slug]/mail/settings/page.tsx b/src/app/(tenant)/[slug]/mail/settings/page.tsx deleted file mode 100644 index 6875cc5..0000000 --- a/src/app/(tenant)/[slug]/mail/settings/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -export const dynamic = 'error' - -import MailSettings from '../../../../components/mail/MailSettings' - -type PageProps = { - params: Promise<{ - slug: string - }> -} - -export default async function MailSettingsPage({ params }: PageProps) { - const { slug: tenantId } = await params - return -} diff --git a/src/app/api/mail/ai/classify/route.ts b/src/app/api/mail/ai/classify/route.ts deleted file mode 100644 index 32042d0..0000000 --- a/src/app/api/mail/ai/classify/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import { getMessage, resolveTenantId } from '../../mockData' - -export async function POST(request: NextRequest) { - const tenantId = resolveTenantId(request.headers.get('x-tenant-id')) - const body = (await request.json()) as { messageId: string } - const message = getMessage(tenantId, body.messageId) - if (!message) { - return NextResponse.json({ error: 'Not found' }, { status: 404 }) - } - - const labels = Array.from(new Set([...message.labels, 'AI-Reviewed'])) - return NextResponse.json({ labels }) -} diff --git a/src/app/api/mail/ai/reply-suggest/route.ts b/src/app/api/mail/ai/reply-suggest/route.ts deleted file mode 100644 index 491ce96..0000000 --- a/src/app/api/mail/ai/reply-suggest/route.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import { getMessage, resolveTenantId } from '../../mockData' - -export async function POST(request: NextRequest) { - const tenantId = resolveTenantId(request.headers.get('x-tenant-id')) - const body = (await request.json()) as { messageId: string; style?: string; language?: string } - const message = body?.messageId ? getMessage(tenantId, body.messageId) : null - - const base = message?.aiInsights?.suggestions ?? [ - '收到,我们将安排同事跟进。', - '感谢提醒,我们将及时回复。', - '请告知是否需要更多信息。', - ] - - return NextResponse.json({ suggestions: base }) -} diff --git a/src/app/api/mail/ai/summarize/route.ts b/src/app/api/mail/ai/summarize/route.ts deleted file mode 100644 index 0834a4f..0000000 --- a/src/app/api/mail/ai/summarize/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import { getMessage, resolveTenantId } from '../../mockData' - -export async function POST(request: NextRequest) { - const tenantId = resolveTenantId(request.headers.get('x-tenant-id')) - const body = (await request.json()) as { messageId?: string; raw?: string } - if (!body.messageId && !body.raw) { - return NextResponse.json({ error: 'messageId or raw is required' }, { status: 400 }) - } - - if (body.messageId) { - const message = getMessage(tenantId, body.messageId) - if (!message) { - return NextResponse.json({ error: 'Not found' }, { status: 404 }) - } - if (message.aiInsights) { - return NextResponse.json(message.aiInsights) - } - } - - return NextResponse.json({ - summary: '示例摘要:邮件内容将提炼为关键句子。', - bullets: ['示例要点一', '示例要点二'], - actions: ['示例行动一'], - tone: '信息', - }) -} diff --git a/src/app/api/mail/inbox/route.ts b/src/app/api/mail/inbox/route.ts deleted file mode 100644 index f33d34f..0000000 --- a/src/app/api/mail/inbox/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import { getInbox, resolveTenantId } from '../mockData' - -export async function GET(request: NextRequest) { - const tenantHeader = request.headers.get('x-tenant-id') - const tenantQuery = request.nextUrl.searchParams.get('tenantId') - const tenantId = resolveTenantId(tenantHeader ?? tenantQuery) - - const inbox = getInbox(tenantId) - - const label = request.nextUrl.searchParams.get('label') - const query = request.nextUrl.searchParams.get('q')?.toLowerCase().trim() - - let filtered = inbox.messages - if (label === 'unread') { - filtered = filtered.filter((item) => item.unread) - } else if (label === 'starred') { - filtered = filtered.filter((item) => item.starred) - } else if (label && label !== 'important') { - filtered = filtered.filter((item) => item.labels.includes(label)) - } - if (query) { - filtered = filtered.filter((item) => - [item.subject, item.snippet, item.from.email, item.from.name] - .filter(Boolean) - .some((field) => field!.toLowerCase().includes(query)), - ) - } - - return NextResponse.json({ - ...inbox, - messages: filtered, - }) -} diff --git a/src/app/api/mail/message/[id]/route.ts b/src/app/api/mail/message/[id]/route.ts deleted file mode 100644 index ddc2426..0000000 --- a/src/app/api/mail/message/[id]/route.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import { getMessage, resolveTenantId } from '../../mockData' - -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { - const tenantId = resolveTenantId(request.headers.get('x-tenant-id')) - const { id } = await params - const message = getMessage(tenantId, id) - if (!message) { - return NextResponse.json({ error: 'Not found' }, { status: 404 }) - } - return NextResponse.json(message) -} - -export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { - const tenantId = resolveTenantId(request.headers.get('x-tenant-id')) - const { id } = await params - const message = getMessage(tenantId, id) - if (!message) { - return NextResponse.json({ error: 'Not found' }, { status: 404 }) - } - return NextResponse.json({ success: true }) -} diff --git a/src/app/api/mail/mockData.ts b/src/app/api/mail/mockData.ts deleted file mode 100644 index 27ec606..0000000 --- a/src/app/api/mail/mockData.ts +++ /dev/null @@ -1,208 +0,0 @@ -import type { MailInboxResponse, MailListMessage, MailMessageDetail, NamespacePolicy } from '@lib/mail/types' - -type TenantMailData = { - inbox: MailListMessage[] - messages: Record - namespace: NamespacePolicy -} - -const now = Date.now() - -const baseMessages: MailListMessage[] = [ - { - id: 'msg-1001', - subject: '【故障通报】核心链路延迟恢复通知', - snippet: '生产集群延迟已恢复至正常指标,详见行动项。', - from: { name: 'SRE 值班', email: 'sre@svc.plus' }, - to: [{ name: 'Ops 团队', email: 'ops@tenant.io' }], - date: new Date(now - 5 * 60 * 1000).toISOString(), - unread: true, - starred: true, - labels: ['Incident', 'Priority'], - hasAttachments: true, - aiSummary: { - preview: '延迟恢复,需确认追踪指标。', - tone: '紧急', - }, - }, - { - id: 'msg-1002', - subject: '月度账单与消耗对账单', - snippet: '附件包含 5 月份资源使用与费用明细,请于本周内确认。', - from: { name: 'Finance Robot', email: 'billing@svc.plus' }, - to: [{ name: 'Finance', email: 'finance@tenant.io' }], - date: new Date(now - 2 * 60 * 60 * 1000).toISOString(), - unread: false, - labels: ['Billing'], - hasAttachments: true, - aiSummary: { - preview: '账单结算提醒,需核对折扣。', - tone: '正式', - }, - }, - { - id: 'msg-1003', - subject: 'AI 助手联调会议记录', - snippet: '会议纪要包含下一步联调行动项与 SLA 讨论。', - from: { name: '产品经理', email: 'pm@svc.plus' }, - to: [{ name: 'AI 团队', email: 'ai@tenant.io' }], - date: new Date(now - 5 * 60 * 60 * 1000).toISOString(), - unread: false, - labels: ['Product'], - aiSummary: { - preview: '提炼三条关键任务。', - tone: '合作', - }, - }, - { - id: 'msg-1004', - subject: '【提醒】IAM 权限矩阵变更审批', - snippet: '审批单待确认,涉及新的只读角色授权,请于 24 小时内处理。', - from: { name: 'Access Bot', email: 'iam@svc.plus' }, - to: [{ name: 'Security', email: 'sec@tenant.io' }], - date: new Date(now - 12 * 60 * 60 * 1000).toISOString(), - unread: true, - labels: ['Security'], - aiSummary: { - preview: '审批截止前需确认。', - tone: '提醒', - }, - }, -] - -const detailMap: Record = { - 'msg-1001': { - ...baseMessages[0], - text: '生产链路延迟恢复。请确认后续监控指标与复盘会议安排。', - html: '

生产链路延迟已恢复。

  • 核对 Prometheus 延迟指标
  • 更新状态页面
  • 准备 18:00 复盘会议
', - attachments: [ - { - id: 'att-1', - fileName: 'incident-report.pdf', - contentType: 'application/pdf', - size: 234567, - downloadUrl: '#', - }, - ], - aiInsights: { - summary: '生产链路延迟恢复,需跟进指标及复盘会议。', - bullets: ['Prometheus 延迟恢复', '状态页面需更新', '18:00 复盘会议'], - actions: ['确认状态页', '同步客户邮件', '准备复盘材料'], - tone: '紧急', - suggestions: [ - '感谢通知,已安排团队核查 Prometheus 指标。', - '收到,我们将于 18:00 准备复盘材料。', - '请同步可能影响的客户列表,方便统一公告。', - ], - }, - }, - 'msg-1002': { - ...baseMessages[1], - text: '随信附上 5 月份账单,包含折扣与超额费用明细,请在本周内完成对账。', - attachments: [ - { - id: 'att-2', - fileName: 'may-usage.xlsx', - contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - size: 54567, - downloadUrl: '#', - }, - ], - aiInsights: { - summary: '账单需要财务团队在本周内确认。', - bullets: ['包含折扣明细', '有部分资源超额', '需在周五前回复'], - actions: ['核对折扣', '确认超额原因', '回邮确认'], - tone: '正式', - suggestions: ['已收悉,我们将在周四前完成对账并回复。'], - }, - }, - 'msg-1003': { - ...baseMessages[2], - html: '

联调会议要点:

  1. 六月上线 Beta,需补充监控指标
  2. AI 模型回退策略需评审
  3. 下一次联调会议安排在周五上午
', - aiInsights: { - summary: '会议聚焦上线计划、模型回退与下次会议时间。', - bullets: ['六月 Beta 上线', '确认模型回退策略', '周五上午继续联调'], - actions: ['同步监控指标清单', '准备回退方案文档', '发送会议邀请'], - tone: '合作', - }, - }, - 'msg-1004': { - ...baseMessages[3], - text: 'IAM 角色矩阵变更涉及新建只读角色,需要安全团队审批。', - aiInsights: { - summary: '安全团队需在 24 小时内确认新角色审批。', - bullets: ['新增只读角色', '审批截止 24 小时内', '需评估权限边界'], - actions: ['审阅角色权限', '评估风险', '确认审批或驳回'], - tone: '提醒', - }, - }, -} - -const TENANT_DATA: Record = { - 'tenant-alpha': { - inbox: baseMessages, - messages: detailMap, - namespace: { - model: 'gpt-4o-mini', - temperature: 0.3, - maxTokens: 2048, - rateLimitPerMinute: 60, - vectorIndex: 's3://tenant-alpha-mail', - policy: '{"blockedKeywords": ["NDA", "秘密"]}', - updatedAt: new Date(now - 3600 * 1000).toISOString(), - }, - }, - default: { - inbox: baseMessages, - messages: detailMap, - namespace: { - model: 'gpt-4o-mini', - temperature: 0.5, - maxTokens: 2048, - rateLimitPerMinute: 30, - vectorIndex: 's3://default-mail', - policy: '{"allowExternal": true}', - updatedAt: new Date(now - 7200 * 1000).toISOString(), - }, - }, -} - -export function resolveTenantId(raw: string | null | undefined) { - if (!raw) { - return 'default' - } - return TENANT_DATA[raw] ? raw : 'default' -} - -export function getInbox(tenantId: string): MailInboxResponse { - const data = TENANT_DATA[tenantId] ?? TENANT_DATA.default - return { - messages: data.inbox, - labels: [ - { id: 'Incident', name: 'Incident', color: '#f97316', unread: data.inbox.filter((item) => item.unread && item.labels.includes('Incident')).length }, - { id: 'Billing', name: 'Billing', color: '#2563eb', unread: data.inbox.filter((item) => item.unread && item.labels.includes('Billing')).length }, - { id: 'Security', name: 'Security', color: '#7c3aed', unread: data.inbox.filter((item) => item.unread && item.labels.includes('Security')).length }, - { id: 'Product', name: 'Product', color: '#0f766e', unread: data.inbox.filter((item) => item.unread && item.labels.includes('Product')).length }, - ], - unreadCount: data.inbox.filter((item) => item.unread).length, - nextCursor: null, - } -} - -export function getMessage(tenantId: string, id: string): MailMessageDetail | null { - const data = TENANT_DATA[tenantId] ?? TENANT_DATA.default - return data.messages[id] ?? null -} - -export function getNamespace(tenantId: string): NamespacePolicy { - const data = TENANT_DATA[tenantId] ?? TENANT_DATA.default - return data.namespace -} - -export function updateNamespace(tenantId: string, patch: Partial): NamespacePolicy { - const key = TENANT_DATA[tenantId] ? tenantId : 'default' - const current = TENANT_DATA[key].namespace - const next = { ...current, ...patch, updatedAt: new Date().toISOString() } - TENANT_DATA[key].namespace = next - return next -} diff --git a/src/app/api/mail/namespace/route.ts b/src/app/api/mail/namespace/route.ts deleted file mode 100644 index 45a3828..0000000 --- a/src/app/api/mail/namespace/route.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import { getNamespace, resolveTenantId, updateNamespace } from '../mockData' - -export async function GET(request: NextRequest) { - const tenantId = resolveTenantId(request.headers.get('x-tenant-id')) - return NextResponse.json(getNamespace(tenantId)) -} - -export async function PUT(request: NextRequest) { - const tenantId = resolveTenantId(request.headers.get('x-tenant-id')) - const patch = (await request.json()) as Record - return NextResponse.json(updateNamespace(tenantId, patch)) -} diff --git a/src/app/api/mail/send/route.ts b/src/app/api/mail/send/route.ts deleted file mode 100644 index ec3fb9d..0000000 --- a/src/app/api/mail/send/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -import type { ComposePayload } from '@lib/mail/types' - -export async function POST(request: NextRequest) { - const payload = (await request.json()) as ComposePayload - void payload - return NextResponse.json({ success: true }) -} diff --git a/src/app/components/mail/ComposeForm.tsx b/src/app/components/mail/ComposeForm.tsx deleted file mode 100644 index 74e300b..0000000 --- a/src/app/components/mail/ComposeForm.tsx +++ /dev/null @@ -1,139 +0,0 @@ -'use client' - -import { FormEvent, useCallback, useMemo, useState } from 'react' -import { Loader2, Send } from 'lucide-react' -import { useRouter, useSearchParams } from 'next/navigation' - -import { sendMessage } from '@lib/mail/apiClient' -import type { ComposePayload } from '@lib/mail/types' - -interface ComposeFormProps { - tenantId: string -} - -function parseList(value: string): string[] { - return value - .split(',') - .map((item) => item.trim()) - .filter((item) => item.length > 0) -} - -export default function ComposeForm({ tenantId }: ComposeFormProps) { - const router = useRouter() - const params = useSearchParams() - const replySubject = params.get('subject') ?? '' - const replyTo = params.get('to') ?? '' - const [submitting, setSubmitting] = useState(false) - const [form, setForm] = useState({ - to: replyTo ? parseList(replyTo) : [], - cc: [], - bcc: [], - subject: replySubject, - text: '', - html: '', - attachments: [], - }) - const [status, setStatus] = useState(null) - - const handleSubmit = useCallback( - async (event: FormEvent) => { - event.preventDefault() - if (form.to.length === 0) { - setStatus('至少填写一个收件人') - return - } - try { - setSubmitting(true) - await sendMessage(tenantId, form) - setStatus('已成功发送!') - setTimeout(() => { - router.push(`/panel/mail?tenantId=${tenantId}`) - }, 600) - } catch (error) { - setStatus(error instanceof Error ? error.message : '发送失败') - } finally { - setSubmitting(false) - } - }, - [form, router, tenantId], - ) - - const handleFieldChange = useCallback( - (field: keyof ComposePayload, value: string | string[]) => { - setForm((prev) => ({ - ...prev, - [field]: Array.isArray(value) ? value : value, - })) - }, - [], - ) - - const recipientsPreview = useMemo(() => form.to.join(', '), [form.to]) - - return ( -
-
-

写信

-

通过 TLS-only 通道发送邮件,附件将使用 S3 预签名直传。

-
-
- - - - -
-