docs: add product usage and operations guides
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
.vite/
|
||||||
|
coverage/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
src/generated/
|
||||||
|
|
||||||
|
# Generated runtime artifacts
|
||||||
|
public/search-index-*.json
|
||||||
|
public/changelog-index-*.json
|
||||||
|
public/doc-git-meta.json
|
||||||
|
public/llms.txt
|
||||||
|
public/llms-full.txt
|
||||||
|
public/feed.xml
|
||||||
|
public/sitemap.xml
|
||||||
|
public/access/
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>无限邮箱文档</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "docs",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"prepare:docs": "react-docs-ui generate-doc-git-meta && react-docs-ui generate-llms-files && react-docs-ui generate-changelog-index",
|
||||||
|
"predev": "react-docs-ui generate-doc-git-meta && react-docs-ui generate-llms-files && react-docs-ui generate-changelog-index && react-docs-ui generate-sitemap && react-docs-ui generate-feed && react-docs-ui generate-search-index && react-docs-ui prepare-static-access",
|
||||||
|
"dev": "react-docs-ui generate-shiki-bundle && vite",
|
||||||
|
"prebuild": "react-docs-ui generate-doc-git-meta && react-docs-ui generate-llms-files && react-docs-ui generate-changelog-index && react-docs-ui generate-sitemap && react-docs-ui generate-feed",
|
||||||
|
"build": "react-docs-ui generate-search-index && react-docs-ui prepare-static-access && react-docs-ui generate-shiki-bundle && tsc -b && vite build && react-docs-ui finalize-static-access",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build:llms": "react-docs-ui generate-llms-files",
|
||||||
|
"build:search": "react-docs-ui generate-search-index",
|
||||||
|
"build:shiki": "react-docs-ui generate-shiki-bundle"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-docs-ui": "0.9.5",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.13.2",
|
||||||
|
"shiki": "^4.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.1.9",
|
||||||
|
"@types/react-dom": "^19.1.7",
|
||||||
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
|
"typescript": "^6.0.2",
|
||||||
|
"vite": "^8.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+4614
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
|||||||
|
allowBuilds:
|
||||||
|
core-js: false
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
# yaml-language-server: $schema=./site.schema.json
|
||||||
|
site:
|
||||||
|
title: "Email Unlimited Docs"
|
||||||
|
description: "User, deployment, and operations documentation for Email Unlimited"
|
||||||
|
url: "https://mail.shenjianl.cn"
|
||||||
|
logo:
|
||||||
|
light: "/images/favicon.svg"
|
||||||
|
dark: "/images/favicon-dark.svg"
|
||||||
|
author: "Email Unlimited"
|
||||||
|
|
||||||
|
navbar:
|
||||||
|
showLogo: true
|
||||||
|
showTitle: true
|
||||||
|
showLanguageSwitcher: true
|
||||||
|
items:
|
||||||
|
- title: "Home"
|
||||||
|
link: "/"
|
||||||
|
active: true
|
||||||
|
- title: "Documentation"
|
||||||
|
link: "/docs"
|
||||||
|
actions:
|
||||||
|
- type: "gitea"
|
||||||
|
link: "https://gitea.shenjianl.cn/shenjianZ/email-unlimit"
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
announcement:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
versions:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
enabled: false
|
||||||
|
title: "Changelog"
|
||||||
|
pageSize: 10
|
||||||
|
|
||||||
|
sidebar:
|
||||||
|
enabled: true
|
||||||
|
collapseControl:
|
||||||
|
enabled: true
|
||||||
|
defaultCollapsed: false
|
||||||
|
collections:
|
||||||
|
docs:
|
||||||
|
sections:
|
||||||
|
- title: "Getting Started"
|
||||||
|
path: "/docs/getting-started"
|
||||||
|
children:
|
||||||
|
- title: "Introduction"
|
||||||
|
path: "/docs/getting-started/introduction"
|
||||||
|
- title: "Local Development"
|
||||||
|
path: "/docs/getting-started/local-development"
|
||||||
|
- title: "Docker Deployment"
|
||||||
|
path: "/docs/getting-started/docker-deployment"
|
||||||
|
- title: "DNS and SMTP"
|
||||||
|
path: "/docs/getting-started/dns-and-smtp"
|
||||||
|
- title: "User Guide"
|
||||||
|
path: "/docs/user-guide"
|
||||||
|
children:
|
||||||
|
- title: "Account and Credits"
|
||||||
|
path: "/docs/user-guide/account-and-credits"
|
||||||
|
- title: "Mailbox Management"
|
||||||
|
path: "/docs/user-guide/mailboxes"
|
||||||
|
- title: "Inbox and Attachments"
|
||||||
|
path: "/docs/user-guide/inbox"
|
||||||
|
- title: "Admin Guide"
|
||||||
|
path: "/docs/admin-guide"
|
||||||
|
children:
|
||||||
|
- title: "Operations Dashboard"
|
||||||
|
path: "/docs/admin-guide/dashboard"
|
||||||
|
- title: "Resource and Mail Governance"
|
||||||
|
path: "/docs/admin-guide/governance"
|
||||||
|
- title: "Credits and Audit"
|
||||||
|
path: "/docs/admin-guide/credits-and-audit"
|
||||||
|
- title: "Operations"
|
||||||
|
path: "/docs/operations"
|
||||||
|
children:
|
||||||
|
- title: "Configuration"
|
||||||
|
path: "/docs/operations/configuration"
|
||||||
|
- title: "Backup and Restore"
|
||||||
|
path: "/docs/operations/backup-and-restore"
|
||||||
|
- title: "Troubleshooting"
|
||||||
|
path: "/docs/operations/troubleshooting"
|
||||||
|
- title: "API Conventions"
|
||||||
|
path: "/docs/api"
|
||||||
|
children:
|
||||||
|
- title: "Authentication and Common Rules"
|
||||||
|
path: "/docs/api/authentication"
|
||||||
|
|
||||||
|
theme:
|
||||||
|
defaultMode: "auto"
|
||||||
|
allowToggle: true
|
||||||
|
|
||||||
|
toc:
|
||||||
|
enabled: true
|
||||||
|
maxLevel: 4
|
||||||
|
collapseControl:
|
||||||
|
enabled: true
|
||||||
|
defaultCollapsed: false
|
||||||
|
title: "On this page"
|
||||||
|
|
||||||
|
imageViewer:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
footer:
|
||||||
|
enabled: true
|
||||||
|
copyright: "© 2026 Email Unlimited"
|
||||||
|
poweredBy: true
|
||||||
|
repository:
|
||||||
|
url: "https://gitea.shenjianl.cn/shenjianZ/email-unlimit"
|
||||||
|
branch: "limit-mail-freq"
|
||||||
|
lastUpdated: "2026-07-25"
|
||||||
|
version: "v1"
|
||||||
|
groups:
|
||||||
|
- title: "Documentation"
|
||||||
|
items:
|
||||||
|
- title: "Getting Started"
|
||||||
|
link: "/docs/getting-started"
|
||||||
|
- title: "User Guide"
|
||||||
|
link: "/docs/user-guide"
|
||||||
|
- title: "Operations"
|
||||||
|
link: "/docs/operations"
|
||||||
|
- title: "Project"
|
||||||
|
items:
|
||||||
|
- title: "Source"
|
||||||
|
link: "https://gitea.shenjianl.cn/shenjianZ/email-unlimit"
|
||||||
|
external: true
|
||||||
|
- title: "License"
|
||||||
|
link: "https://gitea.shenjianl.cn/shenjianZ/email-unlimit/src/branch/limit-mail-freq/LICENSE"
|
||||||
|
external: true
|
||||||
|
links:
|
||||||
|
- title: "Documentation"
|
||||||
|
link: "/docs"
|
||||||
|
- title: "Source"
|
||||||
|
link: "https://gitea.shenjianl.cn/shenjianZ/email-unlimit"
|
||||||
|
external: true
|
||||||
|
|
||||||
|
pwa:
|
||||||
|
enabled: true
|
||||||
|
name: "Email Unlimited Docs"
|
||||||
|
shortName: "Email Unlimited"
|
||||||
|
description: "User, deployment, and operations documentation for Email Unlimited"
|
||||||
|
themeColor: "#ffffff"
|
||||||
|
backgroundColor: "#ffffff"
|
||||||
|
|
||||||
|
contextMenu:
|
||||||
|
enabled: true
|
||||||
|
page:
|
||||||
|
copySelectedText: true
|
||||||
|
copyUrl: true
|
||||||
|
copyTitle: true
|
||||||
|
copyMarkdownLink: true
|
||||||
|
openInNewTab: true
|
||||||
|
reload: true
|
||||||
|
printPage: true
|
||||||
|
scrollToTop: true
|
||||||
|
scrollToBottom: true
|
||||||
|
site:
|
||||||
|
goHome: true
|
||||||
|
quickNav: true
|
||||||
|
language: true
|
||||||
|
appearance:
|
||||||
|
theme: true
|
||||||
|
resetThemePref: true
|
||||||
|
|
||||||
|
mdx:
|
||||||
|
componentsPath: "/src/components"
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
pageMeta:
|
||||||
|
showLastUpdated: true
|
||||||
|
showEditLink: false
|
||||||
|
showAuthors: true
|
||||||
|
preferGitMeta: true
|
||||||
|
|
||||||
|
editLink:
|
||||||
|
enabled: false
|
||||||
|
label: "Edit this page"
|
||||||
|
urlTemplate: ""
|
||||||
|
|
||||||
|
feedback:
|
||||||
|
enabled: false
|
||||||
|
endpoint: "/api/feedback"
|
||||||
|
method: "POST"
|
||||||
|
includePageMeta: true
|
||||||
|
labels:
|
||||||
|
helpful: "Helpful"
|
||||||
|
unhelpful: "Not helpful"
|
||||||
|
inputPlaceholder: "Tell us what is unclear or missing"
|
||||||
|
submit: "Submit"
|
||||||
|
thanks: "Thanks for your feedback"
|
||||||
|
|
||||||
|
ai:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
backend:
|
||||||
|
enabled: false
|
||||||
|
baseUrl: "/api"
|
||||||
|
features:
|
||||||
|
auth: false
|
||||||
|
comments: false
|
||||||
|
bookmarks: false
|
||||||
|
analytics: false
|
||||||
|
feedback: false
|
||||||
|
notifications: false
|
||||||
|
admin: false
|
||||||
|
|
||||||
|
fonts:
|
||||||
|
fontFamilyZhCn: "Noto Sans SC,MiSans, PingFang SC, Microsoft YaHei, sans-serif"
|
||||||
|
fontFamilyEn: "Fragment Mono, system-ui, sans-serif"
|
||||||
|
|
||||||
|
codeHighlight:
|
||||||
|
langs:
|
||||||
|
- "bash"
|
||||||
|
- "powershell"
|
||||||
|
- "yaml"
|
||||||
|
- "toml"
|
||||||
|
- "json"
|
||||||
|
- "javascript"
|
||||||
|
- "vue"
|
||||||
|
- "tsx"
|
||||||
|
- "typescript"
|
||||||
|
- "rust"
|
||||||
|
- "sql"
|
||||||
|
- "dockerfile"
|
||||||
|
- "nginx"
|
||||||
|
- "mermaid"
|
||||||
|
lightTheme: "github-light"
|
||||||
|
darkTheme: "github-dark"
|
||||||
|
|
||||||
|
search:
|
||||||
|
enabled: true
|
||||||
|
placeholder: "Search user, deployment, and operations docs..."
|
||||||
|
maxResults: 20
|
||||||
|
snippetLength: 120
|
||||||
|
|
||||||
|
seo:
|
||||||
|
enabled: true
|
||||||
|
defaultTitle: "Email Unlimited Docs"
|
||||||
|
titleTemplate: "{title} | {siteTitle}"
|
||||||
|
defaultDescription: "User, deployment, and operations documentation for Email Unlimited"
|
||||||
|
defaultOgImage: "https://mail.shenjianl.cn/images/og-default.png"
|
||||||
|
robots: "index, follow"
|
||||||
|
twitterCard: "summary_large_image"
|
||||||
|
|
||||||
|
breadcrumb:
|
||||||
|
enabled: true
|
||||||
|
showHome: true
|
||||||
|
|
||||||
|
reading:
|
||||||
|
showTime: true
|
||||||
|
showProgress: true
|
||||||
|
fullscreen:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
sitemap:
|
||||||
|
enabled: true
|
||||||
|
changefreq: "weekly"
|
||||||
|
priority: 0.7
|
||||||
|
exclude: []
|
||||||
|
|
||||||
|
feed:
|
||||||
|
enabled: false
|
||||||
|
title: "Email Unlimited Docs"
|
||||||
|
description: "Email Unlimited documentation updates"
|
||||||
|
limit: 20
|
||||||
|
|
||||||
|
export:
|
||||||
|
enabled: true
|
||||||
|
markdown: true
|
||||||
|
pdf: true
|
||||||
|
word: true
|
||||||
|
allDocs: true
|
||||||
|
pdfServer:
|
||||||
|
enabled: false
|
||||||
|
url: "http://localhost:3001"
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"title": "React Docs UI site config",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"breadcrumb": { "$ref": "#/$defs/breadcrumb" },
|
||||||
|
"reading": { "$ref": "#/$defs/reading" },
|
||||||
|
"sidebar": { "$ref": "#/$defs/sidebar" },
|
||||||
|
"toc": { "$ref": "#/$defs/toc" },
|
||||||
|
"footer": { "$ref": "#/$defs/footer" },
|
||||||
|
"sitemap": { "$ref": "#/$defs/sitemap" },
|
||||||
|
"feed": { "$ref": "#/$defs/feed" }
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"collapseControl": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"defaultCollapsed": { "type": "boolean" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"collapseControl": { "$ref": "#/$defs/collapseControl" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"toc": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"collapseControl": { "$ref": "#/$defs/collapseControl" },
|
||||||
|
"maxLevel": { "type": "integer", "minimum": 1, "maximum": 6 },
|
||||||
|
"title": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"copyright": { "type": "string" },
|
||||||
|
"beian": { "$ref": "#/$defs/footerLink" },
|
||||||
|
"policeBeian": { "$ref": "#/$defs/footerLink" },
|
||||||
|
"poweredBy": { "type": "boolean" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"footerLink": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"text": { "type": "string" },
|
||||||
|
"url": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"breadcrumb": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"showHome": { "type": "boolean" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reading": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"showTime": { "type": "boolean" },
|
||||||
|
"showProgress": { "type": "boolean" },
|
||||||
|
"fullscreen": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sitemap": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"changefreq": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"]
|
||||||
|
},
|
||||||
|
"priority": { "type": "number", "minimum": 0, "maximum": 1 },
|
||||||
|
"exclude": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"feed": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"title": { "type": "string" },
|
||||||
|
"description": { "type": "string" },
|
||||||
|
"limit": { "type": "integer", "minimum": 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
# yaml-language-server: $schema=./site.schema.json
|
||||||
|
site:
|
||||||
|
title: "无限邮箱文档"
|
||||||
|
description: "无限邮箱的使用、部署与运维治理手册"
|
||||||
|
url: "https://mail.shenjianl.cn"
|
||||||
|
logo:
|
||||||
|
light: "/images/favicon.svg"
|
||||||
|
dark: "/images/favicon-dark.svg"
|
||||||
|
author: "Email Unlimited"
|
||||||
|
|
||||||
|
navbar:
|
||||||
|
showLogo: true
|
||||||
|
showTitle: true
|
||||||
|
showLanguageSwitcher: true
|
||||||
|
items:
|
||||||
|
- title: "首页"
|
||||||
|
link: "/"
|
||||||
|
active: true
|
||||||
|
- title: "使用文档"
|
||||||
|
link: "/docs"
|
||||||
|
actions:
|
||||||
|
- type: "gitea"
|
||||||
|
link: "https://gitea.shenjianl.cn/shenjianZ/email-unlimit"
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
announcement:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
versions:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
enabled: false
|
||||||
|
title: "更新日志"
|
||||||
|
pageSize: 10
|
||||||
|
|
||||||
|
sidebar:
|
||||||
|
enabled: true
|
||||||
|
collapseControl:
|
||||||
|
enabled: true
|
||||||
|
defaultCollapsed: false
|
||||||
|
collections:
|
||||||
|
docs:
|
||||||
|
sections:
|
||||||
|
- title: "快速开始"
|
||||||
|
path: "/docs/getting-started"
|
||||||
|
children:
|
||||||
|
- title: "产品介绍"
|
||||||
|
path: "/docs/getting-started/introduction"
|
||||||
|
- title: "本地开发"
|
||||||
|
path: "/docs/getting-started/local-development"
|
||||||
|
- title: "Docker 部署"
|
||||||
|
path: "/docs/getting-started/docker-deployment"
|
||||||
|
- title: "DNS 与 SMTP"
|
||||||
|
path: "/docs/getting-started/dns-and-smtp"
|
||||||
|
- title: "用户指南"
|
||||||
|
path: "/docs/user-guide"
|
||||||
|
children:
|
||||||
|
- title: "账号与积分"
|
||||||
|
path: "/docs/user-guide/account-and-credits"
|
||||||
|
- title: "创建与管理邮箱"
|
||||||
|
path: "/docs/user-guide/mailboxes"
|
||||||
|
- title: "收件箱与附件"
|
||||||
|
path: "/docs/user-guide/inbox"
|
||||||
|
- title: "管理后台"
|
||||||
|
path: "/docs/admin-guide"
|
||||||
|
children:
|
||||||
|
- title: "运行概览"
|
||||||
|
path: "/docs/admin-guide/dashboard"
|
||||||
|
- title: "资源与邮件治理"
|
||||||
|
path: "/docs/admin-guide/governance"
|
||||||
|
- title: "积分与审计"
|
||||||
|
path: "/docs/admin-guide/credits-and-audit"
|
||||||
|
- title: "运维手册"
|
||||||
|
path: "/docs/operations"
|
||||||
|
children:
|
||||||
|
- title: "配置参考"
|
||||||
|
path: "/docs/operations/configuration"
|
||||||
|
- title: "备份与恢复"
|
||||||
|
path: "/docs/operations/backup-and-restore"
|
||||||
|
- title: "故障排查"
|
||||||
|
path: "/docs/operations/troubleshooting"
|
||||||
|
- title: "接口约定"
|
||||||
|
path: "/docs/api"
|
||||||
|
children:
|
||||||
|
- title: "认证与公共约定"
|
||||||
|
path: "/docs/api/authentication"
|
||||||
|
|
||||||
|
theme:
|
||||||
|
defaultMode: "auto"
|
||||||
|
allowToggle: true
|
||||||
|
|
||||||
|
toc:
|
||||||
|
enabled: true
|
||||||
|
maxLevel: 4
|
||||||
|
collapseControl:
|
||||||
|
enabled: true
|
||||||
|
defaultCollapsed: false
|
||||||
|
title: "本页目录"
|
||||||
|
|
||||||
|
imageViewer:
|
||||||
|
enabled: true
|
||||||
|
labels:
|
||||||
|
preview: "预览图片"
|
||||||
|
zoomIn: "放大"
|
||||||
|
zoomOut: "缩小"
|
||||||
|
fit: "适配窗口"
|
||||||
|
actualSize: "原始比例"
|
||||||
|
reset: "重置"
|
||||||
|
rotateLeft: "左旋"
|
||||||
|
rotateRight: "右旋"
|
||||||
|
fullscreen: "全屏"
|
||||||
|
download: "下载"
|
||||||
|
openInNewTab: "新标签打开"
|
||||||
|
close: "关闭图片查看器"
|
||||||
|
|
||||||
|
footer:
|
||||||
|
enabled: true
|
||||||
|
copyright: "© 2026 Email Unlimited"
|
||||||
|
poweredBy: true
|
||||||
|
repository:
|
||||||
|
url: "https://gitea.shenjianl.cn/shenjianZ/email-unlimit"
|
||||||
|
branch: "limit-mail-freq"
|
||||||
|
lastUpdated: "2026-07-25"
|
||||||
|
version: "v1"
|
||||||
|
groups:
|
||||||
|
- title: "文档"
|
||||||
|
items:
|
||||||
|
- title: "快速开始"
|
||||||
|
link: "/docs/getting-started"
|
||||||
|
- title: "用户指南"
|
||||||
|
link: "/docs/user-guide"
|
||||||
|
- title: "运维手册"
|
||||||
|
link: "/docs/operations"
|
||||||
|
- title: "项目"
|
||||||
|
items:
|
||||||
|
- title: "源代码"
|
||||||
|
link: "https://gitea.shenjianl.cn/shenjianZ/email-unlimit"
|
||||||
|
external: true
|
||||||
|
- title: "开源许可"
|
||||||
|
link: "https://gitea.shenjianl.cn/shenjianZ/email-unlimit/src/branch/limit-mail-freq/LICENSE"
|
||||||
|
external: true
|
||||||
|
links:
|
||||||
|
- title: "使用文档"
|
||||||
|
link: "/docs"
|
||||||
|
- title: "源代码"
|
||||||
|
link: "https://gitea.shenjianl.cn/shenjianZ/email-unlimit"
|
||||||
|
external: true
|
||||||
|
|
||||||
|
pwa:
|
||||||
|
enabled: true
|
||||||
|
name: "无限邮箱文档"
|
||||||
|
shortName: "无限邮箱"
|
||||||
|
description: "无限邮箱的使用、部署与运维治理手册"
|
||||||
|
themeColor: "#ffffff"
|
||||||
|
backgroundColor: "#ffffff"
|
||||||
|
|
||||||
|
contextMenu:
|
||||||
|
enabled: true
|
||||||
|
page:
|
||||||
|
copySelectedText: true
|
||||||
|
copyUrl: true
|
||||||
|
copyTitle: true
|
||||||
|
copyMarkdownLink: true
|
||||||
|
openInNewTab: true
|
||||||
|
reload: true
|
||||||
|
printPage: true
|
||||||
|
scrollToTop: true
|
||||||
|
scrollToBottom: true
|
||||||
|
site:
|
||||||
|
goHome: true
|
||||||
|
quickNav: true
|
||||||
|
language: true
|
||||||
|
appearance:
|
||||||
|
theme: true
|
||||||
|
resetThemePref: true
|
||||||
|
|
||||||
|
mdx:
|
||||||
|
componentsPath: "/src/components"
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
pageMeta:
|
||||||
|
showLastUpdated: true
|
||||||
|
showEditLink: false
|
||||||
|
showAuthors: true
|
||||||
|
preferGitMeta: true
|
||||||
|
|
||||||
|
editLink:
|
||||||
|
enabled: false
|
||||||
|
label: "编辑此页"
|
||||||
|
urlTemplate: ""
|
||||||
|
|
||||||
|
feedback:
|
||||||
|
enabled: false
|
||||||
|
endpoint: "/api/feedback"
|
||||||
|
method: "POST"
|
||||||
|
includePageMeta: true
|
||||||
|
labels:
|
||||||
|
helpful: "有帮助"
|
||||||
|
unhelpful: "没帮助"
|
||||||
|
inputPlaceholder: "可以补充哪里不清楚或不准确"
|
||||||
|
submit: "提交反馈"
|
||||||
|
thanks: "感谢反馈"
|
||||||
|
|
||||||
|
ai:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
backend:
|
||||||
|
enabled: false
|
||||||
|
baseUrl: "/api"
|
||||||
|
features:
|
||||||
|
auth: false
|
||||||
|
comments: false
|
||||||
|
bookmarks: false
|
||||||
|
analytics: false
|
||||||
|
feedback: false
|
||||||
|
notifications: false
|
||||||
|
admin: false
|
||||||
|
|
||||||
|
fonts:
|
||||||
|
fontFamilyZhCn: "Noto Sans SC,MiSans, PingFang SC, Microsoft YaHei, sans-serif"
|
||||||
|
fontFamilyEn: "Fragment Mono, system-ui, sans-serif"
|
||||||
|
|
||||||
|
codeHighlight:
|
||||||
|
langs:
|
||||||
|
- "bash"
|
||||||
|
- "powershell"
|
||||||
|
- "yaml"
|
||||||
|
- "toml"
|
||||||
|
- "json"
|
||||||
|
- "javascript"
|
||||||
|
- "vue"
|
||||||
|
- "tsx"
|
||||||
|
- "typescript"
|
||||||
|
- "rust"
|
||||||
|
- "sql"
|
||||||
|
- "dockerfile"
|
||||||
|
- "nginx"
|
||||||
|
- "mermaid"
|
||||||
|
lightTheme: "github-light"
|
||||||
|
darkTheme: "github-dark"
|
||||||
|
|
||||||
|
search:
|
||||||
|
enabled: true
|
||||||
|
placeholder: "搜索使用、部署和运维文档..."
|
||||||
|
maxResults: 20
|
||||||
|
snippetLength: 120
|
||||||
|
|
||||||
|
seo:
|
||||||
|
enabled: true
|
||||||
|
defaultTitle: "无限邮箱文档"
|
||||||
|
titleTemplate: "{title} | {siteTitle}"
|
||||||
|
defaultDescription: "无限邮箱的使用、部署与运维治理手册"
|
||||||
|
defaultOgImage: "https://mail.shenjianl.cn/images/og-default.png"
|
||||||
|
robots: "index, follow"
|
||||||
|
twitterCard: "summary_large_image"
|
||||||
|
|
||||||
|
breadcrumb:
|
||||||
|
enabled: true
|
||||||
|
showHome: true
|
||||||
|
|
||||||
|
reading:
|
||||||
|
showTime: true
|
||||||
|
showProgress: true
|
||||||
|
fullscreen:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
sitemap:
|
||||||
|
enabled: true
|
||||||
|
changefreq: "weekly"
|
||||||
|
priority: 0.7
|
||||||
|
exclude: []
|
||||||
|
|
||||||
|
feed:
|
||||||
|
enabled: false
|
||||||
|
title: "无限邮箱文档"
|
||||||
|
description: "无限邮箱文档更新"
|
||||||
|
limit: 20
|
||||||
|
|
||||||
|
export:
|
||||||
|
enabled: true
|
||||||
|
markdown: true
|
||||||
|
pdf: true
|
||||||
|
word: true
|
||||||
|
allDocs: true
|
||||||
|
pdfServer:
|
||||||
|
enabled: false
|
||||||
|
url: "http://localhost:3001"
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
title: Documentation
|
||||||
|
description: Complete user, deployment, and operations documentation
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
The documentation follows the real operating flow: deploy the services, configure DNS, let users create mailboxes, and use the admin console for risk and credit governance.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
U[User workspace] -->|HTTPS API| B[Rust backend]
|
||||||
|
A[Admin console] -->|Admin API| B
|
||||||
|
M[External mail servers] -->|SMTP 25| B
|
||||||
|
B --> DB[(Database)]
|
||||||
|
B --> R[(Redis)]
|
||||||
|
B --> O[Outbound mail provider]
|
||||||
|
```
|
||||||
|
|
||||||
|
Recommended order:
|
||||||
|
|
||||||
|
1. [Introduction](/docs/getting-started/introduction)
|
||||||
|
2. [Docker Deployment](/docs/getting-started/docker-deployment)
|
||||||
|
3. [DNS and SMTP](/docs/getting-started/dns-and-smtp)
|
||||||
|
4. [Account and Credits](/docs/user-guide/account-and-credits)
|
||||||
|
5. [Operations Dashboard](/docs/admin-guide/dashboard)
|
||||||
|
6. [Backup and Restore](/docs/operations/backup-and-restore)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: Admin Guide
|
||||||
|
description: Monitor runtime health and govern users, mailboxes, and messages
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
The local admin console runs at `http://127.0.0.1:4173`. Access requires an active account with the `admin` role.
|
||||||
|
|
||||||
|
<Warning title="Current administrator protection">
|
||||||
|
The signed-in administrator cannot change their own role or status. They may still adjust their own credit balance.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
- [Operations Dashboard](/docs/admin-guide/dashboard)
|
||||||
|
- [Resource and Mail Governance](/docs/admin-guide/governance)
|
||||||
|
- [Credits and Audit](/docs/admin-guide/credits-and-audit)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: Credits and Audit
|
||||||
|
description: Edit credit rules, adjust balances, and trace privileged actions
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
Administrators can edit registration reward, check-in reward, reward cap, mailbox cost, email cost, daily mailbox limit, and daily email limit.
|
||||||
|
|
||||||
|
Saving requires a reason and the expected version. A stale version returns `409`; reload before applying another change. Rules are stored in the database and are not configured with `CREDIT__*` environment variables.
|
||||||
|
|
||||||
|
Each operation uses one rule snapshot. Lowering the reward cap does not remove existing credits, and lowering daily limits blocks further operations only after the new rule takes effect.
|
||||||
|
|
||||||
|
Audit coverage includes message views, attachment downloads, user state changes, credit adjustments, rule updates, mailbox revocation, message release or deletion, and blacklist changes. Batch records share a `batch_id`.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
title: Operations Dashboard
|
||||||
|
description: Metrics, trends, health checks, and risk queues
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
The dashboard refreshes every 60 seconds and supports 24-hour, 7-day, and 30-day ranges.
|
||||||
|
|
||||||
|
It shows total and new users, active and expiring mailboxes, daily and retained mail, quarantine rate, logical storage, and active blocks. Compact metrics use `K` and `M`, while paginated lists keep exact totals.
|
||||||
|
|
||||||
|
Trends separate normal receipt, quarantine, and SMTP rejection. Security distributions aggregate SPF, DKIM, DMARC, and risk levels.
|
||||||
|
|
||||||
|
Runtime health distinguishes healthy, starting, disabled, and failed states for:
|
||||||
|
|
||||||
|
- database connectivity
|
||||||
|
- Redis `PING`
|
||||||
|
- SMTP listener
|
||||||
|
- outbound mail and queue
|
||||||
|
- cleanup worker
|
||||||
|
|
||||||
|
<Tip title="Delivery diagnosis">
|
||||||
|
When mail does not arrive, check SMTP and database health first, then quarantine, blacklists, credits, and daily limits.
|
||||||
|
</Tip>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
title: Resource and Mail Governance
|
||||||
|
description: Govern users, mailboxes, messages, quarantine, and blacklists
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
Governance lists use server-side pagination, URL-synchronized filters, column visibility, and batches of up to 100 resources.
|
||||||
|
|
||||||
|
- **Users:** filter accounts, inspect resource and credit totals, change status, and adjust credits.
|
||||||
|
- **Mailboxes:** inspect ownership, capacity, lifecycle, and revoke in batches.
|
||||||
|
- **Messages:** inspect metadata and sanitized bodies, download audited attachments, delete, release, or block sources.
|
||||||
|
- **Quarantine:** release only after atomic daily-usage and credit settlement.
|
||||||
|
- **Blacklists:** manage sender, domain, pattern, and IP blocks with permanent or expiring entries.
|
||||||
|
|
||||||
|
<Warning title="Physical deletion">
|
||||||
|
Deleting a message removes its body and attachments and releases capacity. The operation cannot be undone, but its audit record remains.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
Viewing a message body and downloading an attachment are both audited.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: API Conventions
|
||||||
|
description: API prefixes, authentication, responses, and pagination
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
| Scope | Example | Authentication |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Authentication | `/auth/login` | Not required for login |
|
||||||
|
| User API | `/api/mailboxes` | Bearer token |
|
||||||
|
| Admin API | `/admin/overview` | Admin bearer token |
|
||||||
|
| Health | `/health` | Not required |
|
||||||
|
|
||||||
|
See [Authentication and Common Rules](/docs/api/authentication).
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
title: Authentication and Common Rules
|
||||||
|
description: Bearer tokens, response envelopes, pagination, and batch operations
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
Protected APIs use:
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer ACCESS_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
Admin APIs additionally require an active administrator account.
|
||||||
|
|
||||||
|
Response envelope:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "success",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Paginated data:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [],
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 20,
|
||||||
|
"total": 0,
|
||||||
|
"total_pages": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Batch operations require a non-empty reason, deduplicate IDs, accept up to 100 resources, and return successful IDs plus per-item failures.
|
||||||
|
|
||||||
|
API timestamps are UTC ISO values. User and admin interfaces display them in `Asia/Shanghai`.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: Getting Started
|
||||||
|
description: Understand the system and complete a local or Docker deployment
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
- [Introduction](/docs/getting-started/introduction)
|
||||||
|
- [Local Development](/docs/getting-started/local-development)
|
||||||
|
- [Docker Deployment](/docs/getting-started/docker-deployment)
|
||||||
|
- [DNS and SMTP](/docs/getting-started/dns-and-smtp)
|
||||||
|
|
||||||
|
<Tip title="Local evaluation">
|
||||||
|
Public DNS is not required for API-only local evaluation. Start the backend with SQLite, then run the user and admin frontends.
|
||||||
|
</Tip>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
title: DNS and SMTP
|
||||||
|
description: Configure receiving domains, MX records, and TCP port 25
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
For `example.com` with SMTP host `mail.example.com`:
|
||||||
|
|
||||||
|
| Type | Name | Value |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| A | `mail` | Public server IPv4 |
|
||||||
|
| MX | `@` | `mail.example.com` |
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[smtp]
|
||||||
|
enabled = true
|
||||||
|
listen_host = "0.0.0.0"
|
||||||
|
listen_port = 25
|
||||||
|
hostname = "mail.example.com"
|
||||||
|
local_domains = ["example.com", "mail.example.com"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify DNS and network reachability:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nslookup -type=mx example.com
|
||||||
|
nslookup mail.example.com
|
||||||
|
nc -vz mail.example.com 25
|
||||||
|
```
|
||||||
|
|
||||||
|
<Warning title="Port 25 restrictions">
|
||||||
|
Some cloud and residential providers block TCP 25. Check cloud firewalls, the host firewall, and provider policy when DNS resolves but SMTP is unreachable.
|
||||||
|
</Warning>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
title: Docker Deployment
|
||||||
|
description: Deploy Email Unlimited with the available Compose profiles
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
| Compose file | Use case |
|
||||||
|
| --- | --- |
|
||||||
|
| `docker-compose.yml` | Local or internal deployment |
|
||||||
|
| `docker-compose.full.yml` | Nginx and HTTPS |
|
||||||
|
| `docker-compose.self.yml` | External database |
|
||||||
|
|
||||||
|
For a local deployment, replace every `CHANGE_ME` value in `compose.env` and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
docker compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
For an external database:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Copy-Item compose.self.env.example compose.self.env
|
||||||
|
docker compose -f docker-compose.self.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Bootstrap the first administrator with:
|
||||||
|
|
||||||
|
```env
|
||||||
|
AUTH__BOOTSTRAP_ADMIN_EMAIL=admin@example.com
|
||||||
|
AUTH__INVITE_CODE=replace-with-at-least-16-characters
|
||||||
|
```
|
||||||
|
|
||||||
|
Register that email after startup.
|
||||||
|
|
||||||
|
<Warning title="Production secrets">
|
||||||
|
Replace the JWT secret, invite code, database password, and body encryption key.
|
||||||
|
Production rejects default secrets, wildcard CORS, and placeholder encryption keys.
|
||||||
|
</Warning>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
title: Introduction
|
||||||
|
description: Components, capability boundaries, and mail processing flow
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
Email Unlimited is a self-hosted temporary mailbox platform. It does not provide payments, top-ups, credit transfers, redemption, or withdrawals.
|
||||||
|
|
||||||
|
| Component | Stack | Responsibility |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `server` | Rust, Axum, SeaORM, Tokio | API, SMTP, credits, cleanup |
|
||||||
|
| `frontend` | Vue 3, Pinia, Element Plus | User accounts and inbox |
|
||||||
|
| `admin` | React, TanStack Query, shadcn/ui | Monitoring and governance |
|
||||||
|
| `docs` | React Docs UI, MDX | Product documentation |
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant S as Sending server
|
||||||
|
participant M as SMTP service
|
||||||
|
participant DB as Database
|
||||||
|
participant U as User workspace
|
||||||
|
S->>M: Deliver to temporary address
|
||||||
|
M->>M: Policy and risk checks
|
||||||
|
M->>DB: Atomically save mail, usage, and credits
|
||||||
|
DB-->>M: Commit
|
||||||
|
M-->>S: 250 Accepted
|
||||||
|
M-->>U: WebSocket event
|
||||||
|
```
|
||||||
|
|
||||||
|
Quarantined mail consumes storage but is not charged until an administrator releases it. Expired mail and attachments are physically deleted by the cleanup worker, while mailbox records remain expired or revoked and their addresses are not reassigned.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
title: Local Development
|
||||||
|
description: Run the backend, user workspace, and admin console locally
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
Requirements: stable Rust, Node.js 20.19 or newer, and pnpm. Redis 7 is optional.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="PowerShell">
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd server
|
||||||
|
cargo run
|
||||||
|
|
||||||
|
cd ..\frontend
|
||||||
|
pnpm install
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
cd ..\admin
|
||||||
|
pnpm install
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Bash">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd server && cargo run
|
||||||
|
cd ../frontend && pnpm install && pnpm dev
|
||||||
|
cd ../admin && pnpm install && pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
Run the three commands in separate terminals. The backend uses `server/config/development.toml` and `server/data/app.db` by default.
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_API_PROXY_TARGET=http://127.0.0.1:3000
|
||||||
|
VITE_API_BASE_URL=https://api.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the proxy variable for development and the base URL for production builds.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
title: Operations
|
||||||
|
description: Configuration, backup, restore, and troubleshooting
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
Operators should monitor the database, Redis, SMTP listener, outbound mail, queues, and cleanup worker, and keep database and encryption-key backups.
|
||||||
|
|
||||||
|
- [Configuration](/docs/operations/configuration)
|
||||||
|
- [Backup and Restore](/docs/operations/backup-and-restore)
|
||||||
|
- [Troubleshooting](/docs/operations/troubleshooting)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
title: Backup and Restore
|
||||||
|
description: Protect databases, Redis state, secrets, and deployment configuration
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
For a live SQLite database, use the online backup command:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
sqlite3 server/data/app.db ".backup 'server/data/app.backup.db'"
|
||||||
|
sqlite3 server/data/app.backup.db "PRAGMA integrity_check;"
|
||||||
|
```
|
||||||
|
|
||||||
|
PostgreSQL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pg_dump --format=custom --file=email-unlimit.dump email_unlimit
|
||||||
|
pg_restore --clean --if-exists --dbname=email_unlimit email-unlimit.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
MySQL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mysqldump --single-transaction email_unlimit > email-unlimit.sql
|
||||||
|
mysql email_unlimit < email-unlimit.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Back up the JWT secret, body encryption key, database password, and outbound SMTP password separately.
|
||||||
|
|
||||||
|
<Warning title="Body encryption key">
|
||||||
|
Database backups do not include environment variables. Losing the body encryption key makes encrypted historical bodies unrecoverable.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
After restore, verify `/health`, admin login, mailbox creation, SMTP receipt, credits, capacity, and audit records.
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
title: Configuration
|
||||||
|
description: TOML, environment variables, secrets, and optional services
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
Configuration precedence is CLI arguments, environment variables, TOML files, then application defaults. Nested environment keys use double underscores.
|
||||||
|
|
||||||
|
```env
|
||||||
|
AUTH__JWT_SECRET=replace-with-at-least-32-random-characters
|
||||||
|
AUTH__INVITE_CODE=replace-with-at-least-16-random-characters
|
||||||
|
AUTH__BOOTSTRAP_ADMIN_EMAIL=admin@example.com
|
||||||
|
SMTP__BODY_ENCRYPTION_KEY=replace-with-a-32-byte-key
|
||||||
|
```
|
||||||
|
|
||||||
|
Database example:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DATABASE__DATABASE_TYPE=postgresql
|
||||||
|
DATABASE__HOST=database.example.com
|
||||||
|
DATABASE__PORT=5432
|
||||||
|
DATABASE__USER=email_unlimit
|
||||||
|
DATABASE__PASSWORD=CHANGE_ME
|
||||||
|
DATABASE__DATABASE=email_unlimit
|
||||||
|
```
|
||||||
|
|
||||||
|
Redis and outbound mail are optional:
|
||||||
|
|
||||||
|
```env
|
||||||
|
REDIS__ENABLED=true
|
||||||
|
REDIS__HOST=redis
|
||||||
|
EMAIL__ENABLED=true
|
||||||
|
EMAIL__SMTP_HOST=smtp.example.com
|
||||||
|
EMAIL__SMTP_PORT=587
|
||||||
|
EMAIL__QUEUE_ENABLED=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Redis keys use a project-specific prefix. Outbound SMTP is separate from the inbound TCP 25 listener.
|
||||||
|
|
||||||
|
<Tip title="Credit rules">
|
||||||
|
Credit values are initialized from built-in defaults and stored in the database. Edit them in the admin console; do not define `CREDIT__*` variables.
|
||||||
|
</Tip>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
title: Troubleshooting
|
||||||
|
description: Common login, delivery, Redis, outbound mail, and database issues
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
## Admin login
|
||||||
|
|
||||||
|
The account must have the correct password, `role=admin`, and `status=active`. Configure `AUTH__BOOTSTRAP_ADMIN_EMAIL` before registering the first administrator.
|
||||||
|
|
||||||
|
## Mail does not arrive
|
||||||
|
|
||||||
|
Check MX and A records, public TCP 25 reachability, SMTP health, local domains, mailbox status and capacity, user credits and daily usage, blacklists, and quarantine.
|
||||||
|
|
||||||
|
## Repeated mailbox addresses in logs
|
||||||
|
|
||||||
|
This is normally one mailbox with multiple events. The mailbox resource remains unique. If an upstream SMTP server retries the exact same message, the current version does not guarantee cross-connection deduplication; compare recipient, Message-ID, sender, and receive time.
|
||||||
|
|
||||||
|
## Redis disabled
|
||||||
|
|
||||||
|
`REDIS__ENABLED=false` is an intentional disabled state. When enabled but unhealthy, verify host, port, credentials, network policy, and `redis-cli ping`.
|
||||||
|
|
||||||
|
## Outbound mail disabled
|
||||||
|
|
||||||
|
Inbound SMTP and outbound mail are independent. Temporary mailboxes can still receive mail while `EMAIL__ENABLED=false`, but verification and system messages cannot be sent through an upstream provider.
|
||||||
|
|
||||||
|
## Diagnostics
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsS http://127.0.0.1:3000/health
|
||||||
|
docker compose ps
|
||||||
|
docker compose logs --tail=200 backend
|
||||||
|
```
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: User Guide
|
||||||
|
description: Register, earn credits, create mailboxes, and receive messages
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
The user workflow is:
|
||||||
|
|
||||||
|
1. Register or sign in.
|
||||||
|
2. Claim the daily check-in reward.
|
||||||
|
3. Spend credits to create a temporary mailbox.
|
||||||
|
4. Receive and inspect messages while the mailbox is active.
|
||||||
|
5. Download attachments or delete messages.
|
||||||
|
|
||||||
|
Continue with [Account and Credits](/docs/user-guide/account-and-credits), [Mailbox Management](/docs/user-guide/mailboxes), and [Inbox and Attachments](/docs/user-guide/inbox).
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: Account and Credits
|
||||||
|
description: Registration, sign-in, daily check-in, and credit rules
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
Registration may require an invite code depending on deployment configuration.
|
||||||
|
|
||||||
|
| Rule | Default |
|
||||||
|
| --- | --- |
|
||||||
|
| Registration bonus | `30` credits |
|
||||||
|
| Daily check-in | `3` credits |
|
||||||
|
| Reward balance cap | `90` credits |
|
||||||
|
| Create mailbox | `2` credits |
|
||||||
|
| Receive normal email | `1` credit |
|
||||||
|
| Daily mailbox limit | `3` |
|
||||||
|
| Daily email limit | `20` |
|
||||||
|
|
||||||
|
<Warning title="No free operations">
|
||||||
|
The first mailbox and the first normal email both consume credits. Credit balance and daily limits are enforced together.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
Check-in can be claimed once per configured calendar day. Rewards only fill the balance up to the reward cap. Deleting mail, revoking a mailbox, or expiry does not refund credits or daily usage.
|
||||||
|
|
||||||
|
Rules are stored in the database and managed from the admin console. They take effect on the next operation.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
title: Inbox and Attachments
|
||||||
|
description: Real-time mail, message bodies, attachments, and quarantine
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
Normal mail is accepted only while the mailbox is active and has sufficient storage, daily usage, and credits. WebSocket events update the workspace in real time; the API remains the source of truth after reconnects.
|
||||||
|
|
||||||
|
Message details include sender, subject, bodies, source IP, SPF, DKIM, DMARC, risk score, and attachments.
|
||||||
|
|
||||||
|
<Warning title="Attachment safety">
|
||||||
|
Treat unknown attachments as untrusted. Download authorization does not guarantee that the file is safe to execute.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
Quarantined mail consumes storage but is not charged until release. If release cannot reserve usage or debit credits, the message stays quarantined.
|
||||||
|
|
||||||
|
Deleting a message physically removes its body and attachments and releases storage. It does not refund credits or daily usage.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
title: Mailbox Management
|
||||||
|
description: Creation, tokens, capacity, expiry, and revocation
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
Mailbox creation reserves daily usage, creates the address, debits credits, and records a transaction in one database transaction. A failure rolls back every step.
|
||||||
|
|
||||||
|
Addresses are normalized to lowercase and protected by a database unique constraint.
|
||||||
|
|
||||||
|
<Tip title="Repeated addresses in logs">
|
||||||
|
One mailbox can receive many messages, so its address appears in multiple email and SMTP audit events. This is a one-to-many relationship, not duplicate mailbox records.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
| Status | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| Active | Can receive mail |
|
||||||
|
| Expired | Past its expiry time |
|
||||||
|
| Revoked | Explicitly disabled |
|
||||||
|
|
||||||
|
Expired and revoked records remain in the database and their addresses are not reassigned. Each mailbox also has a separate access token for message APIs, attachments, and WebSocket events.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
title: Email Unlimited
|
||||||
|
description: A self-hosted temporary mailbox, credit, and mail governance platform
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
Email Unlimited combines a Rust API and SMTP service, a Vue user workspace, and a React operations console. It receives public email directly and provides credit accounting, quarantine, blacklists, auditing, and runtime health monitoring.
|
||||||
|
|
||||||
|
<Tip title="Where to start">
|
||||||
|
For a first deployment, read [Docker Deployment](/docs/getting-started/docker-deployment)
|
||||||
|
and [DNS and SMTP](/docs/getting-started/dns-and-smtp). End users can start with
|
||||||
|
the [User Guide](/docs/user-guide).
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
<Card title="User workspace">
|
||||||
|
Register, check in, create temporary mailboxes, receive messages in real time, and download attachments.
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="Operations console">
|
||||||
|
Govern users, mailboxes, quarantine, blacklists, credit rules, and audit records.
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="Self-hosted">
|
||||||
|
Run with SQLite, PostgreSQL, or MySQL and deploy the complete stack with Docker Compose.
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
## Default endpoints
|
||||||
|
|
||||||
|
| Service | Local development | Docker default |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| User workspace | `http://127.0.0.1:5173` | `http://localhost:8080` |
|
||||||
|
| Admin console | `http://127.0.0.1:4173` | `http://localhost:8081` |
|
||||||
|
| API | `http://127.0.0.1:3000` | `http://localhost:3000` |
|
||||||
|
| SMTP | TCP `25` | TCP `25` |
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
title: 使用文档
|
||||||
|
description: 无限邮箱从部署到日常治理的完整文档入口
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
这套文档按实际使用角色组织。部署人员先完成服务和 DNS 配置,用户通过用户端创建邮箱和收信,管理员在治理后台处理风险与积分规则。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
U[用户端] -->|HTTPS API| B[Rust 后端]
|
||||||
|
A[管理后台] -->|管理员 API| B
|
||||||
|
M[外部邮件服务器] -->|SMTP 25| B
|
||||||
|
B --> DB[(数据库)]
|
||||||
|
B --> R[(Redis)]
|
||||||
|
B --> O[外发邮件服务]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 推荐阅读顺序
|
||||||
|
|
||||||
|
1. [产品介绍](/docs/getting-started/introduction)
|
||||||
|
2. [Docker 部署](/docs/getting-started/docker-deployment)
|
||||||
|
3. [DNS 与 SMTP](/docs/getting-started/dns-and-smtp)
|
||||||
|
4. [账号与积分](/docs/user-guide/account-and-credits)
|
||||||
|
5. [运行概览](/docs/admin-guide/dashboard)
|
||||||
|
6. [备份与恢复](/docs/operations/backup-and-restore)
|
||||||
|
|
||||||
|
<Warning title="生产部署">
|
||||||
|
生产环境必须更换 JWT 密钥、邀请口令和正文加密密钥,并限制 CORS
|
||||||
|
来源。不要把真实密钥提交到 Git。
|
||||||
|
</Warning>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: 管理后台
|
||||||
|
description: 使用管理端监控运行状态并治理用户、邮箱和邮件
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
管理后台默认在本地 `http://127.0.0.1:4173` 运行。只有角色为 `admin` 且状态正常的账号可以进入。
|
||||||
|
|
||||||
|
<Warning title="当前管理员保护">
|
||||||
|
当前登录管理员不能修改自己的角色或状态,避免误操作导致后台失去管理员。但可以调整自己的积分。
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
- [运行概览](/docs/admin-guide/dashboard)
|
||||||
|
- [资源与邮件治理](/docs/admin-guide/governance)
|
||||||
|
- [积分与审计](/docs/admin-guide/credits-and-audit)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
title: 积分与审计
|
||||||
|
description: 修改积分规则、调整用户余额并追踪管理员和 SMTP 操作
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
## 积分规则
|
||||||
|
|
||||||
|
管理端可以修改七项数值规则:
|
||||||
|
|
||||||
|
1. 注册赠送
|
||||||
|
2. 每日签到奖励
|
||||||
|
3. 奖励余额上限
|
||||||
|
4. 创建邮箱成本
|
||||||
|
5. 接收邮件成本
|
||||||
|
6. 每日创建邮箱上限
|
||||||
|
7. 每日接收邮件上限
|
||||||
|
|
||||||
|
保存时必须填写原因并提交当前版本号。其他管理员已经修改规则时,旧版本提交会返回 `409`,需要重新加载后再保存。
|
||||||
|
|
||||||
|
规则保存在数据库中,不从 `CREDIT__*` 环境变量读取。容器重启不会覆盖管理端设置。
|
||||||
|
|
||||||
|
## 规则生效方式
|
||||||
|
|
||||||
|
- 每个业务操作开始后固定使用同一个规则快照。
|
||||||
|
- 新规则从下一次操作生效。
|
||||||
|
- 降低奖励余额上限不会扣减已有余额。
|
||||||
|
- 降低每日上限后,已经达到新上限的用户当天不能继续操作。
|
||||||
|
- 管理员调整不受奖励余额上限约束,但不能把余额扣成负数。
|
||||||
|
|
||||||
|
## 积分流水
|
||||||
|
|
||||||
|
标准原因包括:
|
||||||
|
|
||||||
|
| 原因 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| `register_bonus` | 注册赠送 |
|
||||||
|
| `migration_bonus` | 存量迁移赠送 |
|
||||||
|
| `daily_check_in` | 每日签到 |
|
||||||
|
| `create_mailbox` | 创建邮箱 |
|
||||||
|
| `receive_email` | 接收邮件 |
|
||||||
|
| `admin_adjust` | 管理员调整 |
|
||||||
|
|
||||||
|
界面显示中文名称,接口和数据库仍保留稳定的英文枚举。
|
||||||
|
|
||||||
|
## 审计日志
|
||||||
|
|
||||||
|
以下操作会写入审计:
|
||||||
|
|
||||||
|
- 查看邮件正文、下载附件
|
||||||
|
- 修改用户状态和角色
|
||||||
|
- 调整积分、修改积分规则
|
||||||
|
- 吊销邮箱、删除或放行邮件
|
||||||
|
- 新增或解除黑名单
|
||||||
|
|
||||||
|
批量操作为每个资源写独立记录,并通过 `batch_id` 关联。SMTP 审计与管理员操作分别提供只读检索页面。
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
title: 运行概览
|
||||||
|
description: 理解管理后台的指标、趋势、健康状态和风险队列
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
运行概览每 60 秒自动刷新,也可以手动刷新。时间范围支持 24 小时、7 天和 30 天。
|
||||||
|
|
||||||
|
## 核心指标
|
||||||
|
|
||||||
|
| 指标 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| 总用户 / 今日新增 | 当前账号总量与上海时区当日注册量 |
|
||||||
|
| 活跃邮箱 / 24 小时到期 | 可收信邮箱和即将到期邮箱 |
|
||||||
|
| 今日邮件 / 累计邮件 | 当日接收量与当前保留邮件量 |
|
||||||
|
| 待处理隔离 / 今日隔离率 | 风险队列规模和当日占比 |
|
||||||
|
| 邮件存储量 | 邮件正文与附件的逻辑大小 |
|
||||||
|
| 生效封禁 | 未过期的发件人、域和 IP 封禁 |
|
||||||
|
|
||||||
|
大于等于一千的概览数值使用 `K`,大于等于一百万时使用 `M`。列表分页总数继续显示精确值。
|
||||||
|
|
||||||
|
## 趋势与安全分布
|
||||||
|
|
||||||
|
邮件趋势区分正常接收、隔离和 SMTP 拒绝。安全分布聚合 SPF、DKIM、DMARC 以及低、中、高风险区间。
|
||||||
|
|
||||||
|
Top 列表用于发现:
|
||||||
|
|
||||||
|
- 收件量异常集中的邮箱
|
||||||
|
- 高频发件域
|
||||||
|
- 高频来源 IP
|
||||||
|
|
||||||
|
## 运行健康
|
||||||
|
|
||||||
|
健康状态区分“正常”“启动中”“未启用”和“故障”:
|
||||||
|
|
||||||
|
- 数据库使用真实连接探测。
|
||||||
|
- Redis 使用实际 `PING`,禁用和故障不会混为一谈。
|
||||||
|
- SMTP 显示监听状态。
|
||||||
|
- 外发邮件和队列独立显示。
|
||||||
|
- 清理任务显示最近成功或失败时间。
|
||||||
|
|
||||||
|
<Tip title="排查顺序">
|
||||||
|
出现邮件收不到时,先看 SMTP 监听和数据库,再检查风险队列、封禁和用户积分,不要只看网页是否可访问。
|
||||||
|
</Tip>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
title: 资源与邮件治理
|
||||||
|
description: 管理用户、邮箱、邮件、隔离队列和黑名单
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
治理列表使用服务端分页、URL 同步筛选和可配置列。单次批量操作最多处理 100 项,并允许部分成功。
|
||||||
|
|
||||||
|
## 用户管理
|
||||||
|
|
||||||
|
- 按邮箱、用户 ID、角色和状态筛选。
|
||||||
|
- 查看邮箱数、邮件数和积分累计。
|
||||||
|
- 批量恢复、暂停或封禁用户。
|
||||||
|
- 调整任意用户积分并填写原因。
|
||||||
|
|
||||||
|
当前管理员不能修改自己的角色或状态。
|
||||||
|
|
||||||
|
## 邮箱管理
|
||||||
|
|
||||||
|
每一行代表一个唯一邮箱资源。邮件数量按 `mailbox_id` 聚合;同一邮箱收到多封邮件不会导致邮箱列表重复。
|
||||||
|
|
||||||
|
管理员可以批量吊销邮箱。吊销不删除历史邮件,也不退款。
|
||||||
|
|
||||||
|
## 全部邮件与隔离队列
|
||||||
|
|
||||||
|
邮件详情抽屉会记录查看审计。HTML 正文会移除脚本、表单、远程资源和危险链接;附件只能下载,下载也会记录审计。
|
||||||
|
|
||||||
|
危险操作必须填写原因:
|
||||||
|
|
||||||
|
- 物理删除邮件
|
||||||
|
- 放行隔离邮件
|
||||||
|
- 封禁发件地址
|
||||||
|
- 封禁发件域
|
||||||
|
- 封禁来源 IP
|
||||||
|
|
||||||
|
<Warning title="物理删除">
|
||||||
|
删除邮件会同时删除附件并释放邮箱容量,无法恢复。审计日志仍会保留。
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
## 黑名单
|
||||||
|
|
||||||
|
黑名单支持邮件地址、发件域、模式和来源 IP。记录可以永久生效,也可以设置有效期;自动封禁和人工封禁会显示不同来源。
|
||||||
|
|
||||||
|
过期封禁不再影响 SMTP 流程。批量解除后,下一次 SMTP 操作立即使用新状态。
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
title: 接口约定
|
||||||
|
description: API 前缀、认证、响应格式与分页规则
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
后端同时提供用户接口、管理员接口和健康检查:
|
||||||
|
|
||||||
|
| 范围 | 路径示例 | 认证 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 认证 | `/auth/login` | 登录接口不需要 |
|
||||||
|
| 用户 | `/api/mailboxes` | Bearer Token |
|
||||||
|
| 管理 | `/admin/overview` | 管理员 Bearer Token |
|
||||||
|
| 健康 | `/health` | 不需要 |
|
||||||
|
|
||||||
|
详细说明见 [认证与公共约定](/docs/api/authentication)。
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
title: 认证与公共约定
|
||||||
|
description: Bearer Token、统一响应、分页和管理员接口规则
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
## 认证
|
||||||
|
|
||||||
|
登录成功后返回访问令牌和刷新令牌。受保护接口使用:
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer ACCESS_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
管理员接口还会验证用户角色为 `admin`、状态为 `active`。
|
||||||
|
|
||||||
|
## 统一响应
|
||||||
|
|
||||||
|
成功和失败响应都包含:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "success",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
HTTP 状态码表达协议结果,`code` 和 `message` 便于前端展示业务信息。
|
||||||
|
|
||||||
|
## 分页
|
||||||
|
|
||||||
|
治理列表使用统一分页结构:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [],
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 20,
|
||||||
|
"total": 0,
|
||||||
|
"total_pages": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
页码从 `1` 开始。管理端筛选条件会同步到 URL,刷新或分享页面时可以保留查询状态。
|
||||||
|
|
||||||
|
## 关键接口
|
||||||
|
|
||||||
|
| 方法 | 路径 | 用途 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `POST` | `/auth/register` | 注册 |
|
||||||
|
| `POST` | `/auth/login` | 登录 |
|
||||||
|
| `POST` | `/auth/refresh` | 刷新令牌 |
|
||||||
|
| `GET` | `/api/credits` | 余额、价格和今日用量 |
|
||||||
|
| `POST` | `/api/credits/check-in` | 每日签到 |
|
||||||
|
| `GET/POST` | `/api/mailboxes` | 邮箱列表与创建 |
|
||||||
|
| `GET` | `/admin/overview` | 运行概览 |
|
||||||
|
| `GET` | `/admin/emails` | 邮件治理列表 |
|
||||||
|
|
||||||
|
## 批量操作
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ids": [1, 2],
|
||||||
|
"action": "delete",
|
||||||
|
"reason": "清理恶意邮件",
|
||||||
|
"ttl_seconds": 3600
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 原因必填,最多 200 字。
|
||||||
|
- ID 会去重。
|
||||||
|
- 单次最多 100 项。
|
||||||
|
- 返回成功 ID 和逐项失败原因;部分失败不会回滚已经成功的其他资源。
|
||||||
|
|
||||||
|
## 时间
|
||||||
|
|
||||||
|
接口返回 UTC ISO 时间。用户端和管理端统一按 `Asia/Shanghai` 展示,积分每日结算也默认使用该时区。
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
title: 快速开始
|
||||||
|
description: 了解系统组成并完成本地或 Docker 部署
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
快速开始部分覆盖以下内容:
|
||||||
|
|
||||||
|
- [产品介绍](/docs/getting-started/introduction):能力、组件和数据流。
|
||||||
|
- [本地开发](/docs/getting-started/local-development):启动三个项目并运行检查。
|
||||||
|
- [Docker 部署](/docs/getting-started/docker-deployment):选择 Compose 部署模式。
|
||||||
|
- [DNS 与 SMTP](/docs/getting-started/dns-and-smtp):让公网邮件到达服务器。
|
||||||
|
|
||||||
|
<Tip title="最短路径">
|
||||||
|
只想本地体验时不需要公网 DNS。后端使用 SQLite,依次启动后端、用户端和管理端即可。
|
||||||
|
</Tip>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
title: DNS 与 SMTP
|
||||||
|
description: 配置收信域名、MX 记录和 TCP 25
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
公网收信要求外部邮件服务器能够通过 DNS 找到你的 SMTP 服务,并连接 TCP 25。
|
||||||
|
|
||||||
|
## 最小 DNS 记录
|
||||||
|
|
||||||
|
假设收信域名是 `example.com`,SMTP 主机是 `mail.example.com`:
|
||||||
|
|
||||||
|
| 类型 | 名称 | 值 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| A | `mail` | 服务器公网 IPv4 | SMTP 主机地址 |
|
||||||
|
| MX | `@` | `mail.example.com` | 数字优先级建议 `10` |
|
||||||
|
|
||||||
|
如果使用子域收件,例如 `user@mail.example.com`,MX 记录应配置在对应子域。
|
||||||
|
|
||||||
|
## 后端配置
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[smtp]
|
||||||
|
enabled = true
|
||||||
|
listen_host = "0.0.0.0"
|
||||||
|
listen_port = 25
|
||||||
|
hostname = "mail.example.com"
|
||||||
|
local_domains = ["example.com", "mail.example.com"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Docker 需要发布 SMTP 端口:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "25:25"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nslookup -type=mx example.com
|
||||||
|
nslookup mail.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
从另一台公网主机测试端口:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nc -vz mail.example.com 25
|
||||||
|
```
|
||||||
|
|
||||||
|
<Warning title="常见网络限制">
|
||||||
|
部分云服务商默认封锁入站或出站 TCP 25。家庭网络也可能没有固定公网 IP。
|
||||||
|
DNS 正确但端口不可达时,应先检查云防火墙、系统防火墙和运营商策略。
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
## PTR、SPF 与 DMARC
|
||||||
|
|
||||||
|
- PTR 反向解析由公网 IP 提供商配置。
|
||||||
|
- SPF 和 DMARC 用于判断发件域可信度,不是接收邮件所必需,但会影响风险评分。
|
||||||
|
- DKIM 当前识别签名头并记录结果,管理员可在邮件详情中查看 SPF、DKIM 和 DMARC 状态。
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
title: Docker 部署
|
||||||
|
description: 使用三套 Docker Compose 配置部署无限邮箱
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
项目提供三种 Compose 入口。
|
||||||
|
|
||||||
|
| 文件 | 场景 | 前端端口 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `docker-compose.yml` | 本地或内网快速部署 | `8080`、`8081` |
|
||||||
|
| `docker-compose.full.yml` | Nginx、HTTPS 与完整服务 | 由 Nginx 暴露 |
|
||||||
|
| `docker-compose.self.yml` | 使用外部数据库 | `8080`、`8081` |
|
||||||
|
|
||||||
|
## 快速部署
|
||||||
|
|
||||||
|
1. 修改 `compose.env` 中所有 `CHANGE_ME`。
|
||||||
|
2. 启动服务。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
docker compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
用户端为 `http://localhost:8080`,管理端为 `http://localhost:8081`,API 为 `http://localhost:3000`。
|
||||||
|
|
||||||
|
## 使用外部数据库
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Copy-Item compose.self.env.example compose.self.env
|
||||||
|
docker compose -f docker-compose.self.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
至少需要填写:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DATABASE__DATABASE_TYPE=postgresql
|
||||||
|
DATABASE__HOST=database.example.com
|
||||||
|
DATABASE__PORT=5432
|
||||||
|
DATABASE__USER=email_unlimit
|
||||||
|
DATABASE__PASSWORD=CHANGE_ME
|
||||||
|
DATABASE__DATABASE=email_unlimit
|
||||||
|
```
|
||||||
|
|
||||||
|
## 创建首个管理员
|
||||||
|
|
||||||
|
设置以下环境变量:
|
||||||
|
|
||||||
|
```env
|
||||||
|
AUTH__BOOTSTRAP_ADMIN_EMAIL=admin@example.com
|
||||||
|
AUTH__INVITE_CODE=replace-with-at-least-16-characters
|
||||||
|
```
|
||||||
|
|
||||||
|
然后通过用户端注册该邮箱,或调用注册接口。匹配引导邮箱的首个账号会获得管理员角色。
|
||||||
|
|
||||||
|
<Warning title="生产安全检查">
|
||||||
|
生产环境会拒绝默认 JWT 密钥、通配 CORS,以及缺失或占位的正文加密密钥。
|
||||||
|
`AUTH__JWT_SECRET` 和 `SMTP__BODY_ENCRYPTION_KEY` 必须使用独立随机值。
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
## 完整 HTTPS 部署
|
||||||
|
|
||||||
|
1. 修改 `compose.full.env`。
|
||||||
|
2. 修改 `nginx.full.conf` 中的域名和证书文件名。
|
||||||
|
3. 将证书放入 `certs/`。
|
||||||
|
4. 执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.full.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
部署完成后检查:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsS https://api.example.com/health
|
||||||
|
docker compose logs --tail=100 backend
|
||||||
|
```
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
title: 产品介绍
|
||||||
|
description: 无限邮箱的组件、能力边界与邮件处理流程
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
无限邮箱面向需要临时收件地址的个人或团队。产品不提供支付、充值、积分交易或提现能力,所有资源由管理员设定的积分规则约束。
|
||||||
|
|
||||||
|
## 系统组件
|
||||||
|
|
||||||
|
| 组件 | 技术 | 职责 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `server` | Rust、Axum、SeaORM、Tokio | API、SMTP、积分结算、清理任务 |
|
||||||
|
| `frontend` | Vue 3、Pinia、Element Plus | 用户注册、邮箱与收件箱 |
|
||||||
|
| `admin` | React、TanStack Query、shadcn/ui | 监控、治理和审计 |
|
||||||
|
| `docs` | React Docs UI、MDX | 使用与运维文档 |
|
||||||
|
|
||||||
|
数据库支持 SQLite、PostgreSQL 和 MySQL。Redis 用于限流、灰名单、滥用计数和可选外发队列;禁用 Redis 时,依赖它的能力会明确显示为未启用。
|
||||||
|
|
||||||
|
## 邮件处理流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant S as 外部发件服务器
|
||||||
|
participant M as SMTP 服务
|
||||||
|
participant DB as 数据库
|
||||||
|
participant U as 用户端
|
||||||
|
S->>M: RCPT TO 临时邮箱
|
||||||
|
M->>M: 黑名单、PTR、SPF、风险评分
|
||||||
|
M->>DB: 原子写入邮件、容量、次数和积分
|
||||||
|
DB-->>M: 提交成功
|
||||||
|
M-->>S: 250 Accepted
|
||||||
|
M-->>U: WebSocket 新邮件事件
|
||||||
|
```
|
||||||
|
|
||||||
|
高风险邮件先进入隔离队列,不立即扣除用户积分和每日次数。管理员放行时才执行原子结算;积分不足或已达每日上限时,邮件保持隔离。
|
||||||
|
|
||||||
|
## 数据生命周期
|
||||||
|
|
||||||
|
- 邮箱在到期后标记为 `expired`,吊销后标记为 `revoked`。
|
||||||
|
- 邮箱地址在数据库中唯一,过期或吊销后不会自动重新分配。
|
||||||
|
- 过期邮件和附件由清理任务物理删除,并释放邮箱容量。
|
||||||
|
- 删除、吊销和自然过期不会返还积分或每日次数。
|
||||||
|
- 管理审计默认永久保留。
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
title: 本地开发
|
||||||
|
description: 在 Windows、macOS 或 Linux 上启动后端、用户端和管理端
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
- Rust stable 工具链
|
||||||
|
- Node.js 20.19 或更高版本
|
||||||
|
- pnpm
|
||||||
|
- 可选:Redis 7
|
||||||
|
|
||||||
|
后端开发配置位于 `server/config/development.toml`,默认使用 `server/data/app.db`。用户端和管理端通过 Vite 代理访问 `http://127.0.0.1:3000`。
|
||||||
|
|
||||||
|
## 启动服务
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="PowerShell">
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd server
|
||||||
|
cargo run
|
||||||
|
|
||||||
|
cd ..\frontend
|
||||||
|
pnpm install
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
cd ..\admin
|
||||||
|
pnpm install
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Bash">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
cargo run
|
||||||
|
|
||||||
|
cd ../frontend
|
||||||
|
pnpm install
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
cd ../admin
|
||||||
|
pnpm install
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
三个命令需要分别在独立终端运行。
|
||||||
|
|
||||||
|
| 服务 | 地址 |
|
||||||
|
| --- | --- |
|
||||||
|
| 后端 | `http://127.0.0.1:3000` |
|
||||||
|
| 用户端 | `http://127.0.0.1:5173` |
|
||||||
|
| 管理端 | `http://127.0.0.1:4173` |
|
||||||
|
|
||||||
|
## 使用其他后端
|
||||||
|
|
||||||
|
开发代理:
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_API_PROXY_TARGET=http://127.0.0.1:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
生产构建直连 API:
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_API_BASE_URL=https://api.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## 运行检查
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd server
|
||||||
|
cargo test
|
||||||
|
cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
|
||||||
|
cd ..\frontend
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
cd ..\admin
|
||||||
|
pnpm lint
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
<Tip title="SMTP 本地测试">
|
||||||
|
普通操作系统通常需要管理员权限才能监听 TCP 25。仅调试 API 时可以在开发配置中关闭 SMTP,或临时改用未占用的高位端口。
|
||||||
|
</Tip>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
title: 运维手册
|
||||||
|
description: 配置、备份、恢复和故障排查
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
运维人员应持续关注数据库、Redis、SMTP 监听、外发邮件和清理任务的健康状态,并建立数据库和密钥的备份策略。
|
||||||
|
|
||||||
|
- [配置参考](/docs/operations/configuration)
|
||||||
|
- [备份与恢复](/docs/operations/backup-and-restore)
|
||||||
|
- [故障排查](/docs/operations/troubleshooting)
|
||||||
|
|
||||||
|
<Tip title="配置优先级">
|
||||||
|
CLI 参数覆盖环境变量,环境变量覆盖 TOML 配置。积分数值规则是例外:
|
||||||
|
它们保存在数据库中,通过管理后台修改。
|
||||||
|
</Tip>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
title: 备份与恢复
|
||||||
|
description: 备份数据库、Redis、密钥和部署配置
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
完整备份至少包含数据库、正文加密密钥和部署配置。Redis 主要保存短期状态,不应替代数据库备份。
|
||||||
|
|
||||||
|
## SQLite
|
||||||
|
|
||||||
|
服务运行时使用 SQLite 在线备份命令,避免直接复制正在写入的数据库文件:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
sqlite3 server/data/app.db ".backup 'server/data/app.backup.db'"
|
||||||
|
sqlite3 server/data/app.backup.db "PRAGMA integrity_check;"
|
||||||
|
```
|
||||||
|
|
||||||
|
恢复前停止后端,保留当前文件,再替换数据库:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Copy-Item server/data/app.db server/data/app.before-restore.db
|
||||||
|
Copy-Item server/data/app.backup.db server/data/app.db -Force
|
||||||
|
sqlite3 server/data/app.db "PRAGMA integrity_check;"
|
||||||
|
```
|
||||||
|
|
||||||
|
## PostgreSQL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pg_dump --format=custom --file=email-unlimit.dump email_unlimit
|
||||||
|
pg_restore --clean --if-exists --dbname=email_unlimit email-unlimit.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
## MySQL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mysqldump --single-transaction email_unlimit > email-unlimit.sql
|
||||||
|
mysql email_unlimit < email-unlimit.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## Redis
|
||||||
|
|
||||||
|
Compose 默认开启 AOF。需要保留限流、队列或灰名单短期状态时,备份 `redis-data` 卷。数据库恢复后,即使不恢复 Redis,用户、邮箱、邮件、积分和审计数据仍然存在。
|
||||||
|
|
||||||
|
## 密钥
|
||||||
|
|
||||||
|
必须安全保存:
|
||||||
|
|
||||||
|
- `AUTH__JWT_SECRET`
|
||||||
|
- `SMTP__BODY_ENCRYPTION_KEY`
|
||||||
|
- 数据库密码
|
||||||
|
- 外发 SMTP 密码
|
||||||
|
|
||||||
|
<Warning title="正文加密密钥">
|
||||||
|
数据库备份不包含环境变量中的正文加密密钥。丢失该密钥后,加密正文无法恢复。
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
## 恢复验证
|
||||||
|
|
||||||
|
1. 检查 `/health`。
|
||||||
|
2. 使用管理员登录。
|
||||||
|
3. 查看运行概览的数据库和清理任务状态。
|
||||||
|
4. 创建测试邮箱并投递一封邮件。
|
||||||
|
5. 核对积分流水、容量和审计日志。
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
---
|
||||||
|
title: 配置参考
|
||||||
|
description: TOML、环境变量、密钥和可选服务配置
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置优先级
|
||||||
|
|
||||||
|
后端按以下顺序解析配置:
|
||||||
|
|
||||||
|
1. CLI 参数
|
||||||
|
2. 环境变量
|
||||||
|
3. `config/development.toml` 或 `config/production.toml`
|
||||||
|
4. 程序默认值
|
||||||
|
|
||||||
|
环境变量使用双下划线表示层级,例如:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DATABASE__DATABASE_TYPE=postgresql
|
||||||
|
SERVER__CORS_ORIGINS=https://mail.example.com,https://admin.example.com
|
||||||
|
SMTP__LOCAL_DOMAINS=example.com,mail.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## 必要安全配置
|
||||||
|
|
||||||
|
```env
|
||||||
|
AUTH__JWT_SECRET=replace-with-at-least-32-random-characters
|
||||||
|
AUTH__INVITE_CODE=replace-with-at-least-16-random-characters
|
||||||
|
AUTH__BOOTSTRAP_ADMIN_EMAIL=admin@example.com
|
||||||
|
SMTP__BODY_ENCRYPTION_KEY=replace-with-a-32-byte-encryption-key
|
||||||
|
```
|
||||||
|
|
||||||
|
不要重复使用 JWT 密钥和正文加密密钥。更换正文加密密钥后,使用旧密钥加密的历史正文将无法解密。
|
||||||
|
|
||||||
|
## 数据库
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="SQLite">
|
||||||
|
|
||||||
|
```env
|
||||||
|
DATABASE__DATABASE_TYPE=sqlite
|
||||||
|
DATABASE__PATH=data/app.db
|
||||||
|
```
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
<Tab title="PostgreSQL">
|
||||||
|
|
||||||
|
```env
|
||||||
|
DATABASE__DATABASE_TYPE=postgresql
|
||||||
|
DATABASE__HOST=database.example.com
|
||||||
|
DATABASE__PORT=5432
|
||||||
|
DATABASE__USER=email_unlimit
|
||||||
|
DATABASE__PASSWORD=CHANGE_ME
|
||||||
|
DATABASE__DATABASE=email_unlimit
|
||||||
|
```
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
<Tab title="MySQL">
|
||||||
|
|
||||||
|
```env
|
||||||
|
DATABASE__DATABASE_TYPE=mysql
|
||||||
|
DATABASE__HOST=database.example.com
|
||||||
|
DATABASE__PORT=3306
|
||||||
|
DATABASE__USER=email_unlimit
|
||||||
|
DATABASE__PASSWORD=CHANGE_ME
|
||||||
|
DATABASE__DATABASE=email_unlimit
|
||||||
|
```
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## Redis
|
||||||
|
|
||||||
|
```env
|
||||||
|
REDIS__ENABLED=true
|
||||||
|
REDIS__HOST=redis
|
||||||
|
REDIS__PORT=6379
|
||||||
|
REDIS__DB=0
|
||||||
|
```
|
||||||
|
|
||||||
|
Redis Key 自动使用项目名称前缀,与同一 Redis 实例中的其他服务隔离。禁用 Redis 时,限流降级到进程内实现,灰名单、分布式滥用计数和 Redis 队列能力不可用。
|
||||||
|
|
||||||
|
## 外发邮件
|
||||||
|
|
||||||
|
```env
|
||||||
|
EMAIL__ENABLED=true
|
||||||
|
EMAIL__SMTP_HOST=smtp.example.com
|
||||||
|
EMAIL__SMTP_PORT=587
|
||||||
|
EMAIL__SMTP_USERNAME=mailer
|
||||||
|
EMAIL__SMTP_PASSWORD=CHANGE_ME
|
||||||
|
EMAIL__FROM_EMAIL=noreply@example.com
|
||||||
|
EMAIL__QUEUE_ENABLED=true
|
||||||
|
EMAIL__WORKER_POOL_SIZE=2
|
||||||
|
```
|
||||||
|
|
||||||
|
外发邮件用于验证码和系统通知,与 TCP 25 的收信 SMTP 服务是两套独立能力。
|
||||||
|
|
||||||
|
## 积分配置
|
||||||
|
|
||||||
|
<Tip title="积分不使用环境变量">
|
||||||
|
注册奖励、签到奖励、成本和每日上限使用程序内置初始值,并保存在数据库中。
|
||||||
|
请在管理后台“积分规则”页面修改,不要设置 `CREDIT__*` 环境变量。
|
||||||
|
</Tip>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
title: 故障排查
|
||||||
|
description: 登录、收信、Redis、外发邮件和数据库的常见问题
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
## 管理端无法登录
|
||||||
|
|
||||||
|
检查账号是否同时满足:
|
||||||
|
|
||||||
|
- 邮箱和密码正确
|
||||||
|
- `role=admin`
|
||||||
|
- `status=active`
|
||||||
|
|
||||||
|
首个管理员应在部署前配置 `AUTH__BOOTSTRAP_ADMIN_EMAIL`,并使用该邮箱完成注册。不要直接修改当前登录管理员的角色或状态。
|
||||||
|
|
||||||
|
## 用户端或管理端请求旧后端
|
||||||
|
|
||||||
|
开发环境检查:
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_API_PROXY_TARGET=http://127.0.0.1:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
生产构建检查:
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_API_BASE_URL=https://api.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
修改 Vite 环境变量后需要重新启动开发服务或重新构建镜像。
|
||||||
|
|
||||||
|
## 邮件无法到达
|
||||||
|
|
||||||
|
按顺序检查:
|
||||||
|
|
||||||
|
1. MX 和 A 记录是否指向正确主机。
|
||||||
|
2. TCP 25 是否从公网可达。
|
||||||
|
3. SMTP 监听健康状态。
|
||||||
|
4. 收件域是否在 `SMTP__LOCAL_DOMAINS`。
|
||||||
|
5. 邮箱是否有效、容量是否足够。
|
||||||
|
6. 用户积分和每日收信次数是否足够。
|
||||||
|
7. 来源是否被黑名单或策略拒绝。
|
||||||
|
8. 邮件是否进入隔离队列。
|
||||||
|
|
||||||
|
## 同一邮箱在日志中出现多次
|
||||||
|
|
||||||
|
这是正常的一对多关系:邮箱管理中只有一条邮箱资源,邮件和 SMTP 审计按每次事件记录,因此同一地址会重复出现。
|
||||||
|
|
||||||
|
如果完全相同的邮件被上游 SMTP 重试,当前版本不能保证跨连接去重。排查时比较收件人、Message-ID、发件人和接收时间,不要仅按收件地址判断重复。
|
||||||
|
|
||||||
|
## Redis 显示未启用
|
||||||
|
|
||||||
|
`REDIS__ENABLED=false` 时属于正常的“未启用”,不是故障。设置为 `true` 后仍显示故障,应检查主机、端口、密码、网络和 `redis-cli ping`。
|
||||||
|
|
||||||
|
## 外发邮件未启用
|
||||||
|
|
||||||
|
收信 SMTP 和外发邮件服务互相独立。`EMAIL__ENABLED=false` 时,临时邮箱仍可收信,但验证码和系统邮件不能通过上游 SMTP 发送。
|
||||||
|
|
||||||
|
## SQLite 被锁定
|
||||||
|
|
||||||
|
- 确认没有多个错误配置的进程同时写入同一文件。
|
||||||
|
- 不要用文件复制工具直接覆盖运行中的数据库。
|
||||||
|
- 使用 `.backup` 在线备份。
|
||||||
|
- 高并发生产环境建议使用 PostgreSQL。
|
||||||
|
|
||||||
|
## 获取诊断信息
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsS http://127.0.0.1:3000/health
|
||||||
|
docker compose ps
|
||||||
|
docker compose logs --tail=200 backend
|
||||||
|
```
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
title: 用户指南
|
||||||
|
description: 注册、获取积分、创建邮箱和接收邮件
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
用户端提供临时邮箱的完整工作流:
|
||||||
|
|
||||||
|
1. 注册或登录账号。
|
||||||
|
2. 每日签到获取奖励积分。
|
||||||
|
3. 消耗积分创建临时邮箱。
|
||||||
|
4. 在邮箱有效期内接收和查看邮件。
|
||||||
|
5. 下载附件或删除不再需要的邮件。
|
||||||
|
|
||||||
|
继续阅读:
|
||||||
|
|
||||||
|
- [账号与积分](/docs/user-guide/account-and-credits)
|
||||||
|
- [创建与管理邮箱](/docs/user-guide/mailboxes)
|
||||||
|
- [收件箱与附件](/docs/user-guide/inbox)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
title: 账号与积分
|
||||||
|
description: 注册、登录、签到和积分消耗规则
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
## 注册与登录
|
||||||
|
|
||||||
|
打开用户端,使用邮箱和密码注册。部署方可能要求填写邀请口令;是否开放注册由管理员的部署配置决定。
|
||||||
|
|
||||||
|
登录成功后,用户端会保存访问令牌和刷新令牌。访问令牌过期时会自动刷新;刷新失败后需要重新登录。
|
||||||
|
|
||||||
|
## 默认积分规则
|
||||||
|
|
||||||
|
| 规则 | 默认值 |
|
||||||
|
| --- | --- |
|
||||||
|
| 注册赠送 | `30` 积分 |
|
||||||
|
| 每日签到 | `3` 积分 |
|
||||||
|
| 奖励余额上限 | `90` 积分 |
|
||||||
|
| 创建邮箱 | `2` 积分/个 |
|
||||||
|
| 接收正常邮件 | `1` 积分/封 |
|
||||||
|
| 每日创建邮箱上限 | `3` 个 |
|
||||||
|
| 每日收信上限 | `20` 封 |
|
||||||
|
|
||||||
|
<Warning title="没有免费次数">
|
||||||
|
第一个邮箱和第一封正常邮件都会立即消耗积分。每日上限和积分余额是两套同时生效的约束。
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
## 每日签到
|
||||||
|
|
||||||
|
- 每个自然日只能签到一次。
|
||||||
|
- 日期按照服务端配置的结算时区计算,默认 `Asia/Shanghai`。
|
||||||
|
- 奖励最多补到奖励余额上限。例如余额为 `89`、奖励为 `3` 时,实际只增加 `1`。
|
||||||
|
- 余额已经达到上限时仍会记录当天已签到,但实际奖励为 `0`。
|
||||||
|
|
||||||
|
## 积分不会退款
|
||||||
|
|
||||||
|
删除邮件、吊销邮箱或等待资源自然过期,都不会返还积分或当日次数。积分永久有效,但不能购买、转账、兑换或提现。
|
||||||
|
|
||||||
|
## 规则由管理员控制
|
||||||
|
|
||||||
|
积分数值保存在数据库中。管理员保存新规则后,下一次注册、签到、创建邮箱或收信操作会立即使用新值;已经完成的流水不会被追溯修改。
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
title: 收件箱与附件
|
||||||
|
description: 查看实时邮件、HTML 正文、附件和隔离邮件
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
## 接收邮件
|
||||||
|
|
||||||
|
邮箱有效且未达到容量、每日收信上限和积分约束时,正常邮件会写入收件箱并扣除当前收信成本。
|
||||||
|
|
||||||
|
用户端通过 WebSocket 接收新邮件通知。断线或页面刷新后,仍会通过 API 重新读取数据库中的邮件列表,不会依赖浏览器内存保存邮件。
|
||||||
|
|
||||||
|
## 正文与安全结果
|
||||||
|
|
||||||
|
邮件详情包含:
|
||||||
|
|
||||||
|
- 发件地址和显示名
|
||||||
|
- 主题与接收时间
|
||||||
|
- 纯文本和 HTML 正文
|
||||||
|
- 来源 IP、SPF、DKIM、DMARC 和风险分
|
||||||
|
- 附件名称、类型与大小
|
||||||
|
|
||||||
|
用户端展示的 HTML 邮件会经过安全处理。不要在临时邮件中提交密码、支付信息或长期有效的账号凭据。
|
||||||
|
|
||||||
|
## 附件
|
||||||
|
|
||||||
|
附件通过邮箱令牌授权下载。浏览器不会直接公开附件存储路径。
|
||||||
|
|
||||||
|
<Warning title="附件安全">
|
||||||
|
邮件附件可能包含恶意内容。下载后仍应使用操作系统和安全软件检查,不要直接运行未知可执行文件或脚本。
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
## 隔离邮件
|
||||||
|
|
||||||
|
风险分达到阈值的邮件进入隔离队列:
|
||||||
|
|
||||||
|
- 隔离时不扣积分,也不占用户每日收信次数。
|
||||||
|
- 隔离邮件仍占用存储空间。
|
||||||
|
- 管理员放行时才结算积分和次数。
|
||||||
|
- 结算失败时继续保持隔离,用户收件箱不会出现该邮件。
|
||||||
|
|
||||||
|
## 删除邮件
|
||||||
|
|
||||||
|
删除会物理移除正文和附件并释放容量,但不会返还积分或每日收信次数。
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
title: 创建与管理邮箱
|
||||||
|
description: 临时邮箱的创建、令牌、容量、到期和吊销规则
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
## 创建邮箱
|
||||||
|
|
||||||
|
在用户工作台选择域名,可填写自定义本地部分,也可以让系统随机生成。提交前页面会显示当前创建成本和今日剩余次数。
|
||||||
|
|
||||||
|
创建操作会在一个数据库事务中完成:
|
||||||
|
|
||||||
|
1. 占用当日创建次数。
|
||||||
|
2. 创建唯一邮箱地址。
|
||||||
|
3. 扣除积分。
|
||||||
|
4. 写入积分流水。
|
||||||
|
|
||||||
|
任一步失败都会整体回滚,不会留下半创建邮箱或扣分记录。
|
||||||
|
|
||||||
|
## 地址唯一性
|
||||||
|
|
||||||
|
邮箱地址统一转换为小写,并在数据库中设置唯一约束。同一地址不会对应多条邮箱资源记录。
|
||||||
|
|
||||||
|
<Tip title="日志中的重复地址">
|
||||||
|
同一邮箱可以收到多封邮件,因此它会在邮件和 SMTP 审计日志中出现多次。
|
||||||
|
这是“一条邮箱资源对应多条邮件事件”,不是邮箱记录重复。
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
## 邮箱状态
|
||||||
|
|
||||||
|
| 状态 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| 有效 | 可以接收邮件 |
|
||||||
|
| 已过期 | 超过到期时间,不能继续收信 |
|
||||||
|
| 已吊销 | 用户或管理员主动停止使用 |
|
||||||
|
|
||||||
|
过期和吊销只修改邮箱状态,不会删除邮箱行或重新分配地址。
|
||||||
|
|
||||||
|
## 邮箱令牌
|
||||||
|
|
||||||
|
每个邮箱有独立访问令牌,用于读取邮件、下载附件和建立 WebSocket。浏览器首次打开已有邮箱时会轮换令牌,并仅在当前浏览器保存新令牌。
|
||||||
|
|
||||||
|
不要把邮箱令牌写入日志、截图或公开链接。令牌丢失时应在用户端重新打开邮箱,让系统执行安全轮换。
|
||||||
|
|
||||||
|
## 容量
|
||||||
|
|
||||||
|
每封邮件和附件都会增加邮箱已用容量。达到容量上限后,SMTP 投递会被拒绝。删除邮件或清理任务删除过期邮件时会释放相应容量。
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
title: 无限邮箱
|
||||||
|
description: 可自托管的临时邮箱、积分与邮件治理平台
|
||||||
|
author: Email Unlimited
|
||||||
|
date: 2026-07-25
|
||||||
|
---
|
||||||
|
|
||||||
|
无限邮箱是一个可自托管的临时邮箱服务,由 Rust API/SMTP 服务、Vue 用户端和 React 管理后台组成。它可以直接接收公网邮件,并提供积分计费、风险隔离、黑名单、审计和运行健康监控。
|
||||||
|
|
||||||
|
<Tip title="从哪里开始">
|
||||||
|
第一次部署请先阅读 [Docker 部署](/docs/getting-started/docker-deployment) 和
|
||||||
|
[DNS 与 SMTP](/docs/getting-started/dns-and-smtp)。普通用户可以直接阅读
|
||||||
|
[用户指南](/docs/user-guide)。
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
<Card title="用户端">
|
||||||
|
注册、签到、创建临时邮箱、实时收信、查看正文与下载附件。
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="管理后台">
|
||||||
|
管理用户和邮箱,处置隔离邮件与黑名单,修改积分规则并审计关键操作。
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="自托管">
|
||||||
|
支持 SQLite、PostgreSQL 和 MySQL,可通过 Docker Compose 部署全部服务。
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
## 默认服务地址
|
||||||
|
|
||||||
|
| 服务 | 本地开发 | Docker 默认端口 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 用户端 | `http://127.0.0.1:5173` | `http://localhost:8080` |
|
||||||
|
| 管理端 | `http://127.0.0.1:4173` | `http://localhost:8081` |
|
||||||
|
| API | `http://127.0.0.1:3000` | `http://localhost:3000` |
|
||||||
|
| SMTP | TCP `25` | TCP `25` |
|
||||||
|
|
||||||
|
## 文档入口
|
||||||
|
|
||||||
|
- [快速开始](/docs/getting-started):开发、部署和 DNS 配置。
|
||||||
|
- [用户指南](/docs/user-guide):账号、积分、邮箱和收件箱。
|
||||||
|
- [管理后台](/docs/admin-guide):运维指标与治理操作。
|
||||||
|
- [运维手册](/docs/operations):配置、备份和故障排查。
|
||||||
|
- [接口约定](/docs/api):认证、分页和响应格式。
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||||
|
<polyline points="14,2 14,8 20,8"/>
|
||||||
|
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||||
|
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||||
|
<polyline points="10,9 9,9 8,9"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 390 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||||
|
<polyline points="14,2 14,8 20,8"/>
|
||||||
|
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||||
|
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||||
|
<polyline points="10,9 9,9 8,9"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 394 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1772871328740" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7872" width="32" height="32"><path d="M558.6176 105.87136a62.8736 62.8736 0 0 0 9.18016-32.8192c0-34.54976-27.70944-62.5664-61.9008-62.5664-34.176 0-61.90592 28.03712-61.90592 62.5664 0 11.66848 3.15392 22.5536 8.63744 31.88736-75.33056 0.93184-263.60832 60.544-263.60832 272.1024 0 167.53664-16.39424 387.28192-159.46752 387.28192l18.01216 59.88864 222.04928 3.69152c30.65344 101.29408 126.5152 178.05312 236.27776 178.05312 109.74208 0 197.46304-76.75904 228.11648-178.05312l223.36-3.65056 18.01216-59.9296c-133.5808 0-159.4624-204.1856-159.4624-387.10784 0-206.47936-165.0688-258.432-257.30048-271.34464z m-205.16352 725.1456h300.43136c-20.224 60.96896-76.17536 101.9904-147.9936 101.9904-72.61696 0-132.08064-40.4992-152.43776-101.9904z m-96.93184-454.2464c0-185.7536 196.10624-221.40928 249.3696-221.40928 26.8544 0 240.48128 17.7664 240.48128 221.40928 0 205.6704 42.73664 317.32736 81.53088 381.312H178.16064c39.00928-63.90784 78.3616-178.3296 78.3616-381.312z" fill="#000000" p-id="7873"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 291 KiB |
@@ -0,0 +1,25 @@
|
|||||||
|
# 图片目录
|
||||||
|
|
||||||
|
将您的图片文件放在这个目录中。
|
||||||
|
|
||||||
|
支持的格式:
|
||||||
|
- PNG
|
||||||
|
- JPG/JPEG
|
||||||
|
- SVG
|
||||||
|
- WebP
|
||||||
|
- GIF
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
在 Markdown 中引用图片:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|

|
||||||
|
```
|
||||||
|
|
||||||
|
在配置文件中使用:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
site:
|
||||||
|
logo: "/images/logo.png"
|
||||||
|
```
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free v7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M434.8 70.1c14.3 10.4 17.5 30.4 7.1 44.7l-256 352c-5.5 7.6-14 12.3-23.4 13.1s-18.5-2.7-25.1-9.3l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l101.5 101.5 234-321.7c10.4-14.3 30.4-17.5 44.7-7.1z"/></svg>
|
||||||
|
After Width: | Height: | Size: 434 B |
@@ -0,0 +1,7 @@
|
|||||||
|
export default {
|
||||||
|
enabled: true,
|
||||||
|
passphraseEnv: "REACT_DOCS_UI_ACCESS_PASSPHRASE",
|
||||||
|
include: ["/*"],
|
||||||
|
exclude: ["/docs/public/*"],
|
||||||
|
rememberSession: false,
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
interface AlertProps {
|
||||||
|
type?: 'info' | 'success' | 'warning' | 'error'
|
||||||
|
title?: string
|
||||||
|
icon?: string | React.ReactNode
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Alert({ type = 'info', title, icon, children }: AlertProps) {
|
||||||
|
const [isDarkMode, setIsDarkMode] = React.useState(false)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setIsDarkMode(document.documentElement.classList.contains('dark'))
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
setIsDarkMode(document.documentElement.classList.contains('dark'))
|
||||||
|
})
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class']
|
||||||
|
})
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const styles: Record<string, React.CSSProperties> = {
|
||||||
|
info: {
|
||||||
|
border: '1px solid #bfdbfe',
|
||||||
|
backgroundColor: '#eff6ff',
|
||||||
|
color: '#1e40af'
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
border: '1px solid #bbf7d0',
|
||||||
|
backgroundColor: '#f0fdf4',
|
||||||
|
color: '#166534'
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
border: '1px solid #fef9c3',
|
||||||
|
backgroundColor: '#fefce8',
|
||||||
|
color: '#854d0e'
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
border: '1px solid #fecaca',
|
||||||
|
backgroundColor: '#fef2f2',
|
||||||
|
color: '#991b1b'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const darkStyles: Record<string, React.CSSProperties> = {
|
||||||
|
info: {
|
||||||
|
border: '1px solid rgba(30, 58, 138, 0.15)',
|
||||||
|
backgroundColor: 'rgba(30, 58, 138, 0.1)',
|
||||||
|
color: '#bfdbfe'
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
border: '1px solid rgba(22, 101, 52, 0.15)',
|
||||||
|
backgroundColor: 'rgba(22, 101, 52, 0.1)',
|
||||||
|
color: '#bbf7d0'
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
border: '1px solid rgba(133, 77, 14, 0.15)',
|
||||||
|
backgroundColor: 'rgba(133, 77, 14, 0.1)',
|
||||||
|
color: '#fef9c3'
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
border: '1px solid rgba(153, 27, 27, 0.2)',
|
||||||
|
backgroundColor: 'rgba(153, 27, 27, 0.15)',
|
||||||
|
color: '#fecaca'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultIcons = {
|
||||||
|
info: 'ℹ️',
|
||||||
|
success: '✅',
|
||||||
|
warning: '⚠️',
|
||||||
|
error: '❌'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断 icon 的类型并渲染
|
||||||
|
const renderIcon = () => {
|
||||||
|
if (icon === undefined) {
|
||||||
|
return defaultIcons[type]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (React.isValidElement(icon)) {
|
||||||
|
// 如果是 React 元素(如 SVG 组件)
|
||||||
|
const iconElement = icon as React.ReactElement<any>
|
||||||
|
return React.cloneElement(iconElement, {
|
||||||
|
style: {
|
||||||
|
display: 'inline-block',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
width: '1.25rem',
|
||||||
|
height: '1.25rem',
|
||||||
|
marginRight: '0.5rem',
|
||||||
|
...(iconElement.props?.style || {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof icon === 'string' && icon.startsWith('/')) {
|
||||||
|
// 如果是路径字符串,渲染为图片
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={icon}
|
||||||
|
alt=""
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
width: '1.25rem',
|
||||||
|
height: '1.25rem',
|
||||||
|
marginRight: '0.5rem'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则作为字符串渲染
|
||||||
|
return icon
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
margin: '1rem 0',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
border: '1px solid',
|
||||||
|
padding: '1rem',
|
||||||
|
...(isDarkMode ? darkStyles[type] : styles[type])
|
||||||
|
}}>
|
||||||
|
{title && (
|
||||||
|
<div style={{
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
fontWeight: '600',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}>
|
||||||
|
{renderIcon()}
|
||||||
|
<span>{title}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
interface BadgeProps {
|
||||||
|
variant?: 'default' | 'success' | 'warning' | 'error' | 'info'
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BadgeListProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
function Badge({ variant = 'default', children }: BadgeProps) {
|
||||||
|
const [isDarkMode, setIsDarkMode] = React.useState(false)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setIsDarkMode(document.documentElement.classList.contains('dark'))
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
setIsDarkMode(document.documentElement.classList.contains('dark'))
|
||||||
|
})
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class']
|
||||||
|
})
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const styles: Record<string, React.CSSProperties> = {
|
||||||
|
default: {
|
||||||
|
backgroundColor: '#f3f4f6',
|
||||||
|
color: '#1f2937'
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
backgroundColor: '#dcfce7',
|
||||||
|
color: '#166534'
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
backgroundColor: '#fef9c3',
|
||||||
|
color: '#854d0e'
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
backgroundColor: '#fee2e2',
|
||||||
|
color: '#991b1b'
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
backgroundColor: '#dbeafe',
|
||||||
|
color: '#1e40af'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const darkStyles: Record<string, React.CSSProperties> = {
|
||||||
|
default: {
|
||||||
|
backgroundColor: 'rgba(55, 65, 81, 0.2)',
|
||||||
|
color: '#e5e7eb'
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
backgroundColor: 'rgba(22, 101, 52, 0.2)',
|
||||||
|
color: '#bbf7d0'
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
backgroundColor: 'rgba(133, 77, 14, 0.2)',
|
||||||
|
color: '#fef9c3'
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
backgroundColor: 'rgba(153, 27, 27, 0.2)',
|
||||||
|
color: '#fecaca'
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
backgroundColor: 'rgba(30, 58, 138, 0.2)',
|
||||||
|
color: '#bfdbfe'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
borderRadius: '9999px',
|
||||||
|
padding: '0.25rem 0.75rem',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
...(isDarkMode ? darkStyles[variant] : styles[variant])
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BadgeList({ children }: BadgeListProps) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
margin: '1rem 0',
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '0.5rem'
|
||||||
|
}}>
|
||||||
|
{React.Children.map(children, child =>
|
||||||
|
React.isValidElement(child) ? child : null
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BadgeList.Badge = Badge
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
interface CalloutProps {
|
||||||
|
variant?: 'default' | 'primary' | 'secondary'
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Callout({ variant = 'default', children }: CalloutProps) {
|
||||||
|
const [isDarkMode, setIsDarkMode] = React.useState(false)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setIsDarkMode(document.documentElement.classList.contains('dark'))
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
setIsDarkMode(document.documentElement.classList.contains('dark'))
|
||||||
|
})
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class']
|
||||||
|
})
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const styles: Record<string, React.CSSProperties> = {
|
||||||
|
default: {
|
||||||
|
borderLeft: '4px solid #6b7280',
|
||||||
|
backgroundColor: '#f9fafb'
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
borderLeft: '4px solid #3b82f6',
|
||||||
|
backgroundColor: '#eff6ff'
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
borderLeft: '4px solid #8b5cf6',
|
||||||
|
backgroundColor: '#faf5ff'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const darkStyles: Record<string, React.CSSProperties> = {
|
||||||
|
default: {
|
||||||
|
borderLeft: '4px solid rgba(75, 85, 99, 0.2)',
|
||||||
|
backgroundColor: 'rgba(17, 24, 39, 0.2)'
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
borderLeft: '4px solid rgba(30, 58, 138, 0.2)',
|
||||||
|
backgroundColor: 'rgba(30, 58, 138, 0.2)'
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
borderLeft: '4px solid rgba(88, 28, 135, 0.2)',
|
||||||
|
backgroundColor: 'rgba(88, 28, 135, 0.2)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
margin: '1rem 0',
|
||||||
|
borderTopRightRadius: '0.5rem',
|
||||||
|
borderBottomRightRadius: '0.5rem',
|
||||||
|
padding: '1rem',
|
||||||
|
...(isDarkMode ? darkStyles[variant] : styles[variant])
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
interface FeatureProps {
|
||||||
|
icon?: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Feature({ icon = '🚀', title, description }: FeatureProps) {
|
||||||
|
return (
|
||||||
|
<div className="my-4 rounded-lg border border-gray-200 p-6 shadow-sm hover:shadow-md transition-shadow dark:border-gray-600">
|
||||||
|
<div className="mb-3 text-4xl">{icon}</div>
|
||||||
|
<h3 className="mb-2 text-lg font-semibold">{title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">{description}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface MyTipProps {
|
||||||
|
title?: string
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MyTip({ title, children }: MyTipProps) {
|
||||||
|
return (
|
||||||
|
<div className="my-4 rounded-lg border border-green-200 bg-green-50 p-4 dark:border-green-800/60 dark:bg-green-950/80">
|
||||||
|
{title && (
|
||||||
|
<div className="mb-2 font-semibold text-green-900 dark:text-green-100">
|
||||||
|
💡 {title}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="text-green-800 dark:text-green-300">{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { startDocsApp } from "./utils/docs-bootstrap.package";
|
||||||
|
|
||||||
|
startDocsApp();
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
declare module "shiki/langs/*" {
|
||||||
|
const lang: any
|
||||||
|
export default lang
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "shiki/themes/*" {
|
||||||
|
const theme: any
|
||||||
|
export default theme
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { Buffer } from "buffer";
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import "react-docs-ui/style.css";
|
||||||
|
import { DocsApp, preloadDocsRuntime } from "react-docs-ui/docs-app";
|
||||||
|
import { MDX_COMPONENTS } from "../generated/mdx-components";
|
||||||
|
import { siteShikiBundle } from "../generated/shiki-bundle";
|
||||||
|
|
||||||
|
export function registerMdxComponents(components: Record<string, unknown>) {
|
||||||
|
(globalThis as typeof globalThis & {
|
||||||
|
__REACT_DOCS_UI_MDX_COMPONENTS__?: Record<string, unknown>;
|
||||||
|
}).__REACT_DOCS_UI_MDX_COMPONENTS__ = components;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bootstrapDocsApp(components: Record<string, unknown>) {
|
||||||
|
registerMdxComponents(components);
|
||||||
|
preloadDocsRuntime();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startDocsApp() {
|
||||||
|
window.Buffer = Buffer;
|
||||||
|
bootstrapDocsApp(MDX_COMPONENTS);
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
React.createElement(
|
||||||
|
React.StrictMode,
|
||||||
|
null,
|
||||||
|
React.createElement(DocsApp, { shikiBundle: siteShikiBundle }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
import { Buffer as BufferType } from "buffer";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
Buffer: typeof BufferType;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"ignoreDeprecations": "6.0",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"references": [{ "path": "./tsconfig.app.json" }],
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import type { Plugin } from 'vite'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从组件文件内容中提取复合组件的子组件
|
||||||
|
* 例如:从 StepList 组件中提取 StepList.Step
|
||||||
|
*/
|
||||||
|
function extractSubComponents(fileContent: string, componentName: string): string[] {
|
||||||
|
const subComponents: string[] = []
|
||||||
|
|
||||||
|
// 匹配模式:ComponentName.SubComponent = SubComponent
|
||||||
|
const pattern1 = new RegExp(`${componentName}\\.(\\w+)\\s*=\\s*(\\w+)`, 'g')
|
||||||
|
let match
|
||||||
|
while ((match = pattern1.exec(fileContent)) !== null) {
|
||||||
|
const subComponentName = match[1]
|
||||||
|
if (!subComponents.includes(subComponentName)) {
|
||||||
|
subComponents.push(subComponentName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配模式:ComponentName.SubComponent = function SubComponent
|
||||||
|
const pattern2 = new RegExp(`${componentName}\\.(\\w+)\\s*=\\s*(?:function|const)\\s+(\\w+)`, 'g')
|
||||||
|
while ((match = pattern2.exec(fileContent)) !== null) {
|
||||||
|
const subComponentName = match[1]
|
||||||
|
if (!subComponents.includes(subComponentName)) {
|
||||||
|
subComponents.push(subComponentName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return subComponents
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建一个 Vite 插件,用于自动扫描和注册 MDX 组件
|
||||||
|
* 这个插件会在构建时扫描 src/components 目录,并生成组件索引文件
|
||||||
|
*/
|
||||||
|
export function mdxComponentsPlugin(options: {
|
||||||
|
componentsPath?: string
|
||||||
|
outputPath?: string
|
||||||
|
}): Plugin {
|
||||||
|
const {
|
||||||
|
componentsPath = './src/components',
|
||||||
|
outputPath = './src/generated/mdx-components.ts'
|
||||||
|
} = options
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'mdx-components-scanner',
|
||||||
|
enforce: 'post',
|
||||||
|
buildStart() {
|
||||||
|
// 构建开始时,扫描组件并生成索引文件
|
||||||
|
try {
|
||||||
|
const componentsDir = path.resolve(process.cwd(), componentsPath)
|
||||||
|
|
||||||
|
if (!fs.existsSync(componentsDir)) {
|
||||||
|
console.log(`[MDX] 组件目录不存在: ${componentsDir}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归扫描所有 .tsx 和 .ts 文件
|
||||||
|
const scanDirectory = (dir: string, baseDir: string = dir): string[] => {
|
||||||
|
const files: string[] = []
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name)
|
||||||
|
const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/')
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
files.push(...scanDirectory(fullPath, baseDir))
|
||||||
|
} else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
|
||||||
|
const componentName = entry.name.replace(/\.(tsx?|jsx?)$/, '')
|
||||||
|
if (componentName !== 'index' && !componentName.startsWith('.')) {
|
||||||
|
files.push(relativePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentFiles = scanDirectory(componentsDir)
|
||||||
|
|
||||||
|
if (componentFiles.length === 0) {
|
||||||
|
console.log(`[MDX] 未找到任何组件文件`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[MDX] 扫描到 ${componentFiles.length} 个组件:`)
|
||||||
|
componentFiles.forEach(file => console.log(` - ${file}`))
|
||||||
|
|
||||||
|
// 分析每个组件文件,提取子组件信息
|
||||||
|
const componentInfo: Array<{
|
||||||
|
file: string
|
||||||
|
componentName: string
|
||||||
|
subComponents: string[]
|
||||||
|
}> = []
|
||||||
|
|
||||||
|
for (const file of componentFiles) {
|
||||||
|
const fullPath = path.join(componentsDir, file)
|
||||||
|
const componentName = path.basename(file).replace(/\.(tsx?|jsx?)$/, '')
|
||||||
|
const fileContent = fs.readFileSync(fullPath, 'utf-8')
|
||||||
|
const subComponents = extractSubComponents(fileContent, componentName)
|
||||||
|
|
||||||
|
componentInfo.push({
|
||||||
|
file,
|
||||||
|
componentName,
|
||||||
|
subComponents
|
||||||
|
})
|
||||||
|
|
||||||
|
if (subComponents.length > 0) {
|
||||||
|
console.log(`[MDX] 发现 ${componentName} 的子组件: ${subComponents.join(', ')}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成组件索引文件
|
||||||
|
const imports: string[] = []
|
||||||
|
const exports: string[] = []
|
||||||
|
const configEntries: string[] = []
|
||||||
|
|
||||||
|
for (const info of componentInfo) {
|
||||||
|
const { file, componentName, subComponents } = info
|
||||||
|
const importPath = `../components/${file.replace(/\.(tsx?|jsx?)$/, '')}`
|
||||||
|
|
||||||
|
// 导入主组件
|
||||||
|
imports.push(`import { ${componentName} } from '${importPath}'`)
|
||||||
|
exports.push(` ${componentName},`)
|
||||||
|
configEntries.push(` ${componentName}: '${importPath}'`)
|
||||||
|
|
||||||
|
// 如果有子组件,也需要注册它们
|
||||||
|
for (const subComponent of subComponents) {
|
||||||
|
const subComponentKey = `${componentName}.${subComponent}`
|
||||||
|
exports.push(` "${subComponentKey}": ${componentName}.${subComponent},`)
|
||||||
|
// 注意:子组件的配置指向父组件
|
||||||
|
configEntries.push(` "${subComponentKey}": '${importPath}'`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = `// 自动生成的 MDX 组件索引文件
|
||||||
|
// 请勿手动编辑此文件
|
||||||
|
|
||||||
|
${imports.join('\n')}
|
||||||
|
|
||||||
|
export const MDX_COMPONENTS = {
|
||||||
|
${exports.join('\n')}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出默认配置对象
|
||||||
|
export const mdxComponentsConfig = {
|
||||||
|
${configEntries.join(',\n')}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// 确保输出目录存在
|
||||||
|
const outputDir = path.dirname(outputPath)
|
||||||
|
if (!fs.existsSync(outputDir)) {
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入文件
|
||||||
|
fs.writeFileSync(outputPath, content, 'utf-8')
|
||||||
|
console.log(`[MDX] 组件索引文件已生成: ${outputPath}`)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[MDX] 扫描组件失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import path from "path";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import { spawn } from "child_process";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import { mdxComponentsPlugin } from "./vite-plugin-mdx-components";
|
||||||
|
import { staticAccessDevPlugin } from "react-docs-ui/static-access-dev";
|
||||||
|
|
||||||
|
function earlyDocsRuntimePreloadPlugin() {
|
||||||
|
return {
|
||||||
|
name: "early-docs-runtime-preload",
|
||||||
|
apply: "build",
|
||||||
|
async writeBundle(options, bundle) {
|
||||||
|
const preloadTargets = Object.values(bundle)
|
||||||
|
.filter((entry) => {
|
||||||
|
if (entry.type !== "chunk") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
/^assets\/MdxContent\.lazy-.*\.js$/.test(entry.fileName) ||
|
||||||
|
/^assets\/mdx-components-.*\.js$/.test(entry.fileName)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((entry) => entry.fileName)
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
if (preloadTargets.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputDir = options.dir ?? path.resolve(__dirname, "dist");
|
||||||
|
const indexHtmlPath = path.resolve(outputDir, "index.html");
|
||||||
|
|
||||||
|
let html;
|
||||||
|
try {
|
||||||
|
html = await fs.readFile(indexHtmlPath, "utf8");
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preloadTags = preloadTargets
|
||||||
|
.map(
|
||||||
|
(fileName) =>
|
||||||
|
` <link rel="modulepreload" crossorigin href="/${fileName}">`,
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
if (html.includes('rel="modulepreload" crossorigin href="/assets/MdxContent.lazy-')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextHtml = html.replace("</head>", `${preloadTags}\n </head>`);
|
||||||
|
await fs.writeFile(indexHtmlPath, nextHtml, "utf8");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createManualChunks(id) {
|
||||||
|
const normalizedId = id.replace(/\\/g, "/");
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedId.includes("/react-docs-ui/dist/docs-app.es.js") ||
|
||||||
|
normalizedId.includes("/react-docs-ui/dist/DocsApp-")
|
||||||
|
) {
|
||||||
|
return "docs-app";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedId.includes("/react-docs-ui/dist/GlobalContextMenu-") ||
|
||||||
|
normalizedId.includes("/react-docs-ui/dist/SearchRuntime-") ||
|
||||||
|
normalizedId.includes("/react-docs-ui/dist/SearchDialog-") ||
|
||||||
|
normalizedId.includes("/components/search/") ||
|
||||||
|
normalizedId.includes("/lib/search") ||
|
||||||
|
normalizedId.includes("/flexsearch/") ||
|
||||||
|
normalizedId.includes("/cmdk/")
|
||||||
|
) {
|
||||||
|
return "docs-search";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedId.includes("/components/ai/") ||
|
||||||
|
normalizedId.includes("/lib/ai")
|
||||||
|
) {
|
||||||
|
return "docs-ai";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedId.includes("/components/MdxContent") ||
|
||||||
|
normalizedId.includes("/react-markdown/") ||
|
||||||
|
normalizedId.includes("/remark-") ||
|
||||||
|
normalizedId.includes("/rehype-") ||
|
||||||
|
normalizedId.includes("/unified/") ||
|
||||||
|
normalizedId.includes("/micromark/") ||
|
||||||
|
normalizedId.includes("/mdast-util-") ||
|
||||||
|
normalizedId.includes("/hast-util-") ||
|
||||||
|
normalizedId.includes("/katex") ||
|
||||||
|
normalizedId.includes("/katex-physics") ||
|
||||||
|
normalizedId.includes("/gray-matter/")
|
||||||
|
) {
|
||||||
|
return "docs-renderer";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedId.includes("/react-router") ||
|
||||||
|
normalizedId.includes("/@radix-ui/") ||
|
||||||
|
normalizedId.includes("/lucide-react/")
|
||||||
|
) {
|
||||||
|
return "docs-ui";
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchIndexPlugin() {
|
||||||
|
return {
|
||||||
|
name: "search-index-plugin",
|
||||||
|
configureServer(server) {
|
||||||
|
const publicDir = path.resolve(__dirname, "public");
|
||||||
|
const docsDir = path.resolve(publicDir, "docs");
|
||||||
|
|
||||||
|
server.watcher.add(docsDir);
|
||||||
|
|
||||||
|
let generating = false;
|
||||||
|
let pending = false;
|
||||||
|
|
||||||
|
const generateIndex = () => {
|
||||||
|
if (generating) {
|
||||||
|
pending = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
generating = true;
|
||||||
|
|
||||||
|
const child = spawn(
|
||||||
|
"react-docs-ui",
|
||||||
|
["generate-search-index"],
|
||||||
|
{
|
||||||
|
cwd: path.resolve(__dirname),
|
||||||
|
stdio: "inherit",
|
||||||
|
shell: process.platform === "win32",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
child.on("close", () => {
|
||||||
|
generating = false;
|
||||||
|
if (pending) {
|
||||||
|
pending = false;
|
||||||
|
generateIndex();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDocFile = (file) => {
|
||||||
|
return (
|
||||||
|
file.includes(path.sep + "docs" + path.sep) &&
|
||||||
|
(file.endsWith(".md") || file.endsWith(".mdx"))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const debounce = (fn, delay) => {
|
||||||
|
let timer = null;
|
||||||
|
return (...args) => {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => fn(...args), delay);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const debouncedGenerate = debounce(generateIndex, 500);
|
||||||
|
|
||||||
|
server.watcher.on("change", (file) => {
|
||||||
|
if (isDocFile(file)) debouncedGenerate();
|
||||||
|
});
|
||||||
|
|
||||||
|
server.watcher.on("add", (file) => {
|
||||||
|
if (isDocFile(file)) debouncedGenerate();
|
||||||
|
});
|
||||||
|
|
||||||
|
server.watcher.on("unlink", (file) => {
|
||||||
|
if (isDocFile(file)) debouncedGenerate();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function publicHmrPlugin() {
|
||||||
|
return {
|
||||||
|
name: "public-hmr",
|
||||||
|
configureServer(server) {
|
||||||
|
const publicDir = path.resolve(__dirname, "public");
|
||||||
|
const configDir = path.resolve(publicDir, "config");
|
||||||
|
const docsDir = path.resolve(publicDir, "docs");
|
||||||
|
|
||||||
|
server.watcher.add([configDir, docsDir]);
|
||||||
|
|
||||||
|
const isTargetFile = (file) => {
|
||||||
|
const relativePath = path.relative(publicDir, file);
|
||||||
|
return (
|
||||||
|
(relativePath.startsWith("config" + path.sep) &&
|
||||||
|
file.endsWith(".yaml")) ||
|
||||||
|
(relativePath.startsWith("docs" + path.sep) &&
|
||||||
|
(file.endsWith(".md") || file.endsWith(".mdx")))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerReload = (file) => {
|
||||||
|
if (isTargetFile(file)) {
|
||||||
|
server.ws.send({ type: "full-reload", path: "*" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
server.watcher.on("change", triggerReload);
|
||||||
|
server.watcher.on("add", triggerReload);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
mdxComponentsPlugin({
|
||||||
|
componentsPath: "./src/components",
|
||||||
|
outputPath: "./src/generated/mdx-components.ts",
|
||||||
|
}),
|
||||||
|
earlyDocsRuntimePreloadPlugin(),
|
||||||
|
staticAccessDevPlugin(),
|
||||||
|
searchIndexPlugin(),
|
||||||
|
publicHmrPlugin(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
dedupe: ["react", "react-dom", "react-router-dom"],
|
||||||
|
alias: {
|
||||||
|
"@": "src",
|
||||||
|
buffer: "buffer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
global: "globalThis",
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: "0.0.0.0",
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: "http://localhost:3000",
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fs: {
|
||||||
|
allow: [
|
||||||
|
path.resolve(__dirname),
|
||||||
|
path.resolve(__dirname, "../../react-docs-ui/dist"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
chunkSizeWarningLimit: 2000,
|
||||||
|
assetsInlineLimit: 0,
|
||||||
|
modulePreload: false,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: createManualChunks,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user