mirror of
https://github.com/arjunkomath/openclaw-railway-template.git
synced 2026-08-14 00:48:11 +00:00
Improve ChatGPT login
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@ RUN apt-get update \
|
||||
zip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN npm install -g openclaw@2026.4.23 clawhub@latest
|
||||
RUN npm install -g openclaw@2026.4.29 clawhub@latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -17,11 +17,17 @@
|
||||
|
||||
- The container runs a wrapper web server.
|
||||
- The wrapper protects `/setup` with `SETUP_PASSWORD`.
|
||||
- During setup, the wrapper runs `openclaw onboard --non-interactive ...` inside the container, writes state to the volume, and then starts the gateway.
|
||||
- During setup, the wrapper runs `openclaw onboard ...` inside the container, writes state to the volume, and then starts the gateway. API-key providers use non-interactive setup; ChatGPT/Codex device pairing uses OpenClaw's interactive device-code flow so the login URL and code can be streamed to `/setup`.
|
||||
- After setup, **`/` is OpenClaw**. The wrapper reverse-proxies all traffic (including WebSockets) to the local gateway process.
|
||||
|
||||
## Getting chat tokens (so you don't have to scramble)
|
||||
|
||||
### ChatGPT / OpenAI Codex login
|
||||
|
||||
In `/setup`, choose **OpenAI → OpenAI Codex device pairing**. Setup will stream a ChatGPT/Codex URL and a short device code; open the URL in your browser, enter the code, and keep the setup page open until OpenClaw finishes saving the OAuth profile.
|
||||
|
||||
This uses OpenClaw's `openai-codex-device-code` onboarding flow, so you do not need to paste an OpenAI API key.
|
||||
|
||||
### Telegram bot token
|
||||
|
||||
1. Open Telegram and message **@BotFather**
|
||||
|
||||
+94
-8
@@ -32,6 +32,10 @@
|
||||
slackAppToken: '',
|
||||
model: '',
|
||||
log: '',
|
||||
loginUrl: '',
|
||||
loginCode: '',
|
||||
setupError: '',
|
||||
setupStreamTail: '',
|
||||
loading: false,
|
||||
pairingChannel: '',
|
||||
pairingCode: '',
|
||||
@@ -50,6 +54,31 @@
|
||||
return this.authGroups.find(g => g.value === this.selectedGroup);
|
||||
},
|
||||
|
||||
get authRequiresSecret() {
|
||||
return ![
|
||||
'openai-codex',
|
||||
'openai-codex-device-code',
|
||||
'google-gemini-cli',
|
||||
'github-copilot',
|
||||
'ollama',
|
||||
'vllm',
|
||||
'sglang'
|
||||
].includes(this.selectedAuth);
|
||||
},
|
||||
|
||||
get authGuidance() {
|
||||
if (this.selectedAuth === 'openai-codex-device-code') {
|
||||
return 'Run setup, then open the ChatGPT/Codex URL and enter the device code shown in the log.';
|
||||
}
|
||||
if (this.selectedAuth === 'openai-codex') {
|
||||
return 'Browser login needs an interactive terminal to paste the redirect URL. Device pairing is recommended here.';
|
||||
}
|
||||
if (!this.authRequiresSecret) {
|
||||
return 'No API key is needed for this auth method.';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
async init() {
|
||||
await this.refreshStatus();
|
||||
},
|
||||
@@ -101,6 +130,10 @@
|
||||
},
|
||||
|
||||
async runSetup() {
|
||||
this.loginUrl = '';
|
||||
this.loginCode = '';
|
||||
this.setupError = '';
|
||||
this.setupStreamTail = '';
|
||||
const payload = {
|
||||
authChoice: this.selectedAuth,
|
||||
authSecret: this.authSecret,
|
||||
@@ -126,18 +159,60 @@
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const text = await res.text();
|
||||
let j;
|
||||
try { j = JSON.parse(text); } catch (_e) { j = { ok: false, output: text }; }
|
||||
this.log += (j.output || JSON.stringify(j, null, 2));
|
||||
if (res.body) {
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
this.processSetupChunk(decoder.decode(value, { stream: true }));
|
||||
this.$nextTick(() => {
|
||||
const el = this.$refs.setupLog;
|
||||
if (el) el.scrollTop = el.scrollHeight;
|
||||
});
|
||||
}
|
||||
this.processSetupChunk(decoder.decode());
|
||||
} else {
|
||||
this.processSetupChunk(await res.text());
|
||||
}
|
||||
if (!res.ok && !this.setupError) {
|
||||
this.setupError = 'Setup request failed with HTTP ' + res.status + '.';
|
||||
}
|
||||
await this.refreshStatus();
|
||||
} catch (e) {
|
||||
this.log += '\nError: ' + String(e) + '\n';
|
||||
this.setupError = 'Setup request failed.';
|
||||
this.processSetupChunk('\nError: ' + String(e) + '\n');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
processSetupChunk(chunk) {
|
||||
if (!chunk) return;
|
||||
this.log += chunk;
|
||||
const scanText = this.setupStreamTail + chunk;
|
||||
this.setupStreamTail = scanText.slice(-1000);
|
||||
this.extractLoginDetails(scanText);
|
||||
this.extractSetupFailure(scanText);
|
||||
},
|
||||
|
||||
extractLoginDetails(text) {
|
||||
const urlMatch = text.match(/https:\/\/auth\.openai\.com\/codex\/device[^\s)]*/);
|
||||
if (urlMatch) this.loginUrl = urlMatch[0];
|
||||
|
||||
const codeMatches = [...text.matchAll(/\bCode:\s*([A-Z0-9][A-Z0-9-]{3,})\b/g)];
|
||||
const visibleCode = codeMatches.map(m => m[1]).find(code => code !== 'shown');
|
||||
if (visibleCode) this.loginCode = visibleCode;
|
||||
},
|
||||
|
||||
extractSetupFailure(text) {
|
||||
if (text.includes('[setup] Internal error')) {
|
||||
this.setupError = 'Setup hit an internal error. Review the log below.';
|
||||
} else if (text.includes('[setup] Failed')) {
|
||||
this.setupError = 'Setup failed. Review the log below.';
|
||||
}
|
||||
},
|
||||
|
||||
openPairingModal() {
|
||||
this.pairingChannel = '';
|
||||
this.pairingCode = '';
|
||||
@@ -521,9 +596,10 @@
|
||||
<option :value="o.value" x-text="o.label + (o.hint ? ' — ' + o.hint : '')"></option>
|
||||
</template>
|
||||
</select>
|
||||
<p x-show="authGuidance" class="form-hint" x-text="authGuidance"></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group" x-show="authRequiresSecret">
|
||||
<label class="label">API Key / Token</label>
|
||||
<input x-model="authSecret" type="password" placeholder="Paste your API key or token" class="input" />
|
||||
</div>
|
||||
@@ -563,7 +639,7 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label class="label">Model</label>
|
||||
<input x-model="model" type="text" placeholder="e.g. openai/gpt-4.1, anthropic/claude-sonnet-4" class="input" />
|
||||
<input x-model="model" type="text" placeholder="e.g. openai-codex/gpt-5.5, openai/gpt-4.1" class="input" />
|
||||
<p class="form-hint">Format: provider/model-name</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -660,7 +736,17 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div x-show="log" class="log-output" x-text="log"></div>
|
||||
<div x-show="loginUrl || loginCode" class="login-code-panel">
|
||||
<div>
|
||||
<p class="login-code-label">ChatGPT Login</p>
|
||||
<a x-show="loginUrl" :href="loginUrl" target="_blank" rel="noopener noreferrer" class="login-code-url" x-text="loginUrl"></a>
|
||||
</div>
|
||||
<div x-show="loginCode" class="login-code-value" x-text="loginCode"></div>
|
||||
</div>
|
||||
|
||||
<div x-show="setupError" class="setup-error-panel" x-text="setupError"></div>
|
||||
|
||||
<div x-show="log" x-ref="setupLog" class="log-output" x-text="log"></div>
|
||||
|
||||
<p class="text-xs text-muted mt-4">Reset deletes the config file so you can rerun onboarding. Channel access approval grants DM access when dmPolicy=pairing.</p>
|
||||
</div>
|
||||
|
||||
@@ -546,6 +546,67 @@ img { max-width: 100%; height: auto; }
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.login-code-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
background: rgba(14, 165, 233, 0.08);
|
||||
border: 1px solid rgba(14, 165, 233, 0.24);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.login-code-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.login-code-url {
|
||||
color: #0284c7;
|
||||
font-size: 0.875rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.login-code-value {
|
||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
color: #0369a1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.setup-error-panel {
|
||||
background: rgba(220, 38, 38, 0.08);
|
||||
border: 1px solid rgba(220, 38, 38, 0.24);
|
||||
border-radius: 0.5rem;
|
||||
color: #b91c1c;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
padding: 0.875rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.login-code-panel {
|
||||
background: rgba(14, 165, 233, 0.12);
|
||||
border-color: rgba(56, 189, 248, 0.32);
|
||||
}
|
||||
.login-code-url { color: #38bdf8; }
|
||||
.login-code-value { color: #7dd3fc; }
|
||||
.setup-error-panel {
|
||||
background: rgba(220, 38, 38, 0.12);
|
||||
border-color: rgba(248, 113, 113, 0.32);
|
||||
color: #fca5a5;
|
||||
}
|
||||
}
|
||||
|
||||
.danger-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid #fecaca;
|
||||
|
||||
+153
-36
@@ -132,6 +132,26 @@ function clawArgs(args) {
|
||||
return [OPENCLAW_ENTRY, ...args];
|
||||
}
|
||||
|
||||
function stripAnsi(value) {
|
||||
return String(value)
|
||||
.replace(/\x1b\]8;;.*?\x1b\\|\x1b\]8;;\x1b\\/g, "")
|
||||
.replace(/\x1b\[[\x20-\x3f]*[\x40-\x7e]/g, "");
|
||||
}
|
||||
|
||||
function isTransientProgressLine(line) {
|
||||
return /^[\s◐◓◑◒⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏.-]*(Requesting device code|Waiting for device authorization|Exchanging device code)/.test(
|
||||
line,
|
||||
);
|
||||
}
|
||||
|
||||
function cleanPtyOutput(value) {
|
||||
const cleaned = stripAnsi(value)
|
||||
.split(/\r|\n/)
|
||||
.filter((line) => line && !isTransientProgressLine(line))
|
||||
.join("\n");
|
||||
return cleaned ? `${cleaned}\n` : "";
|
||||
}
|
||||
|
||||
function configPath() {
|
||||
return (
|
||||
process.env.OPENCLAW_CONFIG_PATH?.trim() ||
|
||||
@@ -488,7 +508,11 @@ app.get("/setup/api/status", requireSetupAuth, async (_req, res) => {
|
||||
hint: "API key / Codex",
|
||||
options: [
|
||||
{ value: "openai-api-key", label: "OpenAI API key" },
|
||||
{ value: "openai-codex", label: "OpenAI Codex (ChatGPT OAuth)" },
|
||||
{
|
||||
value: "openai-codex-device-code",
|
||||
label: "OpenAI Codex device pairing",
|
||||
hint: "ChatGPT login without an API key",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -709,12 +733,15 @@ app.get("/setup/api/status", requireSetupAuth, async (_req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
function requiresInteractiveOnboarding(payload) {
|
||||
return payload.authChoice === "openai-codex-device-code";
|
||||
}
|
||||
|
||||
function buildOnboardArgs(payload) {
|
||||
const interactive = requiresInteractiveOnboarding(payload);
|
||||
const args = [
|
||||
"onboard",
|
||||
"--non-interactive",
|
||||
"--accept-risk",
|
||||
"--json",
|
||||
"--no-install-daemon",
|
||||
"--skip-health",
|
||||
"--workspace",
|
||||
@@ -731,6 +758,19 @@ function buildOnboardArgs(payload) {
|
||||
"quickstart",
|
||||
];
|
||||
|
||||
if (interactive) {
|
||||
args.push(
|
||||
"--mode",
|
||||
"local",
|
||||
"--skip-channels",
|
||||
"--skip-skills",
|
||||
"--skip-search",
|
||||
"--skip-ui",
|
||||
);
|
||||
} else {
|
||||
args.push("--non-interactive", "--json");
|
||||
}
|
||||
|
||||
if (payload.authChoice) {
|
||||
args.push("--auth-choice", payload.authChoice);
|
||||
|
||||
@@ -798,8 +838,9 @@ function buildOnboardArgs(payload) {
|
||||
|
||||
function runCmd(cmd, args, opts = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const { onOutput, stripOutput, ...spawnOpts } = opts;
|
||||
const proc = childProcess.spawn(cmd, args, {
|
||||
...opts,
|
||||
...spawnOpts,
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_STATE_DIR: STATE_DIR,
|
||||
@@ -808,8 +849,14 @@ function runCmd(cmd, args, opts = {}) {
|
||||
});
|
||||
|
||||
let out = "";
|
||||
proc.stdout?.on("data", (d) => (out += d.toString("utf8")));
|
||||
proc.stderr?.on("data", (d) => (out += d.toString("utf8")));
|
||||
const append = (d) => {
|
||||
const rawChunk = d.toString("utf8");
|
||||
const streamChunk = stripOutput ? stripAnsi(rawChunk) : rawChunk;
|
||||
out += rawChunk;
|
||||
onOutput?.(streamChunk);
|
||||
};
|
||||
proc.stdout?.on("data", append);
|
||||
proc.stderr?.on("data", append);
|
||||
|
||||
proc.on("error", (err) => {
|
||||
out += `\n[spawn error] ${String(err)}\n`;
|
||||
@@ -820,10 +867,56 @@ function runCmd(cmd, args, opts = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function runPtyCmd(cmd, args, opts = {}) {
|
||||
return new Promise((resolve) => {
|
||||
let out = "";
|
||||
let proc;
|
||||
try {
|
||||
proc = pty.spawn(cmd, args, {
|
||||
name: "xterm-color",
|
||||
cols: 100,
|
||||
rows: 30,
|
||||
cwd: opts.cwd ?? process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_STATE_DIR: STATE_DIR,
|
||||
OPENCLAW_WORKSPACE_DIR: WORKSPACE_DIR,
|
||||
// Force OpenClaw's local device-code branch so Railway setup can show
|
||||
// the short code in the web UI instead of hiding it as remote-only.
|
||||
DISPLAY: process.env.DISPLAY || ":0",
|
||||
WAYLAND_DISPLAY: process.env.WAYLAND_DISPLAY || "wayland-0",
|
||||
SSH_CLIENT: "",
|
||||
SSH_TTY: "",
|
||||
SSH_CONNECTION: "",
|
||||
FORCE_COLOR: "0",
|
||||
NO_COLOR: "1",
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
out += `\n[spawn error] ${String(err)}\n`;
|
||||
opts.onOutput?.(out);
|
||||
resolve({ code: 127, output: out });
|
||||
return;
|
||||
}
|
||||
|
||||
proc.onData((data) => {
|
||||
const chunk = opts.cleanOutput ? cleanPtyOutput(data) : stripAnsi(data);
|
||||
if (!chunk) return;
|
||||
out += chunk;
|
||||
opts.onOutput?.(chunk);
|
||||
});
|
||||
|
||||
proc.onExit(({ exitCode }) => {
|
||||
resolve({ code: exitCode ?? 0, output: out });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const VALID_AUTH_CHOICES = [
|
||||
"apiKey",
|
||||
"openai-api-key",
|
||||
"openai-codex",
|
||||
"openai-codex-device-code",
|
||||
"gemini-api-key",
|
||||
"google-gemini-cli",
|
||||
"deepseek-api-key",
|
||||
@@ -875,6 +968,9 @@ function validatePayload(payload) {
|
||||
if (payload.authChoice && !VALID_AUTH_CHOICES.includes(payload.authChoice)) {
|
||||
return `Invalid authChoice: ${payload.authChoice}`;
|
||||
}
|
||||
if (payload.authChoice === "openai-codex") {
|
||||
return "OpenAI Codex browser login needs redirect-url input in an interactive terminal. Choose OpenAI Codex device pairing in web setup.";
|
||||
}
|
||||
const stringFields = [
|
||||
"telegramToken",
|
||||
"discordToken",
|
||||
@@ -897,14 +993,16 @@ function validatePayload(payload) {
|
||||
}
|
||||
|
||||
app.post("/setup/api/run", requireSetupAuth, async (req, res) => {
|
||||
const stream = (chunk) => {
|
||||
if (chunk) res.write(chunk);
|
||||
};
|
||||
|
||||
try {
|
||||
if (isConfigured()) {
|
||||
await ensureGatewayRunning();
|
||||
return res.json({
|
||||
ok: true,
|
||||
output:
|
||||
"Already configured.\nUse Reset setup if you want to rerun onboarding.\n",
|
||||
});
|
||||
return res
|
||||
.type("text/plain")
|
||||
.send("Already configured.\nUse Reset setup if you want to rerun onboarding.\n");
|
||||
}
|
||||
|
||||
fs.mkdirSync(STATE_DIR, { recursive: true });
|
||||
@@ -913,18 +1011,34 @@ app.post("/setup/api/run", requireSetupAuth, async (req, res) => {
|
||||
const payload = req.body || {};
|
||||
const validationError = validatePayload(payload);
|
||||
if (validationError) {
|
||||
return res.status(400).json({ ok: false, output: validationError });
|
||||
return res.status(400).type("text/plain").send(`${validationError}\n`);
|
||||
}
|
||||
const onboardArgs = buildOnboardArgs(payload);
|
||||
const onboard = await runCmd(OPENCLAW_NODE, clawArgs(onboardArgs));
|
||||
|
||||
let extra = "";
|
||||
extra += `\n[setup] Onboarding exit=${onboard.code} configured=${isConfigured()}\n`;
|
||||
res.set({
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Cache-Control": "no-cache",
|
||||
});
|
||||
|
||||
const onboardArgs = buildOnboardArgs(payload);
|
||||
const interactive = requiresInteractiveOnboarding(payload);
|
||||
stream(
|
||||
interactive
|
||||
? "Starting OpenAI Codex device pairing. Use the URL and code below, then keep this page open until it completes.\n\n"
|
||||
: "Starting OpenClaw onboarding...\n\n",
|
||||
);
|
||||
|
||||
const onboardRunner = interactive ? runPtyCmd : runCmd;
|
||||
const onboard = await onboardRunner(OPENCLAW_NODE, clawArgs(onboardArgs), {
|
||||
onOutput: stream,
|
||||
cleanOutput: interactive,
|
||||
stripOutput: !interactive,
|
||||
});
|
||||
|
||||
const ok = onboard.code === 0 && isConfigured();
|
||||
stream(`\n[setup] Onboarding exit=${onboard.code} configured=${isConfigured()}\n`);
|
||||
|
||||
if (ok) {
|
||||
extra += "\n[setup] Configuring gateway settings...\n";
|
||||
stream("\n[setup] Configuring gateway settings...\n");
|
||||
|
||||
const allowInsecureResult = await runCmd(
|
||||
OPENCLAW_NODE,
|
||||
@@ -935,7 +1049,9 @@ app.post("/setup/api/run", requireSetupAuth, async (req, res) => {
|
||||
"true",
|
||||
]),
|
||||
);
|
||||
extra += `[config] gateway.controlUi.allowInsecureAuth=true exit=${allowInsecureResult.code}\n`;
|
||||
stream(
|
||||
`[config] gateway.controlUi.allowInsecureAuth=true exit=${allowInsecureResult.code}\n`,
|
||||
);
|
||||
|
||||
const tokenResult = await runCmd(
|
||||
OPENCLAW_NODE,
|
||||
@@ -946,7 +1062,7 @@ app.post("/setup/api/run", requireSetupAuth, async (req, res) => {
|
||||
OPENCLAW_GATEWAY_TOKEN,
|
||||
]),
|
||||
);
|
||||
extra += `[config] gateway.auth.token exit=${tokenResult.code}\n`;
|
||||
stream(`[config] gateway.auth.token exit=${tokenResult.code}\n`);
|
||||
|
||||
const proxiesResult = await runCmd(
|
||||
OPENCLAW_NODE,
|
||||
@@ -958,15 +1074,16 @@ app.post("/setup/api/run", requireSetupAuth, async (req, res) => {
|
||||
'["127.0.0.1"]',
|
||||
]),
|
||||
);
|
||||
extra += `[config] gateway.trustedProxies exit=${proxiesResult.code}\n`;
|
||||
stream(`[config] gateway.trustedProxies exit=${proxiesResult.code}\n`);
|
||||
|
||||
if (payload.model?.trim()) {
|
||||
extra += `[setup] Setting model to ${payload.model.trim()}...\n`;
|
||||
stream(`[setup] Setting model to ${payload.model.trim()}...\n`);
|
||||
const modelResult = await runCmd(
|
||||
OPENCLAW_NODE,
|
||||
clawArgs(["models", "set", payload.model.trim()]),
|
||||
{ onOutput: stream, stripOutput: true },
|
||||
);
|
||||
extra += `[models set] exit=${modelResult.code}\n${modelResult.output || ""}`;
|
||||
stream(`[models set] exit=${modelResult.code}\n`);
|
||||
}
|
||||
|
||||
async function configureChannel(name, cfgObj) {
|
||||
@@ -984,14 +1101,14 @@ app.post("/setup/api/run", requireSetupAuth, async (req, res) => {
|
||||
OPENCLAW_NODE,
|
||||
clawArgs(["config", "get", `channels.${name}`]),
|
||||
);
|
||||
return (
|
||||
stream(
|
||||
`\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)"}`
|
||||
`\n[${name} verify] exit=${get.code} (output ${get.output.length} chars)\n${get.output || "(no output)"}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
if (payload.telegramToken?.trim()) {
|
||||
extra += await configureChannel("telegram", {
|
||||
await configureChannel("telegram", {
|
||||
enabled: true,
|
||||
dmPolicy: "pairing",
|
||||
botToken: payload.telegramToken.trim(),
|
||||
@@ -1001,7 +1118,7 @@ app.post("/setup/api/run", requireSetupAuth, async (req, res) => {
|
||||
}
|
||||
|
||||
if (payload.discordToken?.trim()) {
|
||||
extra += await configureChannel("discord", {
|
||||
await configureChannel("discord", {
|
||||
enabled: true,
|
||||
token: payload.discordToken.trim(),
|
||||
groupPolicy: "open",
|
||||
@@ -1010,27 +1127,27 @@ app.post("/setup/api/run", requireSetupAuth, async (req, res) => {
|
||||
}
|
||||
|
||||
if (payload.slackBotToken?.trim() || payload.slackAppToken?.trim()) {
|
||||
extra += await configureChannel("slack", {
|
||||
await configureChannel("slack", {
|
||||
enabled: true,
|
||||
botToken: payload.slackBotToken?.trim() || undefined,
|
||||
appToken: payload.slackAppToken?.trim() || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
extra += "\n[setup] Starting gateway...\n";
|
||||
stream("\n[setup] Starting gateway...\n");
|
||||
await restartGateway();
|
||||
extra += "[setup] Gateway started.\n";
|
||||
stream("[setup] Gateway started.\n");
|
||||
}
|
||||
|
||||
return res.status(ok ? 200 : 500).json({
|
||||
ok,
|
||||
output: `${onboard.output}${extra}`,
|
||||
});
|
||||
stream(ok ? "\n[setup] Complete.\n" : "\n[setup] Failed. Review the output above.\n");
|
||||
return res.end();
|
||||
} catch (err) {
|
||||
log.error("setup", `run error: ${String(err)}`);
|
||||
return res
|
||||
.status(500)
|
||||
.json({ ok: false, output: `Internal error: ${String(err)}` });
|
||||
if (!res.headersSent) {
|
||||
return res.status(500).type("text/plain").send(`Internal error: ${String(err)}\n`);
|
||||
}
|
||||
stream(`\n[setup] Internal error: ${String(err)}\n`);
|
||||
return res.end();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user