feat: Add SQLite database support and fixed portable extraction path
- Add SQLite as alternative database for offline/firewall environments - Add --database-type parameter to build-client.bat (mysql/sqlite) - Refactor database.py to support both MySQL and SQLite - Add DB_TYPE and SQLITE_PATH configuration options - Set fixed unpackDirName for portable exe (Meeting-Assistant) - Update DEPLOYMENT.md with SQLite mode documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
109
DEPLOYMENT.md
109
DEPLOYMENT.md
@@ -4,7 +4,7 @@
|
||||
|
||||
- Python 3.10+
|
||||
- Node.js 18+
|
||||
- MySQL 8.0+
|
||||
- MySQL 8.0+ 或 SQLite(本地模式)
|
||||
- Access to Dify LLM service
|
||||
|
||||
## Quick Start
|
||||
@@ -233,18 +233,32 @@ npm start
|
||||
- 企業內部部署,簡化用戶操作
|
||||
- 無法獨立架設後端的環境
|
||||
- 快速測試和演示
|
||||
- **離線環境**:使用 SQLite 本地資料庫,無需網路連接資料庫
|
||||
|
||||
### 打包方式
|
||||
|
||||
```batch
|
||||
# Windows 全包打包
|
||||
# Windows 全包打包(MySQL 雲端資料庫,預設)
|
||||
.\scripts\build-client.bat build --embedded-backend --clean
|
||||
|
||||
# Windows 全包打包(SQLite 本地資料庫,適合離線/防火牆環境)
|
||||
.\scripts\build-client.bat build --embedded-backend --database-type sqlite --clean
|
||||
```
|
||||
|
||||
**打包參數說明:**
|
||||
|
||||
| 參數 | 說明 |
|
||||
|------|------|
|
||||
| `--embedded-backend` | 啟用內嵌後端模式 |
|
||||
| `--database-type TYPE` | 資料庫類型:`mysql`(雲端)或 `sqlite`(本地) |
|
||||
| `--clean` | 建置前清理所有暫存檔案 |
|
||||
|
||||
### config.json 配置
|
||||
|
||||
全包模式需要在 `config.json` 中配置資料庫和 API 金鑰:
|
||||
|
||||
#### MySQL 模式(雲端資料庫)
|
||||
|
||||
```json
|
||||
{
|
||||
"apiBaseUrl": "http://localhost:8000/api",
|
||||
@@ -260,6 +274,8 @@ npm start
|
||||
"host": "127.0.0.1",
|
||||
"port": 8000,
|
||||
"database": {
|
||||
"type": "mysql",
|
||||
"sqlitePath": "data/meeting.db",
|
||||
"host": "mysql.theaken.com",
|
||||
"port": 33306,
|
||||
"user": "your_username",
|
||||
@@ -281,6 +297,45 @@ npm start
|
||||
}
|
||||
```
|
||||
|
||||
#### SQLite 模式(本地資料庫)
|
||||
|
||||
適合離線環境或網路防火牆阻擋資料庫連線的情況:
|
||||
|
||||
```json
|
||||
{
|
||||
"apiBaseUrl": "http://localhost:8000/api",
|
||||
"uploadTimeout": 600000,
|
||||
"appTitle": "Meeting Assistant",
|
||||
"whisper": {
|
||||
"model": "medium",
|
||||
"device": "cpu",
|
||||
"compute": "int8"
|
||||
},
|
||||
"backend": {
|
||||
"embedded": true,
|
||||
"host": "127.0.0.1",
|
||||
"port": 8000,
|
||||
"database": {
|
||||
"type": "sqlite",
|
||||
"sqlitePath": "data/meeting.db"
|
||||
},
|
||||
"externalApis": {
|
||||
"authApiUrl": "https://pj-auth-api.vercel.app/api/auth/login",
|
||||
"difyApiUrl": "https://dify.theaken.com/v1",
|
||||
"difyApiKey": "app-xxxxxxxxxx",
|
||||
"difySttApiKey": "app-xxxxxxxxxx"
|
||||
},
|
||||
"auth": {
|
||||
"adminEmail": "admin@example.com",
|
||||
"jwtSecret": "your_secure_jwt_secret",
|
||||
"jwtExpireHours": 24
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**:SQLite 模式下,資料庫檔案位於 `%TEMP%\Meeting-Assistant\resources\backend\backend\data\meeting.db`
|
||||
|
||||
### 配置說明
|
||||
|
||||
| 區段 | 欄位 | 說明 |
|
||||
@@ -288,20 +343,40 @@ npm start
|
||||
| `backend.embedded` | `true`/`false` | 啟用/停用內嵌後端模式 |
|
||||
| `backend.host` | IP | 後端監聽地址(通常為 127.0.0.1) |
|
||||
| `backend.port` | 數字 | 後端監聽端口(預設 8000) |
|
||||
| `backend.database.*` | 各欄位 | MySQL 資料庫連線資訊 |
|
||||
| `backend.database.type` | `mysql`/`sqlite` | 資料庫類型(預設 mysql) |
|
||||
| `backend.database.sqlitePath` | 路徑 | SQLite 資料庫檔案路徑(相對或絕對) |
|
||||
| `backend.database.*` | 各欄位 | MySQL 資料庫連線資訊(僅 MySQL 模式需要) |
|
||||
| `backend.externalApis.*` | 各欄位 | 外部 API 設定(認證、Dify) |
|
||||
| `backend.auth.*` | 各欄位 | 認證設定(管理員信箱、JWT 金鑰) |
|
||||
|
||||
### 資料庫模式比較
|
||||
|
||||
| 特性 | MySQL 模式 | SQLite 模式 |
|
||||
|------|------------|-------------|
|
||||
| 網路需求 | 需連接遠端資料庫 | 完全離線運作 |
|
||||
| 資料位置 | 雲端資料庫伺服器 | 本機檔案 |
|
||||
| 多用戶共享 | ✅ 支援 | ❌ 僅單機使用 |
|
||||
| 適用場景 | 企業部署、多人共用 | 離線環境、防火牆限制 |
|
||||
| 資料備份 | 使用資料庫工具 | 複製 `.db` 檔案即可 |
|
||||
|
||||
### Portable 執行檔解壓縮位置
|
||||
|
||||
Portable exe 執行時會解壓縮到 `%TEMP%\Meeting-Assistant` 資料夾(固定路徑,非隨機資料夾)。
|
||||
|
||||
- **優點**:Windows Defender 不會每次都提示警告
|
||||
- **SQLite 資料庫位置**:`%TEMP%\Meeting-Assistant\resources\backend\backend\data\meeting.db`
|
||||
|
||||
### 啟動流程
|
||||
|
||||
1. 用戶雙擊 exe
|
||||
2. Electron 主程序啟動
|
||||
3. 讀取 `config.json`
|
||||
4. 啟動內嵌後端 (FastAPI)
|
||||
5. 健康檢查等待後端就緒(最多 30 秒)
|
||||
6. 後端就緒後,載入前端頁面
|
||||
7. 啟動 Whisper Sidecar
|
||||
8. 應用就緒
|
||||
2. 解壓縮到 `%TEMP%\Meeting-Assistant`
|
||||
3. Electron 主程序啟動
|
||||
4. 讀取 `config.json`
|
||||
5. 啟動內嵌後端 (FastAPI)
|
||||
6. 健康檢查等待後端就緒(最多 30 秒)
|
||||
7. 後端就緒後,載入前端頁面
|
||||
8. 啟動 Whisper Sidecar
|
||||
9. 應用就緒
|
||||
|
||||
### 向後相容性
|
||||
|
||||
@@ -367,6 +442,8 @@ Copy `sidecar/dist/` to `client/sidecar/` before building Electron app.
|
||||
|
||||
## Database Setup
|
||||
|
||||
### MySQL 模式
|
||||
|
||||
The backend will automatically create tables on first startup. To manually verify:
|
||||
|
||||
```sql
|
||||
@@ -374,7 +451,17 @@ USE your_database;
|
||||
SHOW TABLES LIKE 'meeting_%';
|
||||
```
|
||||
|
||||
Expected tables:
|
||||
### SQLite 模式
|
||||
|
||||
SQLite 資料庫檔案會在首次啟動時自動建立:
|
||||
|
||||
- **開發環境**:`backend/data/meeting.db`
|
||||
- **打包後**:`%TEMP%\Meeting-Assistant\resources\backend\backend\data\meeting.db`
|
||||
|
||||
**備份方式**:直接複製 `.db` 檔案即可。
|
||||
|
||||
### Expected tables
|
||||
|
||||
- `meeting_users`
|
||||
- `meeting_records`
|
||||
- `meeting_conclusions`
|
||||
|
||||
@@ -21,6 +21,8 @@ class Settings:
|
||||
BACKEND_PORT: int = int(os.getenv("BACKEND_PORT", "8000"))
|
||||
|
||||
# Database Configuration
|
||||
DB_TYPE: str = os.getenv("DB_TYPE", "mysql") # "mysql" or "sqlite"
|
||||
SQLITE_PATH: str = os.getenv("SQLITE_PATH", "data/meeting.db")
|
||||
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")
|
||||
@@ -90,6 +92,21 @@ class Settings:
|
||||
return os.path.join(base_dir, self.RECORD_DIR)
|
||||
return os.path.join(base_dir, "record")
|
||||
|
||||
def get_sqlite_path(self, base_dir: str | None = None) -> str:
|
||||
"""Get SQLite database file path, resolving relative paths.
|
||||
|
||||
Args:
|
||||
base_dir: Base directory for relative paths. If None, uses get_base_dir()
|
||||
which supports frozen executables.
|
||||
"""
|
||||
if base_dir is None:
|
||||
base_dir = get_base_dir()
|
||||
if self.SQLITE_PATH:
|
||||
if os.path.isabs(self.SQLITE_PATH):
|
||||
return self.SQLITE_PATH
|
||||
return os.path.join(base_dir, self.SQLITE_PATH)
|
||||
return os.path.join(base_dir, "data", "meeting.db")
|
||||
|
||||
# Timeout helpers (convert ms to seconds for httpx)
|
||||
@property
|
||||
def upload_timeout_seconds(self) -> float:
|
||||
|
||||
@@ -1,14 +1,59 @@
|
||||
"""
|
||||
Database abstraction layer supporting both MySQL and SQLite.
|
||||
|
||||
Usage:
|
||||
from app.database import init_db, get_db_cursor, init_tables
|
||||
|
||||
# At application startup
|
||||
init_db()
|
||||
init_tables()
|
||||
|
||||
# In request handlers
|
||||
with get_db_cursor() as cursor:
|
||||
cursor.execute("SELECT * FROM meeting_records")
|
||||
results = cursor.fetchall()
|
||||
|
||||
with get_db_cursor(commit=True) as cursor:
|
||||
cursor.execute("INSERT INTO ...")
|
||||
"""
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
|
||||
import mysql.connector
|
||||
from mysql.connector import pooling
|
||||
from contextlib import contextmanager
|
||||
|
||||
from .config import settings
|
||||
|
||||
connection_pool = None
|
||||
# Global state
|
||||
_db_type: str = "mysql"
|
||||
_mysql_pool = None
|
||||
_sqlite_conn = None
|
||||
_sqlite_lock = threading.Lock()
|
||||
|
||||
|
||||
def init_db_pool():
|
||||
global connection_pool
|
||||
connection_pool = pooling.MySQLConnectionPool(
|
||||
# ============================================================================
|
||||
# Initialization Functions
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def init_db():
|
||||
"""Initialize database based on DB_TYPE setting."""
|
||||
global _db_type
|
||||
_db_type = settings.DB_TYPE.lower()
|
||||
|
||||
if _db_type == "sqlite":
|
||||
init_sqlite()
|
||||
else:
|
||||
init_mysql()
|
||||
|
||||
|
||||
def init_mysql():
|
||||
"""Initialize MySQL connection pool."""
|
||||
global _mysql_pool
|
||||
_mysql_pool = pooling.MySQLConnectionPool(
|
||||
pool_name="meeting_pool",
|
||||
pool_size=settings.DB_POOL_SIZE,
|
||||
host=settings.DB_HOST,
|
||||
@@ -17,20 +62,125 @@ def init_db_pool():
|
||||
password=settings.DB_PASS,
|
||||
database=settings.DB_NAME,
|
||||
)
|
||||
return connection_pool
|
||||
return _mysql_pool
|
||||
|
||||
|
||||
def init_sqlite():
|
||||
"""Initialize SQLite connection with row_factory for dict-like access."""
|
||||
global _sqlite_conn
|
||||
|
||||
db_path = settings.get_sqlite_path()
|
||||
db_dir = os.path.dirname(db_path)
|
||||
|
||||
# Create directory if needed
|
||||
if db_dir and not os.path.exists(db_dir):
|
||||
os.makedirs(db_dir, exist_ok=True)
|
||||
|
||||
_sqlite_conn = sqlite3.connect(db_path, check_same_thread=False)
|
||||
_sqlite_conn.row_factory = sqlite3.Row
|
||||
_sqlite_conn.execute("PRAGMA foreign_keys=ON")
|
||||
|
||||
print(f"SQLite database initialized at: {db_path}", flush=True)
|
||||
return _sqlite_conn
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Legacy Compatibility
|
||||
# ============================================================================
|
||||
|
||||
def init_db_pool():
|
||||
"""Legacy function for backward compatibility. Use init_db() instead."""
|
||||
return init_db()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Connection Context Managers
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_db_connection():
|
||||
conn = connection_pool.get_connection()
|
||||
"""Get a database connection (MySQL or SQLite)."""
|
||||
if _db_type == "sqlite":
|
||||
# SQLite uses a single connection with thread lock
|
||||
yield _sqlite_conn
|
||||
else:
|
||||
# MySQL uses connection pool
|
||||
conn = _mysql_pool.get_connection()
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
class SQLiteCursorWrapper:
|
||||
"""Wrapper to make SQLite cursor behave more like MySQL cursor with dictionary=True."""
|
||||
|
||||
def __init__(self, cursor):
|
||||
self._cursor = cursor
|
||||
self.lastrowid = None
|
||||
self.rowcount = 0
|
||||
|
||||
def execute(self, query, params=None):
|
||||
# Convert MySQL-style %s placeholders to SQLite ? placeholders
|
||||
query = query.replace("%s", "?")
|
||||
if params:
|
||||
self._cursor.execute(query, params)
|
||||
else:
|
||||
self._cursor.execute(query)
|
||||
self.lastrowid = self._cursor.lastrowid
|
||||
self.rowcount = self._cursor.rowcount
|
||||
|
||||
def executemany(self, query, params_list):
|
||||
query = query.replace("%s", "?")
|
||||
self._cursor.executemany(query, params_list)
|
||||
self.lastrowid = self._cursor.lastrowid
|
||||
self.rowcount = self._cursor.rowcount
|
||||
|
||||
def fetchone(self):
|
||||
row = self._cursor.fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
return dict(row)
|
||||
|
||||
def fetchall(self):
|
||||
rows = self._cursor.fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def fetchmany(self, size=None):
|
||||
if size:
|
||||
rows = self._cursor.fetchmany(size)
|
||||
else:
|
||||
rows = self._cursor.fetchmany()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def close(self):
|
||||
self._cursor.close()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_db_cursor(commit=False):
|
||||
"""Get a database cursor that returns dict-like rows.
|
||||
|
||||
Args:
|
||||
commit: If True, commit the transaction after yield.
|
||||
|
||||
Yields:
|
||||
cursor: A cursor that returns dict-like rows.
|
||||
"""
|
||||
if _db_type == "sqlite":
|
||||
with _sqlite_lock:
|
||||
cursor = SQLiteCursorWrapper(_sqlite_conn.cursor())
|
||||
try:
|
||||
yield cursor
|
||||
if commit:
|
||||
_sqlite_conn.commit()
|
||||
except Exception:
|
||||
_sqlite_conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
cursor.close()
|
||||
else:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
try:
|
||||
@@ -41,9 +191,11 @@ def get_db_cursor(commit=False):
|
||||
cursor.close()
|
||||
|
||||
|
||||
def init_tables():
|
||||
"""Create all required tables if they don't exist."""
|
||||
create_statements = [
|
||||
# ============================================================================
|
||||
# Table Initialization
|
||||
# ============================================================================
|
||||
|
||||
MYSQL_TABLES = [
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS meeting_users (
|
||||
user_id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
@@ -91,6 +243,69 @@ def init_tables():
|
||||
""",
|
||||
]
|
||||
|
||||
with get_db_cursor(commit=True) as cursor:
|
||||
for statement in create_statements:
|
||||
SQLITE_TABLES = [
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS meeting_users (
|
||||
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
display_name TEXT,
|
||||
role TEXT CHECK(role IN ('admin', 'user')) DEFAULT 'user',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS meeting_records (
|
||||
meeting_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
uuid TEXT UNIQUE,
|
||||
subject TEXT NOT NULL,
|
||||
meeting_time DATETIME NOT NULL,
|
||||
location TEXT,
|
||||
chairperson TEXT,
|
||||
recorder TEXT,
|
||||
attendees TEXT,
|
||||
transcript_blob TEXT,
|
||||
created_by TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS meeting_conclusions (
|
||||
conclusion_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
meeting_id INTEGER,
|
||||
content TEXT,
|
||||
system_code TEXT,
|
||||
FOREIGN KEY (meeting_id) REFERENCES meeting_records(meeting_id) ON DELETE CASCADE
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS meeting_action_items (
|
||||
action_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
meeting_id INTEGER,
|
||||
content TEXT,
|
||||
owner TEXT,
|
||||
due_date DATE,
|
||||
status TEXT CHECK(status IN ('Open', 'In Progress', 'Done', 'Delayed')) DEFAULT 'Open',
|
||||
system_code TEXT,
|
||||
FOREIGN KEY (meeting_id) REFERENCES meeting_records(meeting_id) ON DELETE CASCADE
|
||||
)
|
||||
""",
|
||||
]
|
||||
|
||||
|
||||
def init_tables():
|
||||
"""Create all required tables if they don't exist."""
|
||||
tables = SQLITE_TABLES if _db_type == "sqlite" else MYSQL_TABLES
|
||||
|
||||
if _db_type == "sqlite":
|
||||
with _sqlite_lock:
|
||||
cursor = _sqlite_conn.cursor()
|
||||
try:
|
||||
for statement in tables:
|
||||
cursor.execute(statement)
|
||||
_sqlite_conn.commit()
|
||||
finally:
|
||||
cursor.close()
|
||||
else:
|
||||
with get_db_cursor(commit=True) as cursor:
|
||||
for statement in tables:
|
||||
cursor.execute(statement)
|
||||
|
||||
@@ -47,6 +47,10 @@ def apply_config_to_env(config: dict) -> None:
|
||||
|
||||
# Database configuration - use direct assignment to ensure config values are used
|
||||
db_config = backend_config.get("database", {})
|
||||
if "type" in db_config:
|
||||
os.environ["DB_TYPE"] = db_config["type"]
|
||||
if "sqlitePath" in db_config:
|
||||
os.environ["SQLITE_PATH"] = db_config["sqlitePath"]
|
||||
if "host" in db_config:
|
||||
os.environ["DB_HOST"] = db_config["host"]
|
||||
if "port" in db_config:
|
||||
@@ -123,11 +127,13 @@ def main():
|
||||
print(f"DEBUG: Loaded config keys: {list(config.keys())}", flush=True)
|
||||
backend_config = config.get("backend", {})
|
||||
db_config = backend_config.get("database", {})
|
||||
print(f"DEBUG: DB type={db_config.get('type', 'mysql')}", flush=True)
|
||||
print(f"DEBUG: DB config: host={db_config.get('host')}, user={db_config.get('user')}, pass={'***' if db_config.get('password') else 'EMPTY'}", flush=True)
|
||||
|
||||
apply_config_to_env(config)
|
||||
|
||||
# Debug: print env vars after setting
|
||||
print(f"DEBUG: ENV DB_TYPE={os.environ.get('DB_TYPE', 'mysql')}", flush=True)
|
||||
print(f"DEBUG: ENV DB_HOST={os.environ.get('DB_HOST')}", flush=True)
|
||||
print(f"DEBUG: ENV DB_USER={os.environ.get('DB_USER')}", flush=True)
|
||||
print(f"DEBUG: ENV DB_PASS={'***' if os.environ.get('DB_PASS') else 'EMPTY'}", flush=True)
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
"host": "127.0.0.1",
|
||||
"port": 8000,
|
||||
"database": {
|
||||
"type": "mysql",
|
||||
"sqlitePath": "data/meeting.db",
|
||||
"host": "mysql.theaken.com",
|
||||
"port": 33306,
|
||||
"user": "A060",
|
||||
|
||||
@@ -72,7 +72,8 @@
|
||||
"icon": "assets/icon.png"
|
||||
},
|
||||
"portable": {
|
||||
"artifactName": "${productName}-${version}-portable.${ext}"
|
||||
"artifactName": "${productName}-${version}-portable.${ext}",
|
||||
"unpackDirName": "Meeting-Assistant"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ set "SKIP_BACKEND=true"
|
||||
set "EMBEDDED_BACKEND=false"
|
||||
set "CLEAN_BUILD=false"
|
||||
set "API_URL="
|
||||
set "DATABASE_TYPE="
|
||||
|
||||
REM 解析參數
|
||||
set "COMMAND=help"
|
||||
@@ -42,6 +43,7 @@ if /i "%~1"=="--skip-backend" (set "SKIP_BACKEND=true" & shift & goto :parse_arg
|
||||
if /i "%~1"=="--embedded-backend" (set "EMBEDDED_BACKEND=true" & set "SKIP_BACKEND=false" & shift & goto :parse_args)
|
||||
if /i "%~1"=="--clean" (set "CLEAN_BUILD=true" & shift & goto :parse_args)
|
||||
if /i "%~1"=="--api-url" (set "API_URL=%~2" & shift & shift & goto :parse_args)
|
||||
if /i "%~1"=="--database-type" (set "DATABASE_TYPE=%~2" & shift & shift & goto :parse_args)
|
||||
echo %RED%[ERROR]%NC% 未知參數: %~1
|
||||
goto :show_help
|
||||
|
||||
@@ -324,6 +326,42 @@ if errorlevel 1 (
|
||||
echo %GREEN%[OK]%NC% 已啟用內嵌後端模式
|
||||
goto :eof
|
||||
|
||||
:update_config_database
|
||||
REM 更新 config.json 的資料庫類型
|
||||
if "%DATABASE_TYPE%"=="" goto :eof
|
||||
|
||||
echo %BLUE%[STEP]%NC% 設定資料庫類型...
|
||||
|
||||
set "CONFIG_FILE=%CLIENT_DIR%\config.json"
|
||||
if not exist "%CONFIG_FILE%" (
|
||||
echo %YELLOW%[WARN]%NC% 找不到 config.json,跳過資料庫類型設定
|
||||
goto :eof
|
||||
)
|
||||
|
||||
REM 驗證資料庫類型
|
||||
if /i not "%DATABASE_TYPE%"=="mysql" if /i not "%DATABASE_TYPE%"=="sqlite" (
|
||||
echo %RED%[ERROR]%NC% 無效的資料庫類型: %DATABASE_TYPE%
|
||||
echo %BLUE%[INFO]%NC% 有效選項: mysql, sqlite
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM 使用 PowerShell 更新 database.type (使用 UTF8 without BOM)
|
||||
if /i "%DATABASE_TYPE%"=="sqlite" (
|
||||
REM SQLite 模式: 設定 type=sqlite,清空 MySQL 連線資訊
|
||||
powershell -Command "$config = Get-Content '%CONFIG_FILE%' -Raw | ConvertFrom-Json; $config.backend.database.type = 'sqlite'; $config.backend.database.host = ''; $config.backend.database.user = ''; $config.backend.database.password = ''; $config.backend.database.database = ''; $json = $config | ConvertTo-Json -Depth 10; [System.IO.File]::WriteAllText('%CONFIG_FILE%', $json, [System.Text.UTF8Encoding]::new($false))"
|
||||
echo %GREEN%[OK]%NC% 資料庫類型已設定為: SQLite (本地模式)
|
||||
) else (
|
||||
REM MySQL 模式: 僅設定 type=mysql,保留連線資訊
|
||||
powershell -Command "$config = Get-Content '%CONFIG_FILE%' -Raw | ConvertFrom-Json; $config.backend.database.type = 'mysql'; $json = $config | ConvertTo-Json -Depth 10; [System.IO.File]::WriteAllText('%CONFIG_FILE%', $json, [System.Text.UTF8Encoding]::new($false))"
|
||||
echo %GREEN%[OK]%NC% 資料庫類型已設定為: MySQL (雲端模式)
|
||||
)
|
||||
|
||||
if errorlevel 1 (
|
||||
echo %RED%[ERROR]%NC% 更新 config.json database.type 失敗
|
||||
exit /b 1
|
||||
)
|
||||
goto :eof
|
||||
|
||||
:setup_client
|
||||
echo %BLUE%[STEP]%NC% 設置前端建置環境...
|
||||
|
||||
@@ -436,6 +474,9 @@ call :update_config
|
||||
REM 更新 embedded backend 設定(如果有指定)
|
||||
call :update_config_embedded
|
||||
|
||||
REM 更新資料庫類型設定(如果有指定)
|
||||
call :update_config_database
|
||||
|
||||
if "%SKIP_SIDECAR%"=="false" (
|
||||
call :setup_sidecar_venv
|
||||
call :build_sidecar
|
||||
@@ -506,11 +547,13 @@ echo --api-url URL 後端 API URL (預設: http://localhost:8000/api)
|
||||
echo --skip-sidecar 跳過 Sidecar 打包
|
||||
echo --skip-backend 跳過 Backend 打包 (預設)
|
||||
echo --embedded-backend 打包內嵌後端 (全包部署模式)
|
||||
echo --database-type TYPE 資料庫類型: mysql (雲端) 或 sqlite (本地)
|
||||
echo --clean 建置前先清理
|
||||
echo.
|
||||
echo 範例:
|
||||
echo %~nx0 build 完整建置 (前端+Sidecar)
|
||||
echo %~nx0 build --embedded-backend 全包部署 (含內嵌後端)
|
||||
echo %~nx0 build --embedded-backend --database-type sqlite 全包部署 + SQLite
|
||||
echo %~nx0 build --api-url "http://192.168.1.100:8000/api" 指定遠端後端
|
||||
echo %~nx0 sidecar 僅打包 Sidecar
|
||||
echo %~nx0 electron --skip-sidecar 僅打包 Electron
|
||||
@@ -519,6 +562,10 @@ echo 部署模式:
|
||||
echo 分離部署(預設): 前端連接遠端後端,使用 --api-url 指定後端地址
|
||||
echo 全包部署: 使用 --embedded-backend 將後端打包進 exe,雙擊即可運行
|
||||
echo.
|
||||
echo 資料庫模式:
|
||||
echo MySQL(預設): 連接雲端資料庫,需要網路存取
|
||||
echo SQLite: 本地資料庫,適合離線或防火牆環境,使用 --database-type sqlite
|
||||
echo.
|
||||
echo 注意:
|
||||
echo - 首次打包 Sidecar 需下載 Whisper 模型,可能需要較長時間
|
||||
echo - 全包部署需要額外約 50MB 空間用於後端
|
||||
|
||||
Reference in New Issue
Block a user