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 # Generated Excel records
backend/record/ backend/record/
# AI Assistant configuration
.claude/
openspec/
AGENTS.md
CLAUDE.md

View File

@@ -7,6 +7,24 @@
- MySQL 8.0+ - MySQL 8.0+
- Access to Dify LLM service - 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 ## Backend Deployment
### 1. Setup Environment ### 1. Setup Environment
@@ -28,15 +46,33 @@ pip install -r requirements.txt
```bash ```bash
# Copy example and edit # Copy example and edit
cp .env.example .env 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 ### 3. Run Server
```bash ```bash
@@ -54,6 +90,18 @@ curl http://localhost:8000/api/health
# Should return: {"status":"healthy","service":"meeting-assistant"} # 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 ## Electron Client Deployment
### 1. Setup ### 1. Setup
@@ -65,16 +113,34 @@ cd client
npm install 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 ```bash
npm start npm start
``` ```
### 3. Build for Distribution ### 4. Build for Distribution
```bash ```bash
# Build portable executable # Update VITE_API_BASE_URL to production server first
# Then build portable executable
npm run build npm run build
``` ```
@@ -102,7 +168,7 @@ The model will be downloaded automatically on first run. For faster startup, pre
```python ```python
from faster_whisper import WhisperModel from faster_whisper import WhisperModel
model = WhisperModel("small", device="cpu", compute_type="int8") model = WhisperModel("medium", device="cpu", compute_type="int8")
``` ```
### 3. Build Executable ### 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: The backend will automatically create tables on first startup. To manually verify:
```sql ```sql
USE db_A060; USE your_database;
SHOW TABLES LIKE 'meeting_%'; SHOW TABLES LIKE 'meeting_%';
``` ```
@@ -154,24 +220,25 @@ On target hardware (i5/8GB):
### Database Connection Issues ### Database Connection Issues
1. Verify MySQL is accessible from server 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 3. Verify credentials in .env
### Dify API Issues ### Dify API Issues
1. Verify API key is valid 1. Verify API key is valid
2. Check Dify service status 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 ### Transcription Issues
1. Verify microphone permissions 1. Verify microphone permissions
2. Check sidecar executable runs standalone 2. Check sidecar executable runs standalone
3. Review audio format (16kHz, 16-bit, mono) 3. Review audio format (16kHz, 16-bit, mono)
4. Try different `WHISPER_MODEL` sizes (tiny, base, small, medium)
## Security Notes ## Security Notes
- Never commit `.env` files - Never commit `.env` files to version control
- Keep JWT_SECRET secure and unique per deployment - 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 - 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 # Database Configuration
# -----------------------------------------------------------------------------
DB_HOST=mysql.theaken.com DB_HOST=mysql.theaken.com
DB_PORT=33306 DB_PORT=33306
DB_USER=A060 DB_USER=your_username
DB_PASS=your_password_here DB_PASS=your_password_here
DB_NAME=db_A060 DB_NAME=your_database
# Connection pool size (default: 5)
DB_POOL_SIZE=5
# -----------------------------------------------------------------------------
# External APIs # External APIs
# -----------------------------------------------------------------------------
# Company authentication API endpoint
AUTH_API_URL=https://pj-auth-api.vercel.app/api/auth/login 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_API_URL=https://dify.theaken.com/v1
# Dify LLM API key (for summarization)
DIFY_API_KEY=app-xxxxxxxxxxx DIFY_API_KEY=app-xxxxxxxxxxx
# Dify STT API key (for audio transcription)
DIFY_STT_API_KEY=app-xxxxxxxxxxx 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_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: 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_HOST: str = os.getenv("DB_HOST", "mysql.theaken.com")
DB_PORT: int = int(os.getenv("DB_PORT", "33306")) DB_PORT: int = int(os.getenv("DB_PORT", "33306"))
DB_USER: str = os.getenv("DB_USER", "A060") DB_USER: str = os.getenv("DB_USER", "A060")
DB_PASS: str = os.getenv("DB_PASS", "") DB_PASS: str = os.getenv("DB_PASS", "")
DB_NAME: str = os.getenv("DB_NAME", "db_A060") 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: str = os.getenv(
"AUTH_API_URL", "https://pj-auth-api.vercel.app/api/auth/login" "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_API_KEY: str = os.getenv("DIFY_API_KEY", "")
DIFY_STT_API_KEY: str = os.getenv("DIFY_STT_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") ADMIN_EMAIL: str = os.getenv("ADMIN_EMAIL", "ymirliu@panjit.com.tw")
JWT_SECRET: str = os.getenv("JWT_SECRET", "meeting-assistant-secret") 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() settings = Settings()

View File

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

View File

@@ -13,10 +13,6 @@ from ..config import settings
from ..models import SummarizeRequest, SummarizeResponse, ActionItemCreate, TokenPayload from ..models import SummarizeRequest, SummarizeResponse, ActionItemCreate, TokenPayload
from .auth import get_current_user 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() router = APIRouter()
@@ -45,7 +41,7 @@ async def summarize_transcript(
"response_mode": "blocking", "response_mode": "blocking",
"user": current_user.email, "user": current_user.email,
}, },
timeout=120.0, # Long timeout for LLM processing timeout=settings.llm_timeout_seconds,
) )
if response.status_code != 200: if response.status_code != 200:
@@ -135,10 +131,10 @@ async def transcribe_audio(
# Validate file extension # Validate file extension
file_ext = os.path.splitext(file.filename or "")[1].lower() 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( raise HTTPException(
status_code=400, 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 # Create temp directory for processing
@@ -151,10 +147,10 @@ async def transcribe_audio(
with open(temp_file_path, "wb") as f: with open(temp_file_path, "wb") as f:
while chunk := await file.read(1024 * 1024): # 1MB chunks while chunk := await file.read(1024 * 1024): # 1MB chunks
file_size += len(chunk) file_size += len(chunk)
if file_size > MAX_FILE_SIZE: if file_size > settings.MAX_FILE_SIZE:
raise HTTPException( raise HTTPException(
status_code=413, 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) f.write(chunk)
@@ -245,18 +241,18 @@ async def transcribe_audio_stream(
# Validate file extension # Validate file extension
file_ext = os.path.splitext(file.filename or "")[1].lower() 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( raise HTTPException(
status_code=400, 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 # Read file into memory for streaming
file_content = await file.read() file_content = await file.read()
if len(file_content) > MAX_FILE_SIZE: if len(file_content) > settings.MAX_FILE_SIZE:
raise HTTPException( raise HTTPException(
status_code=413, 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]: 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 # Send command and wait for response
stdout, stderr = await asyncio.wait_for( stdout, stderr = await asyncio.wait_for(
process.communicate(input=f"{cmd_input}\n{{\"action\": \"quit\"}}\n".encode()), 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) # 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}") print(f"[Dify] Chat response: {response.status_code}")

View File

@@ -17,7 +17,7 @@ def create_token(email: str, role: str) -> str:
payload = { payload = {
"email": email, "email": email,
"role": role, "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") return jwt.encode(payload, settings.JWT_SECRET, algorithm="HS256")
@@ -67,7 +67,7 @@ async def login(request: LoginRequest):
response = await client.post( response = await client.post(
settings.AUTH_API_URL, settings.AUTH_API_URL,
json={"username": request.email, "password": request.password}, json={"username": request.email, "password": request.password},
timeout=30.0, timeout=settings.auth_timeout_seconds,
) )
if response.status_code == 401: if response.status_code == 401:

View File

@@ -6,14 +6,14 @@ import io
import os import os
from ..database import get_db_cursor from ..database import get_db_cursor
from ..config import settings
from ..models import TokenPayload from ..models import TokenPayload
from .auth import get_current_user, is_admin from .auth import get_current_user, is_admin
router = APIRouter() router = APIRouter()
# Directory paths # Base directory for resolving relative paths
TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "template") BASE_DIR = os.path.join(os.path.dirname(__file__), "..", "..")
RECORD_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "record")
def fill_template_workbook( def fill_template_workbook(
@@ -186,8 +186,12 @@ async def export_meeting(
) )
actions = cursor.fetchall() 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 # 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): if os.path.exists(template_path):
# Load and fill template # Load and fill template
wb = load_workbook(template_path) wb = load_workbook(template_path)
@@ -204,10 +208,10 @@ async def export_meeting(
filename = f"meeting_{meeting.get('uuid', meeting_id)}.xlsx" filename = f"meeting_{meeting.get('uuid', meeting_id)}.xlsx"
# Ensure record directory exists # Ensure record directory exists
os.makedirs(RECORD_DIR, exist_ok=True) os.makedirs(record_dir, exist_ok=True)
# Save to record directory # Save to record directory
record_path = os.path.join(RECORD_DIR, filename) record_path = os.path.join(record_dir, filename)
wb.save(record_path) wb.save(record_path)
# Save to bytes buffer for download # 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"; const pythonPath = fs.existsSync(venvPython) ? venvPython : "python3";
try { 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("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], { sidecarProcess = spawn(pythonPath, [sidecarScript], {
cwd: sidecarDir, cwd: sidecarDir,
stdio: ["pipe", "pipe", "pipe"], stdio: ["pipe", "pipe", "pipe"],
env: whisperEnv,
}); });
// Handle stdout (JSON responses) // 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 authToken = null;
let tokenRefreshTimer = null; let tokenRefreshTimer = null;
@@ -351,7 +352,7 @@ export async function transcribeAudioLegacy(file, onProgress = null) {
}); });
xhr.open("POST", url, true); xhr.open("POST", url, true);
xhr.timeout = 600000; // 10 minutes for large files xhr.timeout = UPLOAD_TIMEOUT;
if (token) { if (token) {
xhr.setRequestHeader("Authorization", `Bearer ${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. TBD - created by archiving change add-meeting-assistant-mvp. Update Purpose after archive.
## Requirements ## Requirements
### Requirement: FastAPI Server Configuration ### 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 #### 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 - **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 - **THEN** the server SHALL start successfully and accept connections on the configured BACKEND_HOST and BACKEND_PORT
#### Scenario: Server startup with missing configuration #### Scenario: Server startup with missing configuration
- **WHEN** the server starts with missing required environment variables - **WHEN** the server starts with missing required environment variables
- **THEN** the server SHALL fail to start with descriptive error message - **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 ### 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 #### Scenario: Database connection success
- **WHEN** the server connects to MySQL with valid credentials - **WHEN** the server connects to MySQL with valid credentials from environment
- **THEN** a connection pool SHALL be established and queries SHALL execute successfully - **THEN** a connection pool SHALL be established with DB_POOL_SIZE connections
#### Scenario: Database connection failure #### Scenario: Database connection failure
- **WHEN** the database is unreachable - **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 - **THEN** the server SHALL skip table creation and continue normally
### Requirement: CORS Configuration ### 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 #### Scenario: CORS preflight request
- **WHEN** Electron client sends OPTIONS request - **WHEN** any client sends OPTIONS request
- **THEN** the server SHALL respond with appropriate CORS headers allowing the 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" CLIENT_DIR="$PROJECT_DIR/client"
SIDECAR_DIR="$PROJECT_DIR/sidecar" SIDECAR_DIR="$PROJECT_DIR/sidecar"
# Port 設定 # Load environment variables from .env files if they exist
BACKEND_PORT=8000 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 語音轉文字設定 if [ -f "$CLIENT_DIR/.env" ]; then
export WHISPER_MODEL="medium" # 模型大小: tiny, base, small, medium, large log_info "Loading client environment from $CLIENT_DIR/.env"
export WHISPER_DEVICE="cpu" # 執行裝置: cpu, cuda export $(grep -v '^#' "$CLIENT_DIR/.env" | grep -v '^$' | xargs)
export WHISPER_COMPUTE="int8" # 運算精度: int8, float16, float32 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 檔案
PID_FILE="$PROJECT_DIR/.running_pids" PID_FILE="$PROJECT_DIR/.running_pids"