# 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.py，app.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 开源协议。
