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:
egg
2025-12-16 20:03:16 +08:00
parent 01aee1fd0d
commit 7d4fc69071
20 changed files with 2454 additions and 32 deletions

View File

@@ -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,34 +82,65 @@ function startSidecar() {
? path.join(process.resourcesPath, "sidecar")
: path.join(__dirname, "..", "..", "sidecar");
const sidecarScript = path.join(sidecarDir, "transcriber.py");
const venvPython = path.join(sidecarDir, "venv", "bin", "python");
// Determine the sidecar executable path based on packaging and platform
let sidecarExecutable;
let sidecarArgs = [];
if (!fs.existsSync(sidecarScript)) {
console.log("Sidecar script not found at:", sidecarScript);
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");
if (!fs.existsSync(sidecarScript)) {
console.log("Sidecar script not found at:", sidecarScript);
console.log("Transcription will not be available.");
return;
}
// 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;
}
const pythonPath = fs.existsSync(venvPython) ? venvPython : "python3";
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`));
});