514 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Speakr - 定制版
> 基于 [murtaza-nasir/speakr](https://github.com/murtaza-nasir/speakr) 二次开发的语音转录与智能笔记平台。
## 项目简介
本项目是一个自托管的 AI 语音转录和智能笔记平台,支持:
- **语音录制与上传** - 浏览器直接录音或上传已有音频文件
- **AI 转录** - 高准确率语音转文字,支持说话人识别
- **交互式对话** - 对录音提问,获取 AI 回答
- **智能标签** - 支持带自定义 AI 提示词的标签
- **安全分享** - 生成安全链接分享录音
- **草稿功能** - 本地草稿保存与上传
- **声纹管理** - 自定义说话人声纹识别
- **标准词映射** - 自动纠正专业术语和人名
---
## 项目结构
```
speakr/
├── app.py # 入口文件(启动脚本)
├── src/
│ ├── app.py # Flask 应用主逻辑create_app 工厂模式)
│ ├── config.py # 统一配置文件(所有环境变量集中管理)
│ ├── logging_config.py # 日志配置模块(格式化、输出目标、日志级别)
│ ├── flask_ext.py # Flask 扩展实例集合LoginManager、Bcrypt、Limiter、CSRFProtect
│ ├── clients/ # 外部服务客户端层
│ │ ├── __init__.py # 统一导出所有客户端和配置
│ │ ├── database.py # 数据库客户端db 实例 + 配置 + 初始化
│ │ ├── llm_client.py # LLM 客户端OpenAI 客户端初始化
│ │ └── voiceprint_client.py # 声纹客户端VoiceprintClient
│ ├── api/ # API 路由层Flask 蓝图)
│ │ ├── __init__.py # 统一蓝图注册函数 register_blueprints()
│ │ ├── template_filters.py # 模板过滤器注册now、localdatetime
│ │ ├── auth.py # 认证路由(登录、注册、账户管理)
│ │ ├── recording.py # 录音管理CRUD、上传、转录、下载
│ │ ├── share.py # 分享功能
│ │ ├── tag.py # 标签管理
│ │ ├── speaker.py # 说话人管理
│ │ ├── admin.py # 管理后台
│ │ ├── search.py # 搜索和 Inquire
│ │ ├── voiceprint.py # 声纹管理
│ │ ├── config.py # 系统配置管理
│ │ ├── hotword.py # 热词管理
│ │ ├── template.py # 转录模板
│ │ ├── event.py # 事件管理
│ │ └── main.py # 主页和杂项
│ ├── models/ # 数据模型层(每个模型独立文件)
│ │ ├── __init__.py # 统一导出所有模型
│ │ ├── user.py # 用户模型
│ │ ├── speaker.py # 说话人模型
│ │ ├── recording.py # 录音模型
│ │ ├── tag.py # 标签模型
│ │ ├── recording_tag.py # 录音-标签关联模型
│ │ ├── event.py # 事件模型
│ │ ├── share.py # 分享模型
│ │ ├── transcript_chunk.py # 转录块模型
│ │ ├── transcript_template.py # 转录模板模型
│ │ ├── inquire_session.py # 询问会话模型
│ │ ├── voiceprint.py # 声纹模型
│ │ ├── system_setting.py # 系统设置模型
│ │ ├── system_config.py # 系统配置模型
│ │ ├── hotword.py # 热词模型
│ │ ├── draft_recording.py # 草稿录音模型
│ │ ├── draft_segment.py # 草稿片段模型
│ │ └── forms.py # 表单类(注册、登录)
│ ├── services/ # 业务逻辑层
│ │ ├── __init__.py # 统一导出所有服务函数
│ │ ├── llm_service.py # LLM 服务
│ │ ├── transcription_service.py # 转录服务
│ │ ├── summary_service.py # 摘要服务
│ │ ├── embedding_service.py # 嵌入向量服务
│ │ ├── event_service.py # 事件服务
│ │ ├── document_service.py # 文档服务
│ │ ├── speaker_service.py # 说话人服务
│ │ ├── voiceprint_service.py # 声纹服务(业务逻辑)
│ │ ├── auto_process_service.py # 自动处理服务
│ │ ├── auth_service.py # 认证服务
│ │ ├── sync_service.py # 远程数据同步服务
│ │ ├── audio_chunking.py # 音频分块服务
│ │ └── file_monitor.py # 文件监控服务
│ ├── utils/ # 工具函数层
│ │ ├── __init__.py
│ │ ├── markdown.py # Markdown 转 HTML 工具
│ │ ├── datetime.py # 时区转换工具
│ │ ├── text_utils.py # 文本处理工具
│ │ ├── json_utils.py # JSON 处理工具
│ │ └── standard_word_mappings.py # 标准词映射工具
├── templates/ # Jinja2 模板
├── static/ # 静态资源CSS/JS/图片)
├── instance/ # SQLite 数据库文件
├── uploads/ # 上传的音频文件
└── .env # 环境变量配置
```
---
## 启动方式
### 前置条件
- Python 环境(已安装 conda 环境 `speakr`
- Nginx 反向代理(必须启动)
### 启动步骤
```bash
# 1. 进入项目根目录speakr 2 目录)
cd "c:\Work\zdht\python-projects\speakr 2"
# 2. 启动 Nginx 反向代理(必须先启动)
# Nginx 负责将 https://localhost:8083/tool/speakr/ 请求转发到 Flask 后端
# ⚠️ 注意:不启动 Nginx 将无法访问应用
nginx # 或根据你的安装方式启动 nginx
# 3. 激活 conda 环境并启动应用
conda activate speakr
python .\speakr\app.py
```
Flask 后端默认监听 **5000 端口**Nginx 负责将 `https://localhost:8083/tool/speakr/` 请求转发到 `http://localhost:5000`
### 访问地址
- 主页面:`https://localhost:8083/tool/speakr/`
- 管理后台:`https://localhost:8083/tool/speakr/admin`
- 账户设置:`https://localhost:8083/tool/speakr/account`
### 常见错误
| 问题 | 原因 | 解决 |
|------|------|------|
| 页面无法访问 | Nginx 未启动 | 启动 Nginx 反向代理 |
| `ModuleNotFoundError: No module named 'xxx'` | conda 环境未激活 | 执行 `conda activate speakr` |
| 数据库文件位置错误 | 从错误目录启动 | 确保从 `speakr 2/` 目录执行 `python .\speakr\app.py` |
---
## 环境配置
编辑 `.env` 文件,主要配置项:
```ini
# ASR 服务配置
USE_ASR_ENDPOINT=true
ASR_BASE_URL=http://10.100.3.22:6688
ASR_DIARIZE=true
# 文本生成模型配置
TEXT_MODEL_BASE_URL=http://10.100.3.22:9800/v1
TEXT_MODEL_API_KEY=EMPTY
TEXT_MODEL_NAME=minimax-m2.5
# 数据库配置
SQLALCHEMY_DATABASE_URI=sqlite:///transcriptions.db
# PostgreSQL 配置示例(切换时只需修改 URI 即可)
# SQLALCHEMY_DATABASE_URI=postgresql://user:password@localhost/speakr
# DB_POOL_SIZE=10
# DB_MAX_OVERFLOW=20
# DB_POOL_TIMEOUT=30
# DB_POOL_RECYCLE=3600
# 外部访问端口Nginx 监听端口)
EXTERNAL_PORT=8083
# 时区配置
TIMEZONE=Asia/Shanghai
```
---
## 重构计划与当前进度
### 拆分原则
1. ⭐ 标记的模块是业务特色,拆分时只改代码组织结构,不改业务逻辑
2. 拆分顺序:
- **第一步**:抽出 Models最安全纯数据定义
- **第二步**:抽出 Services把 app.py 中的业务逻辑函数提取到独立文件)
- **第三步**:抽出 API 蓝图(把路由按功能域分组注册)
- **第四步**:抽出 Utils工具函数独立化
- **第五步**:模板组件化(可选,优先级较低)
3. `app.py` 最终只保留Flask 初始化、数据库初始化、蓝图注册、全局错误处理
4. 保持向后兼容:拆分过程中所有 API 路径不变,前端无需改动
### 需要特别注意的业务逻辑
| 功能 | 说明 | 拆分注意事项 |
|------|------|-------------|
| ⭐ 声纹管理 | VoiceprintClient 类、声纹注册/比对/同步 | 保持 VoiceprintClient 与外部平台的交互逻辑不变 |
| ⭐ 系统配置 | SystemConfig 动态配置读写 | 保持配置缓存和热加载机制 |
| ⭐ 草稿 API | 已有独立蓝图 draft_bp | 直接迁移,几乎不需改动 |
| ⭐ 标准词映射 | 转写后的文本纠正 | 保持映射规则和应用逻辑 |
| ⭐ 自动处理 | 定时/触发式自动转写 | 保持调度逻辑和状态管理 |
| ⭐ 收件箱 | 录音的收件箱/高亮状态 | 保持查询和状态切换逻辑 |
| ⭐ 热词 | 热词管理和应用 | 保持与转写服务的集成 |
### 当前进度
#### ✅ 已完成Phase 1 - Models 层抽取
已将 `src/app.py` 中的 **16 个数据模型****2 个表单类** 从主文件中抽取到独立模块:
| 状态 | 任务 | 说明 |
|------|------|------|
| ✅ | `src/database.py`(已移至 clients | 创建独立数据库实例文件,集中管理 SQLAlchemy db 对象Phase 2.5 移至 clients/database.py |
| ✅ | `src/models/` | 每个模型独立文件user.py, speaker.py, recording.py 等 16 个模型) |
| ✅ | `src/models/forms.py` | 表单类独立RegistrationForm, LoginForm, password_check |
| ✅ | `src/utils/markdown.py` | Markdown 转 HTML 工具函数 |
| ✅ | `src/utils/datetime.py` | 时区转换工具函数 |
| ✅ | `src/app.py` | 删除模型定义,改为从 `src.models` 导入 |
**验证结果**:应用可正常启动,数据库文件路径正确(`speakr/instance/transcriptions.db`
#### ✅ 已完成Phase 2 - Services 层抽取
**服务文件已全部创建**`src/services/` 目录下 11 个服务文件app.py 中的旧函数定义已全部删除。
| 状态 | 服务文件 | 包含函数 |
|------|---------|---------|
| ✅ | `llm_service.py` | `call_llm_completion`, `clean_llm_response`, `format_transcription_for_llm`, `process_streaming_with_thinking`, `extract_thinking_content`, `preprocess_long_transcription`, `extract_corrected_text`, `format_api_error_message` |
| ✅ | `transcription_service.py` | `transcribe_audio_asr`, `transcribe_audio_task`, `transcribe_single_file`, `transcribe_with_chunking`, `extract_audio_from_video`, `convert_to_wav` |
| ✅ | `summary_service.py` | `generate_title_task`, `generate_summary_only_task` |
| ✅ | `embedding_service.py` | `get_embedding_model`, `chunk_transcription`, `generate_embeddings`, `serialize_embedding`, `deserialize_embedding`, `process_recording_chunks`, `basic_text_search_chunks`, `semantic_search_chunks` |
| ✅ | `event_service.py` | `extract_events_from_transcript`, `generate_ics_content`, `escape_ical_text` |
| ✅ | `document_service.py` | `process_markdown_to_docx`, `get_recording_display_title`, `sanitize_download_filename_component`, `build_docx_download_filename`, `set_download_filename_header` |
| ✅ | `speaker_service.py` | `update_speaker_usage`, `identify_speakers_from_text`, `identify_unidentified_speakers_from_text` |
| ✅ | `voiceprint_service.py` | `sync_voiceprint_to_platform`VoiceprintClient 已移至 clients 层) |
| ✅ | `auto_process_service.py` | `initialize_file_monitor`, `get_file_monitor_functions` |
| ✅ | `auth_service.py` | `is_safe_url`, `auto_login` |
| ✅ | `__init__.py` | 统一导出所有服务函数 |
**已完成的工作:**
1. ✅ 删除了 app.py 中的 22 个重复函数定义,改为从服务层导入
2. ✅ 创建了 `src/utils/text_utils.py``src/utils/json_utils.py`,将工具函数移出 app.py
3. ✅ 添加了服务层和工具层的导入
4. ✅ 核心功能测试通过,应用运行正常
#### ✅ 已完成Phase 2.5 - Clients 层创建
按照行业规范创建了 `src/clients/` 层,统一管理与外部服务的连接客户端:
| 状态 | 客户端 | 说明 |
|------|--------|------|
| ✅ | `database.py` | 数据库客户端:`db` 实例 + 配置 + `init_database()` 初始化逻辑 |
| ✅ | `llm_client.py` | LLM 客户端OpenAI 客户端初始化client, TEXT_MODEL_NAME, TEXT_MODEL_API_KEY 等) |
| ✅ | `voiceprint_client.py` | 声纹客户端VoiceprintClient 类 + voiceprint_client() 延迟初始化 |
| ✅ | `__init__.py` | 统一导出所有客户端 |
**重构成果:**
- 删除了旧 `src/database.py`(空实例文件),合并到 `clients/database.py`
- 删除了 `services/voiceprint_service.py` 中的 `VoiceprintClient` 类,移到 `clients/voiceprint_client.py`
- 17 个模型文件 + 6 个服务文件的 `db` 导入统一改为 `from src.clients import db`
- 消除了层层传递 `client` 参数的问题
- 外部服务客户端与业务逻辑分离,架构更清晰
#### ✅ 已完成Phase 2.6 - 服务层注释中文化
将所有服务层文件中的英文注释和日志消息翻译为中文,提升代码可读性:
| 文件 | 翻译内容 |
|------|---------|
| `clients/llm_client.py` | URL 注释清理逻辑、客户端初始化逻辑 |
| `clients/voiceprint_client.py` | 声纹客户端注释和日志(新建文件,全中文) |
| `clients/database.py` | 数据库配置和初始化注释(新建文件,全中文) |
| `services/llm_service.py` | 思考内容提取、流式处理、API 错误格式化、分段总结等函数的注释和日志 |
| `services/summary_service.py` | 摘要生成全流程的注释和日志(标签提示词、语言要求、系统/用户消息构建等) |
| `services/transcription_service.py` | 音频提取、ASR 转录、分块转录、Whisper 调用等全流程注释和日志 |
| `services/embedding_service.py` | 文本分块、嵌入向量生成、语义搜索等全流程注释和日志 |
| `services/voiceprint_service.py` | 声纹同步业务逻辑注释 |
#### ✅ 已完成Phase 3 - API 蓝图抽取
`src/app.py` 中的约 100 个路由定义按功能域分组抽取到独立的 Flask 蓝图中app.py 从约 10000 行精简到约 700 行:
| 状态 | 蓝图文件 | 路由数量 | 说明 |
|------|---------|---------|------|
| ✅ | `api/auth.py` | 5 | 注册、登录、登出、账户管理、修改密码 |
| ✅ | `api/recording.py` | 24 | 录音 CRUD、上传、转录、下载、状态管理、ASR 校正 |
| ✅ | `api/share.py` | 7 | 分享创建、查看、访问 |
| ✅ | `api/tag.py` | 6 | 标签 CRUD、应用到录音 |
| ✅ | `api/speaker.py` | 5 | 说话人管理、搜索 |
| ✅ | `api/admin.py` | 18 | 管理后台、统计分析、录音管理、配置管理 |
| ✅ | `api/search.py` | 7 | 搜索、Inquire 对话、收件箱 |
| ✅ | `api/voiceprint.py` | 6 | 声纹注册、管理、同步 |
| ✅ | `api/config.py` | 6 | 系统配置读写、分段服务管理 |
| ✅ | `api/hotword.py` | 2 | 热词管理 |
| ✅ | `api/template.py` | 5 | 转录模板 CRUD |
| ✅ | `api/event.py` | 3 | 事件提取、ICS 导出 |
| ✅ | `api/main.py` | 5 | 主页、摘要生成、对话 |
**架构改进:**
- 创建 `api/__init__.py` 中的 `register_blueprints()` 函数,统一管理所有蓝图注册和初始化
- 使用 `init_xxx_bp()` 延迟注入模式解决循环导入问题limiter、csrf、bcrypt 等全局对象)
- 所有 API 路径保持不变,前端无需任何修改
- `app.py` 仅保留Flask 初始化、配置加载、蓝图注册、全局错误处理、启动逻辑
**重构后修复的问题:**
| 问题 | 原因 | 解决方案 |
|------|------|---------|
| 重定向循环 | `main.py` 中的 `index()` 路由误加了 `@login_required` 装饰器,丢失了自动登录逻辑 | 恢复备份中的 token 验证和自动登录逻辑,移除装饰器 |
| WebSocket 连接失败 | `getUserConfig` 路由缺少 `FUNASR_WEBSOCKET_IP``SCREEN_PUSH_IP` 配置 | 在 `main.py``get_user_config()` 中补充 WebSocket 配置字段 |
| 循环导入 bcrypt | `main.py` 在模块级别导入 `bcrypt` 导致循环依赖 | 改为在 `init_main_bp()` 中延迟注入 `bcrypt_instance` |
#### ✅ 已完成:统一配置管理
创建 `src/config.py` 统一配置文件,将所有散落的环境变量读取集中管理:
| 配置域 | 包含变量 |
|--------|---------|
| 基础配置 | `SECRET_KEY`, `UPLOAD_FOLDER`, `LOG_LEVEL`, `MAX_FILE_SIZE_MB` |
| ASR 配置 | `USE_ASR_ENDPOINT`, `ASR_BASE_URL`, `ASR_DIARIZE`, `ASR_MIN_SPEAKERS`, `ASR_MAX_SPEAKERS` |
| LLM 配置 | `TEXT_MODEL_API_KEY`, `TEXT_MODEL_BASE_URL`, `TEXT_MODEL_NAME` |
| 分段配置 | `ENABLE_CHUNKING` |
| Inquire 配置 | `ENABLE_INQUIRE_MODE` |
| 数据库配置 | `SQLALCHEMY_DATABASE_URI`, `DB_POOL_SIZE`, `DB_MAX_OVERFLOW`, `DB_POOL_TIMEOUT`, `DB_POOL_RECYCLE` |
| 外部 API | `TRANSCRIPTION_BASE_URL`, `VALID_TOKEN`, `EMBEDDINGS_AVAILABLE` |
| 代理配置 | `HTTP_PROXY`, `HTTPS_PROXY` |
**重构成果:**
- `clients/llm_client.py``clients/voiceprint_client.py``src.config` 导入配置
- `clients/__init__.py` 统一导出所有配置项
- `app.py` 通过 `from src.clients import ...` 统一导入配置,消除散落的环境变量读取
- 提供 `validate_config()` 函数在启动时验证关键配置
#### ✅ 已完成Phase 3.5 - app.py 清理与同步服务抽取
`src/app.py` 中残留的约 400 行业务逻辑彻底清理app.py 从约 700 行精简到约 234 行:
| 状态 | 任务 | 说明 |
|------|------|------|
| ✅ | 删除重复的 preprocess 函数 | 删除了 `preprocess_long_transcription` 等 6 个重复定义的函数(已在 llm_service.py 中) |
| ✅ | 抽取远程同步服务 | 创建了 `services/sync_service.py`,将约 180 行同步逻辑从 app.py 移出 |
| ✅ | 清理无用 import | 删除了 30+ 个未使用的导入render_template、OpenAI、httpx、subprocess、markdown 等) |
| ✅ | 注释中文化 | 将所有英文注释翻译为中文,删除无意义注释 |
| ✅ | 修复函数签名 | `start_sync_thread()` 增加 `app` 参数,调用处同步更新 |
| ✅ | 优化配置读取 | `preprocess_long_transcription` 等函数直接从 `src.config` 读取配置,不再通过参数传递 |
#### ✅ 已完成Phase 4 - app.py 工厂模式重构
采用 Flask `create_app()` 工厂模式,将 `src/app.py` 从 234 行进一步精简到 60 行,各层职责清晰分离:
**新建模块:**
| 文件 | 职责 |
|------|------|
| `src/logging_config.py` | 日志配置模块 |
| `src/flask_ext.py` | Flask 扩展实例集合login_manager、bcrypt、limiter、csrf |
| `src/services/register.py` | 服务层统一初始化(音频分段、同步线程、文件监控、表结构迁移) |
| `src/api/template_filters.py` | 模板过滤器注册 |
**修改模块:**
| 文件 | 修改内容 |
|------|---------|
| `src/config.py` | 新增 `get_app_config()` 函数,一次性返回所有需写入 `app.config` 的配置项。新增配置只需改此函数,无需修改 `app.py` |
| `src/clients/__init__.py` | 集中管理 `chunking_service``EMBEDDINGS_AVAILABLE` 的初始化与导出 |
| `src/api/__init__.py` | 简化 `register_blueprints()` 函数,直接从 `src.flask_ext` 导入扩展实例 |
| `src/app.py` | 采用 `create_app()` 工厂模式,仅负责编排各层初始化顺序 |
**`app.py` 最终结构60 行):**
```
create_app()
├── 1. configure_logging() ← src/logging_config.py
├── 2. 创建 Flask 应用实例
├── 3. 加载应用配置 ← src/config.py 的 get_app_config()
├── 4. 配置反向代理支持
├── 5. 初始化 Flask 扩展 ← src/flask_ext.py
├── 6. 初始化数据库 ← src/clients/database.py
├── 7. 注册蓝图 ← src/api/__init__.py
├── 8. 注册模板过滤器 ← src/api/template_filters.py
└── 9. 初始化服务层 ← src/services/register.py
```
**关键设计决策:**
| 决策 | 理由 |
|------|------|
| `flask_ext.py` 独立 | Flask 扩展是全局单例,不应放在 clients 或 services 中 |
| `get_app_config()` 集中管理 | 新增配置只需改 config.pyapp.py 永远不需要改动 |
| `register_blueprints` 自行读扩展 | 蓝图注册最清楚自己需要什么扩展,无需 app.py 中转 |
| `init_services` 放在最后 | 服务依赖数据库和蓝图,必须在它们之后初始化 |
---
## 开发规范
### API 接口返回格式规范
> **重要:所有新建/修改的 API 接口必须使用 `ApiResponse` 工具类统一返回格式。旧接口暂不强制迁移。**
#### 工具位置
`src/api/api_response.py`
#### 使用方式
```python
from src.api.api_response import ApiResponse
# 成功响应(返回数据)
@blueprint.route('/xxx', methods=['POST'])
def create_xxx():
result = do_something()
return ApiResponse.success({'xxx': result.to_dict()})
# 成功响应(带提示消息)
return ApiResponse.success({'xxx': result.to_dict()}, '创建成功')
# 成功响应(自定义状态码,如 201 Created
return ApiResponse.success({'xxx': result.to_dict()}, '创建成功', status_code=201)
# 错误响应
if not xxx:
return ApiResponse.error('资源未找到', 404)
# 错误响应(带详情)
if not valid:
return ApiResponse.error('参数校验失败', 400, detail='email 格式不正确')
# 分页响应
return ApiResponse.paginated(
items=[item.to_dict() for item in items],
pagination={
'page': page,
'per_page': per_page,
'total': total,
'total_pages': total_pages,
'has_next': has_next,
'has_prev': has_prev
}
)
```
#### 响应格式约定
**成功响应结构:**
```json
{
"success": true,
"message": "操作成功(可选)",
"xxx": {...} // 业务数据直接合并到顶层
}
```
**分页响应结构:**
```json
{
"success": true,
"items": [...],
"pagination": {
"page": 1,
"total": 100,
...
}
}
```
**错误响应结构:**
```json
{
"error": "用户友好的错误提示"
}
```
#### 注意事项
1. `data` 参数必须是字典类型,列表数据请使用 `paginated()` 或直接合并到字典中
2. 前端通过 `response.ok`HTTP 状态码)判断成功/失败,通过 `data.error` 获取错误信息
3. 旧接口暂不迁移,新接口和修改的接口必须使用 `ApiResponse`
4. 不要在 `ApiResponse` 中直接传递原始异常信息给用户(`str(e)`),应转为友好提示
---
## 技术栈
- **后端**: Python/Flask + SQLAlchemy
- **前端**: Vue.js 3 + Tailwind CSS
- **AI**: OpenAI Whisper (ASR) + Ollama/OpenRouter (LLM)
- **数据库**: SQLite默认/ PostgreSQL支持环境变量切换
- **部署**: Nginx 反向代理 + Flask 开发服务器
---
## 依赖安装
```bash
conda activate speakr
pip install -r requirements.txt
```
主要依赖:
- `flask==2.3.3`
- `flask-sqlalchemy==3.1.1`
- `flask-login==0.6.3`
- `flask-wtf==1.2.2`
- `flask-bcrypt==1.0.1`
- `openai==1.3.0`
- `markdown==3.5.1`
- `python-docx==1.1.0`
---
## 许可证
本项目基于 AGPL v3.0 开源协议。