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:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -50,3 +50,9 @@ logs/
|
||||
|
||||
# Generated Excel records
|
||||
backend/record/
|
||||
|
||||
# AI Assistant configuration
|
||||
.claude/
|
||||
openspec/
|
||||
AGENTS.md
|
||||
CLAUDE.md
|
||||
|
||||
@@ -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
61
PRD.md
@@ -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
155
README.md
Normal 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
143
SDD.md
@@ -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
46
TDD.md
@@ -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 過期模擬(手動失效 Token),Client 攔截器是否觸發重試。
|
||||
|
||||
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 打包。
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
39
client/.env.example
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
461
docs/1panel-deployment.md
Normal 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。
|
||||
@@ -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
439
scripts/deploy-backend.sh
Executable 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
|
||||
24
start.sh
24
start.sh
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user