feat: Add build scripts and runtime config support
Backend: - Add setup-backend.sh/bat for one-click backend setup - Fix test_auth.py mock settings (JWT_EXPIRE_HOURS) - Fix test_excel_export.py TEMPLATE_DIR reference Frontend: - Add config.json for runtime API URL configuration - Add init.js and settings.js for config loading - Update main.js to load config from external file - Update api.js to use dynamic API_BASE_URL - Update all pages to initialize config before API calls - Update package.json with extraResources for config Build: - Add build-client.sh/bat for packaging Electron + Sidecar - Add build-all.ps1 PowerShell script with -ApiUrl parameter - Add GitHub Actions workflow for Windows builds - Add scripts/README.md documentation This allows IT to configure backend URL without rebuilding. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
155
.github/workflows/build-windows.yml
vendored
Normal file
155
.github/workflows/build-windows.yml
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
name: Build Windows Client
|
||||
|
||||
on:
|
||||
workflow_dispatch: # 手動觸發
|
||||
inputs:
|
||||
api_url:
|
||||
description: 'Backend API URL (e.g., http://192.168.1.100:8000/api)'
|
||||
required: false
|
||||
default: 'http://localhost:8000/api'
|
||||
type: string
|
||||
whisper_model:
|
||||
description: 'Whisper model size'
|
||||
required: false
|
||||
default: 'medium'
|
||||
type: choice
|
||||
options:
|
||||
- tiny
|
||||
- base
|
||||
- small
|
||||
- medium
|
||||
- large
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # 推送版本 tag 時自動觸發
|
||||
|
||||
jobs:
|
||||
build-sidecar:
|
||||
name: Build Sidecar (Python → exe)
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: sidecar/requirements.txt
|
||||
|
||||
- name: Install sidecar dependencies
|
||||
working-directory: sidecar
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pyinstaller
|
||||
|
||||
- name: Build sidecar with PyInstaller
|
||||
working-directory: sidecar
|
||||
run: |
|
||||
pyinstaller `
|
||||
--onedir `
|
||||
--name transcriber `
|
||||
--distpath dist `
|
||||
--workpath build `
|
||||
--noconfirm `
|
||||
--clean `
|
||||
--console `
|
||||
--hidden-import=faster_whisper `
|
||||
--hidden-import=ctranslate2 `
|
||||
--hidden-import=huggingface_hub `
|
||||
--hidden-import=tokenizers `
|
||||
--hidden-import=onnxruntime `
|
||||
--hidden-import=opencc `
|
||||
--hidden-import=pydub `
|
||||
--hidden-import=numpy `
|
||||
--hidden-import=av `
|
||||
--collect-data=onnxruntime `
|
||||
--collect-data=faster_whisper `
|
||||
transcriber.py
|
||||
|
||||
- name: Upload sidecar artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sidecar-windows
|
||||
path: sidecar/dist/transcriber/
|
||||
retention-days: 1
|
||||
|
||||
build-electron:
|
||||
name: Build Electron App
|
||||
needs: build-sidecar
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: client/package-lock.json
|
||||
|
||||
- name: Download sidecar artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sidecar-windows
|
||||
path: sidecar/dist/transcriber/
|
||||
|
||||
- name: Install client dependencies
|
||||
working-directory: client
|
||||
run: npm ci
|
||||
|
||||
- name: Update config.json with API URL
|
||||
working-directory: client
|
||||
shell: pwsh
|
||||
run: |
|
||||
$configPath = "config.json"
|
||||
$apiUrl = "${{ inputs.api_url || 'http://localhost:8000/api' }}"
|
||||
|
||||
if (Test-Path $configPath) {
|
||||
$config = Get-Content $configPath -Raw | ConvertFrom-Json
|
||||
$config.apiBaseUrl = $apiUrl
|
||||
$config | ConvertTo-Json -Depth 10 | Set-Content $configPath -Encoding UTF8
|
||||
Write-Host "Updated API URL to: $apiUrl"
|
||||
}
|
||||
|
||||
- name: Build Electron app
|
||||
working-directory: client
|
||||
run: npm run build -- --win
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload Windows build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: meeting-assistant-windows
|
||||
path: |
|
||||
client/dist/*.exe
|
||||
client/dist/*.zip
|
||||
retention-days: 30
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
needs: build-electron
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
steps:
|
||||
- name: Download Windows build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: meeting-assistant-windows
|
||||
path: dist/
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: dist/*
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -29,6 +29,7 @@ class TestAdminRoleDetection:
|
||||
def test_create_token_includes_role(self, mock_settings):
|
||||
"""Test that created tokens include the role."""
|
||||
mock_settings.JWT_SECRET = "test-secret"
|
||||
mock_settings.JWT_EXPIRE_HOURS = 24
|
||||
mock_settings.ADMIN_EMAIL = "admin@test.com"
|
||||
|
||||
from app.routers.auth import create_token
|
||||
@@ -51,6 +52,7 @@ class TestTokenValidation:
|
||||
def test_decode_valid_token(self, mock_settings):
|
||||
"""Test decoding a valid token."""
|
||||
mock_settings.JWT_SECRET = "test-secret"
|
||||
mock_settings.JWT_EXPIRE_HOURS = 24
|
||||
|
||||
from app.routers.auth import create_token, decode_token
|
||||
|
||||
@@ -93,10 +95,11 @@ class TestLoginEndpoint:
|
||||
mock_settings.AUTH_API_URL = "https://auth.test.com/login"
|
||||
mock_settings.ADMIN_EMAIL = "admin@test.com"
|
||||
mock_settings.JWT_SECRET = "test-secret"
|
||||
mock_settings.JWT_EXPIRE_HOURS = 24
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"token": "external-token"}
|
||||
mock_response.json.return_value = {"token": "external-token", "success": True}
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.post.return_value = mock_response
|
||||
@@ -120,9 +123,11 @@ class TestLoginEndpoint:
|
||||
mock_settings.AUTH_API_URL = "https://auth.test.com/login"
|
||||
mock_settings.ADMIN_EMAIL = "admin@test.com"
|
||||
mock_settings.JWT_SECRET = "test-secret"
|
||||
mock_settings.JWT_EXPIRE_HOURS = 24
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"token": "external-token", "success": True}
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.post.return_value = mock_response
|
||||
|
||||
@@ -7,7 +7,11 @@ from openpyxl import load_workbook
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app.routers.export import fill_template_workbook, create_default_workbook, TEMPLATE_DIR, RECORD_DIR
|
||||
from app.routers.export import fill_template_workbook, create_default_workbook, BASE_DIR
|
||||
from app.config import settings
|
||||
|
||||
TEMPLATE_DIR = settings.get_template_dir(BASE_DIR)
|
||||
RECORD_DIR = settings.get_record_dir(BASE_DIR)
|
||||
|
||||
|
||||
def test_excel_export():
|
||||
|
||||
10
client/config.json
Normal file
10
client/config.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"apiBaseUrl": "http://localhost:8000/api",
|
||||
"uploadTimeout": 600000,
|
||||
"appTitle": "Meeting Assistant",
|
||||
"whisper": {
|
||||
"model": "medium",
|
||||
"device": "cpu",
|
||||
"compute": "int8"
|
||||
}
|
||||
}
|
||||
@@ -29,19 +29,44 @@
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "../sidecar/dist",
|
||||
"to": "sidecar",
|
||||
"from": "../sidecar/dist/transcriber",
|
||||
"to": "sidecar/transcriber",
|
||||
"filter": ["**/*"]
|
||||
},
|
||||
{
|
||||
"from": "config.json",
|
||||
"to": "config.json"
|
||||
}
|
||||
],
|
||||
"win": {
|
||||
"target": "portable"
|
||||
"target": [
|
||||
{
|
||||
"target": "portable",
|
||||
"arch": ["x64"]
|
||||
}
|
||||
],
|
||||
"icon": "assets/icon.ico"
|
||||
},
|
||||
"mac": {
|
||||
"target": "dmg"
|
||||
"target": [
|
||||
{
|
||||
"target": "dmg",
|
||||
"arch": ["x64", "arm64"]
|
||||
}
|
||||
],
|
||||
"icon": "assets/icon.icns"
|
||||
},
|
||||
"linux": {
|
||||
"target": "AppImage"
|
||||
"target": [
|
||||
{
|
||||
"target": "AppImage",
|
||||
"arch": ["x64"]
|
||||
}
|
||||
],
|
||||
"icon": "assets/icon.png"
|
||||
},
|
||||
"portable": {
|
||||
"artifactName": "${productName}-${version}-portable.${ext}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
116
client/src/config/settings.js
Normal file
116
client/src/config/settings.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Runtime Configuration Module
|
||||
*
|
||||
* Loads settings from external config.json file at runtime,
|
||||
* allowing IT to configure the app without rebuilding.
|
||||
*
|
||||
* Config file location:
|
||||
* - Development: client/config.json
|
||||
* - Packaged app: <app>/resources/config.json
|
||||
*/
|
||||
|
||||
// Default settings (used if config.json is not found)
|
||||
const DEFAULT_SETTINGS = {
|
||||
apiBaseUrl: "http://localhost:8000/api",
|
||||
uploadTimeout: 600000,
|
||||
appTitle: "Meeting Assistant",
|
||||
whisper: {
|
||||
model: "medium",
|
||||
device: "cpu",
|
||||
compute: "int8"
|
||||
}
|
||||
};
|
||||
|
||||
let _settings = null;
|
||||
let _configPath = null;
|
||||
|
||||
/**
|
||||
* Get the config file path based on environment
|
||||
*/
|
||||
function getConfigPath() {
|
||||
if (_configPath) return _configPath;
|
||||
|
||||
// Check if running in Electron
|
||||
if (typeof window !== "undefined" && window.electronAPI) {
|
||||
// Will be set by preload.js
|
||||
return null;
|
||||
}
|
||||
|
||||
// Browser/development fallback
|
||||
return "./config.json";
|
||||
}
|
||||
|
||||
/**
|
||||
* Load settings from config file
|
||||
* Called once at app startup
|
||||
*/
|
||||
export async function loadSettings() {
|
||||
if (_settings) return _settings;
|
||||
|
||||
try {
|
||||
// Try to load from Electron's exposed config
|
||||
if (typeof window !== "undefined" && window.electronAPI?.getConfig) {
|
||||
_settings = await window.electronAPI.getConfig();
|
||||
console.log("Settings loaded from Electron config:", _settings);
|
||||
return _settings;
|
||||
}
|
||||
|
||||
// Fallback: try to fetch config.json
|
||||
const configPath = getConfigPath();
|
||||
if (configPath) {
|
||||
const response = await fetch(configPath);
|
||||
if (response.ok) {
|
||||
const config = await response.json();
|
||||
_settings = { ...DEFAULT_SETTINGS, ...config };
|
||||
console.log("Settings loaded from config.json:", _settings);
|
||||
return _settings;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to load config.json, using defaults:", error.message);
|
||||
}
|
||||
|
||||
// Use defaults
|
||||
_settings = { ...DEFAULT_SETTINGS };
|
||||
console.log("Using default settings:", _settings);
|
||||
return _settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current settings (must call loadSettings first)
|
||||
*/
|
||||
export function getSettings() {
|
||||
if (!_settings) {
|
||||
console.warn("Settings not loaded yet, returning defaults");
|
||||
return { ...DEFAULT_SETTINGS };
|
||||
}
|
||||
return _settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API base URL
|
||||
*/
|
||||
export function getApiBaseUrl() {
|
||||
return getSettings().apiBaseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload timeout
|
||||
*/
|
||||
export function getUploadTimeout() {
|
||||
return getSettings().uploadTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app title
|
||||
*/
|
||||
export function getAppTitle() {
|
||||
return getSettings().appTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Whisper configuration
|
||||
*/
|
||||
export function getWhisperConfig() {
|
||||
return getSettings().whisper;
|
||||
}
|
||||
@@ -8,11 +8,61 @@ let mainWindow;
|
||||
let sidecarProcess;
|
||||
let sidecarReady = false;
|
||||
let streamingActive = false;
|
||||
let appConfig = null;
|
||||
|
||||
/**
|
||||
* Load configuration from external config.json
|
||||
* Config file location:
|
||||
* - Development: client/config.json
|
||||
* - Packaged: <app>/resources/config.json
|
||||
*/
|
||||
function loadConfig() {
|
||||
const configPaths = [
|
||||
// Packaged app: resources folder
|
||||
app.isPackaged ? path.join(process.resourcesPath, "config.json") : null,
|
||||
// Development: client folder
|
||||
path.join(__dirname, "..", "config.json"),
|
||||
// Fallback: same directory
|
||||
path.join(__dirname, "config.json"),
|
||||
].filter(Boolean);
|
||||
|
||||
for (const configPath of configPaths) {
|
||||
try {
|
||||
if (fs.existsSync(configPath)) {
|
||||
const configData = fs.readFileSync(configPath, "utf-8");
|
||||
appConfig = JSON.parse(configData);
|
||||
console.log("Config loaded from:", configPath);
|
||||
console.log("Config:", appConfig);
|
||||
return appConfig;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to load config from:", configPath, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Default configuration
|
||||
appConfig = {
|
||||
apiBaseUrl: "http://localhost:8000/api",
|
||||
uploadTimeout: 600000,
|
||||
appTitle: "Meeting Assistant",
|
||||
whisper: {
|
||||
model: "medium",
|
||||
device: "cpu",
|
||||
compute: "int8"
|
||||
}
|
||||
};
|
||||
console.log("Using default config:", appConfig);
|
||||
return appConfig;
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
// Set window title from config
|
||||
const windowTitle = appConfig?.appTitle || "Meeting Assistant";
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
title: windowTitle,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
@@ -32,8 +82,22 @@ function startSidecar() {
|
||||
? path.join(process.resourcesPath, "sidecar")
|
||||
: path.join(__dirname, "..", "..", "sidecar");
|
||||
|
||||
// Determine the sidecar executable path based on packaging and platform
|
||||
let sidecarExecutable;
|
||||
let sidecarArgs = [];
|
||||
|
||||
if (app.isPackaged) {
|
||||
// Packaged app: use PyInstaller-built executable
|
||||
if (process.platform === "win32") {
|
||||
sidecarExecutable = path.join(sidecarDir, "transcriber", "transcriber.exe");
|
||||
} else if (process.platform === "darwin") {
|
||||
sidecarExecutable = path.join(sidecarDir, "transcriber", "transcriber");
|
||||
} else {
|
||||
sidecarExecutable = path.join(sidecarDir, "transcriber", "transcriber");
|
||||
}
|
||||
} else {
|
||||
// Development mode: use Python script with venv
|
||||
const sidecarScript = path.join(sidecarDir, "transcriber.py");
|
||||
const venvPython = path.join(sidecarDir, "venv", "bin", "python");
|
||||
|
||||
if (!fs.existsSync(sidecarScript)) {
|
||||
console.log("Sidecar script not found at:", sidecarScript);
|
||||
@@ -41,25 +105,42 @@ function startSidecar() {
|
||||
return;
|
||||
}
|
||||
|
||||
const pythonPath = fs.existsSync(venvPython) ? venvPython : "python3";
|
||||
// Check for virtual environment Python
|
||||
let venvPython;
|
||||
if (process.platform === "win32") {
|
||||
venvPython = path.join(sidecarDir, "venv", "Scripts", "python.exe");
|
||||
} else {
|
||||
venvPython = path.join(sidecarDir, "venv", "bin", "python");
|
||||
}
|
||||
|
||||
sidecarExecutable = fs.existsSync(venvPython) ? venvPython : "python3";
|
||||
sidecarArgs = [sidecarScript];
|
||||
}
|
||||
|
||||
if (!fs.existsSync(sidecarExecutable)) {
|
||||
console.log("Sidecar executable not found at:", sidecarExecutable);
|
||||
console.log("Transcription will not be available.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get Whisper configuration from environment variables
|
||||
// Get Whisper configuration from config.json or environment variables
|
||||
const whisperConfig = appConfig?.whisper || {};
|
||||
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",
|
||||
WHISPER_MODEL: process.env.WHISPER_MODEL || whisperConfig.model || "medium",
|
||||
WHISPER_DEVICE: process.env.WHISPER_DEVICE || whisperConfig.device || "cpu",
|
||||
WHISPER_COMPUTE: process.env.WHISPER_COMPUTE || whisperConfig.compute || "int8",
|
||||
};
|
||||
|
||||
console.log("Starting sidecar with:", pythonPath, sidecarScript);
|
||||
console.log("Starting sidecar with:", sidecarExecutable, sidecarArgs.join(" "));
|
||||
console.log("Whisper config:", {
|
||||
model: whisperEnv.WHISPER_MODEL,
|
||||
device: whisperEnv.WHISPER_DEVICE,
|
||||
compute: whisperEnv.WHISPER_COMPUTE,
|
||||
});
|
||||
|
||||
sidecarProcess = spawn(pythonPath, [sidecarScript], {
|
||||
sidecarProcess = spawn(sidecarExecutable, sidecarArgs, {
|
||||
cwd: sidecarDir,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
env: whisperEnv,
|
||||
@@ -121,6 +202,9 @@ function startSidecar() {
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
// Load configuration first
|
||||
loadConfig();
|
||||
|
||||
createWindow();
|
||||
startSidecar();
|
||||
|
||||
@@ -144,6 +228,12 @@ app.on("window-all-closed", () => {
|
||||
});
|
||||
|
||||
// IPC handlers
|
||||
|
||||
// Get app configuration (for renderer to initialize API settings)
|
||||
ipcMain.handle("get-config", () => {
|
||||
return appConfig;
|
||||
});
|
||||
|
||||
ipcMain.handle("navigate", (event, page) => {
|
||||
mainWindow.loadFile(path.join(__dirname, "pages", `${page}.html`));
|
||||
});
|
||||
|
||||
@@ -26,8 +26,12 @@
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { initApp } from '../services/init.js';
|
||||
import { login } from '../services/api.js';
|
||||
|
||||
// Initialize app config before any API calls
|
||||
await initApp();
|
||||
|
||||
const form = document.getElementById('login-form');
|
||||
const errorAlert = document.getElementById('error-alert');
|
||||
const loginBtn = document.getElementById('login-btn');
|
||||
|
||||
@@ -235,6 +235,7 @@
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { initApp } from '../services/init.js';
|
||||
import {
|
||||
getMeeting,
|
||||
updateMeeting,
|
||||
@@ -244,6 +245,9 @@
|
||||
transcribeAudio
|
||||
} from '../services/api.js';
|
||||
|
||||
// Initialize app config before any API calls
|
||||
await initApp();
|
||||
|
||||
const meetingId = localStorage.getItem('currentMeetingId');
|
||||
let currentMeeting = null;
|
||||
let isRecording = false;
|
||||
|
||||
@@ -67,8 +67,12 @@
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { initApp } from '../services/init.js';
|
||||
import { getMeetings, createMeeting, clearToken } from '../services/api.js';
|
||||
|
||||
// Initialize app config before any API calls
|
||||
await initApp();
|
||||
|
||||
const meetingsContainer = document.getElementById('meetings-container');
|
||||
const newMeetingBtn = document.getElementById('new-meeting-btn');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
contextBridge.exposeInMainWorld("electronAPI", {
|
||||
// App configuration (loaded from config.json)
|
||||
getConfig: () => ipcRenderer.invoke("get-config"),
|
||||
|
||||
// Navigation
|
||||
navigate: (page) => ipcRenderer.invoke("navigate", page),
|
||||
|
||||
|
||||
@@ -1,5 +1,34 @@
|
||||
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);
|
||||
/**
|
||||
* API Service Module
|
||||
*
|
||||
* API base URL and settings are loaded from external config at runtime,
|
||||
* allowing deployment configuration without rebuilding.
|
||||
*/
|
||||
|
||||
// Settings will be loaded from config.json via Electron IPC
|
||||
let API_BASE_URL = "http://localhost:8000/api";
|
||||
let UPLOAD_TIMEOUT = 600000;
|
||||
let _settingsLoaded = false;
|
||||
|
||||
/**
|
||||
* Initialize API settings from external config
|
||||
* Called by Electron preload or at app startup
|
||||
*/
|
||||
export async function initializeApi(settings) {
|
||||
if (settings) {
|
||||
API_BASE_URL = settings.apiBaseUrl || API_BASE_URL;
|
||||
UPLOAD_TIMEOUT = settings.uploadTimeout || UPLOAD_TIMEOUT;
|
||||
_settingsLoaded = true;
|
||||
console.log("API initialized with:", { API_BASE_URL, UPLOAD_TIMEOUT });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current API base URL
|
||||
*/
|
||||
export function getApiBaseUrl() {
|
||||
return API_BASE_URL;
|
||||
}
|
||||
|
||||
let authToken = null;
|
||||
let tokenRefreshTimer = null;
|
||||
|
||||
39
client/src/services/init.js
Normal file
39
client/src/services/init.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* App Initialization Module
|
||||
*
|
||||
* Loads configuration from Electron main process and initializes API settings.
|
||||
* Should be imported and called at the start of every page.
|
||||
*/
|
||||
|
||||
import { initializeApi } from './api.js';
|
||||
|
||||
let _initialized = false;
|
||||
|
||||
/**
|
||||
* Initialize the app with configuration from main process
|
||||
* Call this at the start of every page
|
||||
*/
|
||||
export async function initApp() {
|
||||
if (_initialized) return;
|
||||
|
||||
try {
|
||||
// Get config from Electron main process
|
||||
if (window.electronAPI?.getConfig) {
|
||||
const config = await window.electronAPI.getConfig();
|
||||
await initializeApi(config);
|
||||
console.log("App initialized with config:", config);
|
||||
} else {
|
||||
console.warn("electronAPI not available, using defaults");
|
||||
}
|
||||
_initialized = true;
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize app:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if app is initialized
|
||||
*/
|
||||
export function isInitialized() {
|
||||
return _initialized;
|
||||
}
|
||||
199
scripts/README.md
Normal file
199
scripts/README.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# Meeting Assistant 建置腳本
|
||||
|
||||
本目錄包含後端設置與前端打包的自動化腳本。
|
||||
|
||||
## 腳本清單
|
||||
|
||||
| 腳本 | 平台 | 說明 |
|
||||
|------|------|------|
|
||||
| `setup-backend.sh` | Linux/macOS/WSL | 後端一鍵設置與啟動 |
|
||||
| `setup-backend.bat` | Windows | 後端一鍵設置與啟動 |
|
||||
| `build-client.sh` | Linux/macOS/WSL | 前端打包腳本 |
|
||||
| `build-client.bat` | Windows | 前端打包腳本 |
|
||||
| `deploy-backend.sh` | Linux | 生產環境部署腳本 |
|
||||
|
||||
---
|
||||
|
||||
## 後端設置腳本
|
||||
|
||||
### 功能
|
||||
- 自動檢查 Python 環境 (需要 3.10+)
|
||||
- 自動建立虛擬環境
|
||||
- 自動安裝依賴
|
||||
- 自動設置環境變數檔案
|
||||
- 啟動後端服務
|
||||
|
||||
### 使用方式
|
||||
|
||||
**Linux/macOS/WSL:**
|
||||
```bash
|
||||
# 一鍵設置並啟動
|
||||
./scripts/setup-backend.sh start
|
||||
|
||||
# 僅設置環境
|
||||
./scripts/setup-backend.sh setup
|
||||
|
||||
# 背景執行
|
||||
./scripts/setup-backend.sh start-bg
|
||||
|
||||
# 停止背景服務
|
||||
./scripts/setup-backend.sh stop
|
||||
|
||||
# 使用自訂端口
|
||||
./scripts/setup-backend.sh start --port 8080
|
||||
|
||||
# 不安裝 Sidecar 依賴
|
||||
./scripts/setup-backend.sh setup --no-sidecar
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```batch
|
||||
REM 一鍵設置並啟動
|
||||
scripts\setup-backend.bat start
|
||||
|
||||
REM 僅設置環境
|
||||
scripts\setup-backend.bat setup
|
||||
|
||||
REM 使用自訂端口
|
||||
scripts\setup-backend.bat start --port 8080
|
||||
```
|
||||
|
||||
### 環境變數
|
||||
啟動後請編輯 `backend/.env` 設定:
|
||||
- 資料庫連線
|
||||
- API 密鑰
|
||||
- 服務配置
|
||||
|
||||
---
|
||||
|
||||
## 前端打包腳本
|
||||
|
||||
### 功能
|
||||
- 將 Python Sidecar 打包成獨立執行檔 (PyInstaller)
|
||||
- 將 Electron 應用打包成免安裝 exe
|
||||
- 整合 Sidecar 到最終輸出
|
||||
|
||||
### 系統需求
|
||||
- Node.js 18+
|
||||
- Python 3.10+
|
||||
- 磁碟空間 5GB+ (Whisper 模型)
|
||||
|
||||
### 使用方式
|
||||
|
||||
**Linux/macOS/WSL:**
|
||||
```bash
|
||||
# 完整建置 (Sidecar + Electron)
|
||||
./scripts/build-client.sh build
|
||||
|
||||
# 僅打包 Sidecar
|
||||
./scripts/build-client.sh sidecar
|
||||
|
||||
# 僅打包 Electron (需先打包 Sidecar)
|
||||
./scripts/build-client.sh electron
|
||||
|
||||
# 建置前清理
|
||||
./scripts/build-client.sh build --clean
|
||||
|
||||
# 指定目標平台
|
||||
./scripts/build-client.sh build --platform linux
|
||||
./scripts/build-client.sh build --platform mac
|
||||
./scripts/build-client.sh build --platform win
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```batch
|
||||
REM 完整建置
|
||||
scripts\build-client.bat build
|
||||
|
||||
REM 僅打包 Sidecar
|
||||
scripts\build-client.bat sidecar
|
||||
|
||||
REM 僅打包 Electron
|
||||
scripts\build-client.bat electron
|
||||
|
||||
REM 建置前清理
|
||||
scripts\build-client.bat build --clean
|
||||
```
|
||||
|
||||
### 輸出目錄
|
||||
- Sidecar: `sidecar/dist/transcriber/`
|
||||
- Electron: `client/dist/`
|
||||
- 最終輸出: `build/`
|
||||
|
||||
### 注意事項
|
||||
|
||||
1. **跨平台打包限制**
|
||||
- Windows exe 必須在 Windows 環境打包
|
||||
- macOS dmg 必須在 macOS 環境打包
|
||||
- Linux AppImage 可在 Linux 或 WSL 打包
|
||||
|
||||
2. **首次打包時間**
|
||||
- Sidecar 首次打包需下載 Whisper 模型
|
||||
- 根據網路速度可能需要 10-30 分鐘
|
||||
|
||||
3. **磁碟空間**
|
||||
- 完整打包需要約 2-3GB 空間
|
||||
- 建議預留 5GB 以上
|
||||
|
||||
---
|
||||
|
||||
## 生產環境部署
|
||||
|
||||
使用 `deploy-backend.sh` 在 Linux 伺服器上部署:
|
||||
|
||||
```bash
|
||||
# 安裝後端服務
|
||||
sudo ./scripts/deploy-backend.sh install
|
||||
|
||||
# 更新後端服務
|
||||
sudo ./scripts/deploy-backend.sh update
|
||||
|
||||
# 查看狀態
|
||||
./scripts/deploy-backend.sh status
|
||||
|
||||
# 查看日誌
|
||||
./scripts/deploy-backend.sh logs
|
||||
|
||||
# 移除服務
|
||||
sudo ./scripts/deploy-backend.sh uninstall
|
||||
```
|
||||
|
||||
### 自訂配置
|
||||
```bash
|
||||
# 指定安裝目錄和端口
|
||||
sudo ./scripts/deploy-backend.sh install --dir /opt/my-meeting --port 8080 --user meeting
|
||||
```
|
||||
|
||||
部署後會建立 systemd 服務 `meeting-assistant-backend`,可使用標準 systemctl 命令管理。
|
||||
|
||||
---
|
||||
|
||||
## 疑難排解
|
||||
|
||||
### Python 版本問題
|
||||
確保使用 Python 3.10 或更高版本:
|
||||
```bash
|
||||
python3 --version
|
||||
```
|
||||
|
||||
### 虛擬環境問題
|
||||
如果虛擬環境損壞,可刪除後重建:
|
||||
```bash
|
||||
rm -rf backend/venv sidecar/venv
|
||||
./scripts/setup-backend.sh setup
|
||||
```
|
||||
|
||||
### Electron 打包失敗
|
||||
確保已安裝 node_modules:
|
||||
```bash
|
||||
cd client
|
||||
npm install
|
||||
```
|
||||
|
||||
### Sidecar 打包失敗
|
||||
確保 PyInstaller 已安裝:
|
||||
```bash
|
||||
cd sidecar
|
||||
source venv/bin/activate # Linux/macOS
|
||||
pip install pyinstaller
|
||||
```
|
||||
304
scripts/build-all.ps1
Normal file
304
scripts/build-all.ps1
Normal file
@@ -0,0 +1,304 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Meeting Assistant - Windows 一鍵打包腳本
|
||||
.DESCRIPTION
|
||||
在 Windows 環境下一鍵打包 Sidecar + Electron 成免安裝 exe
|
||||
.EXAMPLE
|
||||
.\scripts\build-all.ps1
|
||||
.\scripts\build-all.ps1 -ApiUrl "http://192.168.1.100:8000/api"
|
||||
.\scripts\build-all.ps1 -SkipSidecar
|
||||
.\scripts\build-all.ps1 -Clean
|
||||
#>
|
||||
|
||||
param(
|
||||
[string]$ApiUrl,
|
||||
[switch]$SkipSidecar,
|
||||
[switch]$Clean,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# 顏色輸出函數
|
||||
function Write-Step { param($msg) Write-Host "[STEP] $msg" -ForegroundColor Cyan }
|
||||
function Write-OK { param($msg) Write-Host "[OK] $msg" -ForegroundColor Green }
|
||||
function Write-Warn { param($msg) Write-Host "[WARN] $msg" -ForegroundColor Yellow }
|
||||
function Write-Err { param($msg) Write-Host "[ERROR] $msg" -ForegroundColor Red }
|
||||
|
||||
# 路徑設定
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$ProjectDir = Split-Path -Parent $ScriptDir
|
||||
$SidecarDir = Join-Path $ProjectDir "sidecar"
|
||||
$ClientDir = Join-Path $ProjectDir "client"
|
||||
$BuildDir = Join-Path $ProjectDir "build"
|
||||
|
||||
function Show-Banner {
|
||||
Write-Host ""
|
||||
Write-Host "==========================================" -ForegroundColor Cyan
|
||||
Write-Host " Meeting Assistant - Windows Builder" -ForegroundColor Cyan
|
||||
Write-Host " 一鍵打包 Electron + Sidecar" -ForegroundColor Cyan
|
||||
Write-Host "==========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
function Show-Help {
|
||||
Write-Host @"
|
||||
|
||||
Meeting Assistant - Windows 一鍵打包腳本
|
||||
|
||||
用法: .\build-all.ps1 [參數]
|
||||
|
||||
參數:
|
||||
-ApiUrl URL 後端 API URL (預設: http://localhost:8000/api)
|
||||
-SkipSidecar 跳過 Sidecar 打包(如果已經打包過)
|
||||
-Clean 打包前清理所有建置目錄
|
||||
-Help 顯示此幫助訊息
|
||||
|
||||
範例:
|
||||
.\build-all.ps1 # 完整打包 (使用預設 localhost)
|
||||
.\build-all.ps1 -ApiUrl "http://192.168.1.100:8000/api" # 指定後端 URL
|
||||
.\build-all.ps1 -ApiUrl "https://api.example.com/api" # 使用公司伺服器
|
||||
.\build-all.ps1 -Clean # 清理後打包
|
||||
.\build-all.ps1 -SkipSidecar # 僅打包 Electron
|
||||
|
||||
"@
|
||||
}
|
||||
|
||||
function Update-Config {
|
||||
param([string]$NewApiUrl)
|
||||
|
||||
Write-Step "更新設定檔..."
|
||||
|
||||
$configPath = Join-Path $ClientDir "config.json"
|
||||
|
||||
if (Test-Path $configPath) {
|
||||
$config = Get-Content $configPath -Raw | ConvertFrom-Json
|
||||
|
||||
if ($NewApiUrl) {
|
||||
$config.apiBaseUrl = $NewApiUrl
|
||||
Write-Host " API URL: $NewApiUrl" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
$config | ConvertTo-Json -Depth 10 | Set-Content $configPath -Encoding UTF8
|
||||
Write-OK "設定檔已更新"
|
||||
} else {
|
||||
Write-Warn "找不到 config.json,使用預設設定"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-Prerequisites {
|
||||
Write-Step "檢查建置環境..."
|
||||
|
||||
# 檢查 Python
|
||||
try {
|
||||
$pyVersion = python --version 2>&1
|
||||
Write-OK "Python: $pyVersion"
|
||||
} catch {
|
||||
Write-Err "Python 未安裝,請安裝 Python 3.10+"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 檢查 Node.js
|
||||
try {
|
||||
$nodeVersion = node --version 2>&1
|
||||
Write-OK "Node.js: $nodeVersion"
|
||||
} catch {
|
||||
Write-Err "Node.js 未安裝,請安裝 Node.js 18+"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 檢查 npm
|
||||
try {
|
||||
$npmVersion = npm --version 2>&1
|
||||
Write-OK "npm: $npmVersion"
|
||||
} catch {
|
||||
Write-Err "npm 未安裝"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
function Clear-BuildDirs {
|
||||
Write-Step "清理建置目錄..."
|
||||
|
||||
$dirsToClean = @(
|
||||
$BuildDir,
|
||||
(Join-Path $ClientDir "dist"),
|
||||
(Join-Path $SidecarDir "dist"),
|
||||
(Join-Path $SidecarDir "build")
|
||||
)
|
||||
|
||||
foreach ($dir in $dirsToClean) {
|
||||
if (Test-Path $dir) {
|
||||
Remove-Item -Recurse -Force $dir
|
||||
Write-Host " 已刪除: $dir" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
|
||||
# 刪除 spec 檔案
|
||||
Get-ChildItem -Path $SidecarDir -Filter "*.spec" | Remove-Item -Force
|
||||
|
||||
Write-OK "清理完成"
|
||||
}
|
||||
|
||||
function Build-Sidecar {
|
||||
Write-Step "打包 Sidecar (Python → exe)..."
|
||||
Write-Host " 這可能需要 5-10 分鐘..." -ForegroundColor Gray
|
||||
|
||||
Push-Location $SidecarDir
|
||||
|
||||
try {
|
||||
# 建立/啟動虛擬環境
|
||||
if (-not (Test-Path "venv")) {
|
||||
Write-Host " 建立虛擬環境..." -ForegroundColor Gray
|
||||
python -m venv venv
|
||||
}
|
||||
|
||||
# 啟動虛擬環境並安裝依賴
|
||||
& ".\venv\Scripts\Activate.ps1"
|
||||
|
||||
Write-Host " 安裝依賴..." -ForegroundColor Gray
|
||||
pip install --upgrade pip -q
|
||||
pip install -r requirements.txt -q
|
||||
pip install pyinstaller -q
|
||||
|
||||
# 建立 dist 目錄
|
||||
if (-not (Test-Path "dist")) {
|
||||
New-Item -ItemType Directory -Path "dist" | Out-Null
|
||||
}
|
||||
|
||||
Write-Host " 執行 PyInstaller..." -ForegroundColor Gray
|
||||
|
||||
# PyInstaller 打包
|
||||
pyinstaller `
|
||||
--onedir `
|
||||
--name transcriber `
|
||||
--distpath dist `
|
||||
--workpath build `
|
||||
--noconfirm `
|
||||
--clean `
|
||||
--console `
|
||||
--hidden-import=faster_whisper `
|
||||
--hidden-import=ctranslate2 `
|
||||
--hidden-import=huggingface_hub `
|
||||
--hidden-import=tokenizers `
|
||||
--hidden-import=onnxruntime `
|
||||
--hidden-import=opencc `
|
||||
--hidden-import=pydub `
|
||||
--hidden-import=numpy `
|
||||
--hidden-import=av `
|
||||
--collect-data=onnxruntime `
|
||||
--collect-data=faster_whisper `
|
||||
transcriber.py
|
||||
|
||||
if (Test-Path "dist\transcriber\transcriber.exe") {
|
||||
Write-OK "Sidecar 打包完成"
|
||||
} else {
|
||||
Write-Err "Sidecar 打包失敗"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Build-Electron {
|
||||
Write-Step "打包 Electron 應用..."
|
||||
|
||||
Push-Location $ClientDir
|
||||
|
||||
try {
|
||||
# 檢查 Sidecar 是否存在
|
||||
$sidecarExe = Join-Path $SidecarDir "dist\transcriber\transcriber.exe"
|
||||
if (-not (Test-Path $sidecarExe)) {
|
||||
Write-Err "找不到 Sidecar: $sidecarExe"
|
||||
Write-Warn "請先執行完整打包或移除 -SkipSidecar 參數"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 安裝依賴
|
||||
if (-not (Test-Path "node_modules")) {
|
||||
Write-Host " 安裝 npm 依賴..." -ForegroundColor Gray
|
||||
npm install
|
||||
}
|
||||
|
||||
# 建立 .env 如果不存在
|
||||
if (-not (Test-Path ".env") -and (Test-Path ".env.example")) {
|
||||
Copy-Item ".env.example" ".env"
|
||||
Write-Warn "已建立 .env,請確認設定"
|
||||
}
|
||||
|
||||
Write-Host " 執行 electron-builder..." -ForegroundColor Gray
|
||||
npm run build -- --win
|
||||
|
||||
if (Test-Path "dist\*.exe") {
|
||||
Write-OK "Electron 打包完成"
|
||||
} else {
|
||||
Write-Err "Electron 打包失敗"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Copy-Output {
|
||||
Write-Step "整合輸出..."
|
||||
|
||||
if (-not (Test-Path $BuildDir)) {
|
||||
New-Item -ItemType Directory -Path $BuildDir | Out-Null
|
||||
}
|
||||
|
||||
# 複製 exe 檔案
|
||||
$exeFiles = Get-ChildItem -Path (Join-Path $ClientDir "dist") -Filter "*.exe"
|
||||
foreach ($file in $exeFiles) {
|
||||
Copy-Item $file.FullName -Destination $BuildDir
|
||||
Write-Host " 複製: $($file.Name)" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "==========================================" -ForegroundColor Cyan
|
||||
Write-Host " 打包完成!" -ForegroundColor Green
|
||||
Write-Host "==========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host " 輸出目錄: $BuildDir" -ForegroundColor White
|
||||
Write-Host ""
|
||||
|
||||
# 列出輸出檔案
|
||||
Get-ChildItem -Path $BuildDir | ForEach-Object {
|
||||
$size = [math]::Round($_.Length / 1MB, 2)
|
||||
Write-Host " $($_.Name) ($size MB)" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " 使用說明:" -ForegroundColor Yellow
|
||||
Write-Host " 直接執行 .exe 檔案即可,無需安裝" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# 主程式
|
||||
if ($Help) {
|
||||
Show-Help
|
||||
exit 0
|
||||
}
|
||||
|
||||
Show-Banner
|
||||
Test-Prerequisites
|
||||
|
||||
if ($Clean) {
|
||||
Clear-BuildDirs
|
||||
}
|
||||
|
||||
# Update config.json if API URL is specified
|
||||
if ($ApiUrl) {
|
||||
Update-Config -NewApiUrl $ApiUrl
|
||||
}
|
||||
|
||||
if (-not $SkipSidecar) {
|
||||
Build-Sidecar
|
||||
}
|
||||
|
||||
Build-Electron
|
||||
Copy-Output
|
||||
324
scripts/build-client.bat
Normal file
324
scripts/build-client.bat
Normal file
@@ -0,0 +1,324 @@
|
||||
@echo off
|
||||
REM Meeting Assistant Client - Windows 打包腳本
|
||||
REM 將 Electron 應用與 Python Sidecar 打包成免安裝 exe
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM 顏色設定 (Windows 10+)
|
||||
set "GREEN=[92m"
|
||||
set "YELLOW=[93m"
|
||||
set "RED=[91m"
|
||||
set "BLUE=[94m"
|
||||
set "CYAN=[96m"
|
||||
set "NC=[0m"
|
||||
|
||||
REM 專案路徑
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "PROJECT_DIR=%SCRIPT_DIR%.."
|
||||
set "CLIENT_DIR=%PROJECT_DIR%\client"
|
||||
set "SIDECAR_DIR=%PROJECT_DIR%\sidecar"
|
||||
set "BUILD_DIR=%PROJECT_DIR%\build"
|
||||
|
||||
REM 預設配置
|
||||
set "SKIP_SIDECAR=false"
|
||||
set "CLEAN_BUILD=false"
|
||||
|
||||
REM 解析參數
|
||||
set "COMMAND=help"
|
||||
:parse_args
|
||||
if "%~1"=="" goto :main
|
||||
if /i "%~1"=="build" (set "COMMAND=build" & shift & goto :parse_args)
|
||||
if /i "%~1"=="sidecar" (set "COMMAND=sidecar" & shift & goto :parse_args)
|
||||
if /i "%~1"=="electron" (set "COMMAND=electron" & shift & goto :parse_args)
|
||||
if /i "%~1"=="clean" (set "COMMAND=clean" & shift & goto :parse_args)
|
||||
if /i "%~1"=="help" (set "COMMAND=help" & shift & goto :parse_args)
|
||||
if /i "%~1"=="--skip-sidecar" (set "SKIP_SIDECAR=true" & shift & goto :parse_args)
|
||||
if /i "%~1"=="--clean" (set "CLEAN_BUILD=true" & shift & goto :parse_args)
|
||||
echo %RED%[ERROR]%NC% 未知參數: %~1
|
||||
goto :show_help
|
||||
|
||||
:main
|
||||
if "%COMMAND%"=="help" goto :show_help
|
||||
if "%COMMAND%"=="build" goto :do_build
|
||||
if "%COMMAND%"=="sidecar" goto :do_sidecar
|
||||
if "%COMMAND%"=="electron" goto :do_electron
|
||||
if "%COMMAND%"=="clean" goto :do_clean
|
||||
goto :show_help
|
||||
|
||||
:show_banner
|
||||
echo.
|
||||
echo %CYAN%==========================================
|
||||
echo Meeting Assistant Client Builder
|
||||
echo 打包 Electron + Sidecar 為免安裝執行檔
|
||||
echo ==========================================%NC%
|
||||
echo.
|
||||
goto :eof
|
||||
|
||||
:check_environment
|
||||
echo %BLUE%[STEP]%NC% 檢查建置環境...
|
||||
|
||||
REM 檢查 Node.js
|
||||
where node >nul 2>&1
|
||||
if %errorlevel% equ 0 (
|
||||
for /f "tokens=*" %%i in ('node --version') do echo %GREEN%[OK]%NC% Node.js: %%i
|
||||
) else (
|
||||
echo %RED%[ERROR]%NC% Node.js 未安裝
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM 檢查 npm
|
||||
where npm >nul 2>&1
|
||||
if %errorlevel% equ 0 (
|
||||
for /f "tokens=*" %%i in ('npm --version') do echo %GREEN%[OK]%NC% npm: %%i
|
||||
) else (
|
||||
echo %RED%[ERROR]%NC% npm 未安裝
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM 檢查 Python
|
||||
where python >nul 2>&1
|
||||
if %errorlevel% equ 0 (
|
||||
for /f "tokens=*" %%i in ('python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"') do set "PY_VERSION=%%i"
|
||||
for /f "tokens=*" %%i in ('python -c "import sys; print(sys.version_info.major)"') do set "PY_MAJOR=%%i"
|
||||
for /f "tokens=*" %%i in ('python -c "import sys; print(sys.version_info.minor)"') do set "PY_MINOR=%%i"
|
||||
|
||||
if !PY_MAJOR! geq 3 if !PY_MINOR! geq 10 (
|
||||
set "PYTHON_CMD=python"
|
||||
echo %GREEN%[OK]%NC% Python !PY_VERSION!
|
||||
goto :eof
|
||||
)
|
||||
)
|
||||
|
||||
echo %RED%[ERROR]%NC% 需要 Python 3.10 或更高版本
|
||||
exit /b 1
|
||||
|
||||
:do_clean
|
||||
echo %BLUE%[STEP]%NC% 清理建置目錄...
|
||||
|
||||
if exist "%BUILD_DIR%" rmdir /s /q "%BUILD_DIR%"
|
||||
if exist "%CLIENT_DIR%\dist" rmdir /s /q "%CLIENT_DIR%\dist"
|
||||
if exist "%SIDECAR_DIR%\dist" rmdir /s /q "%SIDECAR_DIR%\dist"
|
||||
if exist "%SIDECAR_DIR%\build" rmdir /s /q "%SIDECAR_DIR%\build"
|
||||
if exist "%SIDECAR_DIR%\*.spec" del /q "%SIDECAR_DIR%\*.spec"
|
||||
|
||||
echo %GREEN%[OK]%NC% 建置目錄已清理
|
||||
goto :eof
|
||||
|
||||
:setup_sidecar_venv
|
||||
echo %BLUE%[STEP]%NC% 設置 Sidecar 建置環境...
|
||||
|
||||
cd /d "%SIDECAR_DIR%"
|
||||
|
||||
if not exist "venv" (
|
||||
echo %BLUE%[INFO]%NC% 創建虛擬環境...
|
||||
%PYTHON_CMD% -m venv venv
|
||||
)
|
||||
|
||||
echo %BLUE%[INFO]%NC% 安裝 Sidecar 依賴...
|
||||
call venv\Scripts\activate.bat
|
||||
pip install --upgrade pip -q
|
||||
pip install -r requirements.txt -q
|
||||
|
||||
echo %BLUE%[INFO]%NC% 安裝 PyInstaller...
|
||||
pip install pyinstaller -q
|
||||
|
||||
echo %GREEN%[OK]%NC% Sidecar 建置環境就緒
|
||||
goto :eof
|
||||
|
||||
:build_sidecar
|
||||
echo %BLUE%[STEP]%NC% 打包 Sidecar (Python → 獨立執行檔)...
|
||||
|
||||
cd /d "%SIDECAR_DIR%"
|
||||
|
||||
call venv\Scripts\activate.bat
|
||||
|
||||
if not exist "dist" mkdir dist
|
||||
|
||||
echo %BLUE%[INFO]%NC% 執行 PyInstaller...
|
||||
echo %BLUE%[INFO]%NC% 這可能需要幾分鐘...
|
||||
|
||||
pyinstaller ^
|
||||
--onedir ^
|
||||
--name transcriber ^
|
||||
--distpath dist ^
|
||||
--workpath build ^
|
||||
--specpath . ^
|
||||
--noconfirm ^
|
||||
--clean ^
|
||||
--log-level WARN ^
|
||||
--console ^
|
||||
--hidden-import=faster_whisper ^
|
||||
--hidden-import=ctranslate2 ^
|
||||
--hidden-import=huggingface_hub ^
|
||||
--hidden-import=tokenizers ^
|
||||
--hidden-import=onnxruntime ^
|
||||
--hidden-import=opencc ^
|
||||
--hidden-import=pydub ^
|
||||
--hidden-import=numpy ^
|
||||
--hidden-import=av ^
|
||||
--collect-data=onnxruntime ^
|
||||
--collect-data=faster_whisper ^
|
||||
transcriber.py
|
||||
|
||||
if exist "dist\transcriber" (
|
||||
echo %GREEN%[OK]%NC% Sidecar 打包完成: %SIDECAR_DIR%\dist\transcriber
|
||||
) else (
|
||||
echo %RED%[ERROR]%NC% Sidecar 打包失敗
|
||||
exit /b 1
|
||||
)
|
||||
goto :eof
|
||||
|
||||
:setup_client
|
||||
echo %BLUE%[STEP]%NC% 設置前端建置環境...
|
||||
|
||||
cd /d "%CLIENT_DIR%"
|
||||
|
||||
if not exist "node_modules" (
|
||||
echo %BLUE%[INFO]%NC% 安裝前端依賴...
|
||||
call npm install
|
||||
) else (
|
||||
echo %BLUE%[INFO]%NC% 前端依賴已安裝
|
||||
)
|
||||
|
||||
if not exist ".env" (
|
||||
if exist ".env.example" (
|
||||
copy .env.example .env >nul
|
||||
echo %YELLOW%[WARN]%NC% 已創建 .env 檔案,請確認設定
|
||||
)
|
||||
)
|
||||
|
||||
echo %GREEN%[OK]%NC% 前端建置環境就緒
|
||||
goto :eof
|
||||
|
||||
:build_electron
|
||||
echo %BLUE%[STEP]%NC% 打包 Electron 應用...
|
||||
|
||||
cd /d "%CLIENT_DIR%"
|
||||
|
||||
echo %BLUE%[INFO]%NC% 目標平台: Windows (Portable)
|
||||
echo %BLUE%[INFO]%NC% 執行 electron-builder...
|
||||
|
||||
call npm run build -- --win
|
||||
|
||||
if exist "dist" (
|
||||
echo %GREEN%[OK]%NC% Electron 打包完成
|
||||
echo %BLUE%[INFO]%NC% 輸出目錄: %CLIENT_DIR%\dist
|
||||
) else (
|
||||
echo %RED%[ERROR]%NC% Electron 打包失敗
|
||||
exit /b 1
|
||||
)
|
||||
goto :eof
|
||||
|
||||
:finalize_build
|
||||
echo %BLUE%[STEP]%NC% 整合建置輸出...
|
||||
|
||||
if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%"
|
||||
|
||||
REM 複製 Electron 輸出
|
||||
if exist "%CLIENT_DIR%\dist" (
|
||||
xcopy /s /e /y "%CLIENT_DIR%\dist\*" "%BUILD_DIR%\" >nul 2>&1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo %CYAN%==========================================
|
||||
echo 建置完成
|
||||
echo ==========================================%NC%
|
||||
echo.
|
||||
echo 輸出目錄: %BUILD_DIR%
|
||||
echo.
|
||||
|
||||
dir /b "%BUILD_DIR%"
|
||||
|
||||
echo.
|
||||
echo %GREEN%[OK]%NC% 打包完成!
|
||||
echo.
|
||||
echo Windows 使用說明:
|
||||
echo 1. 找到 build\ 中的 .exe 檔案
|
||||
echo 2. 直接執行即可,無需安裝
|
||||
echo.
|
||||
goto :eof
|
||||
|
||||
:do_build
|
||||
call :show_banner
|
||||
call :check_environment
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
if "%CLEAN_BUILD%"=="true" call :do_clean
|
||||
|
||||
if "%SKIP_SIDECAR%"=="false" (
|
||||
call :setup_sidecar_venv
|
||||
call :build_sidecar
|
||||
)
|
||||
|
||||
call :setup_client
|
||||
call :build_electron
|
||||
call :finalize_build
|
||||
goto :eof
|
||||
|
||||
:do_sidecar
|
||||
call :show_banner
|
||||
call :check_environment
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
if "%CLEAN_BUILD%"=="true" (
|
||||
if exist "%SIDECAR_DIR%\dist" rmdir /s /q "%SIDECAR_DIR%\dist"
|
||||
if exist "%SIDECAR_DIR%\build" rmdir /s /q "%SIDECAR_DIR%\build"
|
||||
)
|
||||
|
||||
call :setup_sidecar_venv
|
||||
call :build_sidecar
|
||||
goto :eof
|
||||
|
||||
:do_electron
|
||||
call :show_banner
|
||||
call :check_environment
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
if not exist "%SIDECAR_DIR%\dist\transcriber" (
|
||||
if "%SKIP_SIDECAR%"=="false" (
|
||||
echo %YELLOW%[WARN]%NC% Sidecar 尚未打包
|
||||
echo %BLUE%[INFO]%NC% 請先執行: %~nx0 sidecar
|
||||
echo %BLUE%[INFO]%NC% 或使用 --skip-sidecar 跳過
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
if "%CLEAN_BUILD%"=="true" (
|
||||
if exist "%CLIENT_DIR%\dist" rmdir /s /q "%CLIENT_DIR%\dist"
|
||||
)
|
||||
|
||||
call :setup_client
|
||||
call :build_electron
|
||||
call :finalize_build
|
||||
goto :eof
|
||||
|
||||
:show_help
|
||||
echo.
|
||||
echo Meeting Assistant Client - Windows 打包腳本
|
||||
echo.
|
||||
echo 用法: %~nx0 [命令] [選項]
|
||||
echo.
|
||||
echo 命令:
|
||||
echo build 完整建置 (Sidecar + Electron)
|
||||
echo sidecar 僅打包 Sidecar
|
||||
echo electron 僅打包 Electron (需先打包 Sidecar)
|
||||
echo clean 清理建置目錄
|
||||
echo help 顯示此幫助訊息
|
||||
echo.
|
||||
echo 選項:
|
||||
echo --skip-sidecar 跳過 Sidecar 打包
|
||||
echo --clean 建置前先清理
|
||||
echo.
|
||||
echo 範例:
|
||||
echo %~nx0 build 完整建置
|
||||
echo %~nx0 sidecar 僅打包 Sidecar
|
||||
echo %~nx0 electron --skip-sidecar 僅打包 Electron
|
||||
echo.
|
||||
echo 注意:
|
||||
echo - 首次打包 Sidecar 需下載 Whisper 模型,可能需要較長時間
|
||||
echo - 確保有足夠的磁碟空間 (建議 5GB+)
|
||||
echo.
|
||||
goto :eof
|
||||
|
||||
:end
|
||||
endlocal
|
||||
473
scripts/build-client.sh
Executable file
473
scripts/build-client.sh
Executable file
@@ -0,0 +1,473 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Meeting Assistant Client - 打包腳本
|
||||
# 將 Electron 應用與 Python Sidecar 打包成免安裝 exe
|
||||
#
|
||||
# 依賴:
|
||||
# - Node.js 18+
|
||||
# - Python 3.10+
|
||||
# - PyInstaller (會自動安裝)
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# 顏色定義
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
# 專案路徑
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
CLIENT_DIR="$PROJECT_DIR/client"
|
||||
SIDECAR_DIR="$PROJECT_DIR/sidecar"
|
||||
BUILD_DIR="$PROJECT_DIR/build"
|
||||
|
||||
# 預設配置
|
||||
TARGET_PLATFORM="win" # win, mac, linux
|
||||
SKIP_SIDECAR=false
|
||||
CLEAN_BUILD=false
|
||||
|
||||
# 函數:印出訊息
|
||||
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"
|
||||
}
|
||||
|
||||
log_step() {
|
||||
echo -e "${CYAN}[STEP]${NC} $1"
|
||||
}
|
||||
|
||||
# 函數:顯示標題
|
||||
show_banner() {
|
||||
echo ""
|
||||
echo -e "${CYAN}=========================================="
|
||||
echo " Meeting Assistant Client Builder"
|
||||
echo " 打包 Electron + Sidecar 為免安裝執行檔"
|
||||
echo -e "==========================================${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 函數:檢查環境
|
||||
check_environment() {
|
||||
log_step "檢查建置環境..."
|
||||
|
||||
local all_ok=true
|
||||
|
||||
# 檢查 Node.js
|
||||
if command -v node &> /dev/null; then
|
||||
local node_version=$(node --version)
|
||||
log_success "Node.js: $node_version"
|
||||
else
|
||||
log_error "Node.js 未安裝"
|
||||
all_ok=false
|
||||
fi
|
||||
|
||||
# 檢查 npm
|
||||
if command -v npm &> /dev/null; then
|
||||
local npm_version=$(npm --version)
|
||||
log_success "npm: $npm_version"
|
||||
else
|
||||
log_error "npm 未安裝"
|
||||
all_ok=false
|
||||
fi
|
||||
|
||||
# 檢查 Python
|
||||
local python_cmd=""
|
||||
for cmd in python3 python; do
|
||||
if command -v $cmd &> /dev/null; then
|
||||
local version=$($cmd -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
|
||||
local major=$($cmd -c 'import sys; print(sys.version_info.major)')
|
||||
local minor=$($cmd -c 'import sys; print(sys.version_info.minor)')
|
||||
|
||||
if [ "$major" -ge 3 ] && [ "$minor" -ge 10 ]; then
|
||||
python_cmd=$cmd
|
||||
log_success "Python: $version ($cmd)"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$python_cmd" ]; then
|
||||
log_error "需要 Python 3.10+"
|
||||
all_ok=false
|
||||
fi
|
||||
|
||||
if [ "$all_ok" = false ]; then
|
||||
log_error "環境檢查失敗,請安裝缺少的依賴"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$python_cmd"
|
||||
}
|
||||
|
||||
# 函數:清理建置目錄
|
||||
clean_build() {
|
||||
log_step "清理建置目錄..."
|
||||
|
||||
rm -rf "$BUILD_DIR"
|
||||
rm -rf "$CLIENT_DIR/dist"
|
||||
rm -rf "$SIDECAR_DIR/dist"
|
||||
rm -rf "$SIDECAR_DIR/build"
|
||||
rm -rf "$SIDECAR_DIR/*.spec"
|
||||
|
||||
log_success "建置目錄已清理"
|
||||
}
|
||||
|
||||
# 函數:設置 Sidecar 虛擬環境
|
||||
setup_sidecar_venv() {
|
||||
local python_cmd=$1
|
||||
|
||||
log_step "設置 Sidecar 建置環境..."
|
||||
|
||||
cd "$SIDECAR_DIR"
|
||||
|
||||
# 創建或使用現有虛擬環境
|
||||
if [ ! -d "venv" ]; then
|
||||
log_info "創建虛擬環境..."
|
||||
$python_cmd -m venv venv
|
||||
fi
|
||||
|
||||
# 啟動虛擬環境
|
||||
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
|
||||
source venv/Scripts/activate
|
||||
else
|
||||
source venv/bin/activate
|
||||
fi
|
||||
|
||||
# 安裝依賴
|
||||
log_info "安裝 Sidecar 依賴..."
|
||||
pip install --upgrade pip -q
|
||||
pip install -r requirements.txt -q
|
||||
|
||||
# 安裝 PyInstaller
|
||||
log_info "安裝 PyInstaller..."
|
||||
pip install pyinstaller -q
|
||||
|
||||
log_success "Sidecar 建置環境就緒"
|
||||
}
|
||||
|
||||
# 函數:打包 Sidecar
|
||||
build_sidecar() {
|
||||
log_step "打包 Sidecar (Python → 獨立執行檔)..."
|
||||
|
||||
cd "$SIDECAR_DIR"
|
||||
|
||||
# 啟動虛擬環境
|
||||
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
|
||||
source venv/Scripts/activate
|
||||
else
|
||||
source venv/bin/activate
|
||||
fi
|
||||
|
||||
# 確保 dist 目錄存在
|
||||
mkdir -p dist
|
||||
|
||||
# PyInstaller 打包參數
|
||||
local pyinstaller_args=(
|
||||
--onedir # 打包成目錄 (比 onefile 啟動快)
|
||||
--name transcriber # 輸出名稱
|
||||
--distpath dist # 輸出目錄
|
||||
--workpath build # 工作目錄
|
||||
--specpath . # spec 檔案位置
|
||||
--noconfirm # 覆蓋現有檔案
|
||||
--clean # 清理暫存檔
|
||||
--log-level WARN # 日誌等級
|
||||
)
|
||||
|
||||
# 根據平台添加特定參數
|
||||
if [[ "$TARGET_PLATFORM" == "win" ]]; then
|
||||
pyinstaller_args+=(--console) # Windows 保留控制台 (用於 stdio 通訊)
|
||||
fi
|
||||
|
||||
# 收集 faster-whisper 的隱藏導入
|
||||
pyinstaller_args+=(
|
||||
--hidden-import=faster_whisper
|
||||
--hidden-import=ctranslate2
|
||||
--hidden-import=huggingface_hub
|
||||
--hidden-import=tokenizers
|
||||
--hidden-import=onnxruntime
|
||||
--hidden-import=opencc
|
||||
--hidden-import=pydub
|
||||
--hidden-import=numpy
|
||||
--hidden-import=av
|
||||
)
|
||||
|
||||
# 收集 onnxruntime 資料
|
||||
pyinstaller_args+=(
|
||||
--collect-data=onnxruntime
|
||||
--collect-data=faster_whisper
|
||||
)
|
||||
|
||||
log_info "執行 PyInstaller..."
|
||||
log_info "這可能需要幾分鐘..."
|
||||
|
||||
pyinstaller "${pyinstaller_args[@]}" transcriber.py
|
||||
|
||||
if [ -d "dist/transcriber" ]; then
|
||||
log_success "Sidecar 打包完成: $SIDECAR_DIR/dist/transcriber"
|
||||
else
|
||||
log_error "Sidecar 打包失敗"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 函數:安裝前端依賴
|
||||
setup_client() {
|
||||
log_step "設置前端建置環境..."
|
||||
|
||||
cd "$CLIENT_DIR"
|
||||
|
||||
if [ ! -d "node_modules" ]; then
|
||||
log_info "安裝前端依賴..."
|
||||
npm install
|
||||
else
|
||||
log_info "前端依賴已安裝"
|
||||
fi
|
||||
|
||||
# 確保 .env 存在
|
||||
if [ ! -f ".env" ]; then
|
||||
if [ -f ".env.example" ]; then
|
||||
cp .env.example .env
|
||||
log_warn "已創建 .env 檔案,請確認設定"
|
||||
fi
|
||||
fi
|
||||
|
||||
log_success "前端建置環境就緒"
|
||||
}
|
||||
|
||||
# 函數:打包 Electron
|
||||
build_electron() {
|
||||
log_step "打包 Electron 應用..."
|
||||
|
||||
cd "$CLIENT_DIR"
|
||||
|
||||
# 根據目標平台設置建置參數
|
||||
local build_args=()
|
||||
|
||||
case $TARGET_PLATFORM in
|
||||
win)
|
||||
build_args+=(--win)
|
||||
log_info "目標平台: Windows (Portable)"
|
||||
;;
|
||||
mac)
|
||||
build_args+=(--mac)
|
||||
log_info "目標平台: macOS (DMG)"
|
||||
;;
|
||||
linux)
|
||||
build_args+=(--linux)
|
||||
log_info "目標平台: Linux (AppImage)"
|
||||
;;
|
||||
*)
|
||||
log_error "不支援的平台: $TARGET_PLATFORM"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
log_info "執行 electron-builder..."
|
||||
npm run build -- "${build_args[@]}"
|
||||
|
||||
if [ -d "dist" ]; then
|
||||
log_success "Electron 打包完成"
|
||||
log_info "輸出目錄: $CLIENT_DIR/dist"
|
||||
else
|
||||
log_error "Electron 打包失敗"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 函數:整合輸出
|
||||
finalize_build() {
|
||||
log_step "整合建置輸出..."
|
||||
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
# 複製 Electron 輸出
|
||||
if [ -d "$CLIENT_DIR/dist" ]; then
|
||||
cp -r "$CLIENT_DIR/dist/"* "$BUILD_DIR/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 顯示結果
|
||||
echo ""
|
||||
echo -e "${CYAN}=========================================="
|
||||
echo " 建置完成"
|
||||
echo -e "==========================================${NC}"
|
||||
echo ""
|
||||
echo " 輸出目錄: $BUILD_DIR"
|
||||
echo ""
|
||||
|
||||
# 列出輸出檔案
|
||||
if command -v tree &> /dev/null; then
|
||||
tree -L 2 "$BUILD_DIR" 2>/dev/null || ls -la "$BUILD_DIR"
|
||||
else
|
||||
ls -la "$BUILD_DIR"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log_success "打包完成!"
|
||||
echo ""
|
||||
|
||||
# 平台特定說明
|
||||
case $TARGET_PLATFORM in
|
||||
win)
|
||||
echo " Windows 使用說明:"
|
||||
echo " 1. 找到 dist/ 中的 .exe 檔案"
|
||||
echo " 2. 直接執行即可,無需安裝"
|
||||
echo ""
|
||||
;;
|
||||
mac)
|
||||
echo " macOS 使用說明:"
|
||||
echo " 1. 打開 dist/ 中的 .dmg 檔案"
|
||||
echo " 2. 將應用拖到 Applications 資料夾"
|
||||
echo ""
|
||||
;;
|
||||
linux)
|
||||
echo " Linux 使用說明:"
|
||||
echo " 1. 賦予 .AppImage 執行權限: chmod +x *.AppImage"
|
||||
echo " 2. 直接執行 AppImage 檔案"
|
||||
echo ""
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 函數:顯示幫助
|
||||
show_help() {
|
||||
echo ""
|
||||
echo "Meeting Assistant Client - 打包腳本"
|
||||
echo ""
|
||||
echo "用法: $0 [命令] [選項]"
|
||||
echo ""
|
||||
echo "命令:"
|
||||
echo " build 完整建置 (Sidecar + Electron)"
|
||||
echo " sidecar 僅打包 Sidecar"
|
||||
echo " electron 僅打包 Electron (需先打包 Sidecar)"
|
||||
echo " clean 清理建置目錄"
|
||||
echo " help 顯示此幫助訊息"
|
||||
echo ""
|
||||
echo "選項:"
|
||||
echo " --platform PLATFORM 目標平台: win, mac, linux (預設: win)"
|
||||
echo " --skip-sidecar 跳過 Sidecar 打包"
|
||||
echo " --clean 建置前先清理"
|
||||
echo ""
|
||||
echo "範例:"
|
||||
echo " $0 build # 完整建置 Windows 版本"
|
||||
echo " $0 build --platform linux # 建置 Linux 版本"
|
||||
echo " $0 sidecar # 僅打包 Sidecar"
|
||||
echo " $0 electron --skip-sidecar # 僅打包 Electron"
|
||||
echo ""
|
||||
echo "注意:"
|
||||
echo " - Windows 打包需在 Windows 環境執行"
|
||||
echo " - macOS 打包需在 macOS 環境執行"
|
||||
echo " - Linux 打包可在 Linux 或 WSL 環境執行"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 主程式
|
||||
main() {
|
||||
local command=${1:-"help"}
|
||||
|
||||
# 解析參數
|
||||
shift || true
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--platform)
|
||||
TARGET_PLATFORM="$2"
|
||||
shift 2
|
||||
;;
|
||||
--skip-sidecar)
|
||||
SKIP_SIDECAR=true
|
||||
shift
|
||||
;;
|
||||
--clean)
|
||||
CLEAN_BUILD=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
log_error "未知參數: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case $command in
|
||||
build)
|
||||
show_banner
|
||||
|
||||
local python_cmd=$(check_environment)
|
||||
|
||||
if [ "$CLEAN_BUILD" = true ]; then
|
||||
clean_build
|
||||
fi
|
||||
|
||||
if [ "$SKIP_SIDECAR" = false ]; then
|
||||
setup_sidecar_venv "$python_cmd"
|
||||
build_sidecar
|
||||
fi
|
||||
|
||||
setup_client
|
||||
build_electron
|
||||
finalize_build
|
||||
;;
|
||||
sidecar)
|
||||
show_banner
|
||||
local python_cmd=$(check_environment)
|
||||
|
||||
if [ "$CLEAN_BUILD" = true ]; then
|
||||
rm -rf "$SIDECAR_DIR/dist"
|
||||
rm -rf "$SIDECAR_DIR/build"
|
||||
fi
|
||||
|
||||
setup_sidecar_venv "$python_cmd"
|
||||
build_sidecar
|
||||
;;
|
||||
electron)
|
||||
show_banner
|
||||
check_environment > /dev/null
|
||||
|
||||
# 檢查 Sidecar 是否已打包
|
||||
if [ ! -d "$SIDECAR_DIR/dist/transcriber" ] && [ "$SKIP_SIDECAR" = false ]; then
|
||||
log_warn "Sidecar 尚未打包"
|
||||
log_info "請先執行: $0 sidecar"
|
||||
log_info "或使用 --skip-sidecar 跳過"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$CLEAN_BUILD" = true ]; then
|
||||
rm -rf "$CLIENT_DIR/dist"
|
||||
fi
|
||||
|
||||
setup_client
|
||||
build_electron
|
||||
finalize_build
|
||||
;;
|
||||
clean)
|
||||
clean_build
|
||||
;;
|
||||
help|--help|-h)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
log_error "未知命令: $command"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 執行主程式
|
||||
main "$@"
|
||||
237
scripts/setup-backend.bat
Normal file
237
scripts/setup-backend.bat
Normal file
@@ -0,0 +1,237 @@
|
||||
@echo off
|
||||
REM Meeting Assistant Backend - Windows 一鍵設置與啟動腳本
|
||||
REM 自動安裝依賴、設置環境並啟動後端服務
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM 顏色設定 (Windows 10+)
|
||||
set "GREEN=[92m"
|
||||
set "YELLOW=[93m"
|
||||
set "RED=[91m"
|
||||
set "BLUE=[94m"
|
||||
set "CYAN=[96m"
|
||||
set "NC=[0m"
|
||||
|
||||
REM 專案路徑
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "PROJECT_DIR=%SCRIPT_DIR%.."
|
||||
set "BACKEND_DIR=%PROJECT_DIR%\backend"
|
||||
set "SIDECAR_DIR=%PROJECT_DIR%\sidecar"
|
||||
|
||||
REM 預設配置
|
||||
set "DEFAULT_PORT=8000"
|
||||
set "DEFAULT_HOST=0.0.0.0"
|
||||
|
||||
REM 解析參數
|
||||
set "COMMAND=start"
|
||||
set "PORT=%DEFAULT_PORT%"
|
||||
set "HOST=%DEFAULT_HOST%"
|
||||
set "NO_SIDECAR=false"
|
||||
|
||||
:parse_args
|
||||
if "%~1"=="" goto :main
|
||||
if /i "%~1"=="setup" (set "COMMAND=setup" & shift & goto :parse_args)
|
||||
if /i "%~1"=="start" (set "COMMAND=start" & shift & goto :parse_args)
|
||||
if /i "%~1"=="stop" (set "COMMAND=stop" & shift & goto :parse_args)
|
||||
if /i "%~1"=="help" (set "COMMAND=help" & shift & goto :parse_args)
|
||||
if /i "%~1"=="--port" (set "PORT=%~2" & shift & shift & goto :parse_args)
|
||||
if /i "%~1"=="--host" (set "HOST=%~2" & shift & shift & goto :parse_args)
|
||||
if /i "%~1"=="--no-sidecar" (set "NO_SIDECAR=true" & shift & goto :parse_args)
|
||||
echo %RED%[ERROR]%NC% 未知參數: %~1
|
||||
goto :show_help
|
||||
|
||||
:main
|
||||
if "%COMMAND%"=="help" goto :show_help
|
||||
if "%COMMAND%"=="setup" goto :do_setup
|
||||
if "%COMMAND%"=="start" goto :do_start
|
||||
if "%COMMAND%"=="stop" goto :do_stop
|
||||
goto :show_help
|
||||
|
||||
:show_banner
|
||||
echo.
|
||||
echo %CYAN%==========================================
|
||||
echo Meeting Assistant Backend Setup
|
||||
echo Windows 一鍵設置與啟動腳本
|
||||
echo ==========================================%NC%
|
||||
echo.
|
||||
goto :eof
|
||||
|
||||
:check_python
|
||||
echo %BLUE%[STEP]%NC% 檢查 Python 環境...
|
||||
|
||||
REM 嘗試找到 Python
|
||||
where python >nul 2>&1
|
||||
if %errorlevel% equ 0 (
|
||||
for /f "tokens=*" %%i in ('python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"') do set "PY_VERSION=%%i"
|
||||
for /f "tokens=*" %%i in ('python -c "import sys; print(sys.version_info.major)"') do set "PY_MAJOR=%%i"
|
||||
for /f "tokens=*" %%i in ('python -c "import sys; print(sys.version_info.minor)"') do set "PY_MINOR=%%i"
|
||||
|
||||
if !PY_MAJOR! geq 3 if !PY_MINOR! geq 10 (
|
||||
set "PYTHON_CMD=python"
|
||||
echo %GREEN%[OK]%NC% Python !PY_VERSION!
|
||||
goto :eof
|
||||
)
|
||||
)
|
||||
|
||||
echo %RED%[ERROR]%NC% 需要 Python 3.10 或更高版本
|
||||
echo %BLUE%[INFO]%NC% 請安裝 Python: https://www.python.org/downloads/
|
||||
exit /b 1
|
||||
|
||||
:setup_backend_venv
|
||||
echo %BLUE%[STEP]%NC% 設置後端虛擬環境...
|
||||
|
||||
cd /d "%BACKEND_DIR%"
|
||||
|
||||
if not exist "venv" (
|
||||
echo %BLUE%[INFO]%NC% 創建虛擬環境...
|
||||
%PYTHON_CMD% -m venv venv
|
||||
echo %GREEN%[OK]%NC% 虛擬環境已創建
|
||||
) else (
|
||||
echo %BLUE%[INFO]%NC% 虛擬環境已存在
|
||||
)
|
||||
|
||||
echo %BLUE%[INFO]%NC% 安裝後端依賴...
|
||||
call venv\Scripts\activate.bat
|
||||
pip install --upgrade pip -q
|
||||
pip install -r requirements.txt -q
|
||||
echo %GREEN%[OK]%NC% 後端依賴安裝完成
|
||||
goto :eof
|
||||
|
||||
:setup_sidecar_venv
|
||||
echo %BLUE%[STEP]%NC% 設置 Sidecar 虛擬環境...
|
||||
|
||||
cd /d "%SIDECAR_DIR%"
|
||||
|
||||
if not exist "venv" (
|
||||
echo %BLUE%[INFO]%NC% 創建 Sidecar 虛擬環境...
|
||||
%PYTHON_CMD% -m venv venv
|
||||
echo %GREEN%[OK]%NC% Sidecar 虛擬環境已創建
|
||||
) else (
|
||||
echo %BLUE%[INFO]%NC% Sidecar 虛擬環境已存在
|
||||
)
|
||||
|
||||
echo %BLUE%[INFO]%NC% 安裝 Sidecar 依賴 (這可能需要幾分鐘)...
|
||||
call venv\Scripts\activate.bat
|
||||
pip install --upgrade pip -q
|
||||
pip install -r requirements.txt -q
|
||||
echo %GREEN%[OK]%NC% Sidecar 依賴安裝完成
|
||||
goto :eof
|
||||
|
||||
:setup_env_file
|
||||
echo %BLUE%[STEP]%NC% 檢查環境變數配置...
|
||||
|
||||
cd /d "%BACKEND_DIR%"
|
||||
|
||||
if not exist ".env" (
|
||||
if exist ".env.example" (
|
||||
copy .env.example .env >nul
|
||||
echo %YELLOW%[WARN]%NC% 已從 .env.example 創建 .env 檔案
|
||||
echo %YELLOW%[WARN]%NC% 請編輯 %BACKEND_DIR%\.env 設置資料庫和 API 密鑰
|
||||
) else (
|
||||
echo %RED%[ERROR]%NC% 找不到 .env.example 檔案
|
||||
exit /b 1
|
||||
)
|
||||
) else (
|
||||
echo %GREEN%[OK]%NC% 環境變數檔案已存在
|
||||
)
|
||||
goto :eof
|
||||
|
||||
:start_backend
|
||||
echo %BLUE%[STEP]%NC% 啟動後端服務...
|
||||
|
||||
cd /d "%BACKEND_DIR%"
|
||||
|
||||
REM 載入環境變數
|
||||
if exist ".env" (
|
||||
for /f "usebackq tokens=1,* delims==" %%a in (".env") do (
|
||||
set "%%a=%%b"
|
||||
)
|
||||
)
|
||||
|
||||
REM 使用 .env 中的配置或預設值
|
||||
if defined BACKEND_HOST set "HOST=%BACKEND_HOST%"
|
||||
if defined BACKEND_PORT set "PORT=%BACKEND_PORT%"
|
||||
|
||||
REM 啟動虛擬環境
|
||||
call venv\Scripts\activate.bat
|
||||
|
||||
echo.
|
||||
echo %GREEN%[OK]%NC% 後端服務準備就緒!
|
||||
echo.
|
||||
echo %CYAN%==========================================
|
||||
echo 服務資訊
|
||||
echo ==========================================%NC%
|
||||
echo.
|
||||
echo API 地址: http://localhost:%PORT%
|
||||
echo API 文件: http://localhost:%PORT%/docs
|
||||
echo 健康檢查: http://localhost:%PORT%/api/health
|
||||
echo.
|
||||
echo 按 Ctrl+C 停止服務
|
||||
echo.
|
||||
|
||||
uvicorn app.main:app --host %HOST% --port %PORT% --reload
|
||||
goto :eof
|
||||
|
||||
:do_setup
|
||||
call :show_banner
|
||||
call :check_python
|
||||
if errorlevel 1 exit /b 1
|
||||
call :setup_backend_venv
|
||||
if "%NO_SIDECAR%"=="false" call :setup_sidecar_venv
|
||||
call :setup_env_file
|
||||
echo.
|
||||
echo %GREEN%[OK]%NC% 環境設置完成!
|
||||
echo.
|
||||
echo %BLUE%[INFO]%NC% 啟動服務: %~nx0 start
|
||||
goto :eof
|
||||
|
||||
:do_start
|
||||
call :show_banner
|
||||
call :check_python
|
||||
if errorlevel 1 exit /b 1
|
||||
call :setup_backend_venv
|
||||
call :setup_env_file
|
||||
call :start_backend
|
||||
goto :eof
|
||||
|
||||
:do_stop
|
||||
echo %BLUE%[STEP]%NC% 停止後端服務...
|
||||
|
||||
REM 查找並終止 uvicorn 程序
|
||||
for /f "tokens=2" %%i in ('tasklist /fi "imagename eq python.exe" /fo list ^| findstr "PID:"') do (
|
||||
wmic process where "processid=%%i" get commandline 2>nul | findstr /i "uvicorn" >nul
|
||||
if not errorlevel 1 (
|
||||
taskkill /pid %%i /f >nul 2>&1
|
||||
echo %GREEN%[OK]%NC% 已停止 PID: %%i
|
||||
)
|
||||
)
|
||||
|
||||
echo %GREEN%[OK]%NC% 後端服務已停止
|
||||
goto :eof
|
||||
|
||||
:show_help
|
||||
echo.
|
||||
echo Meeting Assistant Backend - Windows 一鍵設置與啟動腳本
|
||||
echo.
|
||||
echo 用法: %~nx0 [命令] [選項]
|
||||
echo.
|
||||
echo 命令:
|
||||
echo setup 僅設置環境 (安裝依賴)
|
||||
echo start 設置並啟動後端服務 (預設)
|
||||
echo stop 停止後端服務
|
||||
echo help 顯示此幫助訊息
|
||||
echo.
|
||||
echo 選項:
|
||||
echo --port PORT 服務端口 (預設: %DEFAULT_PORT%)
|
||||
echo --host HOST 綁定地址 (預設: %DEFAULT_HOST%)
|
||||
echo --no-sidecar 不安裝 Sidecar 依賴
|
||||
echo.
|
||||
echo 範例:
|
||||
echo %~nx0 start 設置並啟動服務
|
||||
echo %~nx0 start --port 8080 使用自訂端口
|
||||
echo %~nx0 setup 僅安裝依賴
|
||||
echo.
|
||||
goto :eof
|
||||
|
||||
:end
|
||||
endlocal
|
||||
393
scripts/setup-backend.sh
Executable file
393
scripts/setup-backend.sh
Executable file
@@ -0,0 +1,393 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Meeting Assistant Backend - 一鍵設置與啟動腳本
|
||||
# 自動安裝依賴、設置環境並啟動後端服務
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# 顏色定義
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 專案路徑
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
BACKEND_DIR="$PROJECT_DIR/backend"
|
||||
SIDECAR_DIR="$PROJECT_DIR/sidecar"
|
||||
|
||||
# 預設配置
|
||||
DEFAULT_PORT=8000
|
||||
DEFAULT_HOST="0.0.0.0"
|
||||
|
||||
# 函數:印出訊息
|
||||
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"
|
||||
}
|
||||
|
||||
log_step() {
|
||||
echo -e "${CYAN}[STEP]${NC} $1"
|
||||
}
|
||||
|
||||
# 函數:顯示標題
|
||||
show_banner() {
|
||||
echo ""
|
||||
echo -e "${CYAN}=========================================="
|
||||
echo " Meeting Assistant Backend Setup"
|
||||
echo " 一鍵設置與啟動腳本"
|
||||
echo -e "==========================================${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 函數:檢查 Python 版本
|
||||
check_python() {
|
||||
log_step "檢查 Python 環境..."
|
||||
|
||||
# 嘗試不同的 Python 命令
|
||||
local python_cmd=""
|
||||
for cmd in python3 python; do
|
||||
if command -v $cmd &> /dev/null; then
|
||||
local version=$($cmd -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
|
||||
local major=$($cmd -c 'import sys; print(sys.version_info.major)')
|
||||
local minor=$($cmd -c 'import sys; print(sys.version_info.minor)')
|
||||
|
||||
if [ "$major" -ge 3 ] && [ "$minor" -ge 10 ]; then
|
||||
python_cmd=$cmd
|
||||
log_success "Python $version ($cmd)"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$python_cmd" ]; then
|
||||
log_error "需要 Python 3.10 或更高版本"
|
||||
log_info "請安裝 Python: https://www.python.org/downloads/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$python_cmd"
|
||||
}
|
||||
|
||||
# 函數:設置後端虛擬環境
|
||||
setup_backend_venv() {
|
||||
local python_cmd=$1
|
||||
|
||||
log_step "設置後端虛擬環境..."
|
||||
|
||||
cd "$BACKEND_DIR"
|
||||
|
||||
if [ ! -d "venv" ]; then
|
||||
log_info "創建虛擬環境..."
|
||||
$python_cmd -m venv venv
|
||||
log_success "虛擬環境已創建"
|
||||
else
|
||||
log_info "虛擬環境已存在"
|
||||
fi
|
||||
|
||||
# 啟動虛擬環境並安裝依賴
|
||||
log_info "安裝後端依賴..."
|
||||
|
||||
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
|
||||
# Windows
|
||||
source venv/Scripts/activate
|
||||
else
|
||||
# Linux/Mac
|
||||
source venv/bin/activate
|
||||
fi
|
||||
|
||||
pip install --upgrade pip -q
|
||||
pip install -r requirements.txt -q
|
||||
|
||||
log_success "後端依賴安裝完成"
|
||||
}
|
||||
|
||||
# 函數:設置 Sidecar 虛擬環境
|
||||
setup_sidecar_venv() {
|
||||
local python_cmd=$1
|
||||
|
||||
log_step "設置 Sidecar 虛擬環境..."
|
||||
|
||||
cd "$SIDECAR_DIR"
|
||||
|
||||
if [ ! -d "venv" ]; then
|
||||
log_info "創建 Sidecar 虛擬環境..."
|
||||
$python_cmd -m venv venv
|
||||
log_success "Sidecar 虛擬環境已創建"
|
||||
else
|
||||
log_info "Sidecar 虛擬環境已存在"
|
||||
fi
|
||||
|
||||
# 啟動虛擬環境並安裝依賴
|
||||
log_info "安裝 Sidecar 依賴 (這可能需要幾分鐘)..."
|
||||
|
||||
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
|
||||
source venv/Scripts/activate
|
||||
else
|
||||
source venv/bin/activate
|
||||
fi
|
||||
|
||||
pip install --upgrade pip -q
|
||||
pip install -r requirements.txt -q
|
||||
|
||||
log_success "Sidecar 依賴安裝完成"
|
||||
}
|
||||
|
||||
# 函數:設置環境變數檔案
|
||||
setup_env_file() {
|
||||
log_step "檢查環境變數配置..."
|
||||
|
||||
cd "$BACKEND_DIR"
|
||||
|
||||
if [ ! -f ".env" ]; then
|
||||
if [ -f ".env.example" ]; then
|
||||
cp .env.example .env
|
||||
log_warn "已從 .env.example 創建 .env 檔案"
|
||||
log_warn "請編輯 $BACKEND_DIR/.env 設置資料庫和 API 密鑰"
|
||||
else
|
||||
log_error "找不到 .env.example 檔案"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log_success "環境變數檔案已存在"
|
||||
fi
|
||||
}
|
||||
|
||||
# 函數:檢查 port 是否被佔用
|
||||
check_port() {
|
||||
local port=$1
|
||||
if command -v lsof &> /dev/null; then
|
||||
if lsof -i :$port > /dev/null 2>&1; then
|
||||
return 0 # port 被佔用
|
||||
fi
|
||||
elif command -v netstat &> /dev/null; then
|
||||
if netstat -tuln | grep -q ":$port "; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1 # port 可用
|
||||
}
|
||||
|
||||
# 函數:釋放 port
|
||||
release_port() {
|
||||
local port=$1
|
||||
if check_port $port; then
|
||||
log_warn "Port $port 被佔用,嘗試釋放..."
|
||||
if command -v lsof &> /dev/null; then
|
||||
local pid=$(lsof -t -i :$port 2>/dev/null)
|
||||
if [ -n "$pid" ]; then
|
||||
kill -9 $pid 2>/dev/null || true
|
||||
sleep 1
|
||||
log_success "Port $port 已釋放"
|
||||
fi
|
||||
else
|
||||
log_error "無法釋放 port,請手動關閉佔用 $port 的程序"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 函數:啟動後端服務
|
||||
start_backend() {
|
||||
local host=${1:-$DEFAULT_HOST}
|
||||
local port=${2:-$DEFAULT_PORT}
|
||||
local mode=${3:-"foreground"} # foreground 或 background
|
||||
|
||||
log_step "啟動後端服務..."
|
||||
|
||||
cd "$BACKEND_DIR"
|
||||
|
||||
# 載入環境變數
|
||||
if [ -f ".env" ]; then
|
||||
export $(grep -v '^#' .env | grep -v '^$' | xargs)
|
||||
fi
|
||||
|
||||
# 使用 .env 中的配置或預設值
|
||||
host=${BACKEND_HOST:-$host}
|
||||
port=${BACKEND_PORT:-$port}
|
||||
|
||||
# 釋放 port
|
||||
release_port $port
|
||||
|
||||
# 啟動虛擬環境
|
||||
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
|
||||
source venv/Scripts/activate
|
||||
else
|
||||
source venv/bin/activate
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log_success "後端服務準備就緒!"
|
||||
echo ""
|
||||
echo -e "${CYAN}=========================================="
|
||||
echo " 服務資訊"
|
||||
echo -e "==========================================${NC}"
|
||||
echo ""
|
||||
echo " API 地址: http://localhost:$port"
|
||||
echo " API 文件: http://localhost:$port/docs"
|
||||
echo " 健康檢查: http://localhost:$port/api/health"
|
||||
echo ""
|
||||
echo " 按 Ctrl+C 停止服務"
|
||||
echo ""
|
||||
|
||||
if [ "$mode" = "background" ]; then
|
||||
nohup uvicorn app.main:app --host $host --port $port --reload > "$PROJECT_DIR/backend.log" 2>&1 &
|
||||
local pid=$!
|
||||
echo $pid > "$PROJECT_DIR/.backend_pid"
|
||||
log_success "後端服務已在背景啟動 (PID: $pid)"
|
||||
log_info "查看日誌: tail -f $PROJECT_DIR/backend.log"
|
||||
else
|
||||
# 前景執行
|
||||
uvicorn app.main:app --host $host --port $port --reload
|
||||
fi
|
||||
}
|
||||
|
||||
# 函數:顯示幫助
|
||||
show_help() {
|
||||
echo ""
|
||||
echo "Meeting Assistant Backend - 一鍵設置與啟動腳本"
|
||||
echo ""
|
||||
echo "用法: $0 [命令] [選項]"
|
||||
echo ""
|
||||
echo "命令:"
|
||||
echo " setup 僅設置環境 (安裝依賴)"
|
||||
echo " start 設置並啟動後端服務 (前景執行)"
|
||||
echo " start-bg 設置並啟動後端服務 (背景執行)"
|
||||
echo " stop 停止背景執行的後端服務"
|
||||
echo " help 顯示此幫助訊息"
|
||||
echo ""
|
||||
echo "選項:"
|
||||
echo " --port PORT 服務端口 (預設: $DEFAULT_PORT)"
|
||||
echo " --host HOST 綁定地址 (預設: $DEFAULT_HOST)"
|
||||
echo " --no-sidecar 不安裝 Sidecar 依賴"
|
||||
echo ""
|
||||
echo "範例:"
|
||||
echo " $0 start # 設置並啟動服務"
|
||||
echo " $0 start --port 8080 # 使用自訂端口"
|
||||
echo " $0 setup # 僅安裝依賴"
|
||||
echo " $0 start-bg # 背景執行"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 函數:停止背景服務
|
||||
stop_backend() {
|
||||
log_step "停止後端服務..."
|
||||
|
||||
if [ -f "$PROJECT_DIR/.backend_pid" ]; then
|
||||
local pid=$(cat "$PROJECT_DIR/.backend_pid")
|
||||
if kill -0 $pid 2>/dev/null; then
|
||||
kill $pid
|
||||
rm -f "$PROJECT_DIR/.backend_pid"
|
||||
log_success "後端服務已停止 (PID: $pid)"
|
||||
else
|
||||
rm -f "$PROJECT_DIR/.backend_pid"
|
||||
log_warn "服務已經停止"
|
||||
fi
|
||||
else
|
||||
# 嘗試通過 port 查找
|
||||
if command -v lsof &> /dev/null; then
|
||||
local port=${BACKEND_PORT:-$DEFAULT_PORT}
|
||||
local pid=$(lsof -t -i :$port 2>/dev/null)
|
||||
if [ -n "$pid" ]; then
|
||||
kill $pid 2>/dev/null || true
|
||||
log_success "已停止 port $port 上的服務"
|
||||
else
|
||||
log_warn "沒有找到運行中的後端服務"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 主程式
|
||||
main() {
|
||||
local command=${1:-"start"}
|
||||
local port=$DEFAULT_PORT
|
||||
local host=$DEFAULT_HOST
|
||||
local setup_sidecar=true
|
||||
|
||||
# 解析參數
|
||||
shift || true
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--port)
|
||||
port="$2"
|
||||
shift 2
|
||||
;;
|
||||
--host)
|
||||
host="$2"
|
||||
shift 2
|
||||
;;
|
||||
--no-sidecar)
|
||||
setup_sidecar=false
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
log_error "未知參數: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case $command in
|
||||
setup)
|
||||
show_banner
|
||||
local python_cmd=$(check_python)
|
||||
setup_backend_venv "$python_cmd"
|
||||
if [ "$setup_sidecar" = true ]; then
|
||||
setup_sidecar_venv "$python_cmd"
|
||||
fi
|
||||
setup_env_file
|
||||
echo ""
|
||||
log_success "環境設置完成!"
|
||||
echo ""
|
||||
log_info "啟動服務: $0 start"
|
||||
;;
|
||||
start)
|
||||
show_banner
|
||||
local python_cmd=$(check_python)
|
||||
setup_backend_venv "$python_cmd"
|
||||
setup_env_file
|
||||
start_backend "$host" "$port" "foreground"
|
||||
;;
|
||||
start-bg)
|
||||
show_banner
|
||||
local python_cmd=$(check_python)
|
||||
setup_backend_venv "$python_cmd"
|
||||
setup_env_file
|
||||
start_backend "$host" "$port" "background"
|
||||
;;
|
||||
stop)
|
||||
stop_backend
|
||||
;;
|
||||
help|--help|-h)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
log_error "未知命令: $command"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 捕捉中斷信號
|
||||
trap 'echo ""; log_info "收到中斷信號"; exit 0' INT TERM
|
||||
|
||||
# 執行主程式
|
||||
main "$@"
|
||||
26
start.sh
26
start.sh
@@ -19,17 +19,6 @@ BACKEND_DIR="$PROJECT_DIR/backend"
|
||||
CLIENT_DIR="$PROJECT_DIR/client"
|
||||
SIDECAR_DIR="$PROJECT_DIR/sidecar"
|
||||
|
||||
# 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
|
||||
|
||||
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}"
|
||||
@@ -59,6 +48,17 @@ log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
# 函數:檢查 port 是否被佔用
|
||||
check_port() {
|
||||
local port=$1
|
||||
@@ -221,6 +221,10 @@ start_frontend() {
|
||||
|
||||
cd "$CLIENT_DIR"
|
||||
|
||||
# WSLg 下 Electron 走 X11 通常比 Wayland 穩
|
||||
export ELECTRON_OZONE_PLATFORM_HINT=x11
|
||||
export OZONE_PLATFORM=x11
|
||||
|
||||
# 背景啟動 Electron(它會自動管理 Sidecar)
|
||||
nohup npm start > "$PROJECT_DIR/frontend.log" 2>&1 &
|
||||
local frontend_pid=$!
|
||||
|
||||
Reference in New Issue
Block a user