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: '联调会议要点:
- 六月上线 Beta,需补充监控指标
- AI 模型回退策略需评审
- 下一次联调会议安排在周五上午
',
- 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 (
-
- )
-}
diff --git a/src/app/components/mail/Inbox.tsx b/src/app/components/mail/Inbox.tsx
deleted file mode 100644
index ecb52bf..0000000
--- a/src/app/components/mail/Inbox.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-'use client'
-
-import type { MailLabel, MailListMessage } from '@lib/mail/types'
-
-import MessageItem from './MessageItem'
-
-interface InboxProps {
- messages: MailListMessage[]
- selectedMessageId: string | null
- onSelect: (id: string) => void
- loading?: boolean
- labels: MailLabel[]
-}
-
-export default function Inbox({ messages, selectedMessageId, onSelect, loading, labels }: InboxProps) {
- return (
-
-
-
共 {messages.length} 封邮件
-
- {labels.slice(0, 4).map((label) => (
-
-
- {label.name}
-
- ))}
-
-
-
- {loading ? (
-
- {Array.from({ length: 6 }).map((_, index) => (
-
- ))}
-
- ) : messages.length ? (
-
- {messages.map((message) => (
- onSelect(message.id)}
- />
- ))}
-
- ) : (
-
-
收件箱为空
-
尝试调整筛选条件或搜索关键字。
-
- )}
-
-
- )
-}
diff --git a/src/app/components/mail/MailCenter.tsx b/src/app/components/mail/MailCenter.tsx
deleted file mode 100644
index 3a2d350..0000000
--- a/src/app/components/mail/MailCenter.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-'use client'
-
-import { ChangeEvent, useCallback, useEffect, useMemo } from 'react'
-import { useRouter, useSearchParams } from 'next/navigation'
-
-import { useTenantAuthContext } from '@lib/mail/auth'
-
-import MailDashboard from './MailDashboard'
-
-const MAIL_DEMO_ENABLED =
- process.env.NEXT_PUBLIC_MAIL_DEMO === 'true' ||
- process.env.NEXT_PUBLIC_MAIL_DEMO === '1' ||
- process.env.NODE_ENV !== 'production'
-const MAIL_DEMO_TENANT_ID = process.env.NEXT_PUBLIC_MAIL_DEMO_TENANT_ID ?? 'default'
-const MAIL_DEMO_TENANT_NAME = process.env.NEXT_PUBLIC_MAIL_DEMO_TENANT_NAME ?? '演示租户'
-
-export default function MailCenter() {
- const router = useRouter()
- const params = useSearchParams()
- const { tenants, defaultTenantId } = useTenantAuthContext()
- const queryTenantId = params.get('tenantId')
- const fallbackTenant = useMemo(
- () =>
- MAIL_DEMO_ENABLED && (!tenants || tenants.length === 0)
- ? { id: MAIL_DEMO_TENANT_ID, name: MAIL_DEMO_TENANT_NAME }
- : null,
- [tenants],
- )
-
- const tenantOptions = useMemo(() => {
- if (tenants && tenants.length > 0) {
- return tenants
- }
- return fallbackTenant ? [fallbackTenant] : []
- }, [fallbackTenant, tenants])
-
- const preferredTenantId = useMemo(() => {
- if (queryTenantId) {
- return queryTenantId
- }
-
- if (defaultTenantId && tenantOptions.some((tenant) => tenant.id === defaultTenantId)) {
- return defaultTenantId
- }
-
- return tenantOptions[0]?.id ?? null
- }, [defaultTenantId, queryTenantId, tenantOptions])
-
- useEffect(() => {
- if (!queryTenantId && preferredTenantId) {
- router.replace(`/panel/mail?tenantId=${preferredTenantId}`, { scroll: false })
- }
- }, [preferredTenantId, queryTenantId, router])
-
- const tenantId = queryTenantId ?? preferredTenantId
-
- const handleTenantChange = useCallback(
- (event: ChangeEvent) => {
- const value = event.target.value
- if (!value) {
- return
- }
- router.replace(`/panel/mail?tenantId=${value}`)
- },
- [router],
- )
-
- if (!tenantOptions.length) {
- return (
-
-
尚未加入任何租户
-
联系管理员邀请你加入租户后即可在此处访问邮件中心。
-
- )
- }
-
- if (!tenantId) {
- return (
-
-
选择要查看的租户邮箱
-
-
- )
- }
-
- const activeTenant = tenantOptions.find((tenant) => tenant.id === tenantId)
- const showDemoBanner = Boolean(fallbackTenant && tenantOptions.length === 1 && tenantOptions[0].id === fallbackTenant.id)
-
- return (
-
- {showDemoBanner ? (
-
-
演示模式
-
- 当前展示的是基于 docs/dashboard-mail-module-plan.md 的示例数据,用于本地预览邮件模块布局。
-
-
- ) : null}
-
-
-
邮件中心
-
多租户邮箱统一收件、AI 摘要与智能回复。
-
-
-
-
-
- )
-}
diff --git a/src/app/components/mail/MailDashboard.tsx b/src/app/components/mail/MailDashboard.tsx
deleted file mode 100644
index 5d2caa7..0000000
--- a/src/app/components/mail/MailDashboard.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-'use client'
-
-import { useEffect, useMemo } from 'react'
-import useSWR from 'swr'
-
-import { fetchInbox } from '@lib/mail/apiClient'
-import type { MailListMessage, MailLabel, MailInboxResponse } from '@lib/mail/types'
-
-import { useMailStore } from '../../store/mail.store'
-import Inbox from './Inbox'
-import MessageView from './MessageView'
-import Toolbar from './Toolbar'
-
-interface MailDashboardProps {
- tenantId: string
- tenantName: string
-}
-
-export default function MailDashboard({ tenantId, tenantName }: MailDashboardProps) {
- const { label, search, pageSize, cursor, setTenant, setSelectedMessageId, selectedMessageId } = useMailStore()
-
- useEffect(() => {
- setTenant(tenantId)
- }, [setTenant, tenantId])
-
- const inboxKey = useMemo(
- () =>
- tenantId
- ? ['mail-inbox', tenantId, label ?? 'all', search, pageSize, cursor ?? '']
- : null,
- [cursor, label, pageSize, search, tenantId],
- )
-
- const inbox = useSWR(inboxKey, () => fetchInbox(tenantId, { cursor, label, pageSize, q: search }), {
- keepPreviousData: true,
- })
-
- const messages: MailListMessage[] = inbox.data?.messages ?? []
- const labels: MailLabel[] = inbox.data?.labels ?? []
-
- useEffect(() => {
- const current = inbox.data?.messages ?? []
- if (!current.length) {
- setSelectedMessageId(null)
- return
- }
-
- const stillExists = current.some((message) => message.id === selectedMessageId)
- if (!stillExists) {
- setSelectedMessageId(current[0]?.id ?? null)
- }
- }, [inbox.data, selectedMessageId, setSelectedMessageId])
-
- return (
-
-
- inbox.mutate()}
- labels={labels}
- tenantName={tenantName}
- />
-
-
-
- inbox.mutate()}
- onRefreshed={() => inbox.mutate()}
- />
-
-
- )
-}
diff --git a/src/app/components/mail/MailSettings.tsx b/src/app/components/mail/MailSettings.tsx
deleted file mode 100644
index e33c825..0000000
--- a/src/app/components/mail/MailSettings.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-'use client'
-
-import { useCallback, useMemo, useState } from 'react'
-import useSWR from 'swr'
-import { Loader2, Save } from 'lucide-react'
-
-import { fetchNamespacePolicy, updateNamespacePolicy } from '@lib/mail/apiClient'
-import type { NamespacePolicy } from '@lib/mail/types'
-
-interface MailSettingsProps {
- tenantId: string
-}
-
-export default function MailSettings({ tenantId }: MailSettingsProps) {
- const namespace = useSWR(['mail-namespace', tenantId], () => fetchNamespacePolicy(tenantId))
- const [draft, setDraft] = useState | null>(null)
- const [saving, setSaving] = useState(false)
- const [status, setStatus] = useState(null)
-
- const data = useMemo(() => {
- if (!namespace.data) {
- return null
- }
- return draft ? { ...namespace.data, ...draft } : namespace.data
- }, [draft, namespace.data])
-
- const handleChange = useCallback((field: keyof NamespacePolicy, value: string) => {
- setDraft((prev) => ({
- ...(prev ?? {}),
- [field]: field === 'temperature' ? parseFloat(value) : field === 'maxTokens' || field === 'rateLimitPerMinute' ? Number(value) : value,
- }))
- }, [])
-
- const handleSubmit = useCallback(
- async (event: React.FormEvent) => {
- event.preventDefault()
- if (!data) {
- return
- }
- try {
- setSaving(true)
- const updated = await updateNamespacePolicy(tenantId, draft ?? {})
- namespace.mutate(updated, false)
- setDraft(null)
- setStatus('命名空间已更新')
- } catch (error) {
- setStatus(error instanceof Error ? error.message : '更新失败')
- } finally {
- setSaving(false)
- }
- },
- [data, draft, namespace, tenantId],
- )
-
- if (namespace.isLoading || !data) {
- return (
-
- )
- }
-
- return (
-
- )
-}
diff --git a/src/app/components/mail/MessageItem.tsx b/src/app/components/mail/MessageItem.tsx
deleted file mode 100644
index d1aa50d..0000000
--- a/src/app/components/mail/MessageItem.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-'use client'
-
-import { MailOpen, Star, StarOff } from 'lucide-react'
-
-import type { MailListMessage } from '@lib/mail/types'
-
-interface MessageItemProps {
- message: MailListMessage
- active?: boolean
- onSelect: () => void
-}
-
-function formatAddress(address: { name?: string; email: string }) {
- if (address.name && address.name.trim().length > 0) {
- return `${address.name} <${address.email}>`
- }
- return address.email
-}
-
-function formatDate(input: string) {
- const date = new Date(input)
- return date.toLocaleString()
-}
-
-export default function MessageItem({ message, active, onSelect }: MessageItemProps) {
- return (
-
-
-
- )
-}
diff --git a/src/app/components/mail/MessageView.tsx b/src/app/components/mail/MessageView.tsx
deleted file mode 100644
index 7920af5..0000000
--- a/src/app/components/mail/MessageView.tsx
+++ /dev/null
@@ -1,295 +0,0 @@
-'use client'
-
-import { useCallback, useMemo, useState } from 'react'
-import useSWR from 'swr'
-import { ArrowLeft, FileDown, Loader2, Reply, Sparkles, Trash2 } from 'lucide-react'
-
-import {
- classifyMessage,
- deleteMessage,
- fetchMessage,
- suggestReplies,
- summarizeMessage,
-} from '@lib/mail/apiClient'
-import type { MailMessageDetail } from '@lib/mail/types'
-
-interface MessageViewProps {
- tenantId: string
- messageId: string | null
- onDeleted?: () => void
- onRefreshed?: () => void
- showBackButton?: boolean
- onBack?: () => void
-}
-
-function formatAddress(address: { name?: string; email: string }) {
- if (address.name && address.name.trim()) {
- return `${address.name} <${address.email}>`
- }
- return address.email
-}
-
-function renderHtml(content?: string) {
- if (!content) {
- return null
- }
- return { __html: content }
-}
-
-export default function MessageView({ tenantId, messageId, onDeleted, onRefreshed, showBackButton, onBack }: MessageViewProps) {
- const [aiBusy, setAiBusy] = useState(false)
- const message = useSWR(
- messageId ? ['mail-message', tenantId, messageId] : null,
- () => fetchMessage(tenantId, messageId!),
- )
-
- const handleSummarize = useCallback(async () => {
- if (!messageId) {
- return
- }
- try {
- setAiBusy(true)
- const summary = await summarizeMessage(tenantId, { messageId })
- message.mutate((prev) => (prev ? { ...prev, aiInsights: summary } : prev), false)
- onRefreshed?.()
- } finally {
- setAiBusy(false)
- }
- }, [message, messageId, onRefreshed, tenantId])
-
- const handleSuggestReplies = useCallback(async () => {
- if (!messageId) {
- return
- }
- try {
- setAiBusy(true)
- const { suggestions } = await suggestReplies(tenantId, { messageId, style: 'concise', language: 'zh' })
- message.mutate((prev) =>
- prev
- ? {
- ...prev,
- aiInsights: {
- ...(prev.aiInsights ?? { summary: '', bullets: [], actions: [], tone: '' }),
- suggestions,
- },
- }
- : prev,
- false,
- )
- onRefreshed?.()
- } finally {
- setAiBusy(false)
- }
- }, [message, messageId, onRefreshed, tenantId])
-
- const handleDelete = useCallback(async () => {
- if (!messageId) {
- return
- }
- await deleteMessage(tenantId, messageId)
- message.mutate(undefined, false)
- onDeleted?.()
- }, [message, messageId, onDeleted, tenantId])
-
- const handleClassify = useCallback(async () => {
- if (!messageId) {
- return
- }
- try {
- setAiBusy(true)
- const classification = await classifyMessage(tenantId, messageId)
- message.mutate((prev) =>
- prev
- ? {
- ...prev,
- labels: Array.from(new Set([...(prev.labels ?? []), ...classification.labels])),
- }
- : prev,
- false,
- )
- onRefreshed?.()
- } finally {
- setAiBusy(false)
- }
- }, [message, messageId, onRefreshed, tenantId])
-
- const insights = message.data?.aiInsights
-
- const header = useMemo(() => {
- if (!message.data) {
- return null
- }
- return (
-
-
-
- {showBackButton ? (
-
- ) : null}
-
{message.data.subject}
-
-
-
-
-
-
-
-
-
-
- 来自:{formatAddress(message.data.from)}
-
- 发送时间:{new Date(message.data.date).toLocaleString()}
-
- 收件人:{message.data.to.map((recipient) => formatAddress(recipient)).join(', ')}
-
- {message.data.cc?.length ? (
- 抄送:{message.data.cc.map((recipient) => formatAddress(recipient)).join(', ')}
- ) : null}
-
-
- )
- }, [aiBusy, handleClassify, handleDelete, handleSummarize, handleSuggestReplies, message.data, onBack, showBackButton])
-
- if (!messageId) {
- return (
-
-
-
选择左侧邮件以查看详情
-
AI 摘要、智能回复与行动项将在此处展示。
-
- )
- }
-
- if (message.error) {
- return (
-
-
无法加载邮件
-
{message.error instanceof Error ? message.error.message : 'Unknown error'}
-
- )
- }
-
- if (!message.data) {
- return (
-
- )
- }
-
- return (
-
- {header}
-
- {insights ? (
-
-
-
AI 摘要
- {insights.tone ?
语气:{insights.tone} : null}
-
-
{insights.summary}
- {insights.bullets.length ? (
-
- {insights.bullets.map((item) => (
- - {item}
- ))}
-
- ) : null}
- {insights.actions.length ? (
-
-
建议行动
-
- {insights.actions.map((item) => (
- - {item}
- ))}
-
-
- ) : null}
- {insights.suggestions?.length ? (
-
-
回复建议
-
- {insights.suggestions.map((suggestion, index) => (
- -
- {suggestion}
-
- ))}
-
-
- ) : null}
-
- ) : null}
-
- {message.data.html ? (
-
- ) : (
-
- {message.data.text ?? message.data.snippet}
-
- )}
-
- {message.data.attachments?.length ? (
-
-
附件
-
- {message.data.attachments.map((attachment) => (
- -
-
-
{attachment.fileName}
-
{attachment.contentType}
-
- {attachment.downloadUrl ? (
-
- 下载
-
- ) : null}
-
- ))}
-
-
- ) : null}
-
-
- )
-}
diff --git a/src/app/components/mail/Toolbar.tsx b/src/app/components/mail/Toolbar.tsx
deleted file mode 100644
index 5d276fd..0000000
--- a/src/app/components/mail/Toolbar.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-'use client'
-
-import Link from 'next/link'
-import { useState } from 'react'
-import { Filter, Loader2, MailPlus, RefreshCw, Search } from 'lucide-react'
-
-import { useMailStore } from '../../store/mail.store'
-import type { MailLabel } from '@lib/mail/types'
-
-interface ToolbarProps {
- tenantId: string
- tenantName: string
- loading?: boolean
- refresh: () => void
- labels: MailLabel[]
-}
-
-const PRESET_FILTERS: Array<{ id: string | null; label: string }> = [
- { id: null, label: '全部' },
- { id: 'unread', label: '未读' },
- { id: 'starred', label: '星标' },
- { id: 'important', label: '重要' },
-]
-
-export default function Toolbar({ tenantId, tenantName, loading, refresh, labels }: ToolbarProps) {
- const { label, setLabel, search, setSearch } = useMailStore()
- const [expanded, setExpanded] = useState(false)
-
- return (
-
-
-
-
- {tenantName}
-
-
-
-
- 写邮件
-
-
-
-
- {PRESET_FILTERS.map((filter) => (
-
- ))}
-
-
- {expanded ? (
-
- {labels.map((item) => (
-
- ))}
-
- ) : null}
-
-
- setSearch(event.target.value)}
- placeholder="搜索发件人、主题或内容"
- className="flex-1 bg-transparent text-sm text-[var(--color-text)] focus:outline-none"
- />
-
-
- )
-}
diff --git a/src/app/demo/DemoContent.tsx b/src/app/demo/DemoContent.tsx
deleted file mode 100644
index c694a18..0000000
--- a/src/app/demo/DemoContent.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-'use client'
-
-import { useLanguage } from '@i18n/LanguageProvider'
-import { translations } from '@i18n/translations'
-
-export default function DemoContent() {
- const { language } = useLanguage()
- const { account } = translations[language].nav
-
- return (
-
-
{account.demo}
-
- )
-}
diff --git a/src/app/demo/markdown/page.tsx b/src/app/demo/markdown/page.tsx
deleted file mode 100644
index aef7f5d..0000000
--- a/src/app/demo/markdown/page.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import HomepageLanding from '@modules/homepage/page'
-
-export const dynamic = 'force-static'
-
-export default function MarkdownDemoPage() {
- return
-}
diff --git a/src/app/demo/page.tsx b/src/app/demo/page.tsx
deleted file mode 100644
index 541c37d..0000000
--- a/src/app/demo/page.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-export const dynamic = 'error'
-
-import { notFound } from 'next/navigation'
-
-import { isFeatureEnabled } from '@lib/featureToggles'
-import DemoContent from './DemoContent'
-
-export default function DemoPage() {
- if (!isFeatureEnabled('appModules', '/demo')) {
- notFound()
- }
-
- return
-}
diff --git a/src/app/demo/theme/page.tsx b/src/app/demo/theme/page.tsx
deleted file mode 100644
index 035bf6f..0000000
--- a/src/app/demo/theme/page.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-'use client'
-
-import ThemePreferenceCard from '../../panel/account/ThemePreferenceCard'
-import Card from '../../panel/components/Card'
-import { useTheme } from '@components/theme'
-import type { ThemeTokens } from '@components/theme'
-
-const COLOR_TOKENS: Array<{ key: keyof ThemeTokens['colors']; label: string }> = [
- { key: 'background', label: '背景 Background' },
- { key: 'surface', label: '表面 Surface' },
- { key: 'surface-muted', label: '次级表面 Muted surface' },
- { key: 'text', label: '正文 Text' },
- { key: 'text-subtle', label: '辅助文本 Muted text' },
- { key: 'primary', label: '主色 Primary' },
- { key: 'primary-muted', label: '主色衬底 Primary muted' },
- { key: 'primary-foreground', label: '主色文字 Primary foreground' },
-]
-
-export default function ThemeShowcasePage() {
- const { resolvedTheme, tokens } = useTheme()
- const { colors } = tokens
-
- return (
-
-
-
-
-
-
- 当前主题 token
-
- 这些颜色变量会注入到 document.documentElement 上,可用于 Tailwind
- CSS 的自定义颜色或手写样式。
-
-
- {COLOR_TOKENS.map(({ key, label }) => {
- const value = colors[key]
- return (
-
- {label}
-
- {value}
-
- )
- })}
-
-
-
- )
-}
diff --git a/src/app/panel/mail/page.tsx b/src/app/panel/mail/page.tsx
deleted file mode 100644
index 251683a..0000000
--- a/src/app/panel/mail/page.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-export const dynamic = 'error'
-
-import { redirect } from 'next/navigation'
-
-import { resolveExtensionRouteComponent } from '@extensions/loader'
-
-export default async function MailPage() {
- try {
- const Component = await resolveExtensionRouteComponent('/panel/mail')
- return
- } catch (error) {
- if (error instanceof Error && error.message.includes('disabled')) {
- redirect('/panel')
- }
- throw error
- }
-}
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts
index 588a978..e48182f 100644
--- a/src/app/sitemap.ts
+++ b/src/app/sitemap.ts
@@ -2,7 +2,6 @@ import type { MetadataRoute } from 'next'
import { getBlogPosts } from '@/lib/blogContent'
import { getDocCollections } from '@/lib/docContent'
-import { allWorkshops } from 'contentlayer/generated'
import { PRODUCT_LIST } from '@/modules/products/registry'
const baseUrl = 'https://console.svc.plus'
@@ -44,11 +43,7 @@ export default async function sitemap(): Promise {
changeFrequency: 'monthly',
priority: 0.6,
},
- {
- url: `${baseUrl}/workshop`,
- changeFrequency: 'monthly',
- priority: 0.6,
- },
+
{
url: `${baseUrl}/cloud_iac`,
changeFrequency: 'monthly',
@@ -88,12 +83,5 @@ export default async function sitemap(): Promise {
})),
)
- const workshopEntries: MetadataRoute.Sitemap = allWorkshops.map((workshop) => ({
- url: `${baseUrl}/workshop/${workshop.slug}`,
- lastModified: workshop.updatedAt ? new Date(workshop.updatedAt) : undefined,
- changeFrequency: 'monthly',
- priority: 0.6,
- }))
-
- return [...staticEntries, ...productEntries, ...blogEntries, ...docsEntries, ...workshopEntries]
+ return [...staticEntries, ...productEntries, ...blogEntries, ...docsEntries]
}
diff --git a/src/app/store/mail.store.ts b/src/app/store/mail.store.ts
deleted file mode 100644
index df87924..0000000
--- a/src/app/store/mail.store.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-'use client'
-
-import { create } from 'zustand'
-
-interface MailState {
- tenantId: string | null
- selectedMessageId: string | null
- label: string | null
- search: string
- pageSize: number
- cursor: string | null
- setTenant: (tenantId: string) => void
- setSelectedMessageId: (id: string | null) => void
- setLabel: (label: string | null) => void
- setSearch: (term: string) => void
- setCursor: (cursor: string | null) => void
- setPageSize: (size: number) => void
- reset: () => void
-}
-
-const DEFAULT_STATE: Omit = {
- tenantId: null,
- selectedMessageId: null,
- label: null,
- search: '',
- pageSize: 25,
- cursor: null,
-}
-
-export const useMailStore = create((set) => ({
- ...DEFAULT_STATE,
- setTenant: (tenantId: string) =>
- set((state) => ({
- ...DEFAULT_STATE,
- tenantId,
- search: state.search,
- })),
- setSelectedMessageId: (id) => set({ selectedMessageId: id }),
- setLabel: (label) =>
- set((state) => ({
- label,
- cursor: null,
- selectedMessageId: state.selectedMessageId,
- })),
- setSearch: (term) =>
- set((state) => ({
- search: term,
- cursor: null,
- selectedMessageId: state.selectedMessageId,
- })),
- setCursor: (cursor) => set({ cursor }),
- setPageSize: (size) => set({ pageSize: size }),
- reset: () => set(DEFAULT_STATE),
-}))
diff --git a/src/app/workshop/[slug]/page.tsx b/src/app/workshop/[slug]/page.tsx
deleted file mode 100644
index 597f56e..0000000
--- a/src/app/workshop/[slug]/page.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-export const dynamic = 'error'
-export const revalidate = false
-
-import { notFound } from 'next/navigation'
-import type { Metadata } from 'next'
-
-import WorkshopArticle from '@/components/workshop/WorkshopArticle'
-import { allWorkshops } from 'contentlayer/generated'
-
-export const generateStaticParams = async () => allWorkshops.map((workshop) => ({ slug: workshop.slug }))
-
-export async function generateMetadata({
- params,
-}: {
- params: { slug: string }
-}): Promise {
- const workshop = allWorkshops.find((entry) => entry.slug === params.slug)
- if (!workshop) {
- return { title: 'Workshop | Cloud-Neutral' }
- }
- return {
- title: `${workshop.title} | Workshop`,
- description: workshop.summary,
- }
-}
-
-export default function WorkshopDetailPage({ params }: { params: { slug: string } }) {
- const workshop = allWorkshops.find((entry) => entry.slug === params.slug)
- if (!workshop) {
- notFound()
- }
-
- return (
-
-
-
- )
-}
diff --git a/src/app/workshop/page.tsx b/src/app/workshop/page.tsx
deleted file mode 100644
index 83ef9a5..0000000
--- a/src/app/workshop/page.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import type { Metadata } from 'next'
-
-import WorkshopCard from '@/components/workshop/WorkshopCard'
-import { allWorkshops } from 'contentlayer/generated'
-
-export const dynamic = 'error'
-export const revalidate = false
-
-export const metadata: Metadata = {
- title: 'Workshops | Cloud-Neutral',
- description: 'Hands-on, short-lived experiments built with MDX and Contentlayer.',
-}
-
-export default function WorkshopIndexPage() {
- const workshops = [...allWorkshops].sort((a, b) => {
- const aDate = a.updatedAt ? Date.parse(a.updatedAt) : 0
- const bDate = b.updatedAt ? Date.parse(b.updatedAt) : 0
- if (aDate && bDate && aDate !== bDate) return bDate - aDate
- return a.title.localeCompare(b.title)
- })
-
- return (
-
-
-
-
- {workshops.length === 0 ? (
-
- Workshops will appear here once content is published.
-
- ) : (
-
- {workshops.map((workshop) => (
-
- ))}
-
- )}
-
-
- )
-}
diff --git a/src/components/Features.tsx b/src/components/Features.tsx
index 19a71f1..066073c 100644
--- a/src/components/Features.tsx
+++ b/src/components/Features.tsx
@@ -88,7 +88,7 @@ const productCards: Record<'zh' | 'en', FeatureCard[]> = {
{
title: 'XCloudFlow',
description: 'Pulumi 引擎驱动的多云 IaC,统一 DevOps 与合规审计。',
- href: '/demo?product=xcloudflow',
+ href: '/xcloudflow',
badge: 'IaC + GitOps',
},
{
@@ -108,7 +108,7 @@ const productCards: Record<'zh' | 'en', FeatureCard[]> = {
{
title: 'XCloudFlow',
description: 'Multi-cloud IaC powered by Pulumi with built-in compliance guardrails.',
- href: '/demo?product=xcloudflow',
+ href: '/xcloudflow',
badge: 'IaC + GitOps',
},
{
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index f043ed0..d0a289b 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -165,12 +165,6 @@ export default function Navbar() {
href: "/login",
togglePath: "/login",
},
- {
- key: "demo",
- label: nav.account.demo,
- href: "/demo",
- togglePath: "/demo",
- },
];
const accountLabel = nav.account.title;
@@ -423,8 +417,8 @@ export default function Navbar() {
key={tab.key}
href={tab.href}
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium whitespace-nowrap transition-colors ${tab.active
- ? "bg-primary/10 text-primary border border-primary/20"
- : "text-text-muted hover:text-text hover:bg-surface-muted border border-transparent"
+ ? "bg-primary/10 text-primary border border-primary/20"
+ : "text-text-muted hover:text-text hover:bg-surface-muted border border-transparent"
}`}
>
@@ -663,8 +657,8 @@ export default function Navbar() {
setMenuOpen(false)}
>
@@ -674,8 +668,8 @@ export default function Navbar() {
setMenuOpen(false)}
>
@@ -692,8 +686,8 @@ export default function Navbar() {
setMenuOpen(false)}
>
@@ -703,8 +697,8 @@ export default function Navbar() {
setMenuOpen(false)}
>
@@ -726,8 +720,8 @@ export default function Navbar() {
setMenuOpen(false)}
>
@@ -749,8 +743,8 @@ export default function Navbar() {
setMenuOpen(false)}
>
@@ -761,8 +755,8 @@ export default function Navbar() {
setMenuOpen(false)}
>
diff --git a/src/components/workshop/WorkshopArticle.tsx b/src/components/workshop/WorkshopArticle.tsx
deleted file mode 100644
index fff5b35..0000000
--- a/src/components/workshop/WorkshopArticle.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-'use client'
-
-import { useMDXComponent } from 'next-contentlayer/hooks'
-
-import WorkshopDemo from './WorkshopDemo'
-
-interface WorkshopArticleProps {
- code: string
-}
-
-export default function WorkshopArticle({ code }: WorkshopArticleProps) {
- const MDXContent = useMDXComponent(code)
- return (
-
-
-
- )
-}
diff --git a/src/components/workshop/WorkshopCard.tsx b/src/components/workshop/WorkshopCard.tsx
deleted file mode 100644
index 0eadb9d..0000000
--- a/src/components/workshop/WorkshopCard.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import Link from 'next/link'
-
-import type { Workshop } from 'contentlayer/generated'
-
-interface WorkshopCardProps {
- workshop: Workshop
-}
-
-export default function WorkshopCard({ workshop }: WorkshopCardProps) {
- return (
-
-
-
Workshop
-
{workshop.title}
-
{workshop.summary}
- {workshop.tags?.length ? (
-
- {workshop.tags.map((tag) => (
-
- {tag}
-
- ))}
-
- ) : null}
-
-
-
- {workshop.level}
- {workshop.duration && {workshop.duration}}
-
-
- View
-
-
-
- )
-}
diff --git a/src/components/workshop/WorkshopDemo.tsx b/src/components/workshop/WorkshopDemo.tsx
deleted file mode 100644
index f1697d2..0000000
--- a/src/components/workshop/WorkshopDemo.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-'use client'
-
-import { useState } from 'react'
-
-export default function WorkshopDemo() {
- const [environment, setEnvironment] = useState<'staging' | 'production'>('staging')
- const [enabled, setEnabled] = useState(false)
-
- return (
-
-
-
-
Live Toggle
-
Switch environments to preview workshop actions.
-
-
-
-
- {(['staging', 'production'] as const).map((item) => {
- const isActive = environment === item
- return (
-
- )
- })}
-
-
-
- {enabled ? 'Automation ready' : 'Preview mode'} · {environment}
-
-
Stateful interactions stay inside workshop scope.
-
-
- )
-}
diff --git a/src/content/workshop/fast-feedback-loops.mdx b/src/content/workshop/fast-feedback-loops.mdx
deleted file mode 100644
index 4647fad..0000000
--- a/src/content/workshop/fast-feedback-loops.mdx
+++ /dev/null
@@ -1,25 +0,0 @@
----
-title: Fast Feedback Loops
-summary: Prototype change workflows with interactive toggles before wiring production automation.
-level: Intermediate
-duration: 25 min
-tags:
- - gitops
- - automation
- - rollout
-updatedAt: 2024-12-01
----
-
-Live demos in this workshop stay close to how engineers actually deploy. Toggle environments, flip feature states, and observe how the rollout recipe responds in real time.
-
-## What you will build
-
-- A staged toggle flow that mirrors your GitOps pipelines.
-- A small guardrail to ensure approvals stay attached to risky rollouts.
-- A preview card that highlights the blast radius of each action.
-
-## Try it now
-
-
-
-Because the MDX runs through Contentlayer, interactive components compile at build time while preserving state on the client.
diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts
index 9ba7ca7..2f098e0 100644
--- a/src/i18n/translations.ts
+++ b/src/i18n/translations.ts
@@ -482,7 +482,6 @@ type UserCenterTranslation = {
}
items: {
dashboard: string
- mail: string
agents: string
apis: string
accounts: string
@@ -977,7 +976,6 @@ export const translations: Record<'en' | 'zh', Translation> = {
},
items: {
dashboard: 'Dashboard',
- mail: 'Mail',
agents: 'Agents',
apis: 'APIs',
accounts: 'Accounts',
@@ -1749,7 +1747,6 @@ export const translations: Record<'en' | 'zh', Translation> = {
},
items: {
dashboard: '仪表盘',
- mail: '邮箱服务',
agents: '运行节点',
apis: '接口集成',
accounts: '账户中心',
diff --git a/src/lib/mail/apiClient.ts b/src/lib/mail/apiClient.ts
deleted file mode 100644
index dbb7c16..0000000
--- a/src/lib/mail/apiClient.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-'use client'
-
-import type {
- ClassifyResponse,
- ComposePayload,
- MailInboxResponse,
- MailMessageDetail,
- NamespacePolicy,
- ReplySuggestionRequest,
- ReplySuggestionResponse,
- SummarizeRequest,
- SummarizeResponse,
-} from './types'
-import { buildTenantHeaders } from './auth'
-
-const MAIL_API_BASE = '/api/mail'
-
-async function request(path: string, init: RequestInit & { tenantId: string }): Promise {
- const { tenantId, headers, ...rest } = init
- const response = await fetch(`${MAIL_API_BASE}${path}`, {
- ...rest,
- headers: {
- Accept: 'application/json',
- 'Content-Type': 'application/json',
- ...buildTenantHeaders(tenantId),
- ...(headers as Record | undefined),
- },
- cache: 'no-store',
- })
-
- if (!response.ok) {
- const message = await response.text().catch(() => 'Request failed')
- throw new Error(message || `Failed to fetch ${path}`)
- }
-
- if (response.status === 204) {
- return null as T
- }
-
- return (await response.json()) as T
-}
-
-export function fetchInbox(tenantId: string, params: { cursor?: string | null; pageSize?: number; label?: string | null; q?: string | null }) {
- const url = new URL(`${MAIL_API_BASE}/inbox`, typeof window !== 'undefined' ? window.location.origin : 'http://localhost')
- if (params.cursor) {
- url.searchParams.set('cursor', params.cursor)
- }
- if (params.pageSize) {
- url.searchParams.set('pageSize', String(params.pageSize))
- }
- if (params.label) {
- url.searchParams.set('label', params.label)
- }
- if (params.q) {
- url.searchParams.set('q', params.q)
- }
- return request(`${url.pathname}${url.search}`, { method: 'GET', tenantId })
-}
-
-export function fetchMessage(tenantId: string, messageId: string) {
- return request(`/message/${messageId}`, { method: 'GET', tenantId })
-}
-
-export function sendMessage(tenantId: string, payload: ComposePayload) {
- return request<{ success: boolean; message: MailMessageDetail }>(`/send`, {
- method: 'POST',
- tenantId,
- body: JSON.stringify(payload),
- })
-}
-
-export function deleteMessage(tenantId: string, messageId: string) {
- return request<{ success: boolean }>(`/message/${messageId}`, {
- method: 'DELETE',
- tenantId,
- })
-}
-
-export function summarizeMessage(tenantId: string, payload: SummarizeRequest) {
- return request(`/ai/summarize`, {
- method: 'POST',
- tenantId,
- body: JSON.stringify(payload),
- })
-}
-
-export function suggestReplies(tenantId: string, payload: ReplySuggestionRequest) {
- return request(`/ai/reply-suggest`, {
- method: 'POST',
- tenantId,
- body: JSON.stringify(payload),
- })
-}
-
-export function classifyMessage(tenantId: string, messageId: string) {
- return request(`/ai/classify`, {
- method: 'POST',
- tenantId,
- body: JSON.stringify({ messageId }),
- })
-}
-
-export function fetchNamespacePolicy(tenantId: string) {
- return request(`/namespace`, {
- method: 'GET',
- tenantId,
- })
-}
-
-export function updateNamespacePolicy(tenantId: string, policy: Partial) {
- return request(`/namespace`, {
- method: 'PUT',
- tenantId,
- body: JSON.stringify(policy),
- })
-}
diff --git a/src/lib/mail/auth.ts b/src/lib/mail/auth.ts
deleted file mode 100644
index 0503597..0000000
--- a/src/lib/mail/auth.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-'use client'
-
-import { useMemo } from 'react'
-
-import { useUserStore } from '@lib/userStore'
-
-export function useTenantAuthContext() {
- const user = useUserStore((state) => state.user)
-
- return useMemo(() => {
- const memberships = user?.tenants ?? []
- const defaultTenant =
- memberships.find((tenant) => tenant.id === user?.tenantId) ?? memberships[0] ?? (user?.tenantId ? { id: user.tenantId } : null)
-
- return {
- user,
- defaultTenantId: defaultTenant?.id,
- tenants: memberships,
- }
- }, [user])
-}
-
-export function buildTenantHeaders(tenantId: string, token?: string) {
- const headers: Record = {
- 'x-tenant-id': tenantId,
- }
- if (token) {
- headers.Authorization = token.startsWith('Bearer ') ? token : `Bearer ${token}`
- }
- return headers
-}
diff --git a/src/lib/mail/types.ts b/src/lib/mail/types.ts
deleted file mode 100644
index 5bde781..0000000
--- a/src/lib/mail/types.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-export type MailAddress = {
- name?: string
- email: string
-}
-
-export type MailAttachment = {
- id: string
- fileName: string
- contentType: string
- size: number
- downloadUrl?: string
-}
-
-export type MailListMessage = {
- id: string
- subject: string
- snippet: string
- from: MailAddress
- to: MailAddress[]
- date: string
- unread: boolean
- starred?: boolean
- labels: string[]
- hasAttachments?: boolean
- aiSummary?: {
- preview: string
- tone?: string
- }
-}
-
-export type MailMessageDetail = {
- id: string
- subject: string
- snippet: string
- from: MailAddress
- to: MailAddress[]
- cc?: MailAddress[]
- bcc?: MailAddress[]
- date: string
- unread: boolean
- starred?: boolean
- labels: string[]
- text?: string
- html?: string
- attachments?: MailAttachment[]
- aiInsights?: MailInsights
-}
-
-export type MailInsights = {
- summary: string
- bullets: string[]
- actions: string[]
- tone: string
- suggestions?: string[]
-}
-
-export type MailInboxResponse = {
- messages: MailListMessage[]
- labels: MailLabel[]
- unreadCount: number
- nextCursor?: string | null
-}
-
-export type MailLabel = {
- id: string
- name: string
- color?: string
- unread?: number
-}
-
-export type ComposePayload = {
- to: string[]
- cc?: string[]
- bcc?: string[]
- subject: string
- text?: string
- html?: string
- attachments?: string[]
-}
-
-export type NamespacePolicy = {
- model: string
- temperature: number
- maxTokens: number
- rateLimitPerMinute: number
- vectorIndex?: string
- policy?: string
- updatedAt: string
-}
-
-export type ReplySuggestionRequest = {
- messageId: string
- style?: 'concise' | 'formal' | 'technical'
- language?: 'zh' | 'en'
-}
-
-export type ReplySuggestionResponse = {
- suggestions: string[]
-}
-
-export type SummarizeRequest = {
- messageId?: string
- raw?: string
-}
-
-export type SummarizeResponse = MailInsights
-
-export type ClassifyResponse = {
- labels: string[]
-}
diff --git a/src/lib/marketingContent.ts b/src/lib/marketingContent.ts
index 45df8a6..855b489 100644
--- a/src/lib/marketingContent.ts
+++ b/src/lib/marketingContent.ts
@@ -105,7 +105,7 @@ const HERO_SOLUTIONS: HeroSolution[] = [
bodyHtml:
'XCloudFlow 将 Terraform、Pulumi 等主流 IaC 模型统一到一个工作台,为多云环境提供自助式交付与集中治理。
',
primaryCtaLabel: '立刻体验',
- primaryCtaHref: '/demo?product=xcloudflow',
+ primaryCtaHref: '/xcloudflow',
secondaryCtaLabel: '下载链接',
secondaryCtaHref: '/download?product=xcloudflow',
tertiaryCtaLabel: '文档链接',
@@ -120,7 +120,7 @@ const HERO_SOLUTIONS: HeroSolution[] = [
bodyHtml:
'XScopeHub 通过语义化检索与时序分析,实现跨环境的可观察性汇聚与智能洞察。
',
primaryCtaLabel: '立刻体验',
- primaryCtaHref: '/demo?product=xscopehub',
+ primaryCtaHref: '/xscopehub',
secondaryCtaLabel: '下载链接',
secondaryCtaHref: '/download?product=xscopehub',
tertiaryCtaLabel: '文档链接',
@@ -135,7 +135,7 @@ const HERO_SOLUTIONS: HeroSolution[] = [
bodyHtml:
'XStream 通过软件定义的网络加速技术,为实时互动、音视频与数据分发提供稳定的全球链路。
',
primaryCtaLabel: '立刻体验',
- primaryCtaHref: '/demo?product=xstream',
+ primaryCtaHref: '/xstream',
secondaryCtaLabel: '下载链接',
secondaryCtaHref: '/download?product=xstream',
tertiaryCtaLabel: '文档链接',
diff --git a/src/modules/extensions/builtin/user-center/index.ts b/src/modules/extensions/builtin/user-center/index.ts
index 72ef02d..85e6f5a 100644
--- a/src/modules/extensions/builtin/user-center/index.ts
+++ b/src/modules/extensions/builtin/user-center/index.ts
@@ -1,4 +1,4 @@
-import { Code, CreditCard, Home, Mail, Palette, Server, Settings, Shield, User } from 'lucide-react'
+import { Code, CreditCard, Home, Palette, Server, Settings, Shield, User } from 'lucide-react'
import type { DashboardExtension } from '../../types'
@@ -24,17 +24,6 @@ export const userCenterExtension: DashboardExtension = {
redirect: { unauthenticated: '/login' },
sidebar: { section: 'workspace', order: 0 },
},
- {
- id: 'mail',
- path: '/panel/mail',
- label: 'Mail',
- description: '租户邮件与 AI 助理',
- icon: Mail,
- loader: () => import('./routes/mail'),
- guard: { requireLogin: true },
- redirect: { unauthenticated: '/login' },
- sidebar: { section: 'workspace', order: 1 },
- },
{
id: 'agents',
path: '/panel/agent',
diff --git a/src/modules/extensions/builtin/user-center/routes/mail.tsx b/src/modules/extensions/builtin/user-center/routes/mail.tsx
deleted file mode 100644
index 73d0595..0000000
--- a/src/modules/extensions/builtin/user-center/routes/mail.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import MailCenter from '../../../../../app/components/mail/MailCenter'
-
-export default function MailRoute() {
- return
-}