feat: Extract hardcoded configs to environment variables

- Add environment variable configuration for backend and frontend
- Backend: DB_POOL_SIZE, JWT_EXPIRE_HOURS, timeout configs, directory paths
- Frontend: VITE_API_BASE_URL, VITE_UPLOAD_TIMEOUT, Whisper configs
- Create deployment script (scripts/deploy-backend.sh)
- Create 1Panel deployment guide (docs/1panel-deployment.md)
- Update DEPLOYMENT.md with env var documentation
- Create README.md with project overview
- Remove obsolete PRD.md, SDD.md, TDD.md (replaced by OpenSpec)
- Keep CORS allow_origins=["*"] for Electron EXE distribution

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
egg
2025-12-14 14:31:55 +08:00
parent 43c413c5ce
commit 01aee1fd0d
19 changed files with 1460 additions and 311 deletions

6
.gitignore vendored
View File

@@ -50,3 +50,9 @@ logs/
# Generated Excel records
backend/record/
# AI Assistant configuration
.claude/
openspec/
AGENTS.md
CLAUDE.md

View File

@@ -7,6 +7,24 @@
- MySQL 8.0+
- Access to Dify LLM service
## Quick Start
Use the startup script to run all services locally:
```bash
# Check environment
./start.sh check
# Start all services
./start.sh start
# Stop all services
./start.sh stop
# View status
./start.sh status
```
## Backend Deployment
### 1. Setup Environment
@@ -28,15 +46,33 @@ pip install -r requirements.txt
```bash
# Copy example and edit
cp .env.example .env
# Edit .env with actual values:
# - DB_HOST, DB_PORT, DB_USER, DB_PASS, DB_NAME
# - AUTH_API_URL
# - DIFY_API_URL, DIFY_API_KEY
# - ADMIN_EMAIL
# - JWT_SECRET (generate a secure random string)
```
**Required Environment Variables:**
| Variable | Description |
|----------|-------------|
| `BACKEND_HOST` | Server bind address (default: 0.0.0.0) |
| `BACKEND_PORT` | Server port (default: 8000) |
| `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASS`, `DB_NAME` | MySQL connection |
| `AUTH_API_URL` | Company authentication API |
| `DIFY_API_URL`, `DIFY_API_KEY`, `DIFY_STT_API_KEY` | Dify API settings |
| `ADMIN_EMAIL` | Admin user email |
| `JWT_SECRET` | JWT signing secret (generate secure random string) |
**Optional Environment Variables:**
| Variable | Description | Default |
|----------|-------------|---------|
| `DB_POOL_SIZE` | Database connection pool size | 5 |
| `JWT_EXPIRE_HOURS` | JWT token expiration | 24 |
| `UPLOAD_TIMEOUT` | File upload timeout (ms) | 600000 |
| `DIFY_STT_TIMEOUT` | STT processing timeout (ms) | 300000 |
| `LLM_TIMEOUT` | LLM request timeout (ms) | 120000 |
| `MAX_FILE_SIZE` | Max upload size (bytes) | 524288000 |
See `backend/.env.example` for complete documentation.
### 3. Run Server
```bash
@@ -54,6 +90,18 @@ curl http://localhost:8000/api/health
# Should return: {"status":"healthy","service":"meeting-assistant"}
```
### 5. Production Deployment (1Panel)
For detailed server deployment instructions including Nginx, systemd, and SSL configuration, see:
📖 **[docs/1panel-deployment.md](docs/1panel-deployment.md)**
Or use the deployment script:
```bash
sudo ./scripts/deploy-backend.sh install --port 8000
```
## Electron Client Deployment
### 1. Setup
@@ -65,16 +113,34 @@ cd client
npm install
```
### 2. Development
### 2. Configure Environment
```bash
# Copy example and edit
cp .env.example .env
```
**Environment Variables:**
| Variable | Description | Default |
|----------|-------------|---------|
| `VITE_API_BASE_URL` | Backend API URL | http://localhost:8000/api |
| `VITE_UPLOAD_TIMEOUT` | Upload timeout (ms) | 600000 |
| `WHISPER_MODEL` | Whisper model size | medium |
| `WHISPER_DEVICE` | Execution device | cpu |
| `WHISPER_COMPUTE` | Compute precision | int8 |
### 3. Development
```bash
npm start
```
### 3. Build for Distribution
### 4. Build for Distribution
```bash
# Build portable executable
# Update VITE_API_BASE_URL to production server first
# Then build portable executable
npm run build
```
@@ -102,7 +168,7 @@ The model will be downloaded automatically on first run. For faster startup, pre
```python
from faster_whisper import WhisperModel
model = WhisperModel("small", device="cpu", compute_type="int8")
model = WhisperModel("medium", device="cpu", compute_type="int8")
```
### 3. Build Executable
@@ -122,7 +188,7 @@ Copy `sidecar/dist/` to `client/sidecar/` before building Electron app.
The backend will automatically create tables on first startup. To manually verify:
```sql
USE db_A060;
USE your_database;
SHOW TABLES LIKE 'meeting_%';
```
@@ -154,24 +220,25 @@ On target hardware (i5/8GB):
### Database Connection Issues
1. Verify MySQL is accessible from server
2. Check firewall rules for port 33306
2. Check firewall rules for database port
3. Verify credentials in .env
### Dify API Issues
1. Verify API key is valid
2. Check Dify service status
3. Review timeout settings for long transcripts
3. Review timeout settings for long transcripts (adjust `DIFY_STT_TIMEOUT`, `LLM_TIMEOUT`)
### Transcription Issues
1. Verify microphone permissions
2. Check sidecar executable runs standalone
3. Review audio format (16kHz, 16-bit, mono)
4. Try different `WHISPER_MODEL` sizes (tiny, base, small, medium)
## Security Notes
- Never commit `.env` files
- Never commit `.env` files to version control
- Keep JWT_SECRET secure and unique per deployment
- Ensure HTTPS in production
- Ensure HTTPS in production (see [1panel-deployment.md](docs/1panel-deployment.md))
- Regular security updates for dependencies

61
PRD.md
View File

@@ -1,61 +0,0 @@
1. 產品概述
本系統為企業級會議知識管理解決方案。前端採用 Electron 進行邊緣運算(離線語音轉寫),後端整合公司現有 Auth API、MySQL 資料庫與 Dify LLM 服務。旨在解決會議記錄耗時問題,並透過結構化資料進行後續追蹤。
2. 功能需求 (Functional Requirements)
2.1 身份驗證 (Authentication)
FR-Auth-01 登入機制:
使用公司 API (https://pj-auth-api.vercel.app/api/auth/login) 進行驗證。
支援短效 Token 機制Client 端需實作自動續簽 (Auto-Refresh) 邏輯以維持長時間會議連線。
FR-Auth-02 權限管理:
預設管理員帳號ymirliu@panjit.com.tw (擁有所有會議檢視與 Excel 模板管理權限)。
2.2 會議建立與中繼資料 (Metadata Input)
FR-Meta-01 必填欄位:
由於 AI 無法憑空得知部分資訊,系統需在「建立會議」或「會議資訊」頁面提供以下手動輸入欄位:
會議主題 (Subject)
會議時間 (Date/Time)
會議主席 (Chairperson)
會議地點 (Location)
會議記錄人 (Recorder) - 預設帶入登入者
會議參與人員 (Attendees)
2.3 核心轉寫與編輯 (Core Transcription)
FR-Core-01 邊緣轉寫: 使用 i5/8G 筆電本地跑 faster-whisper (int8) 模型,並加上 OpenCC 強制繁體化。
FR-Core-02 即時修正: 支援雙欄介面,左側顯示 AI 逐字稿,右側為結構化筆記區。
2.4 AI 智慧摘要 (LLM Integration)
FR-LLM-01 Dify 整合:
串接 https://dify.theaken.com/v1。
將逐字稿送往 Dify並要求回傳包含以下資訊的結構化資料
會議結論 (Conclusions)
待辦事項 (Action Items):需解析出 內容、負責人、預計完成日。
FR-LLM-02 資料補全: 若 AI 無法識別負責人或日期UI 需提供介面讓使用者手動補填。
2.5 資料庫與追蹤 (Database & Tracking)
FR-DB-01 資料隔離: 所有資料表必須加上 meeting_ 前綴。
FR-DB-02 事項編號: 系統需自動為每一條「會議結論」與「待辦事項」產生唯一編號 (ID),以便後續追蹤執行現況。
2.6 報表輸出 (Export)
FR-Export-01 Excel 生成:
後端根據 Template 生成 Excel。
需包含所有 FR-Meta-01 及 FR-LLM-01 定義之欄位。

155
README.md Normal file
View File

@@ -0,0 +1,155 @@
# Meeting Assistant
會議助理系統 - 幫助企業高效記錄、整理和追蹤會議內容。
## 功能特色
- **即時語音轉寫**: 使用本地 Whisper 模型進行會議錄音轉文字
- **AI 智能摘要**: 透過 Dify LLM 自動生成會議結論與待辦事項
- **會議紀錄管理**: 建立、編輯、搜尋會議紀錄
- **Excel 匯出**: 支援自訂模板的會議紀錄匯出
- **多用戶支援**: 企業身份驗證整合
## 系統架構
```
Meeting_Assistant/
├── backend/ # FastAPI 後端 API
├── client/ # Electron 桌面應用程式
├── sidecar/ # Whisper 語音轉寫引擎
├── scripts/ # 部署腳本
└── docs/ # 文件
```
## 系統需求
### 開發環境
- Python 3.10+
- Node.js 18+
- MySQL 8.0+
### 運行環境
- Windows 10/11 (Electron 客戶端)
- Linux (後端服務器)
## 快速開始
### 1. 複製專案
```bash
git clone <repository-url>
cd Meeting_Assistant
```
### 2. 設置後端
```bash
cd backend
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env
# 編輯 .env 配置資料庫和 API 金鑰
```
### 3. 設置前端
```bash
cd client
npm install
cp .env.example .env
# 編輯 .env 配置後端 API 地址
```
### 4. 設置 Sidecar (語音轉寫)
```bash
cd sidecar
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
### 5. 啟動服務
```bash
# 使用啟動腳本
./start.sh start
# 或分別啟動
cd backend && uvicorn app.main:app --reload
cd client && npm start
```
## 配置說明
### 後端環境變數 (backend/.env)
| 變數 | 說明 | 必填 |
|------|------|------|
| `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASS`, `DB_NAME` | MySQL 連接資訊 | 是 |
| `AUTH_API_URL` | 企業認證 API | 是 |
| `DIFY_API_URL`, `DIFY_API_KEY`, `DIFY_STT_API_KEY` | Dify 服務配置 | 是 |
| `JWT_SECRET` | JWT 簽名密鑰 | 是 |
| `ADMIN_EMAIL` | 管理員郵箱 | 是 |
### 前端環境變數 (client/.env)
| 變數 | 說明 | 預設值 |
|------|------|--------|
| `VITE_API_BASE_URL` | 後端 API 地址 | http://localhost:8000/api |
| `WHISPER_MODEL` | Whisper 模型大小 | medium |
| `WHISPER_DEVICE` | 執行裝置 (cpu/cuda) | cpu |
## 部署
### 本地開發
```bash
./start.sh start
```
### 生產環境
- 後端獨立部署: 參考 [docs/1panel-deployment.md](docs/1panel-deployment.md)
- 前端打包分發: `cd client && npm run build`
詳細部署說明請參考 [DEPLOYMENT.md](DEPLOYMENT.md)
## API 文件
啟動後端後訪問:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
## 專案結構
```
backend/
├── app/
│ ├── main.py # FastAPI 應用入口
│ ├── config.py # 環境變數配置
│ ├── database.py # 資料庫連接
│ ├── models.py # 資料模型
│ └── routers/ # API 路由
│ ├── auth.py # 身份驗證
│ ├── meetings.py # 會議 CRUD
│ ├── ai.py # AI 摘要/STT
│ └── export.py # Excel 匯出
client/
├── src/
│ ├── main.js # Electron 主程序
│ ├── preload.js # 預載腳本
│ ├── index.html # 主頁面
│ └── services/
│ └── api.js # API 客戶端
sidecar/
├── transcriber.py # Whisper 轉寫服務
└── requirements.txt # Python 依賴
```
## 授權
Internal Use Only

