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
|
# Generated Excel records
|
||||||
backend/record/
|
backend/record/
|
||||||
|
|
||||||
|
# AI Assistant configuration
|
||||||
|
.claude/
|
||||||
|
openspec/
|
||||||
|
AGENTS.md
|
||||||
|
CLAUDE.md
|
||||||
|
|||||||
@@ -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
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
|
# 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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
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";
|
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)
|
||||||
|
|||||||
@@ -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
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.
|
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
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"
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user