From 01aee1fd0ddaafbb373bb8f76cad14a1902fc80c Mon Sep 17 00:00:00 2001 From: egg Date: Sun, 14 Dec 2025 14:31:55 +0800 Subject: [PATCH] feat: Extract hardcoded configs to environment variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .gitignore | 6 + DEPLOYMENT.md | 99 +++++-- PRD.md | 61 ---- README.md | 155 ++++++++++ SDD.md | 143 --------- TDD.md | 46 --- backend/.env.example | 61 +++- backend/app/config.py | 61 ++++ backend/app/database.py | 2 +- backend/app/routers/ai.py | 26 +- backend/app/routers/auth.py | 4 +- backend/app/routers/export.py | 16 +- client/.env.example | 39 +++ client/src/main.js | 15 + client/src/services/api.js | 5 +- docs/1panel-deployment.md | 461 ++++++++++++++++++++++++++++++ openspec/specs/middleware/spec.md | 108 ++++++- scripts/deploy-backend.sh | 439 ++++++++++++++++++++++++++++ start.sh | 24 +- 19 files changed, 1460 insertions(+), 311 deletions(-) delete mode 100644 PRD.md create mode 100644 README.md delete mode 100644 SDD.md delete mode 100644 TDD.md create mode 100644 client/.env.example create mode 100644 docs/1panel-deployment.md create mode 100755 scripts/deploy-backend.sh diff --git a/.gitignore b/.gitignore index 9faee83..839a464 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,9 @@ logs/ # Generated Excel records backend/record/ + +# AI Assistant configuration +.claude/ +openspec/ +AGENTS.md +CLAUDE.md diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 030c819..2cb95e3 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.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 diff --git a/PRD.md b/PRD.md deleted file mode 100644 index 298f11f..0000000 --- a/PRD.md +++ /dev/null @@ -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 定義之欄位。 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..96ff980 --- /dev/null +++ b/README.md @@ -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 +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 diff --git a/SDD.md b/SDD.md deleted file mode 100644 index 7ee2f5d..0000000 --- a/SDD.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/TDD.md b/TDD.md deleted file mode 100644 index b8a1c82..0000000 --- a/TDD.md +++ /dev/null @@ -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 打包。 \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example index 740e941..13fa931 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -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 diff --git a/backend/app/config.py b/backend/app/config.py index 3cda8e2..77a9bd5 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -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() diff --git a/backend/app/database.py b/backend/app/database.py index bafe5d6..ab0e0f7 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -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, diff --git a/backend/app/routers/ai.py b/backend/app/routers/ai.py index b749843..0bf150a 100644 --- a/backend/app/routers/ai.py +++ b/backend/app/routers/ai.py @@ -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}") diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 39ad7e8..9d55118 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -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: diff --git a/backend/app/routers/export.py b/backend/app/routers/export.py index 558f2e0..bc8069f 100644 --- a/backend/app/routers/export.py +++ b/backend/app/routers/export.py @@ -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 diff --git a/client/.env.example b/client/.env.example new file mode 100644 index 0000000..0df6a83 --- /dev/null +++ b/client/.env.example @@ -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://:/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 diff --git a/client/src/main.js b/client/src/main.js index b039e21..26fb0d3 100644 --- a/client/src/main.js +++ b/client/src/main.js @@ -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) diff --git a/client/src/services/api.js b/client/src/services/api.js index 0390ec3..5af38b5 100644 --- a/client/src/services/api.js +++ b/client/src/services/api.js @@ -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}`); } diff --git a/docs/1panel-deployment.md b/docs/1panel-deployment.md new file mode 100644 index 0000000..b78ad11 --- /dev/null +++ b/docs/1panel-deployment.md @@ -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。 diff --git a/openspec/specs/middleware/spec.md b/openspec/specs/middleware/spec.md index 8dd84db..2354ff3 100644 --- a/openspec/specs/middleware/spec.md +++ b/openspec/specs/middleware/spec.md @@ -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 diff --git a/scripts/deploy-backend.sh b/scripts/deploy-backend.sh new file mode 100755 index 0000000..e48285e --- /dev/null +++ b/scripts/deploy-backend.sh @@ -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 diff --git a/start.sh b/start.sh index 0210bf8..405697f 100755 --- a/start.sh +++ b/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"