源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
import { spawn } from "node:child_process"
|
||||
import net from "node:net"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import process from "node:process"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const rootDir = path.resolve(__dirname, "..")
|
||||
|
||||
// Load committed local-dev defaults. Real env vars take precedence (loadEnvFile
|
||||
// never overwrites variables that are already set).
|
||||
try {
|
||||
process.loadEnvFile(path.join(rootDir, ".env.dev"))
|
||||
} catch {
|
||||
// .env.dev is optional; the inline fallbacks below still apply.
|
||||
}
|
||||
const composeFile = path.join(rootDir, "packaging", "docker", "docker-compose.web-local.yml")
|
||||
const composeProject = "openwork-den-local"
|
||||
|
||||
const apiPort = process.env.DEN_API_PORT?.trim() || process.env.DEN_CONTROLLER_PORT?.trim() || "8788"
|
||||
const inferencePort = process.env.INFERENCE_PORT?.trim() || "8791"
|
||||
const webPort = process.env.DEN_WEB_PORT?.trim() || "3005"
|
||||
const appPort = process.env.OPENWORK_APP_PORT?.trim() || process.env.PORT?.trim() || "5173"
|
||||
const extraAppPorts = (process.env.OPENWORK_EXTRA_APP_PORTS?.trim() || "5174")
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
const databaseUrl = process.env.DATABASE_URL?.trim() || "mysql://root:password@127.0.0.1:3306/openwork_den"
|
||||
const databaseRedisUrl = process.env.DATABASE_REDIS_URL?.trim() || "redis://127.0.0.1:6379"
|
||||
const dbEncryptionKey =
|
||||
process.env.DEN_DB_ENCRYPTION_KEY?.trim() ||
|
||||
"local-dev-db-encryption-key-please-change-1234567890"
|
||||
|
||||
function detectWebOrigins() {
|
||||
const origins = new Set([
|
||||
`http://localhost:${webPort}`,
|
||||
`http://127.0.0.1:${webPort}`,
|
||||
`http://0.0.0.0:${webPort}`,
|
||||
`http://localhost:${appPort}`,
|
||||
`http://127.0.0.1:${appPort}`,
|
||||
])
|
||||
|
||||
for (const port of extraAppPorts) {
|
||||
origins.add(`http://localhost:${port}`)
|
||||
origins.add(`http://127.0.0.1:${port}`)
|
||||
}
|
||||
|
||||
for (const entries of Object.values(os.networkInterfaces())) {
|
||||
for (const entry of entries || []) {
|
||||
if (!entry || entry.internal || entry.family !== "IPv4") {
|
||||
continue
|
||||
}
|
||||
|
||||
origins.add(`http://${entry.address}:${webPort}`)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(origins).join(",")
|
||||
}
|
||||
|
||||
function parseUrlEndpoint(value, defaultPort) {
|
||||
const parsed = new URL(value)
|
||||
return {
|
||||
host: parsed.hostname,
|
||||
port: Number(parsed.port || defaultPort),
|
||||
}
|
||||
}
|
||||
|
||||
function canReachTcp(host, port) {
|
||||
return new Promise((resolve) => {
|
||||
const socket = net.createConnection({ host, port })
|
||||
|
||||
const finalize = (result) => {
|
||||
socket.destroy()
|
||||
resolve(result)
|
||||
}
|
||||
|
||||
socket.setTimeout(1500)
|
||||
socket.once("connect", () => finalize(true))
|
||||
socket.once("error", () => finalize(false))
|
||||
socket.once("timeout", () => finalize(false))
|
||||
})
|
||||
}
|
||||
|
||||
function canListenOnPort(port) {
|
||||
return new Promise((resolve) => {
|
||||
const server = net.createServer()
|
||||
|
||||
const finalize = (result) => {
|
||||
server.close(() => resolve(result))
|
||||
}
|
||||
|
||||
server.once("error", () => resolve(false))
|
||||
server.once("listening", () => finalize(true))
|
||||
server.listen(port, "0.0.0.0")
|
||||
})
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd: rootDir,
|
||||
stdio: "inherit",
|
||||
...options,
|
||||
shell: false,
|
||||
})
|
||||
|
||||
child.once("error", reject)
|
||||
child.once("exit", (code, signal) => {
|
||||
if (code === 0) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
const detail = signal ? `signal ${signal}` : `exit code ${code ?? 1}`
|
||||
reject(new Error(`${command} ${args.join(" ")} failed with ${detail}`))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const startedDockerServices = new Set()
|
||||
let turboChild = null
|
||||
let cleaningUp = false
|
||||
|
||||
function stopTurboChild() {
|
||||
if (!turboChild || turboChild.exitCode !== null) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
turboChild.once("exit", resolve)
|
||||
|
||||
try {
|
||||
if (process.platform !== "win32") {
|
||||
process.kill(-turboChild.pid, "SIGINT")
|
||||
} else {
|
||||
turboChild.kill("SIGINT")
|
||||
}
|
||||
} catch {
|
||||
turboChild.kill("SIGINT")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function cleanup(exitCode = 0) {
|
||||
if (cleaningUp) {
|
||||
return
|
||||
}
|
||||
|
||||
cleaningUp = true
|
||||
|
||||
await stopTurboChild()
|
||||
|
||||
if (startedDockerServices.size > 0) {
|
||||
const services = Array.from(startedDockerServices)
|
||||
await run("docker", ["compose", "-p", composeProject, "-f", composeFile, "stop", ...services], {
|
||||
stdio: "inherit",
|
||||
}).catch(() => {})
|
||||
await run("docker", ["compose", "-p", composeProject, "-f", composeFile, "rm", "-f", ...services], {
|
||||
stdio: "inherit",
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
process.exit(exitCode)
|
||||
}
|
||||
|
||||
for (const signal of ["SIGINT", "SIGTERM"]) {
|
||||
process.on(signal, () => {
|
||||
void cleanup(0)
|
||||
})
|
||||
}
|
||||
|
||||
async function main() {
|
||||
for (const [name, port] of [["den-web", webPort], ["den-api", apiPort], ["inference", inferencePort]]) {
|
||||
const available = await canListenOnPort(Number(port))
|
||||
if (!available) {
|
||||
throw new Error(`${name} local port ${port} is already in use. Stop the existing process or rerun with a different port env override.`)
|
||||
}
|
||||
}
|
||||
|
||||
const { host, port } = parseUrlEndpoint(databaseUrl, "3306")
|
||||
const mysqlAvailable = await canReachTcp(host, port)
|
||||
|
||||
if (!mysqlAvailable) {
|
||||
if (!(host === "127.0.0.1" || host === "localhost")) {
|
||||
throw new Error(`MySQL at ${host}:${port} is not reachable, and auto-start only supports localhost`)
|
||||
}
|
||||
|
||||
console.log(`[den] MySQL not reachable at ${host}:${port}; starting Docker MySQL...`)
|
||||
await run("docker", ["compose", "-p", composeProject, "-f", composeFile, "up", "-d", "--wait", "mysql"])
|
||||
startedDockerServices.add("mysql")
|
||||
} else {
|
||||
console.log(`[den] Using existing MySQL at ${host}:${port}`)
|
||||
}
|
||||
|
||||
const redis = parseUrlEndpoint(databaseRedisUrl, "6379")
|
||||
const redisAvailable = await canReachTcp(redis.host, redis.port)
|
||||
if (!redisAvailable) {
|
||||
if (!(redis.host === "127.0.0.1" || redis.host === "localhost")) {
|
||||
throw new Error(`Redis at ${redis.host}:${redis.port} is not reachable, and auto-start only supports localhost`)
|
||||
}
|
||||
|
||||
console.log(`[den] Redis not reachable at ${redis.host}:${redis.port}; starting Docker Redis...`)
|
||||
await run("docker", ["compose", "-p", composeProject, "-f", composeFile, "up", "-d", "--wait", "redis"])
|
||||
startedDockerServices.add("redis")
|
||||
} else {
|
||||
console.log(`[den] Using existing Redis at ${redis.host}:${redis.port}`)
|
||||
}
|
||||
|
||||
console.log("[den] Syncing Den schema...")
|
||||
await run("bash", ["-c", "pnpm --filter @openwork-ee/den-db build && pnpm --filter @openwork-ee/den-db exec node --import tsx ./node_modules/drizzle-kit/bin.cjs push --config drizzle.config.ts --force"], {
|
||||
env: {
|
||||
...process.env,
|
||||
DATABASE_URL: databaseUrl,
|
||||
},
|
||||
})
|
||||
|
||||
const webOrigins = detectWebOrigins()
|
||||
console.log(`[den] Allowed local web origins: ${webOrigins}`)
|
||||
|
||||
turboChild = spawn(
|
||||
"pnpm",
|
||||
[
|
||||
"exec",
|
||||
"turbo",
|
||||
"run",
|
||||
"dev:local",
|
||||
"--output-logs=full",
|
||||
"--filter=@openwork-ee/den-api",
|
||||
"--filter=@openwork-ee/inference",
|
||||
"--filter=@openwork-ee/den-web",
|
||||
],
|
||||
{
|
||||
cwd: rootDir,
|
||||
stdio: "inherit",
|
||||
detached: process.platform !== "win32",
|
||||
env: {
|
||||
...process.env,
|
||||
DATABASE_URL: databaseUrl,
|
||||
DATABASE_REDIS_URL: databaseRedisUrl,
|
||||
DEN_DB_ENCRYPTION_KEY: dbEncryptionKey,
|
||||
BETTER_AUTH_URL: process.env.BETTER_AUTH_URL?.trim() || `http://localhost:${webPort}`,
|
||||
DEN_BASE_URL: process.env.DEN_BASE_URL?.trim() || `http://localhost:${webPort}`,
|
||||
DEN_MCP_RESOURCE_URL: process.env.DEN_MCP_RESOURCE_URL?.trim() || `http://127.0.0.1:${apiPort}/mcp`,
|
||||
DEN_BETTER_AUTH_TRUSTED_ORIGINS: process.env.DEN_BETTER_AUTH_TRUSTED_ORIGINS?.trim() || webOrigins,
|
||||
CORS_ORIGINS: process.env.CORS_ORIGINS?.trim() || webOrigins,
|
||||
DEN_API_PORT: apiPort,
|
||||
DEN_CONTROLLER_PORT: apiPort,
|
||||
INFERENCE_PORT: inferencePort,
|
||||
INFERENCE_PROXY_BASE_URL: process.env.INFERENCE_PROXY_BASE_URL?.trim() || `http://127.0.0.1:${inferencePort}`,
|
||||
DEN_WEB_PORT: webPort,
|
||||
DEN_API_BASE: process.env.DEN_API_BASE?.trim() || `http://127.0.0.1:${apiPort}`,
|
||||
DEN_API_PUBLIC_URL: process.env.DEN_API_PUBLIC_URL?.trim() || `http://localhost:${apiPort}`,
|
||||
DEN_AUTH_ORIGIN: process.env.DEN_AUTH_ORIGIN?.trim() || `http://localhost:${webPort}`,
|
||||
DEN_AUTH_FALLBACK_BASE: process.env.DEN_AUTH_FALLBACK_BASE?.trim() || `http://127.0.0.1:${apiPort}`,
|
||||
},
|
||||
shell: false,
|
||||
},
|
||||
)
|
||||
|
||||
turboChild.once("exit", (code, signal) => {
|
||||
const exitCode = code ?? (signal ? 1 : 0)
|
||||
void cleanup(exitCode)
|
||||
})
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error))
|
||||
void cleanup(1)
|
||||
})
|
||||
Reference in New Issue
Block a user