143
SDD.md
View File

@@ -1,143 +0,0 @@
1. 系統架構圖 (System Architecture)
Plaintext
[Client: Electron App]
|
|-- (1. Auth API) --> [Ext: PJ-Auth API (Vercel)]
|
|-- (2. Meeting Data) --> [Middleware Server (Python FastAPI)]
|
|-- (3. SQL Query) --> [DB: MySQL (Shared)]
|
|-- (4. Summarize) --> [Ext: Dify LLM]
注意: 為了安全,資料庫連線資訊與 Dify API Key 嚴禁打包在 Electron Client 端,必須放在 Middleware Server。
2. 資料庫設計 (Database Schema)
Host: mysql.theaken.com (Port 33306)
User/Pass: A060 / WLeSCi0yhtc7
DB Name: db_A060
Prefix: meeting_
SQL
-- 1. 使用者表 (與 Auth API 對應,本地快取用)
CREATE TABLE meeting_users (
user_id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(100) UNIQUE NOT NULL, -- 對應 ymirliu@panjit.com.tw
display_name VARCHAR(50),
role ENUM('admin', 'user') DEFAULT 'user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 2. 會議主表
CREATE TABLE meeting_records (
meeting_id INT PRIMARY KEY AUTO_INCREMENT,
uuid VARCHAR(64) UNIQUE, -- 系統唯一識別碼
subject VARCHAR(200) NOT NULL, -- 會議主題
meeting_time DATETIME NOT NULL, -- 會議時間
location VARCHAR(100), -- 會議地點
chairperson VARCHAR(50), -- 會議主席
recorder VARCHAR(50), -- 會議記錄人
attendees TEXT, -- 參與人員 (逗號分隔或 JSON)
transcript_blob LONGTEXT, -- AI 原始逐字稿
created_by VARCHAR(100), -- 建立者 Email
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 3. 會議結論表 (Conclusions)
CREATE TABLE meeting_conclusions (
conclusion_id INT PRIMARY KEY AUTO_INCREMENT,
meeting_id INT,
content TEXT,
system_code VARCHAR(20), -- 會議結論編號 (如: C-20251210-01)
FOREIGN KEY (meeting_id) REFERENCES meeting_records(meeting_id)
);
-- 4. 待辦追蹤表 (Action Items)
CREATE TABLE meeting_action_items (
action_id INT PRIMARY KEY AUTO_INCREMENT,
meeting_id INT,
content TEXT, -- 追蹤事項內容
owner VARCHAR(50), -- 負責人
due_date DATE, -- 預計完成日期
status ENUM('Open', 'In Progress', 'Done', 'Delayed') DEFAULT 'Open', -- 執行現況
system_code VARCHAR(20), -- 會議事項編號 (如: A-20251210-01)
FOREIGN KEY (meeting_id) REFERENCES meeting_records(meeting_id)
);
3. Middleware Server 配置 (FastAPI 範例)
Client 端不直接連 MySQL而是呼叫此 Middleware。
3.1 環境變數 (.env)
Ini, TOML
DB_HOST=mysql.theaken.com
DB_PORT=33306
DB_USER=A060
DB_PASS=WLeSCi0yhtc7
DB_NAME=db_A060
AUTH_API_URL=https://pj-auth-api.vercel.app/api/auth/login
DIFY_API_URL=https://dify.theaken.com/v1
DIFY_API_KEY=app-xxxxxxxxxxx # 需至 Dify 後台取得
ADMIN_EMAIL=ymirliu@panjit.com.tw
3.2 API 介面規格
A. 登入代理 (Proxy)
Endpoint: POST /api/login
Logic: Middleware 轉發請求至 pj-auth-api.vercel.app。成功後若該 Email 為 ymirliu@panjit.com.tw則在回傳的 JWT Payload 中標記 { "role": "admin" }。
B. 上傳/同步會議
Endpoint: POST /api/meetings
Payload:
JSON
{
"meta": { "subject": "...", "chairperson": "...", ... },
"transcript": "...",
"conclusions": [ { "content": "..." } ],
"actions": [ { "content": "...", "owner": "...", "due_date": "..." } ]
}
Logic:
Insert into meeting_records.
Loop insert meeting_conclusions (自動生成 ID: C-{YYYYMMDD}-{Seq}).
Loop insert meeting_action_items (自動生成 ID: A-{YYYYMMDD}-{Seq}).
C. Dify 摘要請求
Endpoint: POST /api/ai/summarize
Payload: { "transcript": "..." }
Logic: 呼叫 Dify API。
Dify Prompt 設定 (System):
Plaintext
你是一個會議記錄助手。請根據逐字稿,回傳 JSON 格式。
必要欄位:
1. conclusions (Array): 結論內容
2. action_items (Array): { content, owner, due_date }
若逐字稿未提及日期或負責人,該欄位請留空字串。
D. Excel 匯出
Endpoint: POST /api/meetings/{id}/export
Logic:
SQL Join 查詢 records, conclusions, action_items。
Load template.xlsx.
Replace Placeholders:
{{subject}}, {{time}}, {{chair}}...
Table Filling: 動態插入 Rows 填寫結論與待辦事項。
Return File Stream.

46
TDD.md
View File

@@ -1,46 +0,0 @@
1. 單元測試 (Middleware)
Test-DB-Connect:
嘗試連線至 mysql.theaken.com:33306。
驗證 meeting_ 前綴表是否存在,若不存在則執行 CREATE TABLE 初始化腳本。
驗證 ymirliu@panjit.com.tw 是否能被識別為管理員。
Test-Dify-Proxy:
發送 Mock 文字至 /api/ai/summarize。
驗證 Server 能否正確解析 Dify 回傳的 JSON並處理 Dify 可能的 Timeout 或 500 錯誤。
2. 整合測試 (Client-Server)
Test-Auth-Flow:
Client 輸入帳密 -> Middleware -> Vercel Auth API。
驗證 Token 取得後Client 能否成功存取 /api/meetings。
重要: 驗證 Token 過期模擬(手動失效 TokenClient 攔截器是否觸發重試。
Test-Full-Cycle:
建立: 填寫表單(主席、地點...)。
錄音: 模擬 1 分鐘語音輸入。
摘要: 點擊「AI 摘要」,確認 Dify 回傳資料填入右側欄位。
補填: 手動修改「負責人」欄位。
存檔: 檢查 MySQL 資料庫是否正確寫入 meeting_action_items 且 status 預設為 'Open'。
匯出: 下載 Excel檢查所有欄位包含手動補填的負責人是否正確顯示。
3. 部署檢核表 (Deployment Checklist)
[ ] Middleware Server 的 requirements.txt 包含 mysql-connector-python, fastapi, requests, openpyxl。
[ ] Middleware Server 的環境變數 (.env) 已設定且保密。
[ ] Client 端 electron-builder 設定 target: portable。
[ ] Client 端 Python Sidecar 已包含 faster-whisper, opencc 並完成 PyInstaller 打包。

View File

@@ -1,16 +1,69 @@
# =============================================================================
# Meeting Assistant Backend Configuration
# Copy this file to .env and fill in your values
# =============================================================================
# -----------------------------------------------------------------------------
# Server Configuration
# -----------------------------------------------------------------------------
# Host address to bind (0.0.0.0 for all interfaces)
BACKEND_HOST=0.0.0.0
# Port number to listen on
BACKEND_PORT=8000
# -----------------------------------------------------------------------------
# Database Configuration
# -----------------------------------------------------------------------------
DB_HOST=mysql.theaken.com
DB_PORT=33306
DB_USER=A060
DB_USER=your_username
DB_PASS=your_password_here
DB_NAME=db_A060
DB_NAME=your_database
# Connection pool size (default: 5)
DB_POOL_SIZE=5
# -----------------------------------------------------------------------------
# External APIs
# -----------------------------------------------------------------------------
# Company authentication API endpoint
AUTH_API_URL=https://pj-auth-api.vercel.app/api/auth/login
# Dify API base URL
DIFY_API_URL=https://dify.theaken.com/v1
# Dify LLM API key (for summarization)
DIFY_API_KEY=app-xxxxxxxxxxx
# Dify STT API key (for audio transcription)
DIFY_STT_API_KEY=app-xxxxxxxxxxx
# Application Settings
ADMIN_EMAIL=ymirliu@panjit.com.tw
# -----------------------------------------------------------------------------
# Authentication Settings
# -----------------------------------------------------------------------------
# Email address with admin privileges
ADMIN_EMAIL=admin@example.com
# JWT signing secret (use a strong random string in production)
JWT_SECRET=your_jwt_secret_here
# JWT token expiration time in hours (default: 24)
JWT_EXPIRE_HOURS=24
# -----------------------------------------------------------------------------
# Timeout Configuration (in milliseconds)
# -----------------------------------------------------------------------------
# File upload timeout (default: 600000 = 10 minutes)
UPLOAD_TIMEOUT=600000
# Dify STT transcription timeout per chunk (default: 300000 = 5 minutes)
DIFY_STT_TIMEOUT=300000
# Dify LLM processing timeout (default: 120000 = 2 minutes)
LLM_TIMEOUT=120000
# Authentication API timeout (default: 30000 = 30 seconds)
AUTH_TIMEOUT=30000
# -----------------------------------------------------------------------------
# File Configuration
# -----------------------------------------------------------------------------
# Template directory path (leave empty for default: ./template)
# TEMPLATE_DIR=/path/to/templates
# Record output directory path (leave empty for default: ./record)
# RECORD_DIR=/path/to/records
# Maximum upload file size in bytes (default: 524288000 = 500MB)
MAX_FILE_SIZE=524288000
# Supported audio formats (comma-separated)
SUPPORTED_AUDIO_FORMATS=.mp3,.wav,.m4a,.webm,.ogg,.flac,.aac

View File

@@ -5,12 +5,19 @@ load_dotenv()
class Settings:
# Server Configuration
BACKEND_HOST: str = os.getenv("BACKEND_HOST", "0.0.0.0")
BACKEND_PORT: int = int(os.getenv("BACKEND_PORT", "8000"))
# Database Configuration
DB_HOST: str = os.getenv("DB_HOST", "mysql.theaken.com")
DB_PORT: int = int(os.getenv("DB_PORT", "33306"))
DB_USER: str = os.getenv("DB_USER", "A060")
DB_PASS: str = os.getenv("DB_PASS", "")
DB_NAME: str = os.getenv("DB_NAME", "db_A060")
DB_POOL_SIZE: int = int(os.getenv("DB_POOL_SIZE", "5"))
# External API Configuration
AUTH_API_URL: str = os.getenv(
"AUTH_API_URL", "https://pj-auth-api.vercel.app/api/auth/login"
)
@@ -18,8 +25,62 @@ class Settings:
DIFY_API_KEY: str = os.getenv("DIFY_API_KEY", "")
DIFY_STT_API_KEY: str = os.getenv("DIFY_STT_API_KEY", "")
# Authentication Configuration
ADMIN_EMAIL: str = os.getenv("ADMIN_EMAIL", "ymirliu@panjit.com.tw")
JWT_SECRET: str = os.getenv("JWT_SECRET", "meeting-assistant-secret")
JWT_EXPIRE_HOURS: int = int(os.getenv("JWT_EXPIRE_HOURS", "24"))
# Timeout Configuration (in milliseconds)
UPLOAD_TIMEOUT: int = int(os.getenv("UPLOAD_TIMEOUT", "600000")) # 10 minutes
DIFY_STT_TIMEOUT: int = int(os.getenv("DIFY_STT_TIMEOUT", "300000")) # 5 minutes
LLM_TIMEOUT: int = int(os.getenv("LLM_TIMEOUT", "120000")) # 2 minutes
AUTH_TIMEOUT: int = int(os.getenv("AUTH_TIMEOUT", "30000")) # 30 seconds
# File Configuration
TEMPLATE_DIR: str = os.getenv("TEMPLATE_DIR", "")
RECORD_DIR: str = os.getenv("RECORD_DIR", "")
MAX_FILE_SIZE: int = int(os.getenv("MAX_FILE_SIZE", str(500 * 1024 * 1024))) # 500MB
SUPPORTED_AUDIO_FORMATS: str = os.getenv(
"SUPPORTED_AUDIO_FORMATS", ".mp3,.wav,.m4a,.webm,.ogg,.flac,.aac"
)
@property
def supported_audio_formats_set(self) -> set:
"""Return supported audio formats as a set."""
return set(self.SUPPORTED_AUDIO_FORMATS.split(","))
def get_template_dir(self, base_dir: str) -> str:
"""Get template directory path, resolving relative paths."""
if self.TEMPLATE_DIR:
if os.path.isabs(self.TEMPLATE_DIR):
return self.TEMPLATE_DIR
return os.path.join(base_dir, self.TEMPLATE_DIR)
return os.path.join(base_dir, "template")
def get_record_dir(self, base_dir: str) -> str:
"""Get record directory path, resolving relative paths."""
if self.RECORD_DIR:
if os.path.isabs(self.RECORD_DIR):
return self.RECORD_DIR
return os.path.join(base_dir, self.RECORD_DIR)
return os.path.join(base_dir, "record")
# Timeout helpers (convert ms to seconds for httpx)
@property
def upload_timeout_seconds(self) -> float:
return self.UPLOAD_TIMEOUT / 1000.0
@property
def dify_stt_timeout_seconds(self) -> float:
return self.DIFY_STT_TIMEOUT / 1000.0
@property
def llm_timeout_seconds(self) -> float:
return self.LLM_TIMEOUT / 1000.0
@property
def auth_timeout_seconds(self) -> float:
return self.AUTH_TIMEOUT / 1000.0
settings = Settings()

View File

@@ -10,7 +10,7 @@ def init_db_pool():
global connection_pool
connection_pool = pooling.MySQLConnectionPool(
pool_name="meeting_pool",
pool_size=5,
pool_size=settings.DB_POOL_SIZE,
host=settings.DB_HOST,
port=settings.DB_PORT,
user=settings.DB_USER,

View File

@@ -13,10 +13,6 @@ from ..config import settings
from ..models import SummarizeRequest, SummarizeResponse, ActionItemCreate, TokenPayload
from .auth import get_current_user
# Supported audio formats
SUPPORTED_AUDIO_FORMATS = {".mp3", ".wav", ".m4a", ".webm", ".ogg", ".flac", ".aac"}
MAX_FILE_SIZE = 500 * 1024 * 1024 # 500MB
router = APIRouter()
@@ -45,7 +41,7 @@ async def summarize_transcript(
"response_mode": "blocking",
"user": current_user.email,
},
timeout=120.0, # Long timeout for LLM processing
timeout=settings.llm_timeout_seconds,
)
if response.status_code != 200:
@@ -135,10 +131,10 @@ async def transcribe_audio(
# Validate file extension
file_ext = os.path.splitext(file.filename or "")[1].lower()
if file_ext not in SUPPORTED_AUDIO_FORMATS:
if file_ext not in settings.supported_audio_formats_set:
raise HTTPException(
status_code=400,
detail=f"Unsupported audio format. Supported: {', '.join(SUPPORTED_AUDIO_FORMATS)}"
detail=f"Unsupported audio format. Supported: {settings.SUPPORTED_AUDIO_FORMATS}"
)
# Create temp directory for processing
@@ -151,10 +147,10 @@ async def transcribe_audio(
with open(temp_file_path, "wb") as f:
while chunk := await file.read(1024 * 1024): # 1MB chunks
file_size += len(chunk)
if file_size > MAX_FILE_SIZE:
if file_size > settings.MAX_FILE_SIZE:
raise HTTPException(
status_code=413,
detail=f"File too large. Maximum size: {MAX_FILE_SIZE // (1024*1024)}MB"
detail=f"File too large. Maximum size: {settings.MAX_FILE_SIZE // (1024*1024)}MB"
)
f.write(chunk)
@@ -245,18 +241,18 @@ async def transcribe_audio_stream(
# Validate file extension
file_ext = os.path.splitext(file.filename or "")[1].lower()
if file_ext not in SUPPORTED_AUDIO_FORMATS:
if file_ext not in settings.supported_audio_formats_set:
raise HTTPException(
status_code=400,
detail=f"Unsupported audio format. Supported: {', '.join(SUPPORTED_AUDIO_FORMATS)}"
detail=f"Unsupported audio format. Supported: {settings.SUPPORTED_AUDIO_FORMATS}"
)
# Read file into memory for streaming
file_content = await file.read()
if len(file_content) > MAX_FILE_SIZE:
if len(file_content) > settings.MAX_FILE_SIZE:
raise HTTPException(
status_code=413,
detail=f"File too large. Maximum size: {MAX_FILE_SIZE // (1024*1024)}MB"
detail=f"File too large. Maximum size: {settings.MAX_FILE_SIZE // (1024*1024)}MB"
)
async def generate_progress() -> AsyncGenerator[str, None]:
@@ -366,7 +362,7 @@ async def segment_audio_with_sidecar(audio_path: str, output_dir: str) -> dict:
# Send command and wait for response
stdout, stderr = await asyncio.wait_for(
process.communicate(input=f"{cmd_input}\n{{\"action\": \"quit\"}}\n".encode()),
timeout=600 # 10 minutes timeout for large files
timeout=settings.upload_timeout_seconds
)
# Parse response (skip status messages, find the segment result)
@@ -490,7 +486,7 @@ async def transcribe_chunk_with_dify(
}
]
},
timeout=300.0, # 5 minutes per chunk (increased for longer segments)
timeout=settings.dify_stt_timeout_seconds,
)
print(f"[Dify] Chat response: {response.status_code}")

View File

@@ -17,7 +17,7 @@ def create_token(email: str, role: str) -> str:
payload = {
"email": email,
"role": role,
"exp": datetime.utcnow() + timedelta(hours=24),
"exp": datetime.utcnow() + timedelta(hours=settings.JWT_EXPIRE_HOURS),
}
return jwt.encode(payload, settings.JWT_SECRET, algorithm="HS256")
@@ -67,7 +67,7 @@ async def login(request: LoginRequest):
response = await client.post(
settings.AUTH_API_URL,
json={"username": request.email, "password": request.password},
timeout=30.0,
timeout=settings.auth_timeout_seconds,
)
if response.status_code == 401:

View File

@@ -6,14 +6,14 @@ import io
import os
from ..database import get_db_cursor
from ..config import settings
from ..models import TokenPayload
from .auth import get_current_user, is_admin
router = APIRouter()
# Directory paths
TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "template")
RECORD_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "record")
# Base directory for resolving relative paths
BASE_DIR = os.path.join(os.path.dirname(__file__), "..", "..")
def fill_template_workbook(
@@ -186,8 +186,12 @@ async def export_meeting(
)
actions = cursor.fetchall()
# Get directory paths from config
template_dir = settings.get_template_dir(BASE_DIR)
record_dir = settings.get_record_dir(BASE_DIR)
# Check for template file
template_path = os.path.join(TEMPLATE_DIR, "meeting_template.xlsx")
template_path = os.path.join(template_dir, "meeting_template.xlsx")
if os.path.exists(template_path):
# Load and fill template
wb = load_workbook(template_path)
@@ -204,10 +208,10 @@ async def export_meeting(
filename = f"meeting_{meeting.get('uuid', meeting_id)}.xlsx"
# Ensure record directory exists
os.makedirs(RECORD_DIR, exist_ok=True)
os.makedirs(record_dir, exist_ok=True)
# Save to record directory
record_path = os.path.join(RECORD_DIR, filename)
record_path = os.path.join(record_dir, filename)
wb.save(record_path)
# Save to bytes buffer for download

39
client/.env.example Normal file
View File

@@ -0,0 +1,39 @@
# =============================================================================
# Meeting Assistant Frontend Configuration
# Copy this file to .env and fill in your values
# =============================================================================
# -----------------------------------------------------------------------------
# API Configuration (Vite build-time variables)
# -----------------------------------------------------------------------------
# Backend API base URL
# For development: http://localhost:8000/api
# For production: http://<server-ip>:<port>/api or https://api.example.com/api
VITE_API_BASE_URL=http://localhost:8000/api
# Upload timeout in milliseconds (default: 600000 = 10 minutes)
VITE_UPLOAD_TIMEOUT=600000
# Application title (shown in window title)
VITE_APP_TITLE=Meeting Assistant
# -----------------------------------------------------------------------------
# Sidecar/Whisper Configuration (Electron runtime variables)
# -----------------------------------------------------------------------------
# These environment variables are read by Electron main process at runtime
# and passed to the Sidecar (Whisper transcription service)
# Whisper model size
# Options: tiny, base, small, medium, large
# Larger models are more accurate but require more memory and are slower
WHISPER_MODEL=medium
# Execution device
# Options: cpu, cuda
# Use "cuda" if you have an NVIDIA GPU with CUDA support
WHISPER_DEVICE=cpu
# Compute precision
# Options: int8, float16, float32
# int8 is fastest but less accurate, float32 is most accurate but slowest
WHISPER_COMPUTE=int8

View File

@@ -44,10 +44,25 @@ function startSidecar() {
const pythonPath = fs.existsSync(venvPython) ? venvPython : "python3";
try {
// Get Whisper configuration from environment variables
const whisperEnv = {
...process.env,
WHISPER_MODEL: process.env.WHISPER_MODEL || "medium",
WHISPER_DEVICE: process.env.WHISPER_DEVICE || "cpu",
WHISPER_COMPUTE: process.env.WHISPER_COMPUTE || "int8",
};
console.log("Starting sidecar with:", pythonPath, sidecarScript);
console.log("Whisper config:", {
model: whisperEnv.WHISPER_MODEL,
device: whisperEnv.WHISPER_DEVICE,
compute: whisperEnv.WHISPER_COMPUTE,
});
sidecarProcess = spawn(pythonPath, [sidecarScript], {
cwd: sidecarDir,
stdio: ["pipe", "pipe", "pipe"],
env: whisperEnv,
});
// Handle stdout (JSON responses)

View File

@@ -1,4 +1,5 @@
const API_BASE_URL = "http://localhost:8000/api";
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:8000/api";
const UPLOAD_TIMEOUT = parseInt(import.meta.env.VITE_UPLOAD_TIMEOUT || "600000", 10);
let authToken = null;
let tokenRefreshTimer = null;
@@ -351,7 +352,7 @@ export async function transcribeAudioLegacy(file, onProgress = null) {
});
xhr.open("POST", url, true);
xhr.timeout = 600000; // 10 minutes for large files
xhr.timeout = UPLOAD_TIMEOUT;
if (token) {
xhr.setRequestHeader("Authorization", `Bearer ${token}`);
}

461
docs/1panel-deployment.md Normal file
View File

@@ -0,0 +1,461 @@
# Meeting Assistant 後端部署指南 (1Panel)
本文件說明如何在 1Panel 服務器上獨立部署 Meeting Assistant 後端服務。
## 目錄
1. [系統需求](#系統需求)
2. [快速部署](#快速部署)
3. [手動部署](#手動部署)
4. [Nginx 反向代理配置](#nginx-反向代理配置)
5. [SSL 證書配置](#ssl-證書配置)
6. [環境變數配置](#環境變數配置)
7. [維護與監控](#維護與監控)
8. [常見問題](#常見問題)
---
## 系統需求
### 硬體需求
- CPU: 2 核心以上
- 記憶體: 2GB 以上
- 硬碟: 10GB 以上可用空間
### 軟體需求
- 操作系統: Ubuntu 20.04+ / Debian 11+ / CentOS 8+
- Python: 3.10 或更高版本
- 1Panel: 已安裝並運行
### 網路需求
- 開放端口: 8000 (或自定義端口)
- 能夠連接外部 MySQL 數據庫
- 能夠連接 Dify API 服務
---
## 快速部署
使用部署腳本一鍵安裝:
```bash
# 1. 上傳專案到服務器
scp -r Meeting_Assistant user@server:/tmp/
# 2. 執行安裝腳本
cd /tmp/Meeting_Assistant
sudo ./scripts/deploy-backend.sh install --port 8000
# 3. 編輯配置檔案
sudo nano /opt/meeting-assistant/.env
# 4. 重啟服務
sudo systemctl restart meeting-assistant-backend
```
### 腳本選項
```bash
# 查看幫助
./scripts/deploy-backend.sh help
# 自定義安裝目錄和端口
sudo ./scripts/deploy-backend.sh install --dir /opt/my-app --port 8080
# 更新服務
sudo ./scripts/deploy-backend.sh update
# 查看狀態
./scripts/deploy-backend.sh status
# 查看日誌
./scripts/deploy-backend.sh logs
# 移除服務
sudo ./scripts/deploy-backend.sh uninstall
```
---
## 手動部署
如果需要更精細的控制,可以按以下步驟手動部署。
### 步驟 1: 安裝系統依賴
**Ubuntu/Debian:**
```bash
sudo apt update
sudo apt install -y python3 python3-pip python3-venv
```
**CentOS/RHEL:**
```bash
sudo dnf install -y python3 python3-pip python3-devel
```
### 步驟 2: 創建應用目錄
```bash
# 創建目錄
sudo mkdir -p /opt/meeting-assistant
sudo mkdir -p /opt/meeting-assistant/logs
sudo mkdir -p /opt/meeting-assistant/templates
sudo mkdir -p /opt/meeting-assistant/records
# 創建服務用戶
sudo useradd --system --no-create-home --shell /bin/false meeting
# 設置權限
sudo chown -R meeting:meeting /opt/meeting-assistant
```
### 步驟 3: 部署代碼
```bash
# 複製後端代碼
sudo cp -r backend/app /opt/meeting-assistant/
sudo cp backend/requirements.txt /opt/meeting-assistant/
sudo cp backend/.env.example /opt/meeting-assistant/.env
# 複製模板檔案(如果有)
sudo cp -r backend/templates/* /opt/meeting-assistant/templates/ 2>/dev/null || true
```
### 步驟 4: 創建虛擬環境並安裝依賴
```bash
cd /opt/meeting-assistant
# 創建虛擬環境
sudo python3 -m venv venv
# 安裝依賴
sudo ./venv/bin/pip install --upgrade pip
sudo ./venv/bin/pip install -r requirements.txt
# 設置權限
sudo chown -R meeting:meeting /opt/meeting-assistant
```
### 步驟 5: 配置環境變數
編輯 `/opt/meeting-assistant/.env`:
```bash
sudo nano /opt/meeting-assistant/.env
```
最小必要配置:
```env
# 服務器配置
BACKEND_HOST=0.0.0.0
BACKEND_PORT=8000
# 數據庫配置 (必須)
DB_HOST=your-mysql-host.com
DB_PORT=3306
DB_USER=your_user
DB_PASS=your_password
DB_NAME=your_database
# Dify API 配置 (必須)
DIFY_API_URL=https://your-dify-server.com/v1
DIFY_API_KEY=your-dify-api-key
DIFY_STT_API_KEY=your-dify-stt-api-key
# 認證 API (必須)
AUTH_API_URL=https://your-auth-api.com/api/auth/login
ADMIN_EMAIL=admin@company.com
JWT_SECRET=your-secure-jwt-secret
```
設置配置檔案權限:
```bash
sudo chmod 640 /opt/meeting-assistant/.env
sudo chown meeting:meeting /opt/meeting-assistant/.env
```
### 步驟 6: 創建 Systemd 服務
創建服務檔案 `/etc/systemd/system/meeting-assistant-backend.service`:
```bash
sudo nano /etc/systemd/system/meeting-assistant-backend.service
```
內容:
```ini
[Unit]
Description=Meeting Assistant Backend API
After=network.target
[Service]
Type=simple
User=meeting
Group=meeting
WorkingDirectory=/opt/meeting-assistant
Environment="PATH=/opt/meeting-assistant/venv/bin"
EnvironmentFile=/opt/meeting-assistant/.env
ExecStart=/opt/meeting-assistant/venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/meeting-assistant/logs /opt/meeting-assistant/records /opt/meeting-assistant/templates
PrivateTmp=true
[Install]
WantedBy=multi-user.target
```
### 步驟 7: 啟動服務
```bash
# 重新載入 systemd
sudo systemctl daemon-reload
# 啟用開機自動啟動
sudo systemctl enable meeting-assistant-backend
# 啟動服務
sudo systemctl start meeting-assistant-backend
# 檢查狀態
sudo systemctl status meeting-assistant-backend
```
---
## Nginx 反向代理配置
### 在 1Panel 中配置
1. 登入 1Panel 管理介面
2. 進入「網站」→「創建網站」
3. 選擇「反向代理」
4. 配置以下內容:
- 主域名: `meeting-api.yourdomain.com`
- 代理地址: `http://127.0.0.1:8000`
### 手動配置 Nginx
創建配置檔案 `/etc/nginx/sites-available/meeting-assistant`:
```nginx
server {
listen 80;
server_name meeting-api.yourdomain.com;
# 上傳檔案大小限制
client_max_body_size 500M;
# 超時設置
proxy_connect_timeout 60s;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 健康檢查端點
location /api/health {
proxy_pass http://127.0.0.1:8000;
proxy_connect_timeout 5s;
proxy_read_timeout 5s;
}
}
```
啟用配置:
```bash
sudo ln -s /etc/nginx/sites-available/meeting-assistant /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```
---
## SSL 證書配置
### 使用 1Panel 自動申請
1. 在 1Panel 中進入「網站」
2. 選擇已創建的網站
3. 點擊「HTTPS」
4. 選擇「申請證書」→「Let's Encrypt」
5. 啟用「強制 HTTPS」
### 手動使用 Certbot
```bash
# 安裝 certbot
sudo apt install -y certbot python3-certbot-nginx
# 申請證書
sudo certbot --nginx -d meeting-api.yourdomain.com
# 設置自動續期
sudo systemctl enable certbot.timer
```
---
## 環境變數配置
### 完整配置說明
| 變數名 | 說明 | 預設值 | 必填 |
|--------|------|--------|------|
| **服務器配置** ||||
| `BACKEND_HOST` | 監聽地址 | `0.0.0.0` | 否 |
| `BACKEND_PORT` | 監聽端口 | `8000` | 否 |
| **數據庫配置** ||||
| `DB_HOST` | MySQL 主機 | - | 是 |
| `DB_PORT` | MySQL 端口 | `3306` | 否 |
| `DB_USER` | 數據庫用戶 | - | 是 |
| `DB_PASS` | 數據庫密碼 | - | 是 |
| `DB_NAME` | 數據庫名稱 | - | 是 |
| `DB_POOL_SIZE` | 連接池大小 | `5` | 否 |
| **外部 API** ||||
| `AUTH_API_URL` | 認證 API 地址 | - | 是 |
| `DIFY_API_URL` | Dify API 地址 | - | 是 |
| `DIFY_API_KEY` | Dify LLM API 密鑰 | - | 是 |
| `DIFY_STT_API_KEY` | Dify STT API 密鑰 | - | 是 |
| **認證設置** ||||
| `ADMIN_EMAIL` | 管理員郵箱 | - | 是 |
| `JWT_SECRET` | JWT 密鑰 | - | 是 |
| `JWT_EXPIRE_HOURS` | JWT 過期時間(小時) | `24` | 否 |
| **超時配置 (毫秒)** ||||
| `UPLOAD_TIMEOUT` | 檔案上傳超時 | `600000` | 否 |
| `DIFY_STT_TIMEOUT` | STT 處理超時 | `300000` | 否 |
| `LLM_TIMEOUT` | LLM 請求超時 | `120000` | 否 |
| `AUTH_TIMEOUT` | 認證請求超時 | `30000` | 否 |
| **檔案配置** ||||
| `TEMPLATE_DIR` | 模板目錄路徑 | `./templates` | 否 |
| `RECORD_DIR` | 記錄目錄路徑 | `./records` | 否 |
| `MAX_FILE_SIZE` | 最大檔案大小(bytes) | `524288000` | 否 |
| `SUPPORTED_AUDIO_FORMATS` | 支援的音訊格式 | `.mp3,.wav,.m4a,...` | 否 |
---
## 維護與監控
### 常用命令
```bash
# 查看服務狀態
sudo systemctl status meeting-assistant-backend
# 查看日誌
sudo journalctl -u meeting-assistant-backend -f
# 重啟服務
sudo systemctl restart meeting-assistant-backend
# 停止服務
sudo systemctl stop meeting-assistant-backend
# 查看最近錯誤
sudo journalctl -u meeting-assistant-backend --since "1 hour ago" | grep -i error
```
### 健康檢查
API 提供健康檢查端點:
```bash
# 檢查 API 是否正常運行
curl http://localhost:8000/api/health
```
### 備份策略
建議備份以下內容:
1. 配置檔案: `/opt/meeting-assistant/.env`
2. 模板檔案: `/opt/meeting-assistant/templates/`
3. 會議記錄: `/opt/meeting-assistant/records/`
4. 數據庫: 使用 mysqldump 定期備份
---
## 常見問題
### Q: 服務啟動失敗?
檢查日誌:
```bash
sudo journalctl -u meeting-assistant-backend -n 50 --no-pager
```
常見原因:
1. Python 依賴未安裝完全
2. .env 配置錯誤
3. 數據庫連接失敗
4. 端口被佔用
### Q: 數據庫連接失敗?
1. 確認數據庫服務器可訪問:
```bash
telnet your-db-host 3306
```
2. 確認用戶權限:
```sql
GRANT ALL PRIVILEGES ON your_database.* TO 'your_user'@'%';
FLUSH PRIVILEGES;
```
3. 檢查防火牆設置
### Q: Dify API 請求超時?
1. 增加超時配置:
```env
DIFY_STT_TIMEOUT=600000
LLM_TIMEOUT=180000
```
2. 確認 Dify 服務器可訪問:
```bash
curl https://your-dify-server.com/v1/health
```
### Q: 如何更新到新版本?
```bash
# 方法 1: 使用腳本
sudo ./scripts/deploy-backend.sh update
# 方法 2: 手動更新
sudo systemctl stop meeting-assistant-backend
sudo cp -r backend/app /opt/meeting-assistant/
sudo /opt/meeting-assistant/venv/bin/pip install -r backend/requirements.txt
sudo systemctl start meeting-assistant-backend
```
### Q: 如何查看 API 文檔?
啟動服務後訪問:
- Swagger UI: `http://your-server:8000/docs`
- ReDoc: `http://your-server:8000/redoc`
---
## 聯繫支援
如有問題,請聯繫 IT 部門或查看專案 README。

View File

@@ -4,22 +4,26 @@
TBD - created by archiving change add-meeting-assistant-mvp. Update Purpose after archive.
## Requirements
### Requirement: FastAPI Server Configuration
The middleware server SHALL be implemented using Python FastAPI framework with environment-based configuration.
The middleware server SHALL be implemented using Python FastAPI framework with comprehensive environment-based configuration supporting standalone deployment.
#### Scenario: Server startup with valid configuration
- **WHEN** the server starts with valid .env file containing DB_HOST, DB_PORT, DB_USER, DB_PASS, DB_NAME, DIFY_API_URL, DIFY_API_KEY
- **THEN** the server SHALL start successfully and accept connections
- **WHEN** the server starts with valid .env file containing all required variables (DB_HOST, DB_PORT, DB_USER, DB_PASS, DB_NAME, DIFY_API_URL, DIFY_API_KEY, AUTH_API_URL)
- **THEN** the server SHALL start successfully and accept connections on the configured BACKEND_HOST and BACKEND_PORT
#### Scenario: Server startup with missing configuration
- **WHEN** the server starts with missing required environment variables
- **THEN** the server SHALL fail to start with descriptive error message
#### Scenario: Server startup with optional configuration
- **WHEN** optional environment variables (BACKEND_PORT, DB_POOL_SIZE, etc.) are not set
- **THEN** the server SHALL use sensible defaults and start normally
### Requirement: Database Connection Pool
The middleware server SHALL maintain a connection pool to the MySQL database at mysql.theaken.com:33306.
The middleware server SHALL maintain a configurable connection pool to the MySQL database using environment variables.
#### Scenario: Database connection success
- **WHEN** the server connects to MySQL with valid credentials
- **THEN** a connection pool SHALL be established and queries SHALL execute successfully
- **WHEN** the server connects to MySQL with valid credentials from environment
- **THEN** a connection pool SHALL be established with DB_POOL_SIZE connections
#### Scenario: Database connection failure
- **WHEN** the database is unreachable
@@ -37,9 +41,95 @@ The middleware server SHALL ensure all required tables exist on startup with the
- **THEN** the server SHALL skip table creation and continue normally
### Requirement: CORS Configuration
The middleware server SHALL allow cross-origin requests from the Electron client.
The middleware server SHALL allow cross-origin requests from all origins to support Electron desktop application clients.
#### Scenario: CORS preflight request
- **WHEN** Electron client sends OPTIONS request
- **THEN** the server SHALL respond with appropriate CORS headers allowing the request
- **WHEN** any client sends OPTIONS request
- **THEN** the server SHALL respond with CORS headers allowing the request (allow_origins=["*"])
### Requirement: Backend Server Configuration
The middleware server SHALL support configurable host and port through environment variables for flexible deployment.
#### Scenario: Custom port binding
- **WHEN** BACKEND_PORT environment variable is set to 9000
- **THEN** the server SHALL listen on port 9000
#### Scenario: Production host binding
- **WHEN** BACKEND_HOST is set to 0.0.0.0
- **THEN** the server SHALL accept connections from any network interface
#### Scenario: Default configuration
- **WHEN** BACKEND_HOST and BACKEND_PORT are not set
- **THEN** the server SHALL default to 0.0.0.0:8000
### Requirement: Timeout Configuration
The middleware server SHALL support configurable timeout values for different operations through environment variables.
#### Scenario: File upload timeout
- **WHEN** UPLOAD_TIMEOUT is set to 900000 (15 minutes)
- **THEN** file upload operations SHALL allow up to 15 minutes before timeout
#### Scenario: LLM processing timeout
- **WHEN** LLM_TIMEOUT is set to 180000 (3 minutes)
- **THEN** Dify LLM summarization operations SHALL allow up to 3 minutes before timeout
#### Scenario: Dify STT timeout
- **WHEN** DIFY_STT_TIMEOUT is set to 600000 (10 minutes)
- **THEN** Dify STT audio transcription per chunk SHALL allow up to 10 minutes before timeout
#### Scenario: Authentication timeout
- **WHEN** AUTH_TIMEOUT is set to 60000 (1 minute)
- **THEN** authentication API calls SHALL allow up to 1 minute before timeout
### Requirement: File Path Configuration
The middleware server SHALL support configurable directory paths for templates and records.
#### Scenario: Custom template directory
- **WHEN** TEMPLATE_DIR environment variable is set to /data/templates
- **THEN** Excel templates SHALL be loaded from /data/templates
#### Scenario: Custom record directory
- **WHEN** RECORD_DIR environment variable is set to /data/records
- **THEN** exported meeting records SHALL be saved to /data/records
#### Scenario: Relative path resolution
- **WHEN** directory paths are relative
- **THEN** they SHALL be resolved relative to the backend application root
### Requirement: Frontend Environment Configuration
The frontend Electron application SHALL support environment-based API URL configuration for connecting to deployed backend.
#### Scenario: Custom API URL in production build
- **WHEN** VITE_API_BASE_URL is set to http://192.168.1.100:8000/api during build
- **THEN** the built Electron app SHALL connect to http://192.168.1.100:8000/api
#### Scenario: Default API URL in development
- **WHEN** VITE_API_BASE_URL is not set
- **THEN** the frontend SHALL default to http://localhost:8000/api
### Requirement: Sidecar Whisper Configuration
The Electron frontend's Sidecar (local Whisper transcription service) SHALL support environment-based model configuration.
#### Scenario: Custom Whisper model
- **WHEN** WHISPER_MODEL environment variable is set to "large"
- **THEN** the Sidecar SHALL load the large Whisper model for transcription
#### Scenario: GPU acceleration
- **WHEN** WHISPER_DEVICE is set to "cuda" and WHISPER_COMPUTE is set to "float16"
- **THEN** the Sidecar SHALL use GPU for faster transcription
#### Scenario: Default CPU mode
- **WHEN** WHISPER_DEVICE is not set
- **THEN** the Sidecar SHALL default to CPU with int8 compute type
### Requirement: Environment Example Files
The project SHALL provide example environment files documenting all configuration options.
#### Scenario: Backend environment example
- **WHEN** developer sets up backend
- **THEN** backend/.env.example SHALL list all environment variables with descriptions and example values (without sensitive data)
#### Scenario: Frontend environment example
- **WHEN** developer sets up frontend
- **THEN** client/.env.example SHALL list all VITE_ prefixed and WHISPER_ prefixed environment variables with descriptions

439
scripts/deploy-backend.sh Executable file
View File

@@ -0,0 +1,439 @@
#!/bin/bash
#
# Meeting Assistant Backend - Deployment Script
# 用於在 1Panel 或其他 Linux 服務器上部署後端服務
#
set -e
# 顏色定義
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 預設配置
DEFAULT_INSTALL_DIR="/opt/meeting-assistant"
DEFAULT_USER="meeting"
DEFAULT_PORT="8000"
# 函數:印出訊息
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[OK]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 函數:顯示幫助
show_help() {
echo ""
echo "Meeting Assistant Backend 部署腳本"
echo ""
echo "用法: $0 [命令] [選項]"
echo ""
echo "命令:"
echo " install 安裝後端服務"
echo " update 更新後端服務"
echo " uninstall 移除後端服務"
echo " status 顯示服務狀態"
echo " logs 顯示服務日誌"
echo " help 顯示此幫助訊息"
echo ""
echo "選項:"
echo " --dir DIR 安裝目錄 (預設: $DEFAULT_INSTALL_DIR)"
echo " --user USER 運行服務的用戶 (預設: $DEFAULT_USER)"
echo " --port PORT 服務端口 (預設: $DEFAULT_PORT)"
echo ""
echo "範例:"
echo " $0 install --port 8080"
echo " $0 update"
echo " $0 status"
echo ""
}
# 函數:檢查是否為 root
check_root() {
if [ "$EUID" -ne 0 ]; then
log_error "請使用 root 權限執行此腳本"
log_info "使用: sudo $0 $@"
exit 1
fi
}
# 函數:檢查依賴
check_dependencies() {
log_info "檢查系統依賴..."
local missing=()
# 檢查 Python 3.10+
if command -v python3 &> /dev/null; then
local py_version=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
log_success "Python: $py_version"
else
missing+=("python3")
fi
# 檢查 pip
if ! command -v pip3 &> /dev/null; then
missing+=("python3-pip")
fi
# 檢查 venv
if ! python3 -c "import venv" &> /dev/null 2>&1; then
missing+=("python3-venv")
fi
if [ ${#missing[@]} -gt 0 ]; then
log_error "缺少依賴: ${missing[*]}"
log_info "請先安裝: apt install ${missing[*]}"
exit 1
fi
log_success "所有依賴已滿足"
}
# 函數:創建系統用戶
create_user() {
local username=$1
if id "$username" &>/dev/null; then
log_info "用戶 $username 已存在"
else
log_info "創建系統用戶: $username"
useradd --system --no-create-home --shell /bin/false "$username"
log_success "用戶 $username 已創建"
fi
}
# 函數:安裝後端
install_backend() {
local install_dir=$1
local username=$2
local port=$3
log_info "開始安裝 Meeting Assistant Backend..."
log_info "安裝目錄: $install_dir"
log_info "服務用戶: $username"
log_info "服務端口: $port"
echo ""
# 檢查依賴
check_dependencies
# 創建用戶
create_user "$username"
# 創建目錄
log_info "創建安裝目錄..."
mkdir -p "$install_dir"
mkdir -p "$install_dir/logs"
mkdir -p "$install_dir/templates"
mkdir -p "$install_dir/records"
# 複製後端代碼
log_info "複製後端代碼..."
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
local source_dir="$(dirname "$script_dir")/backend"
if [ ! -d "$source_dir" ]; then
log_error "找不到後端源碼目錄: $source_dir"
exit 1
fi
cp -r "$source_dir/app" "$install_dir/"
cp "$source_dir/requirements.txt" "$install_dir/"
# 複製 .env.example 如果 .env 不存在
if [ ! -f "$install_dir/.env" ]; then
if [ -f "$source_dir/.env.example" ]; then
cp "$source_dir/.env.example" "$install_dir/.env"
log_warn "已創建 .env 檔案,請編輯配置: $install_dir/.env"
fi
fi
# 複製模板檔案
if [ -d "$source_dir/templates" ]; then
cp -r "$source_dir/templates/"* "$install_dir/templates/" 2>/dev/null || true
fi
# 創建虛擬環境
log_info "創建 Python 虛擬環境..."
python3 -m venv "$install_dir/venv"
# 安裝依賴
log_info "安裝 Python 依賴..."
"$install_dir/venv/bin/pip" install --upgrade pip
"$install_dir/venv/bin/pip" install -r "$install_dir/requirements.txt"
# 更新 .env 中的端口配置
if [ -f "$install_dir/.env" ]; then
sed -i "s/^BACKEND_PORT=.*/BACKEND_PORT=$port/" "$install_dir/.env"
fi
# 設置權限
log_info "設置檔案權限..."
chown -R "$username:$username" "$install_dir"
chmod 750 "$install_dir"
chmod 640 "$install_dir/.env" 2>/dev/null || true
# 創建 systemd 服務
create_systemd_service "$install_dir" "$username" "$port"
# 啟動服務
log_info "啟動服務..."
systemctl daemon-reload
systemctl enable meeting-assistant-backend
systemctl start meeting-assistant-backend
# 等待啟動
sleep 3
# 檢查狀態
if systemctl is-active --quiet meeting-assistant-backend; then
log_success "Meeting Assistant Backend 安裝成功!"
echo ""
echo "=========================================="
echo " 安裝完成"
echo "=========================================="
echo ""
echo " 服務狀態: systemctl status meeting-assistant-backend"
echo " 查看日誌: journalctl -u meeting-assistant-backend -f"
echo " API 地址: http://localhost:$port"
echo " API 文件: http://localhost:$port/docs"
echo ""
echo " 配置檔案: $install_dir/.env"
echo " 請確保已正確配置數據庫和 API 密鑰"
echo ""
else
log_error "服務啟動失敗,請檢查日誌"
journalctl -u meeting-assistant-backend --no-pager -n 20
exit 1
fi
}
# 函數:創建 systemd 服務檔案
create_systemd_service() {
local install_dir=$1
local username=$2
local port=$3
log_info "創建 systemd 服務..."
cat > /etc/systemd/system/meeting-assistant-backend.service << EOF
[Unit]
Description=Meeting Assistant Backend API
After=network.target
[Service]
Type=simple
User=$username
Group=$username
WorkingDirectory=$install_dir
Environment="PATH=$install_dir/venv/bin"
EnvironmentFile=$install_dir/.env
ExecStart=$install_dir/venv/bin/uvicorn app.main:app --host 0.0.0.0 --port $port
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=$install_dir/logs $install_dir/records $install_dir/templates
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
log_success "systemd 服務已創建"
}
# 函數:更新後端
update_backend() {
local install_dir=$1
log_info "更新 Meeting Assistant Backend..."
if [ ! -d "$install_dir" ]; then
log_error "安裝目錄不存在: $install_dir"
exit 1
fi
# 停止服務
log_info "停止服務..."
systemctl stop meeting-assistant-backend || true
# 備份配置
log_info "備份配置..."
cp "$install_dir/.env" "$install_dir/.env.backup" 2>/dev/null || true
# 複製新代碼
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
local source_dir="$(dirname "$script_dir")/backend"
log_info "更新代碼..."
rm -rf "$install_dir/app"
cp -r "$source_dir/app" "$install_dir/"
cp "$source_dir/requirements.txt" "$install_dir/"
# 更新依賴
log_info "更新依賴..."
"$install_dir/venv/bin/pip" install -r "$install_dir/requirements.txt"
# 恢復配置
if [ -f "$install_dir/.env.backup" ]; then
mv "$install_dir/.env.backup" "$install_dir/.env"
fi
# 重新設置權限
local username=$(stat -c '%U' "$install_dir")
chown -R "$username:$username" "$install_dir"
# 重啟服務
log_info "重啟服務..."
systemctl daemon-reload
systemctl start meeting-assistant-backend
sleep 2
if systemctl is-active --quiet meeting-assistant-backend; then
log_success "更新完成!"
else
log_error "服務重啟失敗"
exit 1
fi
}
# 函數:移除後端
uninstall_backend() {
local install_dir=$1
log_warn "即將移除 Meeting Assistant Backend"
read -p "確定要繼續嗎?(y/N) " confirm
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
log_info "取消移除"
exit 0
fi
# 停止並禁用服務
log_info "停止服務..."
systemctl stop meeting-assistant-backend 2>/dev/null || true
systemctl disable meeting-assistant-backend 2>/dev/null || true
# 刪除 systemd 服務
log_info "移除 systemd 服務..."
rm -f /etc/systemd/system/meeting-assistant-backend.service
systemctl daemon-reload
# 詢問是否刪除數據
read -p "是否刪除安裝目錄 $install_dir(y/N) " delete_dir
if [ "$delete_dir" = "y" ] || [ "$delete_dir" = "Y" ]; then
log_info "刪除安裝目錄..."
rm -rf "$install_dir"
else
log_info "保留安裝目錄: $install_dir"
fi
log_success "Meeting Assistant Backend 已移除"
}
# 函數:顯示狀態
show_status() {
echo ""
echo "=========================================="
echo " Meeting Assistant Backend 狀態"
echo "=========================================="
echo ""
if systemctl is-active --quiet meeting-assistant-backend; then
log_success "服務狀態: 運行中"
systemctl status meeting-assistant-backend --no-pager | grep -E "(Active|Main PID|Memory|CPU)"
else
log_warn "服務狀態: 未運行"
fi
echo ""
}
# 函數:顯示日誌
show_logs() {
journalctl -u meeting-assistant-backend -f
}
# 解析參數
INSTALL_DIR=$DEFAULT_INSTALL_DIR
SERVICE_USER=$DEFAULT_USER
SERVICE_PORT=$DEFAULT_PORT
COMMAND=""
while [[ $# -gt 0 ]]; do
case $1 in
install|update|uninstall|status|logs|help)
COMMAND=$1
shift
;;
--dir)
INSTALL_DIR="$2"
shift 2
;;
--user)
SERVICE_USER="$2"
shift 2
;;
--port)
SERVICE_PORT="$2"
shift 2
;;
*)
log_error "未知參數: $1"
show_help
exit 1
;;
esac
done
# 執行命令
case $COMMAND in
install)
check_root
install_backend "$INSTALL_DIR" "$SERVICE_USER" "$SERVICE_PORT"
;;
update)
check_root
update_backend "$INSTALL_DIR"
;;
uninstall)
check_root
uninstall_backend "$INSTALL_DIR"
;;
status)
show_status
;;
logs)
show_logs
;;
help|"")
show_help
;;
*)
log_error "未知命令: $COMMAND"
show_help
exit 1
;;
esac

View File

@@ -19,13 +19,25 @@ BACKEND_DIR="$PROJECT_DIR/backend"
CLIENT_DIR="$PROJECT_DIR/client"
SIDECAR_DIR="$PROJECT_DIR/sidecar"
# Port 設定
BACKEND_PORT=8000
# Load environment variables from .env files if they exist
if [ -f "$BACKEND_DIR/.env" ]; then
log_info "Loading backend environment from $BACKEND_DIR/.env"
export $(grep -v '^#' "$BACKEND_DIR/.env" | grep -v '^$' | xargs)
fi
# Whisper 語音轉文字設定
export WHISPER_MODEL="medium" # 模型大小: tiny, base, small, medium, large
export WHISPER_DEVICE="cpu" # 執行裝置: cpu, cuda
export WHISPER_COMPUTE="int8" # 運算精度: int8, float16, float32
if [ -f "$CLIENT_DIR/.env" ]; then
log_info "Loading client environment from $CLIENT_DIR/.env"
export $(grep -v '^#' "$CLIENT_DIR/.env" | grep -v '^$' | xargs)
fi
# Server Configuration (can be overridden by .env)
BACKEND_HOST="${BACKEND_HOST:-0.0.0.0}"
BACKEND_PORT="${BACKEND_PORT:-8000}"
# Whisper Configuration (can be overridden by .env)
export WHISPER_MODEL="${WHISPER_MODEL:-medium}" # 模型大小: tiny, base, small, medium, large
export WHISPER_DEVICE="${WHISPER_DEVICE:-cpu}" # 執行裝置: cpu, cuda
export WHISPER_COMPUTE="${WHISPER_COMPUTE:-int8}" # 運算精度: int8, float16, float32
# PID 檔案
PID_FILE="$PROJECT_DIR/.running_pids"