Self-host compose stack and production stability fixes
这个提交包含在:
@@ -18,6 +18,7 @@ export const ENV = {
|
||||
isProduction: process.env.NODE_ENV === "production",
|
||||
forgeApiUrl: process.env.BUILT_IN_FORGE_API_URL ?? "",
|
||||
forgeApiKey: process.env.BUILT_IN_FORGE_API_KEY ?? "",
|
||||
localStorageDir: process.env.LOCAL_STORAGE_DIR ?? "./data/storage",
|
||||
llmApiUrl:
|
||||
process.env.LLM_API_URL ??
|
||||
(process.env.BUILT_IN_FORGE_API_URL
|
||||
|
||||
@@ -2,12 +2,13 @@ import "dotenv/config";
|
||||
import express from "express";
|
||||
import { createServer } from "http";
|
||||
import net from "net";
|
||||
import path from "node:path";
|
||||
import { createExpressMiddleware } from "@trpc/server/adapters/express";
|
||||
import { registerOAuthRoutes } from "./oauth";
|
||||
import { appRouter } from "../routers";
|
||||
import { createContext } from "./context";
|
||||
import { registerMediaProxy } from "./mediaProxy";
|
||||
import { serveStatic, setupVite } from "./vite";
|
||||
import { serveStatic } from "./static";
|
||||
|
||||
function isPortAvailable(port: number): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
@@ -35,6 +36,10 @@ async function startServer() {
|
||||
// Configure body parser with larger size limit for file uploads
|
||||
app.use(express.json({ limit: "50mb" }));
|
||||
app.use(express.urlencoded({ limit: "50mb", extended: true }));
|
||||
app.use(
|
||||
"/uploads",
|
||||
express.static(path.resolve(process.env.LOCAL_STORAGE_DIR || "data/storage"))
|
||||
);
|
||||
// OAuth callback under /api/oauth/callback
|
||||
registerOAuthRoutes(app);
|
||||
// tRPC API
|
||||
@@ -47,6 +52,7 @@ async function startServer() {
|
||||
);
|
||||
// development mode uses Vite, production mode uses static files
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
const { setupVite } = await import("./vite");
|
||||
await setupVite(app, server);
|
||||
} else {
|
||||
serveStatic(app);
|
||||
|
||||
21
server/_core/static.ts
普通文件
21
server/_core/static.ts
普通文件
@@ -0,0 +1,21 @@
|
||||
import express, { type Express } from "express";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export function serveStatic(app: Express) {
|
||||
const distPath =
|
||||
process.env.NODE_ENV === "development"
|
||||
? path.resolve(import.meta.dirname, "../..", "dist", "public")
|
||||
: path.resolve(import.meta.dirname, "public");
|
||||
if (!fs.existsSync(distPath)) {
|
||||
console.error(
|
||||
`Could not find the build directory: ${distPath}, make sure to build the client first`
|
||||
);
|
||||
}
|
||||
|
||||
app.use(express.static(distPath));
|
||||
|
||||
app.use("*", (_req, res) => {
|
||||
res.sendFile(path.resolve(distPath, "index.html"));
|
||||
});
|
||||
}
|
||||
@@ -46,22 +46,3 @@ export async function setupVite(app: Express, server: Server) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function serveStatic(app: Express) {
|
||||
const distPath =
|
||||
process.env.NODE_ENV === "development"
|
||||
? path.resolve(import.meta.dirname, "../..", "dist", "public")
|
||||
: path.resolve(import.meta.dirname, "public");
|
||||
if (!fs.existsSync(distPath)) {
|
||||
console.error(
|
||||
`Could not find the build directory: ${distPath}, make sure to build the client first`
|
||||
);
|
||||
}
|
||||
|
||||
app.use(express.static(distPath));
|
||||
|
||||
// fall through to index.html if the file doesn't exist
|
||||
app.use("*", (_req, res) => {
|
||||
res.sendFile(path.resolve(distPath, "index.html"));
|
||||
});
|
||||
}
|
||||
|
||||
40
server/storage.test.ts
普通文件
40
server/storage.test.ts
普通文件
@@ -0,0 +1,40 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const ORIGINAL_ENV = { ...process.env };
|
||||
|
||||
describe("storage fallback", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
process.env = { ...ORIGINAL_ENV };
|
||||
delete process.env.BUILT_IN_FORGE_API_URL;
|
||||
delete process.env.BUILT_IN_FORGE_API_KEY;
|
||||
tempDir = await mkdtemp(path.join(os.tmpdir(), "tennis-storage-"));
|
||||
process.env.LOCAL_STORAGE_DIR = tempDir;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
process.env = { ...ORIGINAL_ENV };
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("stores files locally when remote storage is not configured", async () => {
|
||||
const { storagePut, storageGet } = await import("./storage");
|
||||
|
||||
const stored = await storagePut("videos/test/sample.webm", Buffer.from("demo"));
|
||||
const loaded = await storageGet("videos/test/sample.webm");
|
||||
|
||||
expect(stored).toEqual({
|
||||
key: "videos/test/sample.webm",
|
||||
url: "/uploads/videos/test/sample.webm",
|
||||
});
|
||||
expect(loaded).toEqual({
|
||||
key: "videos/test/sample.webm",
|
||||
url: "/uploads/videos/test/sample.webm",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
// Preconfigured storage helpers for Manus WebDev templates
|
||||
// Uses the Biz-provided storage proxy (Authorization: Bearer <token>)
|
||||
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { ENV } from './_core/env';
|
||||
|
||||
type StorageConfig = { baseUrl: string; apiKey: string };
|
||||
@@ -18,6 +20,31 @@ function getStorageConfig(): StorageConfig {
|
||||
return { baseUrl: baseUrl.replace(/\/+$/, ""), apiKey };
|
||||
}
|
||||
|
||||
function canUseRemoteStorage(): boolean {
|
||||
return Boolean(ENV.forgeApiUrl && ENV.forgeApiKey);
|
||||
}
|
||||
|
||||
function getLocalStoragePath(relKey: string): string {
|
||||
return path.join(ENV.localStorageDir, normalizeKey(relKey));
|
||||
}
|
||||
|
||||
async function writeLocalFile(
|
||||
relKey: string,
|
||||
data: Buffer | Uint8Array | string
|
||||
): Promise<void> {
|
||||
const filePath = getLocalStoragePath(relKey);
|
||||
await mkdir(path.dirname(filePath), { recursive: true });
|
||||
const content =
|
||||
typeof data === "string" ? Buffer.from(data) : Buffer.from(data);
|
||||
await writeFile(filePath, content);
|
||||
}
|
||||
|
||||
async function readLocalFile(relKey: string): Promise<string> {
|
||||
const filePath = getLocalStoragePath(relKey);
|
||||
await readFile(filePath);
|
||||
return `/uploads/${normalizeKey(relKey)}`;
|
||||
}
|
||||
|
||||
function buildUploadUrl(baseUrl: string, relKey: string): URL {
|
||||
const url = new URL("v1/storage/upload", ensureTrailingSlash(baseUrl));
|
||||
url.searchParams.set("path", normalizeKey(relKey));
|
||||
@@ -72,6 +99,12 @@ export async function storagePut(
|
||||
data: Buffer | Uint8Array | string,
|
||||
contentType = "application/octet-stream"
|
||||
): Promise<{ key: string; url: string }> {
|
||||
if (!canUseRemoteStorage()) {
|
||||
const key = normalizeKey(relKey);
|
||||
await writeLocalFile(key, data);
|
||||
return { key, url: `/uploads/${key}` };
|
||||
}
|
||||
|
||||
const { baseUrl, apiKey } = getStorageConfig();
|
||||
const key = normalizeKey(relKey);
|
||||
const uploadUrl = buildUploadUrl(baseUrl, key);
|
||||
@@ -93,6 +126,14 @@ export async function storagePut(
|
||||
}
|
||||
|
||||
export async function storageGet(relKey: string): Promise<{ key: string; url: string; }> {
|
||||
if (!canUseRemoteStorage()) {
|
||||
const key = normalizeKey(relKey);
|
||||
return {
|
||||
key,
|
||||
url: await readLocalFile(key),
|
||||
};
|
||||
}
|
||||
|
||||
const { baseUrl, apiKey } = getStorageConfig();
|
||||
const key = normalizeKey(relKey);
|
||||
return {
|
||||
|
||||
在新工单中引用
屏蔽一个用户