mirror of
https://github.com/arjunkomath/openclaw-railway-template.git
synced 2026-08-14 00:48:11 +00:00
Improve setup
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
[tools]
|
||||
node = "24"
|
||||
@@ -25,8 +25,8 @@ npm start
|
||||
# Syntax check
|
||||
npm run lint
|
||||
|
||||
# Local smoke test (requires Docker)
|
||||
npm run smoke
|
||||
# Smoke test in Docker (scripts not included in production image)
|
||||
docker run --rm -v $(pwd)/scripts:/app/scripts openclaw-railway-template npm run smoke
|
||||
```
|
||||
|
||||
## Docker Build & Local Testing
|
||||
|
||||
+11
-2
@@ -1,5 +1,5 @@
|
||||
# Build openclaw from source to avoid npm packaging gaps (some dist files are not shipped).
|
||||
FROM node:22-bookworm AS openclaw-build
|
||||
FROM node:24.1.0-bookworm AS openclaw-build
|
||||
|
||||
# Dependencies needed for openclaw build
|
||||
RUN apt-get update \
|
||||
@@ -39,7 +39,7 @@ RUN pnpm ui:install && pnpm ui:build
|
||||
|
||||
|
||||
# Runtime image
|
||||
FROM node:22-bookworm
|
||||
FROM node:24.1.0-bookworm
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN apt-get update \
|
||||
@@ -87,6 +87,15 @@ RUN printf '%s\n' '#!/usr/bin/env bash' 'exec node /openclaw/dist/entry.js "$@"'
|
||||
|
||||
COPY src ./src
|
||||
|
||||
RUN useradd -m -s /bin/bash openclaw \
|
||||
&& chown -R openclaw:openclaw /app /openclaw \
|
||||
&& mkdir -p /data && chown openclaw:openclaw /data
|
||||
|
||||
ENV PORT=8080
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
|
||||
CMD curl -f http://localhost:8080/setup/healthz || exit 1
|
||||
|
||||
USER openclaw
|
||||
CMD ["node", "src/server.js"]
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
"node": ">=24"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "node src/server.js",
|
||||
|
||||
+65
-6
@@ -1,8 +1,67 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
|
||||
const r = spawnSync("openclaw", ["--version"], { encoding: "utf8" });
|
||||
if (r.status !== 0) {
|
||||
console.error(r.stdout || r.stderr);
|
||||
process.exit(r.status ?? 1);
|
||||
const PORT = process.env.PORT || 8080;
|
||||
|
||||
const versionCheck = spawnSync("openclaw", ["--version"], { encoding: "utf8" });
|
||||
if (versionCheck.status !== 0) {
|
||||
console.error(versionCheck.stdout || versionCheck.stderr);
|
||||
process.exit(versionCheck.status ?? 1);
|
||||
}
|
||||
console.log("openclaw ok:", r.stdout.trim());
|
||||
console.log("✓ openclaw version:", versionCheck.stdout.trim());
|
||||
|
||||
console.log(`Starting server on port ${PORT}...`);
|
||||
const serverProc = spawn("node", ["src/server.js"], {
|
||||
env: { ...process.env, PORT: String(PORT), SETUP_PASSWORD: "smoke-test" },
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let serverOutput = "";
|
||||
serverProc.stdout.on("data", (d) => (serverOutput += d.toString()));
|
||||
serverProc.stderr.on("data", (d) => (serverOutput += d.toString()));
|
||||
|
||||
async function waitForServer(maxWaitMs = 10000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < maxWaitMs) {
|
||||
try {
|
||||
const res = await fetch(`http://localhost:${PORT}/setup/healthz`);
|
||||
if (res.ok) return true;
|
||||
} catch {
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
try {
|
||||
const ready = await waitForServer();
|
||||
if (!ready) {
|
||||
console.error("✗ Server did not become ready");
|
||||
console.error("Server output:", serverOutput);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("✓ Server started on port", PORT);
|
||||
|
||||
const healthRes = await fetch(`http://localhost:${PORT}/setup/healthz`);
|
||||
if (!healthRes.ok) {
|
||||
console.error("✗ /setup/healthz returned", healthRes.status);
|
||||
process.exit(1);
|
||||
}
|
||||
const healthBody = await healthRes.json();
|
||||
if (!healthBody.ok) {
|
||||
console.error("✗ /setup/healthz returned unexpected body:", healthBody);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("✓ /setup/healthz returns 200 with { ok: true }");
|
||||
|
||||
console.log("\n✓ All smoke tests passed");
|
||||
} finally {
|
||||
serverProc.kill("SIGTERM");
|
||||
}
|
||||
}
|
||||
|
||||
runTests().catch((err) => {
|
||||
console.error("Smoke test error:", err);
|
||||
serverProc.kill("SIGTERM");
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
+84
-119
@@ -26,14 +26,16 @@ function resolveGatewayToken() {
|
||||
try {
|
||||
const existing = fs.readFileSync(tokenPath, "utf8").trim();
|
||||
if (existing) return existing;
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.warn(`[gateway-token] could not read existing token: ${err.code || err.message}`);
|
||||
}
|
||||
|
||||
const generated = crypto.randomBytes(32).toString("hex");
|
||||
try {
|
||||
fs.mkdirSync(STATE_DIR, { recursive: true });
|
||||
fs.writeFileSync(tokenPath, generated, { encoding: "utf8", mode: 0o600 });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.warn(`[gateway-token] could not persist token: ${err.code || err.message}`);
|
||||
}
|
||||
return generated;
|
||||
}
|
||||
@@ -92,6 +94,9 @@ async function waitForGatewayReady(opts = {}) {
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code !== "ECONNREFUSED") {
|
||||
console.warn(`[gateway] health check error: ${err.code || err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
await sleep(250);
|
||||
@@ -167,7 +172,8 @@ async function restartGateway() {
|
||||
if (gatewayProc) {
|
||||
try {
|
||||
gatewayProc.kill("SIGTERM");
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.warn(`[gateway] kill error: ${err.message}`);
|
||||
}
|
||||
await sleep(750);
|
||||
gatewayProc = null;
|
||||
@@ -194,7 +200,11 @@ function requireSetupAuth(req, res, next) {
|
||||
const decoded = Buffer.from(encoded, "base64").toString("utf8");
|
||||
const idx = decoded.indexOf(":");
|
||||
const password = idx >= 0 ? decoded.slice(idx + 1) : "";
|
||||
if (password !== SETUP_PASSWORD) {
|
||||
const passwordBuf = Buffer.from(password);
|
||||
const expectedBuf = Buffer.from(SETUP_PASSWORD);
|
||||
const isValid = passwordBuf.length === expectedBuf.length &&
|
||||
crypto.timingSafeEqual(passwordBuf, expectedBuf);
|
||||
if (!isValid) {
|
||||
res.set("WWW-Authenticate", 'Basic realm="OpenClaw Setup"');
|
||||
return res.status(401).send("Invalid password");
|
||||
}
|
||||
@@ -417,6 +427,34 @@ function runCmd(cmd, args, opts = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
const VALID_FLOWS = ["quickstart", "advanced", "manual"];
|
||||
const VALID_AUTH_CHOICES = [
|
||||
"codex-cli", "openai-codex", "openai-api-key",
|
||||
"claude-cli", "token", "apiKey",
|
||||
"gemini-api-key", "google-antigravity", "google-gemini-cli",
|
||||
"openrouter-api-key", "ai-gateway-api-key",
|
||||
"moonshot-api-key", "kimi-code-api-key",
|
||||
"zai-api-key", "minimax-api", "minimax-api-lightning",
|
||||
"qwen-portal", "github-copilot", "copilot-proxy",
|
||||
"synthetic-api-key", "opencode-zen",
|
||||
];
|
||||
|
||||
function validatePayload(payload) {
|
||||
if (payload.flow && !VALID_FLOWS.includes(payload.flow)) {
|
||||
return `Invalid flow: ${payload.flow}. Must be one of: ${VALID_FLOWS.join(", ")}`;
|
||||
}
|
||||
if (payload.authChoice && !VALID_AUTH_CHOICES.includes(payload.authChoice)) {
|
||||
return `Invalid authChoice: ${payload.authChoice}`;
|
||||
}
|
||||
const stringFields = ["telegramToken", "discordToken", "slackBotToken", "slackAppToken", "authSecret"];
|
||||
for (const field of stringFields) {
|
||||
if (payload[field] !== undefined && typeof payload[field] !== "string") {
|
||||
return `Invalid ${field}: must be a string`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
app.post("/setup/api/run", requireSetupAuth, async (req, res) => {
|
||||
try {
|
||||
if (isConfigured()) {
|
||||
@@ -432,6 +470,10 @@ app.post("/setup/api/run", requireSetupAuth, async (req, res) => {
|
||||
fs.mkdirSync(WORKSPACE_DIR, { recursive: true });
|
||||
|
||||
const payload = req.body || {};
|
||||
const validationError = validatePayload(payload);
|
||||
if (validationError) {
|
||||
return res.status(400).json({ ok: false, output: validationError });
|
||||
}
|
||||
const onboardArgs = buildOnboardArgs(payload);
|
||||
const onboard = await runCmd(OPENCLAW_NODE, clawArgs(onboardArgs));
|
||||
|
||||
@@ -440,33 +482,6 @@ app.post("/setup/api/run", requireSetupAuth, async (req, res) => {
|
||||
const ok = onboard.code === 0 && isConfigured();
|
||||
|
||||
if (ok) {
|
||||
await runCmd(OPENCLAW_NODE, clawArgs(["config", "set", "gateway.mode", "local"]));
|
||||
await runCmd(
|
||||
OPENCLAW_NODE,
|
||||
clawArgs(["config", "set", "gateway.auth.mode", "token"]),
|
||||
);
|
||||
await runCmd(
|
||||
OPENCLAW_NODE,
|
||||
clawArgs([
|
||||
"config",
|
||||
"set",
|
||||
"gateway.auth.token",
|
||||
OPENCLAW_GATEWAY_TOKEN,
|
||||
]),
|
||||
);
|
||||
await runCmd(
|
||||
OPENCLAW_NODE,
|
||||
clawArgs(["config", "set", "gateway.bind", "loopback"]),
|
||||
);
|
||||
await runCmd(
|
||||
OPENCLAW_NODE,
|
||||
clawArgs([
|
||||
"config",
|
||||
"set",
|
||||
"gateway.port",
|
||||
String(INTERNAL_GATEWAY_PORT),
|
||||
]),
|
||||
);
|
||||
await runCmd(
|
||||
OPENCLAW_NODE,
|
||||
clawArgs(["config", "set", "gateway.controlUi.allowInsecureAuth", "true"]),
|
||||
@@ -478,100 +493,47 @@ app.post("/setup/api/run", requireSetupAuth, async (req, res) => {
|
||||
);
|
||||
const helpText = channelsHelp.output || "";
|
||||
|
||||
const supports = (name) => helpText.includes(name);
|
||||
async function configureChannel(name, cfgObj) {
|
||||
if (!helpText.includes(name)) {
|
||||
return `\n[${name}] skipped (this openclaw build does not list ${name} in \`channels add --help\`)\n`;
|
||||
}
|
||||
const set = await runCmd(
|
||||
OPENCLAW_NODE,
|
||||
clawArgs(["config", "set", "--json", `channels.${name}`, JSON.stringify(cfgObj)]),
|
||||
);
|
||||
const get = await runCmd(
|
||||
OPENCLAW_NODE,
|
||||
clawArgs(["config", "get", `channels.${name}`]),
|
||||
);
|
||||
return `\n[${name} config] exit=${set.code} (output ${set.output.length} chars)\n${set.output || "(no output)"}` +
|
||||
`\n[${name} verify] exit=${get.code} (output ${get.output.length} chars)\n${get.output || "(no output)"}`;
|
||||
}
|
||||
|
||||
if (payload.telegramToken?.trim()) {
|
||||
if (!supports("telegram")) {
|
||||
extra +=
|
||||
"\n[telegram] skipped (this openclaw build does not list telegram in `channels add --help`)\n";
|
||||
} else {
|
||||
const token = payload.telegramToken.trim();
|
||||
const cfgObj = {
|
||||
enabled: true,
|
||||
dmPolicy: "pairing",
|
||||
botToken: token,
|
||||
groupPolicy: "allowlist",
|
||||
streamMode: "partial",
|
||||
};
|
||||
const set = await runCmd(
|
||||
OPENCLAW_NODE,
|
||||
clawArgs([
|
||||
"config",
|
||||
"set",
|
||||
"--json",
|
||||
"channels.telegram",
|
||||
JSON.stringify(cfgObj),
|
||||
]),
|
||||
);
|
||||
const get = await runCmd(
|
||||
OPENCLAW_NODE,
|
||||
clawArgs(["config", "get", "channels.telegram"]),
|
||||
);
|
||||
extra += `\n[telegram config] exit=${set.code} (output ${set.output.length} chars)\n${set.output || "(no output)"}`;
|
||||
extra += `\n[telegram verify] exit=${get.code} (output ${get.output.length} chars)\n${get.output || "(no output)"}`;
|
||||
}
|
||||
extra += await configureChannel("telegram", {
|
||||
enabled: true,
|
||||
dmPolicy: "pairing",
|
||||
botToken: payload.telegramToken.trim(),
|
||||
groupPolicy: "allowlist",
|
||||
streamMode: "partial",
|
||||
});
|
||||
}
|
||||
|
||||
if (payload.discordToken?.trim()) {
|
||||
if (!supports("discord")) {
|
||||
extra +=
|
||||
"\n[discord] skipped (this openclaw build does not list discord in `channels add --help`)\n";
|
||||
} else {
|
||||
const token = payload.discordToken.trim();
|
||||
const cfgObj = {
|
||||
enabled: true,
|
||||
token,
|
||||
groupPolicy: "allowlist",
|
||||
dm: {
|
||||
policy: "pairing",
|
||||
},
|
||||
};
|
||||
const set = await runCmd(
|
||||
OPENCLAW_NODE,
|
||||
clawArgs([
|
||||
"config",
|
||||
"set",
|
||||
"--json",
|
||||
"channels.discord",
|
||||
JSON.stringify(cfgObj),
|
||||
]),
|
||||
);
|
||||
const get = await runCmd(
|
||||
OPENCLAW_NODE,
|
||||
clawArgs(["config", "get", "channels.discord"]),
|
||||
);
|
||||
extra += `\n[discord config] exit=${set.code} (output ${set.output.length} chars)\n${set.output || "(no output)"}`;
|
||||
extra += `\n[discord verify] exit=${get.code} (output ${get.output.length} chars)\n${get.output || "(no output)"}`;
|
||||
}
|
||||
extra += await configureChannel("discord", {
|
||||
enabled: true,
|
||||
token: payload.discordToken.trim(),
|
||||
groupPolicy: "allowlist",
|
||||
dm: { policy: "pairing" },
|
||||
});
|
||||
}
|
||||
|
||||
if (payload.slackBotToken?.trim() || payload.slackAppToken?.trim()) {
|
||||
if (!supports("slack")) {
|
||||
extra +=
|
||||
"\n[slack] skipped (this openclaw build does not list slack in `channels add --help`)\n";
|
||||
} else {
|
||||
const cfgObj = {
|
||||
enabled: true,
|
||||
botToken: payload.slackBotToken?.trim() || undefined,
|
||||
appToken: payload.slackAppToken?.trim() || undefined,
|
||||
};
|
||||
const set = await runCmd(
|
||||
OPENCLAW_NODE,
|
||||
clawArgs([
|
||||
"config",
|
||||
"set",
|
||||
"--json",
|
||||
"channels.slack",
|
||||
JSON.stringify(cfgObj),
|
||||
]),
|
||||
);
|
||||
const get = await runCmd(
|
||||
OPENCLAW_NODE,
|
||||
clawArgs(["config", "get", "channels.slack"]),
|
||||
);
|
||||
extra += `\n[slack config] exit=${set.code} (output ${set.output.length} chars)\n${set.output || "(no output)"}`;
|
||||
extra += `\n[slack verify] exit=${get.code} (output ${get.output.length} chars)\n${get.output || "(no output)"}`;
|
||||
}
|
||||
extra += await configureChannel("slack", {
|
||||
enabled: true,
|
||||
botToken: payload.slackBotToken?.trim() || undefined,
|
||||
appToken: payload.slackAppToken?.trim() || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
await restartGateway();
|
||||
@@ -741,7 +703,8 @@ server.on("upgrade", async (req, socket, head) => {
|
||||
}
|
||||
try {
|
||||
await ensureGatewayRunning();
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.warn(`[websocket] gateway not ready: ${err.message}`);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
@@ -749,9 +712,11 @@ server.on("upgrade", async (req, socket, head) => {
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
console.log("[wrapper] received SIGTERM, shutting down");
|
||||
try {
|
||||
if (gatewayProc) gatewayProc.kill("SIGTERM");
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.warn(`[wrapper] error killing gateway: ${err.message}`);
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user